TypeScript 技巧

前言前端

很早之前就嘗試過使用 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' }
}

條件類型async

type isBool<T> = T extends boolean ? true : false

// type t1 = false
type t1 = isBool<number>

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

字典類型ide

interface Dictionary<T> {
  [index: string]: T
}

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

infer - 延遲推斷類型函數

type ParamType<T> = T extends (param: infer P) => any ? P : T

interface User {
  name: string
  age: number
}

type Func = (user: User) => void

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

type TTuple = [string, number]

type ToUnion = ElementOf<TTuple> // 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<T> = {
  [P in keyof T]?: T[P]
}

type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

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

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

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

Exclude & Omit

type Exclude<T, U> = T extends U ? never : T

// type A = 'a'
type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'>
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

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

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

巧用 never 類型

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]

type NonFunctionPropertyNames<T> = { [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<Part>  // 'updatePart'
type T41 = NonFunctionPropertyNames<Part>  // 'id' | 'name' | 'subparts'

混合類 ( mixins )

// 全部 mixins 都須要
type Constructor<T = {}> = 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 上關於 TypeScript 的面試題,題目的大體內容爲有一個叫作 EffectModule 的類,它的實現以下:

interface Action<T> {
  payload?: T
  type: string
}

class EffectModule {
  count = 1
  message = 'hello!'
  delay(input: Promise<number>) {
    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<string>
  setMessage(action: Date): Action<number>
}

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

能夠看到在調用 connect 函數以後,返回的新對象只包含 EffectModule 的同名方法,而且方法的類型簽名改變了:

asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>  變成了
asyncMethod<T, U>(input: T): Action<U> 
syncMethod<T, U>(action: Action<T>): Action<U>  變成了
syncMethod<T, U>(action: T): Action<U>

如今就須要咱們來編寫 type Connect = (module: EffectModule) => any 使得最終的編譯可以順利經過。不難看出,這個題目主要考察兩點:

  • 從類中挑選出函數

  • 巧用 infer 進行類型轉換

下面是我解這道題的答案:

type FuncName<T> = { [P in keyof T]: T[P] extends Function ? P : never }[keyof T]

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

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

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

控制反轉與依賴注入

控制反轉 ( Inversion of Control ) 與依賴注入 ( Dependency Injection ) 是面向對象編程中十分重要的思想和法則。維基百科上給出的解釋是 IoC 可以下降計算機代碼之間的耦合度,DI 表明的則是在一個對象被建立時,注入該對象所依賴的全部對象的過程。前端框架 Angular 與基於 Node.js 的後端框架 Nest 都引用了這一思想。對於這兩個概念的具體闡述在這裏就再也不展開,但讀者能夠看看這兩篇文章 [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!')
  })
})

能夠看到咱們要實現的核心功能有三個:

  • 根據提供的類建立 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 < len; 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<T>): (args: any[]) => T {
  return (...args: any[]) => new t(...args)
}

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

function parameters(type: Type<any>) {
  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 類的實現,它的大體做用其實就是基於 ES6 中的 Map 存儲 provider 類。

相關文章
相關標籤/搜索