Type Scriptテクニック

22568 ワード

前言
以前からTypeScriptを用いて日常的な符号化を試みてきたが,静的タイプ言語に対する自分の理解が深くなく,TypeScriptのタイプシステムに一定の複雑さがあるため,この言語の優位性を十分に発揮していないと感じ,コードの可読性とメンテナンス性を向上させた.そこでここ数日、この言語をよく研究し、特別な言語特性と実用的なテクニックをまとめたいと思っています.
オペレータ
typeof-変数のタイプを取得
const colors = {
  red: 'red',
  blue: 'blue'
}

// type res = { red: string; blue: string }
type res = typeof colors

keyof-取得タイプのキー
const data = {
  a: 3,
  hello: 'world'
}

// 
function get<extends object, extends keyof T>(o: T, name: K): T[K] {
  return o[name]
}

get(data, 'a') // 3
get(data, 'b') // Error

組合せtypeofとkeyof-キャプチャキーの名前
const colors = {
  red: 'red',
  blue: 'blue'
}

type Colors = keyof typeof colors

let color: Colors // 'red' | 'blue'
color = 'red' // ok
color = 'blue' // ok
color = 'anythingElse' // Error

in-遍歴キー名
interface Square {
  kind: 'square'
  size: number
}

// type res = (radius: number) => { kind: 'square'; size: number }
type res = (radius: number) => { [T in keyof Square]: Square[T] }

特殊なタイプ
ネストされたインタフェースのタイプ
interface Producer {
  name: string
  cost: number
  production: number
}

interface Province {
  name: string
  demand: number
  price: number
  producers: Producer[]
}

let data: Province = {
  name: 'Asia',
  demand: 30,
  price: 20,
  producers: [
    { name: 'Byzantium', cost: 10, production: 9 },
    { name: 'Attalia', cost: 12, production: 10 },
    { name: 'Sinope', cost: 10, production: 6 }
  ]
}
interface Play {
  name: string
  type: string
}

interface Plays {
  [key: string]: Play
}

let plays: Plays = {
  'hamlet': { name: 'Hamlet', type: 'tragedy' },
  'as-like': { name: 'As You Like It', type: 'comedy' },
  'othello': { name: 'Othello', type: 'tragedy' }
}

条件タイプ
type isBool = T extends boolean ? true : false

// type t1 = false
type t1 = isBool

// type t2 = true
type t2 = isBool<false>

辞書の種類
interface Dictionary {
  [index: string]: T
}

const data: Dictionary = {
  a: 3,
  b: 4,
}

infer-遅延推定タイプ
type ParamType = T extends (param: infer P) => any ? P : T

interface User {
  name: string
  age: number
}

type Func = (user: User) => void

type Param = ParamType // Param = User
type AA = ParamType // string
type ElementOf = T extends Array ? E : never

type TTuple = [string, number]

type ToUnion = ElementOf // string | number

よく使うテクニック
const enumを使用して定数リストを維持する
const enum STATUS {
  TODO = 'TODO',
  DONE = 'DONE',
  DOING = 'DOING'
}

function todos(status: STATUS): Todo[] {
  // ...
}

todos(STATUS.TODO)

Partial & Pick
type Partial = {
  [P in keyof T]?: T[P]
}

type Pick = {
  [P in K]: T[P]
}

interface User {
  id: number
  age: number
  name: string
}

// type PartialUser = { id?: number; age?: number; name?: string }
type PartialUser = Partial

// type PickUser = { id: number; age: number }
type PickUser = Pick'id'|'age'>

Exclude & Omit
type Exclude = T extends U ? never : T

// type A = 'a'
type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'>
type Omit = Pick>

interface User {
  id: number
  age: number
  name: string
}

// type PickUser = { age: number; name: string }
type OmitUser = Omit'id'>

巧みなneverタイプ
type FunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]

type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]

interface Part {
  id: number
  name: string
  subparts: Part[]
  updatePart(newName: string): void
}

type T40 = FunctionPropertyNames  // 'updatePart'
type T41 = NonFunctionPropertyNames  // 'id' | 'name' | 'subparts'

ハイブリッドクラス(mixins)
//    mixins    
type Constructor = new (...args: any[]) => T

// 
function TimesTamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    timestamp = Date.now()
  }
}

// 
function Activatable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    isActivated = false
    activate() {
      this.isActivated = true
    }
    deactivate() {
      this.isActivated = false
    }
  }
}

// 
class User {
  name = ''
}

//   TimesTamped   User
const TimestampedUser = TimesTamped(User)

//   TimesTamped   Activatable 
const TimestampedActivatableUser = TimesTamped(Activatable(User))

// 
const timestampedUserExample = new TimestampedUser()
console.log(timestampedUserExample.timestamp)

const timestampedActivatableUserExample = new TimestampedActivatableUser()
console.log(timestampedActivatableUserExample.timestamp)
console.log(timestampedActivatableUserExample.isActivated)

タイプ変換
次はLeetCodeのType Scriptに関する面接問題を解きます.テーマの大まかな内容はEffectModuleというクラスで、その実現は以下の通りです.
interface Action {
  payload?: T
  type: string
}

class EffectModule {
  count = 1
  message = 'hello!'
  delay(input: Promise) {
    return input.then(i => ({
      payload: `hello ${i}!`,
      type: 'delay'
    }))
  }
  setMessage(action: Action<Date>) {
    return {
      payload: action.payload.getMilliseconds(),
      type: 'set-message'
    }
  }
}

次に、connect関数がEffectModuleクラスのインスタンスオブジェクトをパラメータとして受信し、新しいオブジェクトが返されます.
const connect: Connect = _m => ({
  delay: (input: number) => ({
    type: 'delay',
    payload: `hello ${input}`
  }),
  setMessage: (input: Date) => ({
    type: 'set-message',
    payload: input.getMilliseconds()
  })
})

type Connected = {
  delay(input: number): Action
  setMessage(action: Date): Action
}

const connected: Connected = connect(new EffectModule())

接続関数を呼び出すと、返される新しいオブジェクトにはEffectModuleと同じ名前のメソッドしか含まれておらず、メソッドのタイプ署名が変更されていることがわかります.
asyncMethod(input: Promise): Promise>     
asyncMethod(input: T): Action 
syncMethod(action: Action): Action     
syncMethod(action: T): Action
type Connect = (module: EffectModule) => anyを作成して、最終的なコンパイルをスムーズに通過させる必要があります.このテーマは主に2つの点を考察していることがわかります.
  • クラスから関数
  • を選択
  • 巧みにinferでタイプ変換
  • 次は私がこの問題を解く答えです.
    type FuncName = { [P in keyof T]: T[P] extends Function ? P : never }[keyof T]

    type Middle = { [T in FuncName]: EffectModule[T] }

    type Transfer = {
      [P in keyof T]: T[P] extends (input: Promise) => Promise
      ? (input: J) => K
      : T[P] extends (action: Action) => infer K
      ? (input: J) => K
      : never
    }

    type Connect = (module: EffectModule) => { [T in keyof Transfer]: Transfer[T] }

    制御反転と依存注入
    制御反転(Inversion of Control)と依存注入(Dependency Injection)はオブジェクト向けプログラミングにおいて非常に重要な思想と法則である.Wikipediaでは、IoCはコンピュータコード間の結合度を低下させることができ、DIは、1つのオブジェクトが作成されたときに、そのオブジェクトに依存するすべてのオブジェクトを注入するプロセスを表す.フロントエンドフレームAngularとNode.jsのバックエンドフレームワークNestはこの考えを引用した.この2つの概念の具体的な説明はここでは展開されないが、読者はこの2つの文章を見ることができる[1][2].次に,Angular 5以前のDependency Injectionに基づいて,簡易版の制御反転と依存注入を実現する.
    まず、関連するテストコードを作成します.
    import { expect } from 'chai'
    import { Injectable, createInjector } from './injection'

    class Engine {}

    class DashboardSoftware {}

    @Injectable()
    class Dashboard {
      constructor(public software: DashboardSoftware) {}
    }

    @Injectable()
    class Car {
      constructor(public engine: Engine) {}
    }

    @Injectable()
    class CarWithDashboard {
      constructor(public engine: Engine, public dashboard: Dashboard) {}
    }

    class NoAnnotations {
      constructor(_secretDependency: any) {}
    }

    describe('injector', () => {
      it('should instantiate a class without dependencies', () => {
        const injector = createInjector([Engine])
        const engine = injector.get(Engine)
        expect(engine instanceof Engine).to.be.true
      })

      it('should resolve dependencies based on type information', () => {
        const injector = createInjector([Engine, Car])
        const car = injector.get(Car)
        expect(car instanceof Car).to.be.true
        expect(car.engine instanceof Engine).to.be.true
      })

      it('should resolve nested dependencies based on type information', () => {
        const injector = createInjector([CarWithDashboard, Engine, Dashboard, DashboardSoftware])
        const _CarWithDashboard = injector.get(CarWithDashboard)
        expect(_CarWithDashboard.dashboard.software instanceof DashboardSoftware).to.be.true
      })

      it('should cache instances', () => {
        const injector = createInjector([Engine])
        const e1 = injector.get(Engine)
        const e2 = injector.get(Engine)
        expect(e1).to.equal(e2)
      })

      it('should show the full path when no provider', () => {
        const injector = createInjector([CarWithDashboard, Engine, Dashboard])
        expect(() => injector.get(CarWithDashboard)).to.throw('No provider for DashboardSoftware!')
      })

      it('should throw when no type', () => {
        expect(() => createInjector([NoAnnotations])).to.throw(
          'Make sure that NoAnnotations is decorated with Injectable.'
        )
      })

      it('should throw when no provider defined', () => {
        const injector = createInjector([])
        expect(() => injector.get('NonExisting')).to.throw('No provider for NonExisting!')
      })
    })

    実装するコア機能は3つあります.
  • 提供されたクラスに基づいてIoCコンテナを作成する、クラス間の依存関係を管理することができる
  • .
  • は、IoCコンテナを介してクラスのインスタンスオブジェクトを取得する際に、関連する依存オブジェクト
  • を注入する.
  • マルチレベル依存と処理エッジ状況
  • を実現する.
    まず、最も簡単な@Injectable装飾器を実現します.
    export const Injectable = (): ClassDecorator => target => {
      Reflect.defineMetadata('Injectable', true, target)
    }

    次に、提供されたproviderクラスに基づいて、クラス間の依存関係を管理できるIoCコンテナを作成します.
    abstract class ReflectiveInjector implements Injector {
      abstract get(token: any): any
      static resolve(providers: Provider[]): ResolvedReflectiveProvider[] {
        return providers.map(resolveReflectiveProvider)
      }
      static fromResolvedProviders(providers: ResolvedReflectiveProvider[]): ReflectiveInjector {
        return new ReflectiveInjector_(providers)
      }
      static resolveAndCreate(providers: Provider[]): ReflectiveInjector {
        const resolvedReflectiveProviders = ReflectiveInjector.resolve(providers)
        return ReflectiveInjector.fromResolvedProviders(resolvedReflectiveProviders)
      }
    }

    class ReflectiveInjector_ implements ReflectiveInjector {
      _providers: ResolvedReflectiveProvider[]
      keyIds: number[]
      objs: any[]
      constructor(_providers: ResolvedReflectiveProvider[]) {
        this._providers = _providers

        const len = _providers.length

        this.keyIds = new Array(len)
        this.objs = new Array(len)

        for (let i = 0; i       this.keyIds[i] = _providers[i].key.id
          this.objs[i] = undefined
        }
      }
      // ...
    }

    function resolveReflectiveProvider(provider: Provider): ResolvedReflectiveProvider {
      return new ResolvedReflectiveProvider_(
        ReflectiveKey.get(provider),
        resolveReflectiveFactory(provider)
      )
    }

    function resolveReflectiveFactory(provider: Provider): ResolvedReflectiveFactory {
      let factoryFn: Function
      let resolvedDeps: ReflectiveDependency[]

      factoryFn = factory(provider)
      resolvedDeps = dependenciesFor(provider)

      return new ResolvedReflectiveFactory(factoryFn, resolvedDeps)
    }

    function factory<T>(t: Type): (args: any[]) => T {
      return (...args: any[]) => new t(...args)
    }

    function dependenciesFor(type: Type): ReflectiveDependency[] {
      const params = parameters(type)
      return params.map(extractToken)
    }

    function parameters(type: Type) {
      if (noCtor(type)) return []

      const isInjectable = Reflect.getMetadata('Injectable', type)
      const res = Reflect.getMetadata('design:paramtypes', type)

      if (!isInjectable) throw noAnnotationError(type)

      return res ? res : []
    }

    export const createInjector = (providers: Provider[]): ReflectiveInjector_ => {
      return ReflectiveInjector.resolveAndCreate(providers) as ReflectiveInjector_
    }

    上記のコードから、IoCコンテナが作成されると提供される各クラスおよびそのクラスに依存する他のクラスがResolvedReflectiveProvider_のインスタンスオブジェクトとしてコンテナに格納され、外部に返されるのはコンテナオブジェクトReflectiveInjector_であることがわかります.
    次に、IoCコンテナからクラスのインスタンスオブジェクトを取得するロジックを実装します.
    class ReflectiveInjector_ implements ReflectiveInjector {
      // ...
      get(token: any): any {
        return this._getByKey(ReflectiveKey.get(token))
      }
      private _getByKey(key: ReflectiveKey, isDeps?: boolean) {
        for (let i = 0; i this.keyIds.length; i++) {
          if (this.keyIds[i] === key.id) {
            if (this.objs[i] === undefined) {
              this.objs[i] = this._new(this._providers[i])
            }
            return this.objs[i]
          }
        }

        let res = isDeps ? (key.token as Type).name : key.token

        throw noProviderError(res)
      }
      _new(provider: ResolvedReflectiveProvider) {
        const resolvedReflectiveFactory = provider.resolvedFactory
        const factory = resolvedReflectiveFactory.factory

        let deps = resolvedReflectiveFactory.dependencies.map(dep => this._getByKey(dep.key, true))

        return factory(...deps)
      }
    }
    injector.get()メソッドを呼び出すと、IoCコンテナは、特定のクラスに基づいて対応するReflectiveInjector_オブジェクトを検索し、見つけた後、特定のクラスをインスタンス化する前にクラス依存するすべてのクラスのインスタンスオブジェクトを注入し、最後に特定のクラスのインスタンスオブジェクトを返すことがわかります.
    ここで、上記のコードを振り返ると、マルチレベル依存の機能はすでに実現されています.loCコンテナを初期化すると、クラスの依存関係しか見つかりません.依存クラスによってより深い依存関係を見つけることはできませんが、提供されたクラスごとに同じ操作を実行しているので、自然に複数のクラス間の依存関係が実現されます.
    エッジの場合も対応する処理を行いました.例えば、提供されたproviderクラスは空の配列であり、クラスは@Injectable装飾器によって修飾されておらず、提供されたクラスは不完全であるなどです.対応するコードは次のとおりです.
    let res = isDeps ? (key.token as Type).name : key.token

    throw noProviderError(res)
    if (!isInjectable) throw noAnnotationError(type)

    これにより,制御反転と依存注入のコア機能との差は少なく,残りはインタフェース定義コード,さらにReflectiveKeyクラスの実装であり,その大まかな役割はES 6におけるMapストレージproviderクラスに基づいている.