Classes Are Structurally Typed
-
公称型じゃないよという話
- 名前が違っていても同じ形ならば代入可能
Classes Declare Both Values and Types
- TypeScriptにおいて、値と型とは別の名前空間
const a = 1999
function b() { }
type a = number
interface b { }
- 文脈によってよしなに解釈される
-
classとenumは例外
- 値/型両名前空間に定義される
class C { }
const c: C = new C
enum E { F, G }
const e: E = E.F
c: C
,e: E
はTypeScript特有のコード = 型new C
,E.F
はJSでも有効なコード = 値- classを定義すると、そのクラスのインスタンスの型定義と、クラスのコンストラクタの型定義が生成される:
type State = {
[key: string]: string
}
class StringDatabase {
state: State = {}
get(key: string): string | null {
return key in this.state ? this.state[key] : null
}
set(key: string, value: string) {
this.state[key] = value
}
static from(state: State) {
const db = new StringDatabase
for (const key in state) {
db.set(key, state[key])
}
return db
}
}
- このようなクラスがあった場合、下記の2つのinterface相当の定義が生成される
type State = {
[key: string]: string
}
interface StringDatabase {
state: State
get(key: string): string | null
set(key: string, value: string): void
}
interface StringDatabaseConstructor {
new(): StringDatabase
from(state: State): StringDatabase
}
Polymorphism
class Pair<T, U> {
constructor(
private _first: T,
private _second: U
) { }
public get first() {
return this._first
}
public get second() {
return this._second
}
}
const storage = new Pair(1, 'MB')
const path = new Pair('/var/www/', 'hoge.txt')
- 【補】こういうのはできない
class Pair<T, U> {
constructor(
private _first: T,
private _second: U
) { }
public get first() {
return this._first
}
public get second() {
return this._second
}
// Error: Static members cannot reference class type parameters.
public static from(first:T, second: U) {
return new Pair(first, second)
}
}
const storage = Pair.from(1,'MB')
- これはできる
class Pair<T, U> {
constructor(
private _first: T,
private _second: U
) { }
public get first() {
return this._first
}
public get second() {
return this._second
}
public static from<T, U>(first:T, second: U) {
return new Pair(first, second)
}
}
const storage = Pair.from(1,'MB')
Mixins
-
‘is-a’ではなく’can’ないし’has-a’で表すやつ
- Rectangle is a Shape ではなく
- Rectangle can be measured its area
- Rectangle has four sides
- TS/JSにはmixinやtraitといった類のものはないが自分で作れる
- 下記のような方針:
type ClassConstructor = new(...args: any[]) => {}
function withEZDebug<C extends ClassConstructor>(Class: C) {
return class extends Class {
constructor(...args:any[]) {
super(...args)
}
}
}
-
コンストラクタを受け取って、デコレートする感じ
- 【補】あくまでJSがプロトタイプベースのオブジェクト指向なので、継承関係は1本道になる
- デバッグ情報を出力可能にする
type ClassConstructor<T> = new (...args: any[]) => T
function withEZDebug<C extends ClassConstructor<{
getDebugValue(): object
}>>(Class: C) {
return class extends Class {
debug() {
const Name = Class.name
const value = this.getDebugValue()
return Name + '(' + JSON.stringify(value) + ')'
}
}
}
class HardToDebugUser {
constructor(
private id: number,
private firstName: string,
private lastName: string
) { }
getDebugValue() {
return {
id: this.id,
name: this.firstName + ' ' + this.lastName
}
}
}
const User = withEZDebug(HardToDebugUser)
const user = new User(3, 'Daiki', 'Horiyama')
console.log(user.debug()) // HardToDebugUser({"id":3,"name":"Daiki Horiyama"})
- ミックスイン先のクラスには
getDebugValue(): object
メソッドを実装していることを要求する
Decorators
- 利用するには、tsconfigの設定が必要
{
"compilerOptions": {
+ "experimentalDecorators": true,
"lib": ["es2015"],
"module": "commonjs",
"outDir": "dist",
"sourceMap": true,
"strict": true,
"target": "ESNext"
},
"include": [
"src"
]
}
-
decorateできるもの
- class
- method
- static method
- method parameter
- static method parameter
- property
- static property
- property getter/setter
- static property getter/setter
class
type ClassConstructor<T> = new (...args: any[]) => T
function withEZDebug<C extends ClassConstructor<{
getDebugValue(): object
}>>(Class: C) {
return class extends Class {
debug() {
const Name = Class.name
const value = this.getDebugValue()
return Name + '(' + JSON.stringify(value) + ')'
}
}
}
@withEZDebug
class User {
constructor(
private id: number,
private firstName: string,
private lastName: string
) { }
getDebugValue() {
return {
id: this.id,
name: this.firstName + ' ' + this.lastName
}
}
}
const user = new User(3, 'Daiki', 'Horiyama')
console.log(user.debug()) // Error: Property 'debug' does not exist on type 'User'.
- 残念ながらshapeを変えるようなことはできない
【補】method
type ClassPrototype = {}
type MethodName = string
function loggable(
classPrototype: ClassPrototype,
methodName: MethodName,
descriptor: PropertyDescriptor
) {
const old = descriptor.value
descriptor.value = function(...args: any) {
console.log(methodName + ' is called with: ', args)
old.apply(this, args)
}
}
class Hoge {
@loggable
fuga() {
}
}
const hoge = new Hoge
hoge.fuga()
【補】method parameter
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
const existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = function() {
const requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (const parameterIndex of requiredParameters) {
if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
}
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
const greeter = new Greeter('hogehoge')
console.log(greeter.greet('Daiki'))
-
引数をrequiredにするのに使える
- コンパイル時に取り締まるのはTypeScriptだけで可能
- 生成したJSコードを、JSの世界から直接呼び出したときにも取り締まれる
Simulating final Classes
- TS自体にクラスやメソッドの
final
のサポートはない -
final class
相当のことは、コンストラクタをprivateにすることで実現する- もちろん
new
もできなくなるのでファクトリメソッドを生やす
- もちろん
class FinalClass {
private constructor() { }
static create() {
return new FinalClass
}
}
class Derived extends FinalClass { } // Error: Cannot extend a class 'FinalClass'. Class constructor is marked as private.
const foo = new FinalClass // Error: Constructor of class 'FinalClass' is private and only accessible within the class declaration.
const bar = FinalClass.create()
Exercises
constructorをprotectedにしたらどうなるの
class Base {
protected constructor() { }
static create() {
return new Base
}
}
class Derived extends Base { }
const foo = new Base // Error: Constructor of class 'Base' is private and only accessible within the class declaration.
const bar = Base.create()
- 継承可能
new
だけできない
type saferなfactory pattern
type Shoe = {
purpose: string
}
class BalletFlat implements Shoe {
purpose = 'dancing'
}
class Boot implements Shoe {
purpose = 'woodcutting'
}
class Sneaker implements Shoe {
purpose = 'walking'
}
const ShoeFactory = {
create(type: 'balletFlat' | 'boot' | 'sneaker'): Shoe {
switch (type) {
case 'balletFlat': return new BalletFlat
case 'boot': return new Boot
case 'sneaker': return new Sneaker
default:
const _: never = type
return type
}
}
}
const balletFlat = ShoeFactory.create('balletFlat') // Shoe
BalletFlat
型で返ってきてほしい- こうする:
type Shoe = {
purpose: string
}
class BalletFlat implements Shoe {
purpose = 'dancing'
}
class Boot implements Shoe {
purpose = 'woodcutting'
}
class Sneaker implements Shoe {
purpose = 'walking'
}
type ShoeFactory = {
create(type: 'balletFlat'): BalletFlat
create(type: 'boot'): Boot
create(type: 'sneaker'): Sneaker
create(type: string): Shoe
}
const ShoeFactory: ShoeFactory = {
create(type: 'balletFlat' | 'boot' | 'sneaker'): Shoe {
switch (type) {
case 'balletFlat': return new BalletFlat
case 'boot': return new Boot
case 'sneaker': return new Sneaker
default:
const _: never = type
return type
}
}
}
const balletFlat = ShoeFactory.create('balletFlat') // BalletFlat
type saferなbuilder pattern
class RequestBuilder {
private url: string | null = null
private method: 'get' | 'post' | null = null
private data: object | null = null
setURL(url: string): this {
this.url = url
return this
}
setMethod(method: 'get' | 'post'): this {
this.method = method
return this
}
setData(data: object): this {
this.data = data
return this
}
send(): void {
// to do something
}
}
new RequestBuilder()
.setURL('/users')
.setMethod('get')
.setData({ firstName: 'syaro' })
.send()
- url,method,dataをセットしていなくても
send()
できてしまうのをやめたい
url,methodをこの順でセットしないとsend()できなくする
class RequestBuilder {
protected url: string | null = null
setURL(url: string): RequestBuilderWithURL {
const ret = new RequestBuilderWithURL
ret.url = url
return ret
}
}
class RequestBuilderWithURL extends RequestBuilder {
protected method: 'get' | 'post' | null = null
setMethod(method: 'get' | 'post'): RequestBuilderWithURLAndMethod {
const ret = new RequestBuilderWithURLAndMethod
ret.method = method
return ret
}
}
class RequestBuilderWithURLAndMethod extends RequestBuilderWithURL {
protected data: object | null = null
setData(data: object): this {
this.data = data
return this
}
send(): void {
// to do something
}
}
new RequestBuilder()
.send() // Error: Property 'send' does not exist on type 'RequestBuilder'.
new RequestBuilder()
.setURL('/users')
.send() // Error: Property 'send' does not exist on type 'RequestBuilder'.
new RequestBuilder()
.setURL('/users')
.setMethod('get')
.send()
new RequestBuilder()
.setMethod('get') // Error: Property 'setMethod' does not exist on type 'RequestBuilder'.
.setURL('/users')
.send()
new RequestBuilder()
.setURL('/users')
.setMethod('get')
.setData({ firstName: 'syaro' })
.send()
- 【所感】この場合、
set
という名前は良くない。with
とかがいい
url,methodを任意の順でセットしないとsend()できなくする
type HTTPURL = string
type HTTPMethod = 'get' | 'post'
type HTTPData = object
interface RequestBuilderSendable {
url: HTTPURL
method: HTTPMethod
}
class RequestBuilder {
protected url: HTTPURL | null = null
protected method: HTTPMethod | null = null
protected data: HTTPData | null = null
setURL(url: HTTPURL): this & { url: HTTPURL } {
return Object.assign(this, { url })
}
setMethod(method: HTTPMethod): this & { method: HTTPMethod } {
return Object.assign(this, { method })
}
setData(data: HTTPData): this & { data: HTTPData } {
return Object.assign(this, { data })
}
send(this: RequestBuilderSendable & RequestBuilder): void {
// to do something
}
}
new RequestBuilder()
.send() // Error: The 'this' context of type 'RequestBuilder' is not assignable to method's 'this' of type 'RequestBuilderSendable & RequestBuilder'.
new RequestBuilder()
.setURL('/users')
.send() // Error: The 'this' context of type 'RequestBuilder & { url: string; }' is not assignable to method's 'this' of type 'RequestBuilderSendable & RequestBuilder'.
new RequestBuilder()
.setURL('/users')
.setMethod('get')
.send()
new RequestBuilder()
.setMethod('get')
.setURL('/users')
.send()
new RequestBuilder()
.setURL('/users')
.setMethod('get')
.setData({ firstName: 'syaro' })
.send()
英語
-
phylogenetics
- 系統学