前言

很早以前就尝试过使用 TypeScript 来进行日常编码,但自己对静态类型语言的了解并不深入,再加上 TypeScript 的类型系统有着一定的复杂度,因此感觉自己并没有发挥好这门语言的优势,使代码变得更具可读性可维护性。于是这几天便想着好好研究下这门语言,希望能够总结出一些特别的语言特性与实用技巧。

操作符

typeof - 获取变量的类型

  1. const colors = {
      red: 'red',
      blue: 'blue'
    }
  2. // type res = { red: string; blue: string }
    type res = typeof colors

keyof - 获取类型的键

  1. const data = {
      a: 3,
      hello: 'world'
    }
  2. // 类型保护
    function get<extends object, K extends keyof T>(o: T, name: K): T[K] {
      return o[name]
    }
  3. get(data, 'a') // 3
    get(data, 'b') // Error

组合 typeof 与 keyof - 捕获键的名称

  1. const colors = {
      red: 'red',
      blue: 'blue'
    }
  2. type Colors = keyof typeof colors
  3. let color: Colors // 'red' | 'blue'
    color = 'red' // ok
    color = 'blue' // ok
    color = 'anythingElse' // Error

in - 遍历键名

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

特殊类型

嵌套接口类型

  1. interface Producer {
      name: string
      cost: number
      production: number
    }
  2. interface Province {
      name: string
      demand: number
      price: number
      producers: Producer[]
    }
  3. 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 }
      ]
    }
  1. interface Play {
      name: string
      type: string
    }
  2. interface Plays {
      [key: string]: Play
    }
  3. let plays: Plays = {
      'hamlet': { name: 'Hamlet', type: 'tragedy' },
      'as-like': { name: 'As You Like It', type: 'comedy' },
      'othello': { name: 'Othello', type: 'tragedy' }
    }

条件类型

  1. type isBool<T> = T extends boolean ? true : false
  2. // type t1 = false
    type t1 = isBool<number>
  3. // type t2 = true
    type t2 = isBool<false>

字典类型

  1. interface Dictionary<T> {
      [index: string]: T
    }
  2. const data: Dictionary<number> = {
      a: 3,
      b: 4,
    }

infer - 延迟推断类型

  1. type ParamType<T> = T extends (param: infer P) => any ? P : T
  2. interface User {
      name: string
      age: number
    }
  3. type Func = (user: User) => void
  4. type Param = ParamType<Func> // Param = User
    type AA = ParamType<string> // string
  1. type ElementOf<T> = T extends Array<infer E> ? E : never
  2. type TTuple = [string, number]
  3. type ToUnion = ElementOf<TTuple> // string | number

常用技巧

使用 const enum 维护常量列表

  1. const enum STATUS {
      TODO = 'TODO',
      DONE = 'DONE',
      DOING = 'DOING'
    }
  2. function todos(status: STATUS): Todo[] {
      // ...
    }
  3. todos(STATUS.TODO)

Partial & Pick

  1. type Partial<T> = {
      [in keyof T]?: T[P]
    }
  2. type Pick<T, K extends keyof T> = {
      [in K]: T[P]
    }
  3. interface User {
      id: number
      age: number
      name: string
    }
  4. // type PartialUser = { id?: number; age?: number; name?: string }
    type PartialUser = Partial<User>
  5. // type PickUser = { id: number; age: number }
    type PickUser = Pick<User, 'id'|'age'>

Exclude & Omit

  1. type Exclude<T, U> = T extends U ? never : T
  2. // type A = 'a'
    type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'>
  1. type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
  2. interface User {
      id: number
      age: number
      name: string
    }
  3. // type PickUser = { age: number; name: string }
    type OmitUser = Omit<User, 'id'>

巧用 never 类型

  1. type FunctionPropertyNames<T> = { [in keyof T]: T[K] extends Function ? K : never }[keyof T]
  2. type NonFunctionPropertyNames<T> = { [in keyof T]: T[K] extends Function ? never : K }[keyof T]
  3. interface Part {
      id: number
      name: string
      subparts: Part[]
      updatePart(newName: string): void
    }
  4. type T40 = FunctionPropertyNames<Part>  // 'updatePart'
    type T41 = NonFunctionPropertyNames<Part>  // 'id' | 'name' | 'subparts'

混合类 ( mixins )

  1. // 所有 mixins 都需要
    type Constructor<= {}> = new (...args: any[]) => T
  2. // 添加属性的混合例子
    function TimesTamped<TBase extends Constructor>(Base: TBase) {
      return class extends Base {
        timestamp = Date.now()
      }
    }
  3. // 添加属性和方法的混合例子
    function Activatable<TBase extends Constructor>(Base: TBase) {
      return class extends Base {
        isActivated = false
        activate() {
          this.isActivated = true
        }
        deactivate() {
          this.isActivated = false
        }
      }
    }
  4. // 简单的类
    class User {
      name = ''
    }
  5. // 添加 TimesTamped 的 User
    const TimestampedUser = TimesTamped(User)
  6. // 添加 TimesTamped 和 Activatable 的类
    const TimestampedActivatableUser = TimesTamped(Activatable(User))
  7. // 使用组合类
    const timestampedUserExample = new TimestampedUser()
    console.log(timestampedUserExample.timestamp)
  8. const timestampedActivatableUserExample = new TimestampedActivatableUser()
    console.log(timestampedActivatableUserExample.timestamp)
    console.log(timestampedActivatableUserExample.isActivated)

类型转换

下面来求解一道 LeetCode 上关于 TypeScript 的面试题,题目的大致内容为有一个叫做 EffectModule 的类,它的实现如下:

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

现在有一个 connect 函数接收 EffectModule 类的实例对象作为参数,且该函数会返回新的对象,相关的实现如下:

  1. const connect: Connect = _m => ({
      delay: (input: number) => ({
        type: 'delay',
        payload: `hello ${input}`
      }),
      setMessage: (input: Date) => ({
        type: 'set-message',
        payload: input.getMilliseconds()
      })
    })
  2. type Connected = {
      delay(input: number): Action<string>
      setMessage(action: Date): Action<number>
    }
  3. const connected: Connected = connect(new EffectModule())

可以看到在调用 connect 函数之后,返回的新对象只包含 EffectModule 的同名方法,并且方法的类型签名改变了:

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

现在就需要我们来编写 type Connect = (module: EffectModule) => any 使得最终的编译能够顺利通过。不难看出,这个题目主要考察两点:

  • 从类中挑选出函数

  • 巧用 infer 进行类型转换

下面是我解这道题的答案:

  1. type FuncName<T> = { [in keyof T]: T[P] extends Function ? P : never }[keyof T]
  2. type Middle = { [in FuncName<EffectModule>]: EffectModule[T] }
  3. type Transfer<T> = {
      [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
    }
  4. type Connect = (module: EffectModule) => { [in keyof Transfer<Middle>]: Transfer<Middle>[T] }

控制反转与依赖注入

控制反转 ( Inversion of Control ) 与依赖注入 ( Dependency Injection ) 是面向对象编程中十分重要的思想和法则。维基百科上给出的解释是 IoC 能够降低计算机代码之间的耦合度,DI 代表的则是在一个对象被创建时,注入该对象所依赖的所有对象的过程。前端框架 Angular 与基于 Node.js 的后端框架 Nest 都引用了这一思想。对于这两个概念的具体阐述在这里就不再展开,但读者可以看看这两篇文章 [1] [2] 。下面我们基于 Angular 5 以前的 Dependency Injection 来实现简版的控制反转与依赖注入。

首先让我们来编写一段相关的测试代码:

  1. import { expect } from 'chai'
    import { Injectable, createInjector } from './injection'
  2. class Engine {}
  3. class DashboardSoftware {}
  4. @Injectable()
    class Dashboard {
      constructor(public software: DashboardSoftware) {}
    }
  5. @Injectable()
    class Car {
      constructor(public engine: Engine) {}
    }
  6. @Injectable()
    class CarWithDashboard {
      constructor(public engine: Engine, public dashboard: Dashboard) {}
    }
  7. class NoAnnotations {
      constructor(_secretDependency: any) {}
    }
  8. 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
      })
  9.   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
      })
  10.   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
      })
  11.   it('should cache instances', () => {
        const injector = createInjector([Engine])
        const e1 = injector.get(Engine)
        const e2 = injector.get(Engine)
        expect(e1).to.equal(e2)
      })
  12.   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!')
      })
  13.   it('should throw when no type', () => {
        expect(() => createInjector([NoAnnotations])).to.throw(
          'Make sure that NoAnnotations is decorated with Injectable.'
        )
      })
  14.   it('should throw when no provider defined', () => {
        const injector = createInjector([])
        expect(() => injector.get('NonExisting')).to.throw('No provider for NonExisting!')
      })
    })

可以看到我们要实现的核心功能有三个:

  • 根据提供的类创建 IoC 容器并且能够管理类之间的依赖关系

  • 在通过 IoC 容器获取类的实例对象时注入相关的依赖对象

  • 实现多级依赖与处理边缘情况

首先来实现最简单的 @Injectable 装饰器:

  1. export const Injectable = (): ClassDecorator => target => {
      Reflect.defineMetadata('Injectable', true, target)
    }

然后我们来实现根据提供的 provider 类创建能够管理类之间依赖关系的 IoC 容器:

  1. 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)
      }
    }
  2. class ReflectiveInjector_ implements ReflectiveInjector {
      _providers: ResolvedReflectiveProvider[]
      keyIds: number[]
      objs: any[]
      constructor(_providers: ResolvedReflectiveProvider[]) {
        this._providers = _providers
  3.     const len = _providers.length
  4.     this.keyIds = new Array(len)
        this.objs = new Array(len)
  5.     for (let i = 0; i < len; i++) {
          this.keyIds[i] = _providers[i].key.id
          this.objs[i] = undefined
        }
      }
      // ...
    }
  6. function resolveReflectiveProvider(provider: Provider): ResolvedReflectiveProvider {
      return new ResolvedReflectiveProvider_(
        ReflectiveKey.get(provider),
        resolveReflectiveFactory(provider)
      )
    }
  7. function resolveReflectiveFactory(provider: Provider): ResolvedReflectiveFactory {
      let factoryFn: Function
      let resolvedDeps: ReflectiveDependency[]
  8.   factoryFn = factory(provider)
      resolvedDeps = dependenciesFor(provider)
  9.   return new ResolvedReflectiveFactory(factoryFn, resolvedDeps)
    }
  10. function factory<T>(t: Type<T>): (args: any[]) => T {
      return (...args: any[]) => new t(...args)
    }
  11. function dependenciesFor(type: Type<any>): ReflectiveDependency[] {
      const params = parameters(type)
      return params.map(extractToken)
    }
  12. function parameters(type: Type<any>) {
      if (noCtor(type)) return []
  13.   const isInjectable = Reflect.getMetadata('Injectable', type)
      const res = Reflect.getMetadata('design:paramtypes', type)
  14.   if (!isInjectable) throw noAnnotationError(type)
  15.   return res ? res : []
    }
  16. export const createInjector = (providers: Provider[]): ReflectiveInjector_ => {
      return ReflectiveInjector.resolveAndCreate(providers) as ReflectiveInjector_
    }

从上面的代码不难看出当 IoC 容器创建时会将提供的每个类以及该类所依赖的其他类作为 ResolvedReflectiveProvider_ 的实例对象存储在容器中,对外返回的则是容器对象 ReflectiveInjector_ 。

接下来让我们来实现通过 IoC 容器获取类的实例对象的逻辑:

  1. 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]
          }
        }
  2.     let res = isDeps ? (key.token as Type).name : key.token
  3.     throw noProviderError(res)
      }
      _new(provider: ResolvedReflectiveProvider) {
        const resolvedReflectiveFactory = provider.resolvedFactory
        const factory = resolvedReflectiveFactory.factory
  4.     let deps = resolvedReflectiveFactory.dependencies.map(dep => this._getByKey(dep.key, true))
  5.     return factory(...deps)
      }
    }

可以看到当我们调用 injector.get() 方法时 IoC 容器会根据给定类查找对应的 ReflectiveInjector_ 对象,找到之后便会在实例化给定类之前注入该类依赖的所有类的实例对象,最后再返回给定类的实例化对象。

现在我们再回头看上文的代码,多级依赖的功能其实早已实现。虽然在初始化 loC 容器时我们只能找到某个类的相关依赖,无法再通过依赖类找到更深层级的依赖,但是我们对提供的每个类遍历执行了相同的操作,因此很自然的就实现了多个类之间的依赖。

对于边缘情况我们也做了相应的处理,比如提供的 provider 类为空数组,类并没有被 @Injectable 装饰器修饰,提供的类并不完整等。对应上文的代码为:

  1. let res = isDeps ? (key.token as Type).name : key.token
  2. throw noProviderError(res)
  1. if (!isInjectable) throw noAnnotationError(type)

至此,控制反转与依赖注入的核心功能就实现的差不多了,剩下的就是一些接口定义代码,还有就是 ReflectiveKey 类的实现,它的大致作用其实就是基于 ES6 中的 Map 存储 provider 类。

TypeScript 技巧的更多相关文章

  1. TypeScript技巧集锦(陆续更新)

    在C++项目中编译TypeScript(以下简称ts) 编辑ts文件的属性,项类型选择"自定义生产工具". 命令行输入tsc所在位置与编译参数,我的是"C:\Progra ...

  2. TypeScript 异步代码类型技巧

    在typescript下编写异步代码,会遇到难以自动识别异步返回值类型的情况,本文介绍一些技巧,以辅助编写更健全的异步代码. callback 以读取文件为例: readFile是一个异步函数,包含p ...

  3. 三千字讲清TypeScript与React的实战技巧

    很多时候虽然我们了解了TypeScript相关的基础知识,但是这不足以保证我们在实际项目中可以灵活运用,比如现在绝大部分前端开发者的项目都是依赖于框架的,因此我们需要来讲一下React与TypeScr ...

  4. 四两拨千斤——你不知道的VScode编码TypeScript的技巧

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文参考:https://blog.bitsrc 如果你体验过JAVA这种强类型语言带来的便利,包括其丰富的 ...

  5. React + Typescript领域初学者的常见问题和技巧

    React + Typescript领域初学者的常见问题和技巧 创建一个联合类型的常量 Key const NAME = { HOGE: "hoge", FUGA: "f ...

  6. TypeScript和JavaScript的一些小技巧记录

    项目里使用到的技巧,记录一下,会持续更新. JS的技巧完全可以使用到TS上哦. JS 向下取整 Math.floor(4.5); 简写: var num = 4.5; ~~num; num <& ...

  7. TypeScript专题-Static和使用技巧

    class People { static _name: string; print() { //alert(this.name);// 编译不通过,doex not exist on type Pe ...

  8. 转职成为TypeScript程序员的参考手册

    写在前面 作者并没有任何可以作为背书的履历来证明自己写作这份手册的分量. 其内容大都来自于TypeScript官方资料或者搜索引擎获得,期间掺杂少量作者的私见,并会标明. 大部分内容来自于http:/ ...

  9. WebStorm 常用功能的使用技巧分享

    WebStorm 是 JetBrain 公司开发的一款 JavaScript IDE,使用非常方便,可以使编写代码过程更加流畅. 本文在这里分享一些常用功能的使用技巧,希望能帮助大家更好的使用这款强大 ...

随机推荐

  1. CDN内容分发

    什么是CDN内容分发: CDN的全称是Content Delivery Network,即内容分发网络.CDN是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡.内容分 ...

  2. .js文件中文乱码解决办法

    描述:.js文件里的中文内容在网页中显示乱码 解决办法:把JS文件的编码改为utf-8 VS2013解决步骤:文件——高级保存选项——Unicode (UTF-8带签名)  代码页 65001

  3. Install CUDA 6.0 on Ubuntu 14.04 LTS

    Ubuntu 14.04 LTS is out, loads of new features have been added. Here are some procedures I followed ...

  4. VBA精彩代码分享-2

    VBA开发中经常需要提示消息框,如果不关闭程序就会暂时中断,这里分享下VBA如何实现消息框的自动关闭,总共有三种方法: 第一种方法 Public Declare Function MsgBoxTime ...

  5. 使用 pdb 进行调试

    使用 pdb 进行调试 pdb 是 python 自带的一个包,为 python 程序提供了一种交互的源代码调试功能,主要特性包括设置断点.单步调试.进入函数调试.查看当前代码.查看栈片段.动态改变变 ...

  6. vue-动态路由+动态组件+动态页面

    动态路由 路由组件是vue-router 动态路由即从后端请求路由信息,然后转化生成路由信息.所以这里的关键是不会提前知道什么菜单对应什么组件,因此路由声明的时候不再是写死的组件,而是可替换的动态路径 ...

  7. SpringBoot + Dubbo + zookeeper 搭建简单分布式服务

    SpringBoot + Dubbo + zookeeper 搭建简单分布式服务 详细操作及源码见: https://github.com/BillyYangOne/dubbo-springboot

  8. java EE,java Web中的400,404,405等各种错误介绍

    4 请求失败4xx 4xx应答定义了特定服务器响应的请求失败的情况.客户端不应当在不更改请求的情况下重新尝试同一个请求.(例如,增加合适的认证信息).不过,同一个请求交给不同服务器也许就会成功. 4. ...

  9. Spring Cloud(一)服务的注册与发现(Eureka)

    Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中涉及的配置管理.服务发现.断路器.智能路由.微代理.控制总线.全局锁.决策竞选.分布式会话和集 ...

  10. axios替换jquery的ajax

    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script> <scr ...