Programming TypeScript ch5 -- (2/2)

TypeScript勉強メモ

出典: 


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

    • 系統学