女王控的博客

TypeScript 入门学习

背景知识

TypeScript Handbook 入门教程 深入理解 TypeScript

工具泛型的实现

Partial

Partial 作用是将传入的属性变为可选项

前置知识

首先我们需要理解两个关键字 keyof 和 in,keyof 可以用来取得一个对象接口的所有 key 值,比如

interface Foo {
  name: string;
  age: number;
}
type T = keyof Foo // -> "name" | "age"

而 in 则可以遍历枚举类型, 例如

type Keys = "a" | "b"
type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any }

源码解析

keyof 产生联合类型,in 则可以遍历枚举类型,所以他们经常一起使用,看下 Partial 源码

type Partial<T> = { 
  [P in keyof T]?: T[P] 
};

上面语句的意思是 keyof T 拿到 T 所有属性名,然后 in 进行遍历,将值赋给 P,最后 T[P] 取得相应属性的值

结合中间的 ? 我们就明白了 Partial 的含义了

使用场景

假设我们有一个定义 user 的接口,如下

interface IUser {
  name: string
  age: number
  department: string
}

经过 Partial 类型转化后得到

type optional = Partial<IUser>

// optional的结果如下
type optional = {
  name?: string | undefined;
  age?: number | undefined;
  department?: string | undefined;
}

Required

Required 的作用是将传入的属性变为必选项,源码如下

type Required<T> = { 
  [P in keyof T]-?: T[P] 
};

我们发现一个有意思的用法 -?,这里很好理解就是将可选项代表的 ? 去掉,从而让这个类型变成必选项。

与之对应的还有个+? ,这个含义自然与-?之前相反,它是用来把属性变成可选项的.

Mutable (未包含)

类似地,其实还有对 + 和 -,这里要说的不是变量的之间的进行加减而是对 readonly 进行加减。

以下代码的作用就是将 T 的所有属性的 readonly 移除,你也可以写一个相反的出来.

type Mutable<T> = {
  -readonly [P in keyof T]: T[P]
}

Readonly

将传入的属性变为只读选项, 源码如下

type Readonly<T> = { 
  readonly [P in keyof T]: T[P] 
};

Record

前置知识

此处注意 K extends keyof T 和直接使用 K in keyof T 的区别,keyof T 仅仅代表键的字符串文字类型, 而 extends keyof T 将返回该属性相同的类型

function prop<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

function prop2<T>(obj: T, key: keyof T) {
    return obj[key];
}

let o = {
    p1: 0,
    p2: ''
}

let v = prop(o, 'p1') // is number, K is of type 'p1'
let v2 = prop2(o, 'p1') // is number | string, no extra info is captured

源码解析

将 K 中所有的属性的值转化为 T 类型

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

Pick

从 T 中取出一系列 K 的属性

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

Exclude

前置知识

在 ts 2.8 中引入了一个条件类型,示例如下

T extends U ? X : Y

以上语句的意思就是如果 T 是 U 的子类型的话,那么就会返回 X,否则返回 Y,甚至可以组合多个

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

对于联合类型来说会自动分发条件,例如 T extends U ? X : Y,T 可能是 A | B 的联合类型,那实际情况就变成(A extends U ? X : Y) | (B extends U ? X : Y)

源码解析

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

结合实例

type T = Exclude<1 | 2, 1 | 3> // -> 2

很轻松地得出结果 2,据代码和示例我们可以推断出 Exclude 的作用是从 T 中找出 U 中没有的元素,换种更加贴近语义的说法其实就是从 T 中排除 U

Extract

根据源码我们推断出 Extract 的作用是提取出 T 包含在 U 中的元素,换种更加贴近语义的说法就是从 T 中提取出 U

源码如下

type Extract<T, U> = T extends U ? T : never;

Omit

用之前的 Pick 和 Exclude 进行组合,实现忽略对象某些属性功能,源码如下

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>

// 使用
type Foo = Omit<{name: string, age: number}, 'name'> // -> { age: number }

ReturnType

在阅读源码之前我们需要了解一下 infer 这个关键字,在条件类型语句中,我们可以用 infer 声明一个类型变量并且对它进行使用,我们可以用它获取函数的返回类型,源码如下

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R
  ? R
  : any;

其实这里的 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用

function foo(x: number): Array<number> {
  return [x];
}
type fn = ReturnType<typeof foo>;

AxiosReturnType (未包含)

开发经常使用 axios 进行封装 API 层请求,通常是一个函数返回一个 AxiosPromise<Resp>,现在我想取到它的 Resp 类型,根据上一个工具泛型的知识我们可以这样写

import { AxiosPromise } from 'axios' // 导入接口
type AxiosReturnType<T> = T extends (...args: any[]) => AxiosPromise<infer R> ? R : any

// 使用
type Resp = AxiosReturnType<Api> // 泛型参数中传入你的 Api 请求函数

巧用 TypeScript

函数重载

TypeScript 提供函数重载的功能,用来处理因函数参数不同而返回类型不同的使用场景,使用时只需为同一个函数定义多个类型即可,简单使用如下所示:

declare function test(a: number): number;
declare function test(a: string): string;

const resS = test('Hello World');  // resS 被推断出类型为 string;
const resN = test(1234);           // resN 被推断出类型为 number;

它也适用于参数不同,返回值类型相同的场景,我们只需要知道在哪种函数类型定义下能使用哪些参数即可。

考虑如下例子:

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

declare function test(para: User | number, flag?: boolean): number;

在这个 test 函数里,我们的本意可能是当传入参数 para 是 User 时,不传 flag,当传入 para 是 number 时,传入 flag。TypeScript 并不知道这些,当你传入 para 为 User 时,flag 同样允许你传入:

const user = {
  name: 'Jack',
  age: 666
}

// 没有报错,但是与想法违背
const res = test(user, false);

使用函数重载能帮助我们实现:

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

declare function test(para: User): number;
declare function test(para: number, flag: boolean): number;

const user = {
  name: 'Jack',
  age: 666
};

// bingo
// Error: 参数不匹配
const res = test(user, false);

实际项目中,你可能要多写几步,如在 class 中:

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

const user = {
  name: 'Jack',
  age: 123
};

class SomeClass {

  /**
   * 注释 1
   */
  public test(para: User): number;
  /**
   * 注释 2
   */
  public test(para: number, flag: boolean): number;
  public test(para: User | number, flag?: boolean): number {
    // 具体实现
    return 11;
  }
}

const someClass = new SomeClass();

// ok
someClass.test(user);
someClass.test(123, false);

// Error,涉及到具体实现时,这个地方报错
someClass.test(123);
someClass.test(user, false);

映射类型

自从 TypeScript 2.1 版本推出映射类型以来,它便不断被完善与增强。在 2.1 版本中,可以通过 keyof 拿到对象 key 类型,内置 Partial、Readonly、Record、Pick 映射类型;2.3 版本增加 ThisType;2.8 版本增加 Exclude、Extract、NonNullable、ReturnType、InstanceType;同时在此版本中增加条件类型与增强 keyof 的能力;3.1 版本支持对元组与数组的映射。这些无不意味着映射类型在 TypeScript 有着举足轻重的地位。

其中 ThisType 并没有出现在官方文档中,它主要用来在对象字面量中键入 this:

// Compile with --noImplicitThis

type ObjectDescriptor<D, M> = {
  data?: D;
  methods?: M & ThisType<D & M>;  // Type of 'this' in methods is D & M
}

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  let data: object = desc.data || {};
  let methods: object = desc.methods || {};
  return { ...data, ...methods } as D & M;
}

let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx;  // Strongly typed this
      this.y += dy;  // Strongly typed this
    }
  }
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

正是由于 ThisType 的出现,Vue 2.5 才得以增强对 TypeScript 的支持。

虽已内置了很多映射类型,但在很多时候,我们需要根据自己的项目自定义映射类型:

比如你可能想取出接口类型中的函数类型:

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

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

type T40 = FunctionPropertyNames<Part>;  // "updatePart"
type T42 = FunctionProperties<Part>;     // { updatePart(newName: string): void }

比如你可能为了便捷,把本属于某个属性下的方法,通过一些方式 alias 到其他地方

举个例子:SomeClass 下有个属性 value = [1, 2, 3],你可能在 Decorators 给类添加了此种功能:在 SomeClass 里调用 this.find() 时,实际上是调用 this.value.find(),但是此时 TypeScript 并不知道这些:

class SomeClass {
  value = [1, 2, 3];

  someMethod() {
    this.value.find(/* ... */);  // ok
    this.find(/* ... */);        // Error:SomeClass 没有 find 方法。
  }
}

借助于映射类型和 interface + class 的声明方式,可以实现我们的目的:

type ArrayMethodName = 'filter' | 'forEach' | 'find';

type SelectArrayMethod<T> = {
 [K in ArrayMethodName]: Array<T>[K]
}

interface SomeClass extends SelectArrayMethod<number> {}

class SomeClass {
 value = [1, 2, 3];

 someMethod() {
   this.forEach(/* ... */)        // ok
   this.find(/* ... */)           // ok
   this.filter(/* ... */)         // ok
   this.value                     // ok
   this.someMethod()              // ok
 }
}

const someClass = new SomeClass();
someClass.forEach(/* ... */)        // ok
someClass.find(/* ... */)           // ok
someClass.filter(/* ... */)         // ok
someClass.value                     // ok
someClass.someMethod()              // ok

导出 SomeClass 类时,也能使用。

可能有点不足的地方,在这段代码里 interface SomeClass extends SelectArrayMethod<number> {} 你需要手动添加范型的具体类型(暂时没想到更好方式)。

类型断言

类型断言用来明确的告诉 TypeScript 值的详细类型,合理使用能减少我们的工作量。

比如一个变量并没有初始值,但是我们知道它的类型信息(它可能是从后端返回)有什么办法既能正确推导类型信息,又能正常运行了?有一种网上的推荐方式是设置初始值,然后使用 typeof 拿到类型(可能会给其他地方用)。然而我可能比较懒,不喜欢设置初始值,这时候使用类型断言可以解决这类问题:

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

export default class NewRoom extends Vue {
  private user = {} as User;
}

在设置初始化时,添加断言,我们就无须添加初始值,编辑器也能正常的给予代码提示了。如果 user 属性很多,这样就能解决大量不必要的工作了,定义的 interface 也能给其他地方使用。

枚举类型

枚举类型分为数字类型与字符串类型,其中数字类型的枚举可以当标志使用:

// https://github.com/Microsoft/TypeScript/blob/master/src/compiler/types.ts#L3859
export const enum ObjectFlags {
  Class            = 1 << 0,  // Class
  Interface        = 1 << 1,  // Interface
  Reference        = 1 << 2,  // Generic type reference
  Tuple            = 1 << 3,  // Synthesized generic tuple type
  Anonymous        = 1 << 4,  // Anonymous
  Mapped           = 1 << 5,  // Mapped
  Instantiated     = 1 << 6,  // Instantiated anonymous or mapped type
  ObjectLiteral    = 1 << 7,  // Originates in an object literal
  EvolvingArray    = 1 << 8,  // Evolving array type
  ObjectLiteralPatternWithComputedProperties = 1 << 9,  // Object literal pattern with computed properties
  ContainsSpread   = 1 << 10, // Object literal contains spread operation
  ReverseMapped    = 1 << 11, // Object contains a property from a reverse-mapped type
  JsxAttributes    = 1 << 12, // Jsx attributes type
  MarkerType       = 1 << 13, // Marker type used for variance probing
  JSLiteral        = 1 << 14, // Object type declared in JS - disables errors on read/write of nonexisting members
  ClassOrInterface = Class | Interface
}

在 TypeScript src/compiler/types 源码里,定义了大量如上所示的基于数字类型的常量枚举。它们是一种有效存储和表示布尔值集合的方法。

在 《深入理解 TypeScript》 中有一个使用例子:

enum AnimalFlags {
  None        = 0,
  HasClaws    = 1 << 0,
  CanFly      = 1 << 1,
  HasClawsOrCanFly = HasClaws | CanFly
}

interface Animal {
  flags: AnimalFlags;
  [key: string]: any;
}

function printAnimalAbilities(animal: Animal) {
  var animalFlags = animal.flags;
  if (animalFlags & AnimalFlags.HasClaws) {
    console.log('animal has claws');
  }
  if (animalFlags & AnimalFlags.CanFly) {
    console.log('animal can fly');
  }
  if (animalFlags == AnimalFlags.None) {
    console.log('nothing');
  }
}

var animal = { flags: AnimalFlags.None };
printAnimalAbilities(animal); // nothing
animal.flags |= AnimalFlags.HasClaws;
printAnimalAbilities(animal); // animal has claws
animal.flags &= ~AnimalFlags.HasClaws;
printAnimalAbilities(animal); // nothing
animal.flags |= AnimalFlags.HasClaws | AnimalFlags.CanFly;
printAnimalAbilities(animal); // animal has claws, animal can fly

上例代码中 |= 用来添加一个标志,&= 和 ~ 用来删除标志,| 用来合并标志。

Decorator

Decorator 早已不是什么新鲜事物,在 TypeScript 1.5 + 的版本中,我们可以利用内置类型 ClassDecorator、PropertyDecorator、MethodDecorator 与 ParameterDecorator 更快书写 Decorator,如 MethodDecorator:

declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

使用时,只需在相应地方加上类型注解,匿名函数的参数类型也就会被自动推导出来了。

function methodDecorator(): MethodDecorator {
  return (target, key, descriptor) => {
    // ...
  };
}

值得一提的是,如果你在 Decorator 给目标类的 prototype 添加属性时,TypeScript 并不知道这些:

function testAble(): ClassDecorator {
  return target => {
    target.prototype.someValue = true
  }
}

@testAble()
class SomeClass {}

const someClass = new SomeClass()

someClass.someValue() // Error: Property 'someValue' does not exist on type 'SomeClass'.

这很常见,特别是当你想用 Decorator 来扩展一个类时。

GitHub 上有一个关于此问题的 issues,直至目前,也没有一个合适的方案实现它。其主要问题在于 TypeScript 并不知道目标类是否使用了 Decorator 以及 Decorator 的名称。从这个 issues 来看,建议的解决办法是使用 Mixin:

type Constructor<T> = new(...args: any[]) => T

// mixin 函数的声明,需要实现
declare function mixin<T1, T2>(...MixIns: [Constructor<T1>, Constructor<T2>]): Constructor<T1 & T2>;

class MixInClass1 {
    mixinMethod1() {}
}

class MixInClass2 {
    mixinMethod2() {}
}

class Base extends mixin(MixInClass1, MixInClass2) {
    baseMethod() { }
}

const x = new Base();

x.baseMethod(); // OK
x.mixinMethod1(); // OK
x.mixinMethod2(); // OK
x.mixinMethod3(); // Error

当把大量的 JavaScript Decorator 重构为 Mixin 时,这无疑是一件让人头大的事情。

这有一些偏方,能让你顺利从 JavaScript 迁移至 TypeScript:

  • 显式赋值断言修饰符,即是在类里,明确说明某些属性存在于类上:

    function testAble(): ClassDecorator {
      return target => {
        target.prototype.someValue = true
      }
    }
    
    @testAble()
    class SomeClass {
      public someValue!: boolean;
    }
    
    const someClass = new SomeClass();
    someClass.someValue // true
  • 采用声明合并形式,单独定义一个 interface,把用 Decorator 扩展的属性的类型,放入 interface 中:

    interface SomeClass {
      someValue: boolean;
    }
    
    function testAble(): ClassDecorator {
      return target => {
        target.prototype.someValue = true
      }
    }
    
    @testAble()
    class SomeClass {}
    
    const someClass = new SomeClass();
    someClass.someValue // true

Reflect Metadata

Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它,你只需要:

  • npm i reflect-metadata —save。
  • 在 tsconfig.json 里配置 emitDecoratorMetadata 选项。

它具有诸多使用场景。

获取类型信息

譬如在 vue-property-decorator 6.1 及其以下版本中,通过使用 Reflect.getMetadata API,Prop Decorator 能获取属性类型传至 Vue,简要代码如下:

function Prop(): PropertyDecorator {
  return (target, key: string) => {
    const type = Reflect.getMetadata('design:type', target, key);
    console.log(`${key} type: ${type.name}`);
    // other...
  }
}

class SomeClass {
  @Prop()
  public Aprop!: string;
};

运行代码可在控制台看到 Aprop type: string。除能获取属性类型外,通过 Reflect.getMetadata(“design:paramtypes”, target, key) 和 Reflect.getMetadata(“design:returntype”, target, key) 可以分别获取函数参数类型和返回值类型。

自定义 metadataKey

除能获取类型信息外,常用于自定义 metadataKey,并在合适的时机获取它的值,示例如下:

function classDecorator(): ClassDecorator {
  return target => {
    // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
    Reflect.defineMetadata('classMetaData', 'a', target);
  }
}

function methodDecorator(): MethodDecorator {
  return (target, key, descriptor) => {
    // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
    Reflect.defineMetadata('methodMetaData', 'b', target, key);
  }
}

@classDecorator()
class SomeClass {

  @methodDecorator()
  someMethod() {}
};

Reflect.getMetadata('classMetaData', SomeClass);                         // 'a'
Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod');    // 'b'

用例

控制反转和依赖注入

在 Angular 2+ 的版本中,控制反转与依赖注入便是基于此实现,现在,我们来实现一个简单版:

type Constructor<T = any> = new (...args: any[]) => T;

const Injectable = (): ClassDecorator => target => {}

class OtherService {
  a = 1
}

@Injectable()
class TestService {
  constructor(public readonly otherService: OtherService) {}

  testMethod() {
    console.log(this.otherService.a);
  }
}

const Factory = <T>(target: Constructor<T>): T  => {
  // 获取所有注入的服务
  const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
}

Factory(TestService).testMethod()   // 1

Controller 与 Get 的实现

如果你在使用 TypeScript 开发 Node 应用,相信你对 Controller、Get、POST 这些 Decorator,并不陌生:

@Controller('/test')
class SomeClass {

  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

  @Post('/b')
  somePostMethod() {}
};

它们也是基于 Reflect Metadata 实现,不同的是,这次我们将 metadataKey 定义在 descriptor 的 value 上(稍后解释),简单实现如下:

const METHOD_METADATA = 'method'const PATH_METADATA = 'path'const Controller = (path: string): ClassDecorator => {
  return target => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
  }
}

const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
  }
}

const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');

接着,创建一个函数,映射出 route:

function mapRoute(instance: Object) {
  const prototype = Object.getPrototypeOf(instance);
  
  // 筛选出类的 methodName
  const methodsNames = Object.getOwnPropertyNames(prototype)
                              .filter(item => !isConstructor(item) && isFunction(prototype[item]));
  return methodsNames.map(methodName => {
    const fn = prototype[methodName];

    // 取出定义的 metadata
    const route = Reflect.getMetadata(PATH_METADATA, fn);
    const method = Reflect.getMetadata(METHOD_METADATA, fn);
    return {
      route,
      method,
      fn,
      methodName
    }
  })
};

我们可以得到一些有用的信息:

Reflect.getMetadata(PATH_METADATA, SomeClass);  // '/test'

mapRoute(new SomeClass())

/**
 * [{
 *    route: '/a',
 *    method: 'GET',
 *    fn: someGetMethod() { ... },
 *    methodName: 'someGetMethod'
 *  },{
 *    route: '/b',
 *    method: 'POST',
 *    fn: somePostMethod() { ... },
 *    methodName: 'somePostMethod'
 * }]
 * 
 */

最后,只需把 route 相关信息绑在 express 或者 koa 上就 ok 了。

至于为什么要定义在 descriptor 的 value 上,我们希望 mapRoute 函数的参数是一个实例,而非 class 本身(控制反转)。

数组与元组

创建一个数组很简单:

const arr = [1];

此时 TypeScript 将会推断 arr 类型为 number[]:

arr.push('1');  // Error

当数组元素具有其它类型时,可以通过类型注解的方式:

const arr: Array<string | number> = [1];

arr.push('1');  // OK
arr.push(true);  // Error

或者你也可以通过可选元组的方式:

const arr: [number, string?] = [1];  // arr 的成员类型可以是: number, string, undefined 
arr.push('1');   // OK
arr.push(true);   // Error

使用元组形式,还能提供指定位置的类型检查:

arr[0] = '1';   // Error
arr[1] = 1;    // Error

使用

通常,我们使用 Promise.all 并行发出多个请求:

interface A {
  name: string;
}

interface B {
  age: number;
}

const [{ data: a }, { data: b }] = await Promise.all([
  axios.get<A>('http://some.1'),
  axios.get<B>('http://some.2')
])

此时,TypeScript 能推出 a 的类型是 A, b 的类型是 B。

现在,稍作改变:当满足特定条件时,才发出第二个请求:

// 使用类型注解
const requestList: [Promise<AxiosResponse<A>>, Promise<AxiosResponse<B>>?]
                    = [axios.get<A>('http://some.1')];
if (flag) {
  requestList[1] = (axios.get<B>('http://some.2'));
};
const [ { data: a }, response ] = await Promise.all(requestList);

我们期望它会如预想时那样工作,可是事与愿违,Promise.all(requestList),会出现类型兼容性的报错,在这个 Issues 里,描述了相同的问题。

现在,你可以通过断言的方式,来让程序正常运作:

const requestList: any[]  = [axios.get<A>('http://some.1')];  // 设置为 any[] 类型
if (flag) {
  requestList[1] = (axios.get<B>('http://some.2'));
}
const [
  { data: a },
  response
] = await Promise.all(requestList) as [AxiosResponse<A>, AxiosResponse<B>?] // 类型安全

字面量类型

在 JavaScript 基础上,TypeScript 扩展了一系列字面量类型,用来确保类型的准确性。

如创建一个字符串字面量:

const a = 'hello';  // a 的类型是 'hello'
a = 'world';   // Error

或者你也可以:

let a: 'hello' = 'hello';  // a 的类型是 'hello'
a = 'world';     // Error

其它数据类型与此相似。

你也可以定义交叉类型与联合类型的字面量:

interface A {
  name: string;
}
interface B {
  name: string;
  age: number;
}

type C = A | B;
type D = A & B;

对象字面量类型

对于对象字面量的类型,TypeScript 有一个被称之为 Freshness 的概念,它也被称为更严格的对象字面量检查,如下例子:

let someThing: { name: string };
someThing = { name: 'hello' };              // ok
someThing = { name: 'hello', age: 123 };    // Error, 对象字面量只能指定已知属性, { name: string } 类型中不存在 age 属性

let otherThing = { name: 'hello', age: 123 };
someThing = otherThing;                     // ok

TypeScript 认为创建的每个对象字面量都是 fresh 状态;当一个 fresh 对象字面量赋值给一个变量时,如果对象的类型与变量类型不兼容时,会出现报错(如上例子中 someThine = { name: ‘hello’, age: 123 }; 的错误);当对象字面量的类型变宽,对象字面量的 fresh 状态会消失(如上例子中 someThing = otherThing; ,赋值以后,someThing 的类型变宽)。

一个更实际的用例如下:

function logName(something: { name: string }) {
  console.log(something.name);
}

const obj = {
  name: 'matt',
  job: 'being awesome'
}

logName(obj); // ok
logName({ name: 'matt' }); // ok
logName({ nama: 'matt' }); // Error: nama 属性在 { name: string } 属性中不存在。
logName({ name: 'matt', job: 'being awesome' }); // Error: 对象字面量只能指定已知属性,`job` 属性在这里并不存在。

基本原理与上文中相似,当想用更严格的类型检查时,可以传一个具有 fresh 状态的对象字面量(如 logName({ name: ‘matt’, job: ‘being awesome’ });)。当你想多传一些属性至函数,可以将对象字面量赋值至一个新变量,然后再传至函数(如 logName(obj))。或者你也可以通过给函数形参添加多余类型的方式 function logName(someThing: { name: string; [key: string]: string })。

用 Decorator 限制类型

Decorator 可用于限制类方法的返回类型,如下所示:

const TestDecorator = () => {
  return (
    target: Object,
    key: string | symbol,
    descriptor: TypedPropertyDescriptor<() => number>   // 函数返回值必须是 number
  ) => {
    // 其他代码
  }
}

class Test {
  @TestDecorator()
  testMethod() {
    return '123';   // Error: Type 'string' is not assignable to type 'number'
  }
}

你也可以用泛型让 TestDecorator 的传入参数类型与 testMethod 的返回参数类型兼容:

const TestDecorator = <T>(para: T) => {
  return (
    target: Object,
    key: string | symbol,
    descriptor: TypedPropertyDescriptor<() => T>
  ) => {
    // 其他代码
  }
}

class Test {
  @TestDecorator('hello')
  testMethod() {
    return 123;      // Error: Type 'number' is not assignable to type 'string'
  }
}

泛型的类型推断

在定义泛型后,有两种方式使用,一种是传入泛型类型,另一种使用类型推断,即编译器根据其他参数类型来推断泛型类型。简单示例如下:

declare function fn<T>(arg: T): T;      // 定义一个泛型函数

const fn1 = fn<string>('hello');        // 第一种方式,传入泛型类型 string
const fn2 = fn(1);                      // 第二种方式,从参数 arg 传入的类型 number,来推断出泛型 T 的类型是 number

它通常与映射类型一起使用,用来实现一些比较复杂的功能。

Vue Type 简单实现

如下一个例子:

type Options<T> = {
  [P in keyof T]: T[P];
}

declare function test<T>(o: Options<T>): T;

test({ name: 'Hello' }).name     // string

test 函数将传入参数的所有属性取出来,现在我们来一步一步加工,实现想要的功能。

首先,更改传入参数的形式,由 { name: 'Hello' } 的形式变更为 { data: { name: 'Hello' } },调用函数的返回值类型不变,即 test({ data: { name: 'Hello' } }).name 的值也是 string 类型。

这并不复杂,这只需要把传入参数的 data 类型设置为 T 即可:

declare function test<T>(o: { data: Options<T> }): T;

test({data: { name: 'Hello' }}).name     // string

当 data 对象里,含有函数时,它也能运作:

const param = {
  data: {
    name: 'Hello',
    someMethod() {
      return 'hello world'
    }
  }
}

test(param).someMethod()    // string

接着,考虑一种特殊的函数情景,像 Vue 中 Computed 一样,不调用函数,也能取出函数的返回值类型。现在传入参数的形式变更为:

const param = {
  data: {
    name: 'Hello'
  },
  computed: {
    age() {
      return 20;
    }
  }
}

一个函数的类型可以简单的看成是 () => T 的形式,对象中的方法类型,可以看成 a: () => T 的形式,在反向推导时(由函数返回值,来推断类型 a 的类型)可以利用它,需要添加一个映射类型 Computed<T>,用来处理 computed 里的函数:

type Options<T> = {
  [P in keyof T]: T[P]
}

type Computed<T> = {
  [P in keyof T]: () => T[P]
}

interface Params<T, M> {
  data: Options<T>;
  computed: Computed<M>;
}

declare function test<T, M>(o: Params<T, M>): T & M;

const param = {
  data: {
    name: 'Hello'
  },
  computed: {
    age() {
      return 20
    }
  }
}

test(param).name    // string
test(param).age     // number

最后,结合巧用 ThisType 映射类型,可以轻松的实现在 computed age 方法下访问 data 中的数据:

type Options<T> = {
  [P in keyof T]: T[P]
}

type Computed<T> = {
  [P in keyof T]: () => T[P]
}

interface Params<T, M> {
  data: Options<T>;
  computed: Computed<M>;
}

declare function test<T, M>(o: Params<T, M>): T & M;

const param = {
  data: {
    name: 'Hello'
  },
  computed: {
    age() {
      return 20
    }
  }
}

test(param).name    // string
test(param).age     // number

扁平数组构建树形结构

扁平数组构建树形结构即是将一组扁平数组,根据 parent_id(或者是其他)转换成树形结构:

// 转换前数据
const arr = [
  { id: 1, parentId: 0, name: 'test1'},
  { id: 2, parentId: 1, name: 'test2'},
  { id: 3, parentId: 0, name: 'test3'}
];

// 转化后
[
  {
    id: 1,
    parentId: 0,
    name: 'test1',
    children: [
      { 
        id: 2, 
        parentId: 1, 
        name: 'test2', 
        children: [] 
      }
    ]
  },
  {
    id: 3,
    parentId: 0,
    name: 'test3',
    children: []
  }
]

如果 children 字段名字不变,函数的类型并不难写,它大概是如下样子:

interface Item {
  id: number;
  parentId: number;
  name: string;
}

type TreeItem = Item & { children: TreeItem[] | [] };

declare function listToTree(list: Item[]): TreeItem[];

listToTree(arr).forEach(i => i.children)    // ok

但是在很多时候,children 字段的名字并不固定,而是从参数中传进来:

const options = {
  childrenKey: 'childrenList'
}

listToTree(arr, options);

此时 children 字段名称应该为 childrenList:

[
  {
    id: 1,
    parentId: 0,
    name: 'test1',
    childrenList: [
      { id: 2, parentId: 1, name: 'test2', childrenList: [] }
    ]
  },
  {
    id: 3,
    parentId: 0,
    name: 'test3',
    childrenList: []
  }
]

实现的思路大致是前文所说的利用泛型的类型推断,从传入的 options 参数中,得到 childrenKey 的类型,然后再传给 TreeItem,如下:

interface Options<T extends string> {   // 限制为 string 类型
  childrenKey: T;
}

declare function listToTree<T extends string = 'children'>(list: Item[], options: Options<T>): TreeItem<T>[];

当 options 为 { childrenKey: 'childrenList' } 时,T 能被正确推导出为 childrenList,接着只需要在 TreeItem 中,把 children 修改为传入的 T 即可:

interface Item {
  id: number;
  parentId: number;
  name: string;
}

interface Options<T extends string> {
  childrenKey: T;
}

type TreeItem<T extends string> = Item & { [key in T]: TreeItem<T>[] | [] };

declare function listToTree<T extends string = 'children'>(list: Item[], options: Options<T>): TreeItem<T>[];

listToTree(arr, { childrenKey: 'childrenList' }).forEach(i => i.childrenList)    // ok

有一点局限性,由于对象字面量的 Fresh 的影响,当 options 不是以对象字面量的形式传入时,需要给它断言:

const options = {
  childrenKey: 'childrenList' as 'childrenList'
}

listToTree(arr, options).forEach(i => i.childrenList)    // ok

infer

infer 最早出现在此 PR 中,表示在 extends 条件语句中待推断的类型变量。

简单示例如下:

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

在这个条件语句 T extends (param: infer P) => any ? P : T 中,infer P 表示待推断的函数参数。

整句表示为:如果 T 能赋值给 (param: infer P) => any,则结果是 (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

内置类型

在 2.8 版本中,TypeScript 内置了一些与 infer 有关的映射类型:

  • 用于提取函数类型的返回值类型:

    type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;

    相比于文章开始给出的示例,ReturnType<T> 只是将 infer P 从参数位置移动到返回值位置,因此此时 P 即是表示待推断的返回值类型。

    type Func = () => User;
    type Test = ReturnType<Func>;   // Test = User
  • 用于提取构造函数中参数(实例)类型:

    一个构造函数可以使用 new 来实例化,因此它的类型通常表示如下:

    type Constructor = new (...args: any[]) => any;

    当 infer 用于构造函数类型中,可用于参数位置 new (...args: infer P) => any; 和返回值位置 new (...args: any[]) => infer P;

    因此就内置如下两个映射类型:

    // 获取参数类型
    type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;
    
    // 获取实例类型
    type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;
    
    class TestClass {
    
    constructor(
      public name: string,
      public string: number
    ) {}
    }
    
    type Params = ConstructorParameters<typeof TestClass>;  // [string, number]
    
    type Instance = InstanceType<typeof TestClass>;         // TestClass

一些用例

至此,相信你已经对 infer 已有基本了解,我们来看看一些使用它的「骚操作」:

  • tuple 转 union ,如:[string, number] -> string | number

    解答之前,我们需要了解 tuple 类型在一定条件下,是可以赋值给数组类型:

    type TTuple = [string, number];
    type TArray = Array<string | number>;
    
    type Res = TTuple extends TArray ? true : false;    // true
    type ResO = TArray extends TTuple ? true : false;   // false

    因此,在配合 infer 时,很容易做到:

    type ElementOf<T> = T extends Array<infer E> ? E : never
    
    type TTuple = [string, number];
    
    type ToUnion = ElementOf<TTuple>; // string | number

    在 stackoverflow 上看到另一种解法,比较简(牛)单(逼):

    type TTuple = [string, number];
    type Res = TTuple[number];  // string | number
  • union 转 intersection,如:string | number -> string & number

    这个可能要稍微麻烦一点,需要 infer 配合 Distributive conditional types 使用。

    Distributive conditional types 是由 naked type parameter 构成的条件类型。而 naked type parameter 表示没有被 Wrapped 的类型(如:Array<T>[T]Promise<T> 等都是不是 naked type parameter)。Distributive conditional types 主要用于拆分 extends 左边部分的联合类型,举个例子:在条件类型 T extends U ? X : Y 中,当 T 是 A | B 时,会拆分成 A extends U ? X : Y | B extends U ? X : Y

    有了这个前提,再利用在逆变位置上,同一类型变量的多个候选类型将会被推断为交叉类型的特性,即

    type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
    type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
    type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

    因此,综合以上几点,我们可以得到在 stackoverflow 上的一个答案:

    type UnionToIntersection<U> =
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
    
    type Result = UnionToIntersection<string | number>; // string & number

    当传入 string | number 时:

    • 第一步:(U extends any ? (k: U) => void : never) 会把 union 拆分成 (string extends any ? (k: string) => void : never) | (number extends any ? (k: number)=> void : never),即是得到 (k: string) => void | (k: number) => void

    • 第二步:(k: string) => void | (k: number) => void extends ((k: infer I) => void) ? I : never,根据上文,可以推断出 I 为 string & number。

LeetCode 的一道 TypeScript 面试题

前段时间,在 GitHub 上,发现一道来自 LeetCode TypeScript 的面试题,比较有意思,题目的大致意思是:

假设有一个这样的类型(原题中给出的是类,这里简化为 interface):

interface Module {
  count: number;
  message: string;
  asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>;
  syncMethod<T, U>(action: Action<T>): Action<U>;
}

在经过 Connect 函数之后,返回值类型为

type Result {
  asyncMethod<T, U>(input: T): Action<U>;
  syncMethod<T, U>(action: T): Action<U>;
}

其中 Action<T> 的定义为:

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

这里主要考察两点

  • 挑选出函数
  • 条件类型 + 此篇文章所提及的 infer

接下来就比较简单了,主要是利用条件类型 + infer,如果函数可以赋值给 asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>,则取值为 asyncMethod<T, U>(input: T): Action<U>

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

interface Module {
  count: number;
  message: string;
  asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>;
  syncMethod<T, U>(action: Action<T>): Action<U>;
}

type asyncMethod<T, U> = (input: Promise<T>) => Promise<Action<U>> // 转换前
type asyncMethodConnect<T, U> = (input: T) => Action<U> // 转换后
type syncMethod<T, U> = (action: Action<T>) => Action<U> // 转换前
type syncMethodConnect<T, U> = (action: T) => Action<U> // 转换后

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

type ModuleMethodsConnect<T> = T extends asyncMethod<infer U, infer V>
  ? asyncMethodConnect<U, V>
  : T extends syncMethod<infer U, infer V>
  ? syncMethodConnect<U, V>
  : never

type ModuleMethods = methodsPick<Module>

type Connect = (module: Module) => {
  [T in ModuleMethods]: ModuleMethodsConnect<Module[T]>
}

实战

参数简化

通过一个简单的功能把

distribute({
    type: 'LOGIN',
    email: string
})

这样的函数调用方式给简化为:

distribute('LOGIN', {
    email: string
})

分布条件类型的真实用例

举一个类似 redux 中的 dispatch 的例子。

首先,我们有一个联合类型 Action,用来表示所有可以被 dispatch 接受的参数类型:

type Action =
  | {
      type: "INIT"
    }
  | {
      type: "SYNC"
    }
  | {
      type: "LOG_IN"
      emailAddress: string
    }
  | {
      type: "LOG_IN_SUCCESS"
      accessToken: string
    }

然后我们定义这个 dispatch 方法:

declare function dispatch(action: Action): void

// ok
dispatch({
  type: "INIT"
})

// ok
dispatch({
  type: "LOG_IN",
  emailAddress: "david.sheldrick@artsy.net"
})

// ok
dispatch({
  type: "LOG_IN_SUCCESS",
  accessToken: "038fh239h923908h"
})

这个 API 是类型安全的,当 TS 识别到 type 为 LOG_IN 的时候,它会要求你在参数中传入 emailAddress 这个参数,这样才能完全满足联合类型中的其中一项。

等等,我们好像可以让这个api变得更简单一点:

dispatch("LOG_IN_SUCCESS", {
  accessToken: "038fh239h923908h"
})

参数简化实现

首先,利用方括号选择出 Action 中的所有 type,这个技巧很有用。

type ActionType = Action["type"]
// => "INIT" | "SYNC" | "LOG_IN" | "LOG_IN_SUCCESS"

但是第二个参数的类型取决于第一个参数,我们可以使用类型变量来对该依赖关系建模。

declare function dispatch<T extends ActionType>(
  type: T,
  args: ExtractActionParameters<Action, T>
): void

注意,这里就用到了 extends 语法,规定了我们的入参 type 必须是 ActionType 中一部分。

注意这里的第二个参数 args 用 ExtractActionParameters<Action, T> 这个类型来把 type 和 args 做了关联

来看看 ExtractActionParameters 是如何实现的:

type ExtractActionParameters<A, T> = A extends { type: T } ? A : never

在这次实战中,我们第一次运用到了条件类型,ExtractActionParameters<Action, T> 会按照我们上文提到的分布条件类型,把 Action 中的 4 项依次去和 { type: T } 进行比对,找出符合的那一项。

来看看如何使用它:

type Test = ExtractActionParameters<Action, "LOG_IN">
// => { type: "LOG_IN", emailAddress: string }

这样就筛选出了 type 匹配的一项。

接下来我们要把 type 去掉,第一个参数已经是 type 了,因此我们不想再额外声明 type 了。

// 把类型中key为"type"去掉
type ExcludeTypeField<A> = { [K in Exclude<keyof A, "type">]: A[K] }

这里利用了 keyof 语法,并且利用内置类型 Exclude 把 type 这个 key 去掉,因此只会留下额外的参数。

type Test = ExcludeTypeField<{ type: "LOG_IN", emailAddress: string }>
// { emailAddress: string }

然后用它来剔除参数中的 type

// 把参数对象中的type去掉
type ExtractActionParametersWithoutType<A, T> =
    ExcludeTypeField<ExtractActionParameters<A, T>>;
declare function dispatch<T extends ActionType>(
  type: T,
  args: ExtractActionParametersWithoutType<Action, T>
): void

到此为止,我们就可以实现上文中提到的参数简化功能:

// ok
dispatch({
  type: "LOG_IN",
  emailAddress: "david.sheldrick@artsy.net"
})

利用重载进一步优化

到了这一步为止,虽然带参数的 Action 可以完美支持了,但是对于 “INIT” 这种不需要传参的 Action,我们依然要写下面这样代码:

dispatch("INIT", {})

这肯定是不能接受的!所以我们要利用 TypeScript 的函数重载功能。

// 简单参数类型
function dispatch<T extends SimpleActionType>(type: T): void

// 复杂参数类型
function dispatch<T extends ComplexActionType>(
  type: T,
  args: ExtractActionParametersWithoutType<Action, T>,
): void

// 实现
function dispatch(arg: any, payload?: any) {}

那么关键点就在于 SimpleActionType 和 ComplexActionType 要如何实现了

SimpleActionType 顾名思义就是除了 type 以外不需要额外参数的 Action 类型

type SimpleAction = ExtractSimpleAction<Action>

我们如何定义这个 ExtractSimpleAction 条件类型?如果我们从这个 Action 中删除 type 字段,并且结果是一个空的接口,那么这就是一个 SimpleAction,所以我们可能会凭直觉写出这样的代码:

type ExtractSimpleAction<A> = ExcludeTypeField<A> extends {} ? A : never

但这样是行不通的,几乎所有的类型都可以 extends {},因为 {} 太宽泛了。

我们应该反过来写:

type ExtractSimpleAction<A> = {} extends ExcludeTypeField<A> ? A : never

现在如果 ExcludeTypeField <A> 为空,则 extends 表达式为 true,否则为 false。

但这仍然行不通!因为分布条件类型仅在 extends 关键字的前面是类型变量时发生。

分布条件类型仅发生在如下场景:

type Blah<Var> = Var extends Whatever ? A : B

而不是:

type Blah<Var> = Foo<Var> extends Whatever ? A : B
type Blah<Var> = Whatever extends Var ? A : B

但是我们可以通过一些小技巧绕过这个限制:

type ExtractSimpleAction<A> = A extends any
  ? {} extends ExcludeTypeField<A>
    ? A
    : never
  : never

A extends any 是一定成立的,这只是用来绕过 ts 对于分布条件类型的限制,没错啊,我们的 A 确实是在 extends 的前面了,就是骗你 TS,这里是分布条件类型。

而我们真正想要做的条件判断被放在了中间,因此 Action 联合类型中的每一项又能够分布的去匹配了。

那么我们就可以简单的筛选出所有不需要额外参数的 type

type SimpleAction = ExtractSimpleAction<Action>
type SimpleActionType = SimpleAction['type']

再利用 Exclude 取反,找到复杂类型:

type ComplexActionType = Exclude<ActionType, SimpleActionType>

到此为止,我们所需要的功能就完美实现了:

// 简单参数类型
function dispatch<T extends SimpleActionType>(type: T): void
// 复杂参数类型
function dispatch<T extends ComplexActionType>(
  type: T,
  args: ExtractActionParameters<Action, T>,
): void
// 实现
function dispatch(arg: any, payload?: any) {}

// ok
dispatch("SYNC")

// ok
dispatch({
  type: "LOG_IN",
  emailAddress: "david.sheldrick@artsy.net"
})

完整代码

type Action =
  | {
      type: "INIT";
    }
  | {
      type: "SYNC";
    }
  | {
      type: "LOG_IN";
      emailAddress: string;
    }
  | {
      type: "LOG_IN_SUCCESS";
      accessToken: string;
    };

// 用类型查询查出 Action 中所有 type 的联合类型
type ActionType = Action["type"];

// 把类型中 key 为 type 去掉
type ExcludeTypeField<A> = { [K in Exclude<keyof A, "type">]: A[K] };

type ExtractActionParameters<A, T> = A extends { type: T } ? A : never

// 把参数对象中的 type 去掉
// Extract<A, { type: T } 会挑选出能 extend { type: T } 这个结构的 Action 中的类型
type ExtractActionParametersWithoutType<A, T> = ExcludeTypeField<ExtractActionParameters<A, T>>;

type ExtractSimpleAction<A> = A extends any
  ? {} extends ExcludeTypeField<A>
    ? A
    : never
  : never;

type SimpleActionType = ExtractSimpleAction<Action>["type"];
type ComplexActionType = Exclude<ActionType, SimpleActionType>;

// 简单参数类型
function dispatch<T extends SimpleActionType>(type: T): void;
// 复杂参数类型
function dispatch<T extends ComplexActionType>(
  type: T,
  args: ExtractActionParametersWithoutType<Action, T>
): void;
// 实现
function dispatch(arg: any, payload?: any) {}

dispatch("SYNC");

dispatch('LOG_IN', {
  emailAddress: 'ssh@qq.com'
})

Ref 类型从零实现

const count = ref(ref(ref(ref(2))))

需要支持嵌套后解包,最后只会剩下 { value: number } 这个类型。

泛型的反向推导

泛型的正向用法很多人都知道了。

type Value<T> = T

type NumberValue = Value<number>

这样,NumberValue 解析出的类型就是 number,其实就类似于类型系统里的传参。

那么反向推导呢?

function create<T>(val: T): T

let num: number

const c = create(num)

这里泛型没有传入,居然也能推断出 value 的类型是 number。

因为 create<T> 这里的泛型 T 被分配给了传入的参数 value: T,然后又用这个 T 直接作为返回的类型,

简单来说,这里的三个 T 被关联起来了,并且在传入 create(2) 的那一刻,这个 T 被统一推断成了 number。

function create<2>(value: 2): 2

索引签名

假设我们有一个这样的类型:

type Test = {
  foo: number;
  bar: string
}

type N = Test['foo'] // number

可以通过类似 JavaScript 中的对象属性查找的语法来找出对应的类型。

条件类型

假设我们有一个这样的类型:

type IsNumber<T> = T extends number ? 'yes' : 'no';

type A = IsNumber<2> // yes
type B = isNumber<'3'> // no

这就是一个典型的条件类型,用 extends 关键字配合三元运算符来判断传入的泛型是否可分配给 extends 后面的类型。

同时也支持多层的三元运算符(后面会用到):

type TypeName<T> = T extends string
  ? "string"
  : T extends boolean
      ? "boolean"
      : "object";

type T0 = TypeName<string>; // "string"
type T1 = TypeName<"a">; // "string"
type T2 = TypeName<true>; // "boolean"

keyof

keyof 操作符是 TS 中用来获取对象的 key 值集合的,比如:

type Obj = {
  foo: number;
  bar: string;
}

type Keys = keyof Obj // "foo" | "bar"

这样就轻松获取到了对象 key 值的联合类型:“foo” | “bar”。

它也可以用在遍历中:

type Obj = {
  foo: number;
  bar: string;
}

type Copy = {
  [K in keyof Obj]: Obj[K]
}

// Copy 得到和 Obj 一模一样的类型

可以看出,遍历的过程中右侧也可以通过索引直接访问到原类型 Obj 中对应 key 的类型。

infer

这是一个比较难的点,文档中对它的描述是 条件类型中的类型推断。

它的出现使得 ReturnType、 Parameters 等一众工具类型的支持都成为可能,是 TypeScript 进阶必须掌握的一个知识点了。

注意前置条件,它一定是出现在条件类型中的。

type Get<T> = T extends infer R ? R: never

注意,infer R 的位置代表了一个未知的类型,可以理解为在条件类型中给了它一个占位符,然后就可以在后面的三元运算符中使用它。

type T = Get<number>

// 经过计算
type Get<number> = number extends infer number ? number: never

// 得到
number

它的使用非常灵活,它也可以出现在泛型位置:

type Unpack<T> = T extends Array<infer R> ? R : T
type NumArr = Array<number>
type U = Unpack<NumArr>

// 经过计算
type Unpack<Array<number>> = Array<number> extends Array<infer R> ? R : T

// 得到
number

仔细看看,是不是有那么点感觉了,它就是对于 extends 后面未知的某些类型进行一个占位 infer R,后续就可以使用推断出来的 R 这个类型。

评论

阅读下一篇

React技术解密笔记——理念篇
2020-07-22 19:59:15
0%