TypeScript 高阶教程(第二篇)– 把 TypeScript 当作编程语言来使用

前言

上一篇,我们已经掌握了如何《把 TypeScript 当作静态类型语言来使用

这一篇,我们继续学习如何《把 TypeScript 当作编程语言来使用》

开始 🚀。

 

TypeScript 是一门编程语言

JavaScript 是编程语言,这个我们知道。

TypeScript 把类型和一些静态类型语言的特性(如 abstract、interface、enum、generic、overloading 等等)融入进了 JavaScript,这个我们上一篇讲过了。

但仅凭这些,TypeScript 就算是一门编程语言了吗?

不!

我们之所以说 TypeScript 是一门编程语言,是因为它具备对 "类型" 进行 "编程" 的能力。

看例子,感受一下:

  1. 逻辑类型

    传统的静态类型语言(C# / Java)无法表达类型之间的逻辑关系 —— 你没办法声明 "这个变量的类型是那个函数的第一个参数类型"。

    但 TypeScript 可以!

    function doSomething(param: string): void {}
    
    const value: Parameters<typeof doSomething>[0] = 0; // IDE Error: Type 'number' is not assignable to type 'string'

    Parameters<typeof doSomething>[0] 是一句 TypeScript 编程语法。

    它的意思是:先取出 doSomething 函数的所有参数类型,再从中取出第 0 个参数的类型,作为 variable value 的类型。(先懂意思就好,语法下面会再详细教)

    所以,最终 value 的类型是 string,而 assign value 的类型是 number,因此 IDE 报错了。

  2. 类型 transformation

    类型 A → transform → 类型 B

    transformation 指的是从一个类型,经过一个 transform process,变成另一个类型。

    看例子

    type Person = {
      name: string;
      age: number;
    };

    有一个 Person object literal。

    我想搞一个 PromisePerson,它拥有所有 Person 的属性,但属性值的类型全部要 wrap 一层 Promise<原类型>

    注:Promise 指的就是 JavaScript 的 Promise 类,只是 TypeScript 加上了泛型,Promise<T>T 就是 Promise resolve 值的类型。

    hardcode 的写法是这样

    type PromisePerson = {
      name: Promise<string>;
      age: Promise<number>;
    };

    使用 transform 的写法是这样

    type PromisePerson = {
      [key in keyof Person]: Promise<Person[key]>;
    };

     

    这是一句 TypeScript 编程语法。

    它的意思是:创建一个 object literal → 取出 Person 所有的 keys → for loop keys 添加到新的 object literal,类型则是 wrap 一层 Promise<原类型>。(先懂意思就好,语法下面会再详细教)

    最终出来的类型和上面 hardcode 的写法一模一样。

透过上面两个例子,我们可以清楚看到,TypeScript 是一门编程语言,它具备对类型进行编程的能力,并拥有一套独立的编程语法。

 

TypeScript 编程语法 の variable、function、typeof

TypeScript 作为一门编程语言(给 JavaScript 的类型做编程),少不了几个特性:variable、function、if else、recursive(looping)。

我们分几个阶段来学:

  1. 先学几招简单的语法

  2. 再学几招 build-in 的 utility function

  3. 再把语法学全

  4. 再做一些练习题(下一篇)

  5. 这样就可以毕业了

开始 🚀。

Variable

编程语言最基本的特性之一就是定义变量。

定义变量有两个主要目的:一是为了 code study,二是为了复用。

在上一篇介绍 Type Aliases 时,我们其实已经提前揭晓:它正是 TypeScript 作为编程语言,用来声明变量的手法。

type WhatEverName = string;
type NumberOrStringArray = number[] | string[];

TypeScript 整个语言的目的是去声明或管理 JavaScript 的类型。

因此,在 TypeScript 中声明的变量,其存放的值一定就是 "类型"。这个概念我们要牢记。

注:TypeScript 的变量名是 PascalCase 哦。

function

TypeScript 的变量(variable)用来封装类型,函数(function)则用来封装 transform 的过程。

我们用回上面的例子

type Person = {
  name: string;
  age: number;
};

type PromisePerson = {
  [key in keyof Person]: Promise<Person[key]>;
};
/*
  效果
  type PromisePerson = {
    name: Promise<string>;
    age: Promise<number>;
  }
*/

这是把 Person transform to PromisePerson

那如果我有多一个 Animal 要 transform to PromiseAnimal 该怎么写?

我们可以 copy paste 再写一篇。

type Animal = {
  name: string;
  weight: number;
};

type PromiseAnimal = {
  [key in keyof Animal]: Promise<Animal[key]>;
};
/*
  效果
  type PromiseAnimal = {
    name: Promise<string>;
    weight: Promise<number>;
  }
*/

也可以选择更好的方式 —— 把 transform 过程封装成 TypeScript 函数,像这样:

type ToPromise<T> = {
  [key in keyof T]: Promise<T[key]>;
};

分析一下:

type ToPromise<T> =

看上去长的有点像是 type aliases + 泛型(generic)。但千万不要把它跟泛型混淆,两者是不同的概念。

ToPromise 是函数名(用 PascalCase 哦)

<T> 代表它接收一个参数,T 是代号。如果有多个参数就是这样 <T1, T2, T3> 或者 <T, U, V> 都可以。

= 代表函数的 return

= 之后的代码就是函数内容,里面会使用到参数 T,这个例子的具体语法我们暂且不管,只要知道函数定义的方式就可以了(transform 语法下面会再详细教)。

接着是函数调用

type PromisePerson = ToPromise<Person>;
/*
  效果
  type PromisePerson = {
    name: Promise<string>;
    age: Promise<number>;
  }
*/
type PromiseAnimal = ToPromise<Animal>;
/*
  效果
  type PromiseAnimal = {
    name: Promise<string>;
    weight: Promise<number>;
  }
*/

出来的效果一模一样。

typeof

typeof 是 JavaScript 的语法,但同时它也是 TypeScript 的语法。

首先,我们先学如何区分 TypeScript 类型语法 和 JavaScript 运行时语法。

const value = typeof '';    // 这句是 JavaScript

type MyType = typeof value; // 这句是 TypeScript

很简单,哪一句是 JavaScript 你一定会知道,其余的自然就是 TypeScript。(TypeScript 语句通常都是以 type 作为开头)

那 TypeScript 的 typeof 有什么作用呢?

它可以从 JavaScript 变量中提取出 TypeScript 类型。

上面例子中 value 的类型是 string,在 TypeScript 语句中透过 typeof value 引入 JavaScript 的 value,它就能把 value 的类型放入到 TypeScript MyType 变量中。

那为什么要让 TypeScript 和 JavaScript 如此交错呢?不觉得乱吗?

通常会用到 typeof,是因为我们需要某个类型(如 想把它 transform 成另一个类型),但这个类型并没有被显式声明,而是透过类型推断得出的,因此只能透过 typeof 从 JavaScript 中提取该类型。

// 有显式声明类型
type Person = {
  name: string;
  age: number;
};

// 可以直接拿类型 Person 做 transform
type PromisePerson = {
  [K in keyof Person]: Promise<Person[K]>;
};


// 使用类型推断
const person = {
  name: 'Derrick',
  age: 11,
};

// 需要借助 typeof 获取到 Person 类型才能做 transform
type PromisePerson = {
  [K in keyof typeof person]: Promise<(typeof person)[K]>;
};

更多 typeof 的例子:

function doSomething(param: string) {
  return 0;
}
const values = [1, 2, 3];

type MyFunction = typeof doSomething; // (param: string) => number
type MyArray = typeof values;         // number[]

// class 有点特别哦, 因为它本身已经是类型了.
class Person {
  name = '';
  static age = 0;
}
type PersonInstance = Person;     // { name: string } class 代表实例的类型
type PersonClass = typeof Person; // { age: number } typeof class 是这个 class 本身身为对象的类型

比较特别的是 classtypeof class 的区别。

总结

这 part 介绍了 TypeScript 作为编程语言的两大特性:variable 变量,以及 function 函数。

variable 用于封装类型,function 用来封装类型 transform 过程。

另外还顺便介绍了 typeof 语法,它可以从 JavaScript 代码中提炼出自动推断的类型,并用于 TypeScript 语句。

 

TypeScript Build-in Utility Types

参考:Docs – Utility Types

TypeScript 有许多 built-in 的 transform function(a.k.a utility types)。

我们先过一遍,了解它们的用处,之后再研究它们底层实现的语法。

Partial<Type>

Partial 函数的作用是:把 object literal、class、interface(统称 "对象" / object)的所有 key 变成 optional。

type Person = {
  name: string;
  age: number;
};

type PartialPerson = Partial<Person>;
/* 
  效果
  type PartialPerson = {
    name?: string;  
    age?: number;  
  };
*/

name 变成了 name?

Required<Type>

Partial 相反,它是把 optional key 变成 required

type PartialPerson = {
  name?: string;
  age?: number;
};

type Person = Required<PartialPerson>;
/* 
  效果
  type Person = {
    name: string;  
    age: number;  
  };
*/

name? 变成了 name

Readonly<Type>

顾名思义,就是把 object 或 array 变成 readonly

type Person = { name: string; age: number };
type ReadonlyPerson = Readonly<Person>;
/* 
  效果
  type ReadonlyPerson = {
    readonly name: string;  
    readonly age: number;  
  };
*/

type People = Person[];
type ReadonlyPeople = Readonly<People>;
/* 
  效果
  type ReadonlyPeople = readonly Person[];
  注:只有 array 是 readonly,Person 对象不是 readonly 哦
*/

name 变成了 readonly name,从 Person[] 变成 readonly Person[]

Record<Keys, Type>

Record 用来创建 object literal,特色是所有的 keys 拥有相同的类型。

type Person = Record<'firstName' | 'lastName' | 'fullName', string>;
/*
  效果
  type Person = {
    firstName: string;
    lastName: string;
    fullName: string;
  };
*/

参数一是 keys,用 union types + string literal 来描述。

注:union types 和 tuple 常用来描述 TypeScript 类型编程中 array 的形态。像上面这个就是 string array。

参数二是所有 value 的类型。

Pick<Type, Keys>

Pick 是从一个对象里选出指定的 keys 保留,并舍弃其余的 keys。

type Person = {
  firstName: string;
  lastName: string;
  fullName: string;
};

type PickedPerson = Pick<Person, 'firstName' | 'lastName'>; // 只保留 firstName 和 lastName
/*
  效果
  type PickedPerson = {
    firstName: string;
    lastName: string;
  };
*/

Omit<Type, Keys>

OmitPick 相反,它是选择要删除的 keys,其余的保留。

type Person = {
  firstName: string;
  lastName: string;
  fullName: string;
};

type OmittedPerson = Omit<Person, 'fullName'>; // 删除 fullName

/*
  效果
  type OmittedPerson = {
    firstName: string;
    lastName: string;
  };
*/

Exclude<UnionTypes, ExcludedItems>

Exclude 是一个用来过滤类型的 function。

参数一,二都是 union types。

参数一是所有类型,参数二是要删除的类型。

type Keys = 'key1' | 'key2' | 'key3' | 'key4';
type ExcludedKeys = Exclude<Keys, 'key1' | 'key3'>;
/*
  效果
  type ExcludedKeys = 'key2' | 'key4';
*/

type Types = boolean | string | number | null;
type ExcludedTypes = Exclude<Types, boolean | number>;
/*
  效果
  type ExcludedTypes = string | null;
*/

来一个复杂点的

type Types = 'key1' | 'key2' | 'key3' | number | boolean;
type ExcludedTypes = Exclude<Types, string>;
/*
  效果
  type ExcludedTypes = number | boolean;
*/

string'key1' | 'key2' | 'key3' 都给 exclude 掉了。

因为 'key1' | 'key2' | 'key3' 是 string literal,算是 "一种" string,因此当声明要把 string exclude 掉时,string literral 自然也要被 exclude 掉。

还有一个知识点:当 exclude 到一个都不剩时,它会返回 never

type Types = 'key1' | 'key2';
type ExcludedTypes = Exclude<Types, 'key1' | 'key2'>;
/*
  效果
  type ExcludedTypes = never;
*/

Extract<UnionTypes, ExtractedItems>

它是 Exclude 的相反,参数一是所有类型,参数二是要保留的类型。

type Types = 'key1' | 'key2' | number | boolean;
type ExtractedTypes = Extract<Types, string | boolean>;
/*
  效果
  type ExtractedTypes = boolean | 'key1' | 'key2';
*/

NonNullable<Types>

NonNullable 会过滤掉 nullundefined,参数是 union types。

type Types = string | undefined | number | null | boolean;
type NonNullableTypes = NonNullable<Types>;
/*
  效果
  type NonNullableTypes = string | number | boolean;
*/

揭秘:它原理其实很简单,就是利用了 intersection types 的特性。

type Types = string | undefined | number | null | boolean;

type NonNullableTypes = Types & {}; // string | number | boolean

step by step 演化:

首先是笛卡尔积

string & {} | undefined & {} | number & {} | null & {} | boolean & {}

string & {},依照 intersection types 的规则,"具体 & 抽象" 会保留具体,所以结果是 string

undefined & {},这是 "具体 & 具体",类型不同,所以结果是 never

以此类推,结果是 string | never | number | never | boolean

依照 union types 规则,never 会被自动移除。

因此,最终的类型是 string | number | boolean

Parameters<Type>

Parameters 用于获取函数的参数类型,它返回的是 tuple。

function doSomething(param1: string, param2: number): void {}

type DoSomething = typeof doSomething;
/*
  效果
  type DoSomething = (param1: string, param2: number) => void;
*/

type DoSomethingParameters = Parameters<DoSomething>;
/*
  效果
  type DoSomethingParameters = [param1: string, param2: number];
*/

注:利用了 typeof 取出 doSomething 的类型。

ConstructorParameters<Type>

Parameters 函数一样,只是 ConstructorParameters 用在 classconstructor,而不是普通函数。

class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}
type ClassPerson = typeof Person;
/*
  效果
  type ClassPerson = new (name: string, age: number) => Person
*/

type ClassPersonconstructorParameters = ConstructorParameters<ClassPerson>;
/*
  效果
  type ClassPersonconstructorParameters = [name: string, age: number];
*/

ReturnType<Type>

ReturnType 用于获取函数的返回值类型。

function getValue(): string | null {
  return Math.random() > 0.1 ? '' : null;
}

type GetValueReturn = ReturnType<typeof getValue>;
/*
  效果
  type GetValueReturn = string | null
*/

InstanceType<Type>

class Person {
  name = '';
}

type PersonClass = typeof Person; // 这是 class 的类型
type PersonInstance = Person;     // 这是 class 实例的类型

InstanceType 可以从 class 的类型,获取到 class 的实例类型。

type PersonInstance = InstanceType<PersonClass>;
/*
  效果
  PersonInstance = Person;
*/

ThisParameterType<Type>

ThisParameterType 用于获取函数的 this 类型。

function doSomething(this: { name: string }): void {}
doSomething.call({ name: '' });

type DoSomethingThis = ThisParameterType<typeof doSomething>;
/*
  效果
  DoSomethingThis = { name: string };
*/

如果函数没有声明 this 类型的话,会得到 unknown

function doSomething(): void {}
type DoSomethingThis = ThisParameterType<typeof doSomething>;
/*
  效果
  DoSomethingThis = unknown;
*/

OmitThisParameter<Type>

OmitThisParameter 用于删除函数的 this

function doSomething(this: { name: string }, param1: string): string {
  return param1;
}

type DoSomething = typeof doSomething;
/*
  效果
  DoSomething = (this: { name: string; }, param1: string) => string;
*/

type NoThisDoSomething = OmitThisParameter<DoSomething>;
/*
  效果
  NoThisDoSomething = (param1: string) => string;
*/

返回一个删除了 this 类型的函数,参数和返回则保留。

Awaited<Type>

先了解一下 Promise 泛型

const p1 = new Promise<string>(resolve => resolve(123));
// IDE Error: Argument of type 'number' is not assignable to parameter of type 'string | PromiseLike<string>'

Promise<string> 表示 promise resolve 的类型必须是 string

Awaited 则是用于获取 Promise 的泛型(也就是 resolve 的类型)

const p1 = new Promise<string>(resolve => resolve('abc'));

type PromiseResolved = Awaited<typeof p1>;
/*
  效果
  PromiseResolved = Awaited<Promise<string>>
  PromiseResolved = string
*/

而且它支持无限层

type PromiseResolved = Awaited<Promise<Promise<string>>>;
/*
  效果
  PromiseResolved = string
*/

两层或以上都可以获取到 resolve 类型。

ThisType<Type>

ThisType 会返回一个 object literal,其方法中的 this 会是调用 ThisType 时传入的参数。

type Person = ThisType<{ name: string }>;

const person = {
  doSomething() {
    console.log(this.name); // this 的类型是 { name: string }
  },
} as satisfies Person;

Intrinsic string manipulation types

用来改变 string literal 和 template literal 的 case style。

一共有四个:

type T1 = Uppercase<'MyLove'>;    // 'MYLOVE'

type T2 = Lowercase<'MyLove'>;    // 'mylove'

type T3 = Capitalize<'myLove'>;   // 'MyLove' 它是 firstCharUppercase

type T4 = Uncapitalize<'MyLove'>; // 'myLove' 它是 firstCharLowercase

NoInfer<Type>

上一篇讲解过了。

虽然 NoInfer 被归类为 utility function,但它和其它 utility function 区别很大。

它完全不是类型 transformation,而更像是一个特殊标签或语法。

也因为这样,我才会选择再上一篇就讲解它。

总结

以上就是 TypeScript 所有(截至 version 5.8)的 build-in utility function。

如果不够用,我们就需要自己编写 function。(下面会教)

或者使用社区提供的,如:type-fest

Intrinsic Types

上面所有的 utility function 我们都可以自己用 TypeScript 语法编写出来。

只是因为它们太普遍了,所以 TypeScript 才贴心 built-in 给我们用。

但有几个是我们用 TypeScript 语法编写不出来的:NoInferThisTypeUppercaseLowercaseCapitalizeUncapitalize

它们被统称为 Intrinsic Types。

 

TypeScript 编程语法 の keyof、Indexed Access、Mapped

掌握了 variable、function、utility types 之后,我们要进入 TypeScript 编程语法的细节了。

会有很多语法和规则,大家跟紧了 🚀。

keyof

keyof 的主要用途是从对象中提取出所有 key 的类型(注意:是 key 的类型,不是 value 的类型哦)。

keyof object literal

先看看 object literal 的例子

type Person = {
  name: string;
  age: number;
  1: boolean;
};

type Keys = keyof Person; // 'name' | 'age' | 1

Person 里有三个 key,因此有三个类型:

  • 第一个类型是 'name' string literal

  • 第二个类型是 'age' string literal

  • 第三个类型是 1 number literal

这三个类型会合成一个 union types 'name' | 'age' | 1 作为 keyof 的返回。

用 JavaScript 语法来描述 keyof 大概是长这样

const person = {
  name: 'string',
  age: 'number',
  1: 'boolean'    
};

const keys = Object.keys(person); // ['name', 'age', '1']

由于 TypeScript 的语法比较复杂,所以下面我会尽量搭配一段 JavaScript 语法作为描述,这样有助于理解。

注:不是 TypeScript compile to JavaScript 哦。这就好比我用 JavaScript 来描述一段 C# 代码,对熟悉 JavaScript 的人来说,会更容易理解 C#。

with dynamic key

再看看 object literal with dynamic key 的例子

type Person = {
  [prop: number]: boolean;
};

type Keys = keyof Person; // number

提醒:返回的可不是 string literal 'prop' 哦。keyof 拿的是 key 的类型,而 prop 只是 key 的代号,number 才是 key 的类型。

再看一个

type Person = {
  [prop: symbol]: string;
  [prop: number]: string;
  // 或者 [prop: symbol | number]: string;
};

type Keys = keyof Person; // symbol | number

一样,多个 key 就合并成 union types。

string key 会比较特别

type Person = {
  [prop: string]: string;
};

type Keys = keyof Person; // string | number

除了 string 类型,它还会自动多一个 number 类型。

这是因为 JavaScript 的机制 —— 会自动把 number key 变成 string key,这点我们上一篇有讲解过了。

简单说

type Person1 = {
  [prop: string]: string; // string | number
};

const person1: Person1 = {
  name: '', // string key 可以
  0: '',    // number key 可以,因为会被自动转成 numeric string '0'
};

type Person2 = {
  [prop: number]: string; // number
};

const person2: Person2 = {
  name: '', // ❌ string key 不可以,IDE 会报错
  0: '',    // number key 可以
  '1': '',  // numeric string 可以,因为其实 0 也是转成 numeric string '0'
};

keyof 总结:

type Person = {
  [prop: string]: string; // string | number
  [prop: number]: string; // number 
  [prop: symbol]: string; // symbol
  name: string; // 'name'
  1: string;    // 1
};

type Keys = keyof Person; // string | number | symbol

keyof class

class Person {
  name = '';
  age = 0;
  doSomething() {}
}

type Keys = keyof Person; // 'name' | 'age' | 'doSomething'

属性和方法都是 key,因此都会被 keyof 出来。

class Derrick extends Person {}

type Keys = keyof Derrick; // 'name' | 'age' | 'doSomething'

基类的 key 也会被 keyof 出来。

如果想拿到 class 本身的 static key,那就需要搭配 typeof

class Person {
  name = '';

  static version = 1;
}

type Keys = keyof typeof Person; // 'prototype' | 'version'

另外,private key 是 keyof 不出来的

class Person {
  name = 0;
  private privateName = '';
  #privateName = '';

  static version = 1;
  private static privateVersion = 1;
  static #privateVersion = 1;
}

type Keys1 = keyof Person;        // 'name'(没有 privateName 和 #privateName)
type Keys2 = keyof typeof Person; // 'prototype' | 'version' (没有 privateVersion 和 #privateVersion)

keyof string、number、boolean、biginit、symbol

type Keys1 = keyof '';                  // 'length' | 'substring' | 'indexOf' | ...等等
type Keys2 = keyof `version_${number}`; // 'length' | 'substring' | 'indexOf' | ...等等
type Keys3 = keyof string;              // 'length' | 'substring' | 'indexOf' | ...等等
type Keys4 = keyof String;              // 'length' | 'substring' | 'indexOf' | ...等等

这 4 个是等价的:string literaltemplate literalstring 会被认定为 class String。 

keyof 会把 class String 的所有 key 提取出来。

numberbooleansymbolbigint 也是相同机制

type Keys1 = keyof 1;      // 'toString' | 'toFixed' | ...等等
type Keys2 = keyof number; // 'toString' | 'toFixed' | ...等等
type Keys3 = keyof Number; // 'toString' | 'toFixed' | ...等等

type Keys4 = keyof true;    // 'valueOf'
type Keys5 = keyof boolean; // 'valueOf'
type Keys6 = keyof Boolean; // 'valueOf'

type Keys7 = keyof symbol;  // 'description' | 'toString' | ...等等
type Keys8 = keyof bigint;  // 'toString' | 'toLocaleString' | ...等等

这 5 个类型都有对应的 object 形态,加上 JavaScript 会 auto-boxing,因此 keyof 返回它们 object 形态下的属性方法,完全合理。 

keyof array、tuple

type Keys7 = keyof [];               // 'length' | 'push' | ...等等
type Keys7 = keyof [string, number]; // 'length' | 'push' | ...等等
type Keys8 = keyof Array<unknown>;   // 'length' | 'push' | ...等等

[]tuple [string, number] 都是被当作 class Array 处理。 

non-keyofable

不是所有的类型都有 key。

比如说 nullundefined 就绝对不会有 key。

type Keys1 = keyof null;      // never
type Keys2 = keyof undefined; // never

因此,keyof 它们会得到 never

此外,keyof unknown 也是 never

unknown 是未知,说它没有 key 也算合理。

比较让人意想不到的是 variable function 和 constructable 竟然也是 never

type Keys1 = keyof (() => void);  // never
type Keys2 = keyof (new (...args: unknown[]) => unknown); // never

type Keys3 = keyof Function; // 'bind' | 'name' | ...等等

keyof string 等价于 keyof String,为什么 keyof (() => void) 不是 keyof Function 呢?我也不清楚 😔 。

除了 nullundefinedunknown、variable function 和 constructable 会返回 never 以外,object{} 也返回 never

type Keys1 = keyof object; // never
type Keys2 = keyof {};     // never

我的理解是,() => void ≼ object≼ {}(它们是继承关系)

keyof (() => void)never,那 object{} 自然也是 never 了。

keyof never / any

这两个类型是最特别的

type Keys = keyof never; // string | number | symbol
type Keys = keyof any;   // string | number | symbol

neverany 会被特殊对待,keyof 会返回 "对象能接受的所有 key 类型" —— 也就是 string | number | symbol。 

keyof union types

keyof 遇上 union types,它会变成 intersection types。

怎么说呢?看例子:

// keyof union types
type Keys1 = keyof (string | number);  

// 会变成多个 keyof + intersection types
type Keys2 = keyof string & keyof number;

// 等同于
type Keys3 = keyof string;  // 'toString' | 'valueOf' | 'length' | 'substring' | 'indexOf' | ...等等
type Keys4 = keyof number;  // 'toString' | 'valueOf' | 'toFixed' | 'toExponential' | 'toPrecision' | ...等等
type Keys5 = Keys3 & Keys4; // 'toString' | 'valueOf'(只会留下两边共同拥有的 keys)

keyof object union types 会得到所有 object 共同拥有的 key 类型。

如果没有共同拥有的 key

type Human = { job: string };
type Horse = { breed: string };

type Keys1 = keyof (Human | Horse);     // keyof union types
type Keys2 = keyof Human & keyof Horse; // 会变成多个 keyof + intersection types

type Keys3 = keyof Human;   // 'job'
type Keys4 = keyof Horse;   // 'breed'
type Keys5 = Keys2 & Keys3; // 'job' & 'breed' = never

那最终会是 never

如果其中一个是 non-keyofable

type Keys1 = keyof (undefined | string);     // keyof union types
type Keys2 = keyof undefined & keyof string; // 会变成多个 keyof + intersection types

type Keys3 = keyof undefined; // never
type Keys4 = keyof string;    // 'toString' | 'valueOf' | ...等等
type keys5 = Keys3 & Keys4; 
// never & ('toString' | 'valueOf' | ...等等) 
// 笛卡尔积
// (never & 'toString') | (never & 'valueOf') | ...等等
// never | never | ... 等等
// never

那最终也会是 never

至于 never | stringany | string 的结果会是

type Keys1 = keyof (never | string); // 'length' | 'substring' | 'indexOf' | ...等等
type Keys2 = keyof (any | string);   // string | number | symbol

首先,要有一个概念:keyof (any | string) != keyof any & keyof string

因为要先处理 union types 的规则:union types 中,never 会被自动移除。

因此,keyof (never | string) 其实是 keyof string

keyof (any | string) 同理,先处理 union types 规则:当有更抽象的类型,具体的类型就会被移除,因此 string 会被移除,剩下 any

最后 keyof (any |  string) 其实是 keyof any

如果我不希望它变 intersection types 可以吗?

type Keys1 = keyof ({ name: string } | { age: number});      // keyof union types
type Keys2 = keyof { name: string } & keyof { age: number }; // 会变成 multiple keyof + intersection types,最终是 never 
type keys3 = keyof { name: string } | keyof { age: number }; // 但我希望它不要 intersection 而是 union 可以吗?最终是 'name' | 'age'

可以,但需要更多语法,下面会教。

keyof enum

keyof enum 还蛮特别的

enum Status {
  Processing,
  Shipping,
}

type Keys = keyof Status; // 'toString' | 'toFixed' | ...等等

enum Status 是一个 number enum,所以 keyof Status 等同于 keyof number

换成 string enum 就会变成 keyof string

enum Status {
  Processing = 'Processing',
  Shipping = 'Shipping',
}

type Keys = keyof Status; // 'length' | 'substring' | 'indexOf' | ...等等

如果是综合 string number enum

enum Status {
  Processing,
  Shipping = 'Shipping',
}

type Keys = keyof Status; // 'toString' | 'valueOf'

就会变成 keyof (string | number)

如果想拿到 enum 本身的 key,那就需要搭配 typeof

enum Status {
  Processing,
  Shipping = 'Shipping',
}

type Keys = keyof typeof Status; // 'Shipping' | 'Processing'

总结

keyof 是一个小语法,通常会搭配其它语法一起使用。(下面我们会陆续看到)

它的作用是从对象(key-value pair)中提取出所有 key 的类型。

keyof 不同的类型会有一些小区别:

  1. classobject literalinterface

    这三个都是典型的 key-value pair 对象,keyof 它们就是提取出所有 key 的类型。

  2. stringnumberbooleanbigintsymbol

    keyof string 等价于 keyof String,其余 4 个类型也都是这个规则。

  3. nullundefinedunknown() => voidnew () => unknownobject{}

    keyof 这些会得到 never

  4. neverany

    keyof neverany 会得到 "对象能接受的所有 key 类型" —— 也就是 string | number | symbol

Indexed Access Types の 对象

keyof 可以提取出对象 key 的类型,而 indexed access 则可以提取出对象 value 的类型。

type Person = {
  name: string;
  age: number;
  1: boolean;
};

type TypeOfName = Person['name']; // string
type TypeOfAge = Person['age'];   // number
type TypeOfOne = Person[1];       // boolean
type TypeOfOne1 = Person['1'];    // boolean: Person['1'] 和 Person[1] 是等价的

透过对象 key 的类型(e.g. string literal 'name')指定取出对应的 value 类型。

这就是 indexed access types 的语法。

用 JavaScript 语法来描述,大概是长这样

const person = {
  name: 'string',
  age: 'number',
  1: 'boolean'
};

const valueOfName = person['name']; // 'string'
const valueOfAge = person['age'];   // 'number'
const valueOfOne = person['1'];     // 'boolean'
const valueOfOne1 = person[1];      // 'boolean'

with dynamic key

dynamic key 也是如此,透过对象 key 的类型取出对应的 value 类型。

type Person = {
  [prop: string]: string;
  [prop: symbol]: number;
};

type TypeOfString = Person[string]; // string
type TypeOfNumber = Person[number]; // string:因为 [prop: string] 等价于 [prop: string | number],所有用 number access 也行
type TypeOfSymbol = Person[symbol]; // number

union types

type Person = {
  name: string;
  age: number;
  1: boolean;
};

type ValueTypes1 = Person['name' | 'age']; // string | number
type ValueTypes2 = Person[1 | 'name'];     // string | boolean

透过 union types 可以同时提取出不同 key 对应的 value 类型,它会返回 union types。

我们也可以换一个角度理解:Person['name' | 'age'] 等价于 Person['name'] | Person['age'],只是写法更精简。

依照 union types 的规则,搭配 keyof 就可以一次性取出对象中所有的 value 类型。

type Person = {
  name: string;
  age: number;
  1: boolean;
};

type Keys = keyof Person;          // 'name' | 'age' | 1
type AllValueTypes = Person[Keys]; // string | number | boolean

Person[Keys] 等同于 Person['name' | 'age' | 1]

上面例子 Keys 是 union types,如果换成对象是 union types 呢?

type Person = {
  name: string;
};

type Animal = {
  name: number;
  age : number;
}

type Name = (Person | Animal)['name']; 

等价于

type Name = Person['name'] | Animal['name']; // string | number

需要注意一点,key 必须是 PersonAnimal 共同拥有的。 

type Age = (Person | Animal)['age']; // IDE Error: Property 'age' does not exist on type 'Person | Animal'

像这样就会报错,因为

type Age = Person['age'] | Animal['age']; // IDE Error: Property 'age' does not exist on type 'Person'

其中一个根本拿不到,自然会报错。

enum

enum Status {
  Processing,
  Shipping = 'Shipping',
}

type EnumObject = typeof Status;
type EnumKeys = keyof EnumObject;      // 'Processing' | 'Shipping'
type EnumValue = EnumObject[EnumKeys]; // number | string

Status.ProcessingnumberStatus.Shippingstring

因此,把 enum 对象所有 value 类型提取出来就是 number | string

这段代码里运用到了 typeofkeyof 和 indexed access types 语法,你有开始感受到 TypeScript 编程了吗?

Indexed Access Types の Tuple

indexed access 不只能用在对象,也能用在 Tuple

它可以获取到 Tuple 指定位置的类型。

type Types = [string, number, boolean];

type FirstType = Types[0];  // string
type SecondType = Types[1]; // number
type ThirdType = Types[2];  // boolean

Array 也是可以,只是 Array 没有位置的概念。

type Types = (string | number)[];

type FirstType = Types[0];  // string | number
type SecondType = Types[1]; // string | number

像上面这样,拿 [0] 和 拿 [1] 其实没有区别。

union types

使用 union types 可以同时提取出不同位置的类型

type Types = [string, number, boolean];
type FirstAndThirdTypes = Types[0 | 2]; // string | boolean

[number]

透过 [number] 可以获取所有 Tuple 的类型。

type Types = [string, number, boolean];
type AllTypes = Types[number]; // string | number | boolean

['length']

type Types = [string, number, boolean];
type Length = Types['length']; // 3

'length' 可以拿到 Tuple 当前有多少个 item。

用于 Array 的话,它会返回 number

type Types = string[];
type Length = Types['length']; // number

get last type

type Types = [string, number, boolean];
type LastType = Types[2]; // boolean

上述写法虽然能获取到最后一个的类型,但它是依靠 hardcode [2] 才办到的。

在 JavaScript,想拿 array 的 last item 我们通常会这样写。

const types = ['string', 'number', 'boolean'];
const lastType = types[types.length - 1]; // 'boolean'

我们试试用 TypeScript

type Types = [string, number, boolean];
type Length = Types['length']; // 3
type LastIndex = Length - 1; // IDE error

type LastType = Types[LastIndex]; // boolean 

先获取 Length,再拿 Length - 1 得出 LastIndex,最后再透过 LastIndex 精准取出 LastType

很遗憾,TypeScript 的语法非常有限,上面 Length - 1 会直接报错,因为 TypeScript 不支持这种语法。

如果我们想用目前已掌握的知识去勉强实现,可以这样写  

type Types = [string, number, boolean];

type Length = Types['length']; // 3

type Indexes = [
  -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
];

type Index = Indexes[Length]; // 2 (把 Length 换成 Index)

type LastType = Types[Index]; // boolean

非常巧妙的利用一个 Indexes tuple 来达到 Length - 1 的效果。

好,这里只是想让你感受一些 TypeScript 的编程方式,下面我会用其它方式来获取 LastType,会比目前的实现好很多。

Mapped Types の 对象

Mapped Types 里 'map' 的含义就类似于 JavaScript 里的 Array.map

const values = [1, 2, 3];
const newValues = values.map(v => `version_${v}`); // ['version_1', 'version_2', 'version_3']

Array.map 的作用是 for loop 一个 list,然后把每一个 item 转换成其它形态。

这就是 'map' 的精髓。

回到 TypeScript 的 Mapped Types 也是如此。

我们回看上面提过的一个例子

// 想从这个
type Person = {
  name: string;
  age: number;
};

// 转换成这个
type PromisePerson = {
  name: Promise<string>;
  age: Promise<number>;
};

想把 Person 转换成 PromisePerson

做法是 for loop 每一个 key value,把 value wrap 上一层 Promise

咦...这不就是 'map' 的精髓吗?

'in' keyword

以我们现有的知识,要实现 PersonPromisePerson 的转换,只能写出这样的代码:

type PromisePerson = {
  name: Promise<Person['name']> ;
  age: Promise<Person['age']>;
}

用到了 Indexed Access Types 语法,从 Person 里获取 key value 的类型。

不过这段代码显然还不够好,因为 nameage 是 hardcode 写上去的。

想避免 hardcode,我们就需要一个 for loop Person key value 的技法。

于是,TypeScript 的 in 登场了。

type PromisePerson = {
  [Key in keyof Person] : Promise<Person[Key]>
}

我们来解读一下:

type PromisePerson = {} 就是创建一个 object literal。

keyof Person 会返回 'name' | 'age'

[Key in keyof 'name' | 'age']in 是一个 for loop 语法,它右边是要 loop 的 list(用 union types 表示),左边的 Key 是一个代号(放任何字母都可以),这个 Key 代表当前正在 loop 的那个类型。

以这个例子来说,会 loop 两次,第一次 Key 的类型是 string literal 'name',第二次是 string literal 'age'

{
  [Key in...] : ...
}
方括弧用于 object literal 内,有 dynamic key 的含义。

再搭配 in 就成了 for loop 动态添加 object literal key。

Promise<Person[Key]>:for loop 添加 key 的同时,当然也需要设置 key 的 value。这里利用了 looping 的 Key,从 Person 里取出对应的 value 类型,再 wrap 上一层 Promise

整段代码用 JavaScript 语法来描述,大概是长这样

const person = {
  name: 'string',
  age: 'number',
};

const promisePerson = {}; // 创建新对象

// for loop person keys
for (const key of Object.keys(person)) {
  // 动态添加 person key 给新对象
  // 把 person key value wrap 一层 Promise,作为新对象的 key value。
  promisePerson[key] = `Promise<${person[key]}>`;
}

好,以上就是一个 TypeScript 类型 transformation(类型 A tranform to 类型 B)的具体例子。

用到了 object literal with dynamically added keys、inkeyof、indexed access 等语法。

封装 Mapped Types

// Person to PromisePerson
type Person = {
  name: string;
  age: number;
};
type PromisePerson = {
  [Key in keyof Person] : Promise<Person[Key]>
} 
// PromisePerson = { name: Promise<string>; age: Promise<number> }


// Animal to PromiseAnimal
type Animal = {
  name: string;
  age: number;
}
type PromiseAnimal = {
  [Key in keyof Animal] : Promise<Animal[Key]>
} 
// PromiseAnimal = { name: Promise<string>; age: Promise<number> }

Person 转换成 PromisePersonAnimal 转换成 PromiseAnimal

PromisePersonPromiseAnimal 的代码非常相近,唯一的区别是:一个是采样 Person,另一个是采样 Animal。 

代码相近就应该要封装起来:

type ToPromise<T extends { [prop: string | symbol] : any }> = {
  [Key in keyof T] : Promise<T[Key]>
}

TypeScript function 上面已经教过了,这里不再赘述了。

它主要就是把 PersonAnimal 用参数 T 来代替,其余的语法则都保留。

extends 用来约束参数 T,必须是一个 keyofable。

调用 ToPromise

type PromisePerson = ToPromise<Person>; // { name: Promise<string>; age: Promise<number> }
type PromiseAnimal = ToPromise<Animal>; // { name: Promise<string>; age: Promise<number> }

题外话:约束 T 必须是一个 keyofable。

有一些类型是不能 keyof 的,比如:nullundefined() => voidnew () => unknownobject{}

T extends 这个约束手法只能设定 "T 是一种...",而不能设置成 "T 不是..."。

因此,只能用最接近的写法:

T extends { [prop: string | symbol] : any }

或者 T extends { [prop: keyof never] : any }

或者 T extends Record<keyof never, any>

或者 T extends Record<PropertyKey, any>(PropertyKey 是 TypeScript built-in 的 Type Aliases,它等价于 string | number | symbol

揭秘 Record<T>

上一 part 我们介绍了许多 TypeScript built-in 的 utility function。

其中 Record<T> 就是依靠 Mapped Types 语法完成的。

type Person = Record<'firstName' | 'lastName' | 'fullName', string>;
/*
  效果
  type Person = {
    firstName: string;
    lastName: string;
    fullName: string;
  };
*/

最终要输出一个 object literal。

参数一 string literal union types 作为 object literal 的 keys。

参数二作为 object literal 所有 key 的 value。

用 Mapped Types 语法来实现:

type Keys = 'firstName' | 'lastName' | 'fullName'; // 参数一
type Value = string; // 参数二

type Person = {
  [Key in Keys] : Value
}

主要是依靠 in 来 for loop 参数一的 keys 'firstName' | 'lastName' | 'fullName',而 value 则统一使用参数二 string

接着把语句封装成 function 就可以了。

type MyRecord<Keys, Value> = {
  [Key in Keys] : Value // IDE error: Type 'Keys' is not assignable to type 'string | number | symbol'
}

type Person = MyRecord<'firstName' | 'lastName' | 'fullName', string>;

有一个 error,因为参数一 Keys 要作为 in for loop 的 list,它就必须是 object 可以接受的 Key 类型,也就是 string | number | symbol

type MyRecord<Keys extends string | number | symbol, Value> = {
  [Key in Keys] : Value // ok
}

透过 extends 来约束参数一,确保它是 string | number | symbol,这样就可以了。

如果我们觉得 hardcode string | number | symbol 不够好,还可以改成这样

type MyRecord<Keys extends keyof never, Value> = {
  [Key in Keys] : Value // ok
}

keyof neverkeyof any 会返回 object 可以接受的所有类型,也就是 string | number | symbol。(这一点我们在上面讲解 keyof 时已经提过了)

透过 hover 原生的 Record ultility,IDE 会显示它的实现语法

image

和我们写的大同小异。

微差:KeysKValueTkeyof neverkeyof any,这些都不碍事,命名方式不同而已。

揭秘 Pick<T, Keys>

再揭秘一个 Pick<T, Keys>

type Person = {
  firstName: string;
  lastName: string;
  fullName: string;
};

type PickedPerson = Pick<Person, 'firstName' | 'lastName'>; // 只保留 firstName 和 lastName
/*
  效果
  type PickedPerson = {
    firstName: string;
    lastName: string;
  };
*/

参数一是对象,参数二是要保留的 Keys。

type MyPick<Object, KeysToRetain> = {
  [Key in KeysToRetain] : Object[Key]  
}

同样是用 Mapped Types 语法来实现。

for loop 参数二 KeysToRetain,然后用 looping Key 去参数一 Object 中拿对应的 value 类型。

最后用 extends 约束参数。

type MyPick<
  Object extends Record<PropertyKey, any>, // 参数一必须是 keyofable
  KeysToRetain extends keyof Object        // 参数二必须是参数一对象里的 Keys
> = {
  [Key in KeysToRetain] : Object[Key]  
}

原生的 Pick ultility

image

和我们写的大同小异。

微差:ObjectTKeysToRetain → K,只是命名方式不同而已。

还有一点是,它的 T 没有约束 keyofable。(上面有提到,keyofable 没有绝对的约束方式,所以它选择不约束,其实也还好)

optional key & 揭秘 Partial<Type> —— ?

// 想从这个
type Person = {
  name: string;
  age: number;
};

// 转换成这个
type PartialPerson = {
  name?: string;
  age?: number;
}

想把 Person 转换成 PartialPerson —— 把每个 key 转成 optional。

同样是靠 Mapped Types 语法。

type MyPartial<Object extends Record<PropertyKey, any>> = {
  [Key in keyof Object]? : Object[Key]
}

其它语法都教过了,唯一还没教,也是这里最关键的,就是:key 旁边多了一个问号 ?

它就代表了,key 是 optional。

type PartialPerson = MyPartial<Person>;
/* 
  效果
  type PartialPerson = {
    name?: string;  
    age?: number;  
  };
*/

原生的 Partial ultility

 image

required key & 揭秘 Requried<Type> —— -?

另外,如果想反过来,把 optional 变成不是 optional,那就在 "问号" 前面加一个 "减",形成 -?

type PartialPerson = {
  name?: string;
  age?: number;
};

type MyRequired<Object extends Record<PropertyKey, any>> = {
  [Key in keyof Object]-? : Object[Key]
}

type RequiredPerson = MyRequired<PartialPerson>;
/* 
  效果
  type RequiredPerson = {
    name: string;  
    age: number;  
  };
*/

原生的 Required ultility

 image

readonly key & 揭秘 Readonly<Type> —— readonly

type MyReadonly<Object extends Record<PropertyKey, any>> = {
  readonly [Key in keyof Object] : Object[Key]
}

关键就在 key 的前面加了一个 readonly

使用效果

type Person = {
  name: string;
  age: number;
};

type ReadonlyPerson = MyReadonly<Person>;
/* 
  效果
  type ReadonlyPerson =  {
    readonly name: string;
    readonly age: number;
  };
*/

原生的 Readonly ultility

 image

另外,与 readonly 相反的是 writable key。

它的语法就是在 "readonly" 前面加一个 "减",形成 -readonly

type ReadonlyPerson = {
  readonly name: string;
  readonly age: number;
};

type Writable<Object extends Record<PropertyKey, any>> = {
  -readonly [Key in keyof Object] : Object[Key]
}

type WritablePerson = Writable<ReadonlyPerson>;

/* 
  效果
  type WritablePerson =  {
    name: string;
    age: number;
  };
*/

注:TypeScript 没有 built-in 的 Writable<Type> utility function。

rename key —— as

// 想从这个
type Person = {
  name: string;
  age: number;
};

// 转换成这个
type PersonView = {
  getName: () => string;
  getAge: () => number;
};

想把 Person 转换成 PersonView

有两个区别:

  1. key 不一样,name 变成 getName

  2. value 不一样,string 变成 () => string

type ToView<Object extends Record<PropertyKey, any>> = {
  [Key in keyof Object as `get${Capitalize<Key & string>}`] : () => Object[Key]
}

首先是 as,它用来 rename key。

as 右边是新的 key name,它可以拿原本的 Key 来 modify。

比如 Capitalize 会把原本的 'name' 变成 'Name',再结合整句 template literal 和 prefix 'get',最终就变成了 'getName'

why Key & string?

由于 Capitalize 要求参数必须是 string,而 Keystring | number | symbol,不符合这个约束,因此需要使用 Key & string

它的原理是这样:

Keystring | number | symbol

Key & string 笛卡尔积变成 string & string | number & string | symbol & string

进一步简化变成 string | never | never

再进一步简化成 string

搞定 key rename 后,剩下 value 要处理。

上面教过了,就如同 ToPromise 那样,wrap 一层 function () => Object[Key] 就可以了,把原本的 value 作为 function 的返回。

效果

type PersonView = ToView<Person>;
/* 
  效果
  type PersonView = {
    getName: () => string;
    getAge: () => number;
  }
*/

filter key —— as never

延续上一个例子

type Person = {
  name: string;
  age: number;
  [prop: symbol]: string; // 添加一个 symbol key
};

添加一个 symbol key。

type PersonView = ToView<Person>;
/* 
  效果
  type PersonView = {
    getName: () => string;
    getAge: () => number;
  }
*/

结果 symbol key 没了。why 🤔 ?

因为 as never 不会生成 key。

type ToView<Object extends Record<PropertyKey, any>> = {
  [Key in keyof Object as `get${Capitalize<Key & string>}`] : () => Object[Key]
}

in looping 到 Key = symbol 时,

会变成:as `get${Capitalize<symbol & string>}`

继续简化为:as `get${Capitalize<never>}`

继续简化成:as `get${never}`

继续简化成:as never

as never 就不会生成 key,因此 symbol key 就被移除了。

那如果我们想原封不动地保留 symbol key 可以吗?

可以,但需要更多语法,下面会教。

还有,能不能依据 value 的类型来决定要不要移除 key?

也可以,但需要更多语法,下面会教。

Mapped Types の Distributive Concept

type Objects = { name: string } | { age: number };

type Object = {
  [Key in keyof Objects] : Objects[Key];
}

请问 Object 最终是什么类型?

答案是 {},why 🤔 ?

我们 step by step 看:

第一步演化:[Key in keyof ({ name: string } | { age: number })](关键是 keyof union types) 

第二步演化:[Key in (keyof { name: string } & keyof { age: number })]

第三步演化:[Key in ('name' & 'age')]

第 4 步演化:[Key in never]

最后是 in never,所以一个 key 也没有,就变成了 {}

好,现在我们把 Mapped 封装成 function

type Map<Object> =  {
  [Key in keyof Object] : Object[Key]
}

type Object = Map<Objects>;

请问 Object 最终是什么类型?

你可能会以为它是 {},因为代码和上面一样,只是封装了而已。

不!Object 的类型其实是 { name: string } | { age: number }

为什么不一样 🤔 ?

因为 TypeScript 有一个概念叫 Distributive。

它发生在:

  1. function

  2. 参数是 union types

  3. 参数用于 Mapped

在 Distributive 的作用下,Map<Objects> 会演化成 Map<{ name: string }> | Map<{ age: number }>

因此,最终 Object 的类型是 { name: string } | { age: number }

disable distributive concept

distributive 概念是可以被关闭的。

type Map<Object> =  {
  [Key in keyof (Object & {})] : (Object & {})[Key]
}

type Object = Map<Objects>; // Object 的类型是 {}

这样就没有 distributive 了,结果和没封装成 function 前一样。

它是如何关闭  distributive 的呢?

代码中 keyof (Object & {}) 又是什么意义?

naked type 和 non-naked type

首先,我们要有一个类型概念 —— naked type 和 non-naked type。

在 TypeScript function 里,如果参数 Object 是单独被使用(如:keyof Object),这样 Object 就是 naked type。

反之,如果参数 Object 被 "搭配" 使用(如:keyof (Object & {})Object[]() => Object 等等),这样 Object 就是 non-naked type。

Distributive 概念只会发生在参数是 naked type 的情况,non-naked type 不会有 Distributive 概念。

因此,keyof (Object & {}) 会让 Object 变成 non-naked type,从而阻止 distributive。

keyof (Object & {}) 的演化过程是这样:

keyof (({ name: string } | { age: number }) & {})

keyof (({ name: string } & {}) | ({ age: number } & {}))

keyof ({ name: string } | { age: number }) 

简单说,keyof (Object & {}) 就等同于 keyof Object& {} 只是用来 disable distributive,没有对 Object 产生副作用。 

所以,最后

type Object = Map<Objects>

等同于

type Object = {
  [Key in keyof Objects] : Objects[Key];
}

结果都是 {}

Mapped Types の Tuple

Mapped 可以用来 for loop key-value pair,那 tuple 呢?

能不能 for loop tuple?

// 想从这个
type Types = [string, number, boolean];

// 转换成这个
type PromiseTypes = [Promise<string>, Promise<number>, Promise<boolean>];

我们先试着推理看看

type PromiseTypes = {
  [Key in Types] : Promise<Key>;
};

首先,一开头就错了。

type PromiseTypes = {} 是创建 object literal,不是 tuple。

第二,in Types 也错。

in 的右边必须是 union types,而不是 tuple。

第三,即便我们把 tuple 转成 union types in Types[number]in (string | number | boolean) 还是错,

因为 union types 必须是 object key 能接受的类型,也就说只能是 string | number | symbol

结论:直接用 Mapped for tuple 看来是行不通的。

幸好,TypeScript 有特别开通一条路:

type ToPromise<Tuple extends unknown[]> = {
  [Key in keyof Tuple]: Promise<Tuple[Key]>;
};

看上去就是一般的 Mapped function,唯一特别的是参数的约束类型是 array,而不是 key-value pair 对象。

测试

type PromiseTypes = ToPromise<Types>; // [Promise<string>, Promise<number>, Promise<boolean>]

yeah ~ 成功了 🎉。

这是因为 TypeScript 对:

  1. function

  2. 参数是 tuple

  3. 参数用于 Mapped

有特殊处理。

首先,它会返回 tuple,而不是 object literal。

其次,在 for loop 时,Tuple 就像是 key-value pair

type Tuple = {
  0: string;
  1: number;
  2: boolean;
}

因此它可以 in keyof Tuple 和 Tuple[Key]

总结

keyof 主要用来获取 key-value pair 对象中所有的 key 类型。

Indexed Access Types 用来获取 key-value pair 对象 value 的类型,以及 tuple item 的类型。

Mapped Types 用来 transform key-value pair 对象或 tuple。

 

TypeScript 编程语法 の Conditional、Infer

何谓 Conditional Types?

type T1 = string;
type T2 = T1 extends string ? boolean : number; // T2 的类型是 boolean

它是一个 ternary operation 语法。

如果 T1 是 "一种" string,那么 T2 就是 boolean,否则就是 number

用 JavaScript 语法来描述,大概是长这样

const t1 = 'string';
const t2 = t1 === 'string' ? 'boolean' : 'number'; // t2 是 boolean

有一点要特别注意:Conditional Types 采用的是 extends 而不是 equals,这个 extends 的意思可以理解为,T1 类型能否 assign to T2 类型。

看几个例子:

  1. 'version_1' extends `version_${number}` 是 true

    const v1 = 'version_1';
    const v2: `version_${number}` = v1;

    因为 v1 可以 assign to v2

    通常左边(e.g. string literal)是具体,右边(template literal)是抽象,这样就能 assign。

  2. `version_${number}` extends string 是 true —— template literal 是具体,string 是抽象。

  3. string extends {} 是 true —— string 是具体,empty object type(非 nullundefined)是抽象。

  4. (param: GrandParent) => Child extends (param: Parent) => Parent 是 true

    函数的替代原则,我们上一篇讲解过了。

    简单说就是:参数是逆变,可以更抽象,返回是协变,可以更具体。

    const v1 = (_param: GrandParent) => new Child();
    const v2: (param: Parent) => Parent = v1;

    因此 v1 可以 assign to v2

  5. ['version_1'] extends [`version_${number}`] 是 true —— string literal tuple 是具体,template literal tuple 是抽象

  6. [string, number] extends [string] 是 false

    因为 tuple 是讲求数量的。

    const v1: readonly [string, number] = ['', 0]; 
    const v2: readonly [string] = v1; // IDE Error: Type 'readonly [string, number]' is not assignable to type 'readonly [string]'

    v1 assign 不了给 v2,因为 tuple 的数量不对。

  7. [string, number] extends [string, ...unknown[]] 是 true —— tuple with rest or optional 就可以,因为 rest or optionals 就没有固定数量。

特别的 any、unknown、never

anyunknownnever 比较特殊一点,所以分开讲解。

  1. {} extends unknown 是 true

    任何类型都可以 assign to unknown

    因为左边要具体,右边要抽象,而 unknown 就是最抽象的类型。

  2. {} extends any 是 true

    任何类型都可以 assign to any

    因为 anyunknown 一样,都是最抽象的类型(注:仅当 any 作为被 assign to 的对象时)。
  3. any extends never 是 false

    任何类型(除了 never)都不可以 assign to never 

    如果说 anyunknown 是 top type(最抽象),那么 never 就是 bottom type(最具体,比 string literal 还具体)。

    const v1: any = ''
    const v2: never = v1; // Type 'any' is not assignable to type 'never'

    any 都不能 assign to never 哦。

  4. unknown extends {} 是 false

    unknown 不能 assign to 任何类型(除了 unknownany

    因为最抽象不可能 assign to 任何具体。

  5. never extends '' 是 true

    never 可以 assign to 任何类型

    const throwError = () => { throw new Error() };
    const v1: never = throwError();
    const v2: '' = v1;

    因为 never 是最具体的。

  6. any extends never是 true | false

    我们上面说过,any 是最抽象的,但仅限于当 any 作为 assign to 的对象时 —— 也就是 extends any 的时候。

    反观,当 any extends 的时候,any 就不再是最抽象,反而是最具体的(仅次于 nevernever 才是最最具体的)。

    因此,any 可以 assign to 任何类型(除了 never)。

    const v1: any = 'abc';
    const v2: '' = v1;    // ok
    const v3: never = v1; // IDE Error: Type 'any' is not assignable to type 'never'

    问题来了,既然 any 无法 assign to never,那为什么 any extends never ? true : false 结果会是 true | false,而不是 false 呢?

    因为,TypeScript 针对 any 做了特殊处理,一般 T1 extends T2 是看 T1 是否可以 assign to T2,但 any 不是这样。

    any extends 任何类型结果都是正负的 union types。

    e.g. any extends never ? string : number 结果是 string | number

    e.g. any extends {} ? string : number 结果是 string | number

  7. any[] extends ''[] 是 true

    上面我们说 any extends 任何类型都是正负的 union types。

    不过,这个规则仅限于 any 是 naked type 的情况。

    像这一题,左右都是 array,any 可以 assign to string literal,因此结果是 true。

  8. any[] extends never[] 是 false

    左右都是 array,any 不可以 assign to never,因此是 false。

Condition Types 的用途

上面我们有提过一个 Mapped Types 例子

type Person = {
  name: string;
  age: number;
  [prop: symbol]: string;
};

type ToView<Object extends Record<PropertyKey, any>> = {
  [Key in keyof Object as `get${Capitalize<Key & string>}`] : () => Object[Key]
}

type PersonView = ToView<Person>;
/* 
  效果
  type PersonView = {
    getName: () => string;
    getAge: () => number;
  }
*/

[prop: symbol]: string 被移除掉了。

如果我们想保留 symbol key,那就需要采用 conditional types 语法。

[Key in keyof Object as Key extends string ? `get${Capitalize<Key>}` : Key]

如果 Keystring 才 rename key,如果不是 string 就直接返回 Key 就好。

这样在遇到 symbol 时,它就会保留,而不是像之前那样被 Capitalize<Key & string> 变成 never 过滤掉。

value 的部分也是一样采用 conditional types 语法。

Key extends string ? () => Object[Key] : Object[Key]

如果 Keystring 就 wrap 成函数,如果不是 string 就直接返回 value 类型就好。

最终代码

type Person = {
  name: string;
  age: number;
  [prop: symbol]: string;
};

type ToView<Object extends Record<PropertyKey, any>> = {
  [Key in keyof Object as Key extends string ? `get${Capitalize<Key>}` : Key]: Key extends string ? () => Object[Key] : Object[Key];
};

type PersonView = ToView<Person>;
/* 
  效果
  type PersonView = {
    getName: () => string;
    getAge: () => number;
    [prop: symbol]: string;
  }
*/

yeah ~ symbol key 被保留了 🎉。

Condition Types in function

直接使用 Condition Types 和把它封装成 TypeScript function 来使用是有区别的。

never

type T1 = never extends string ? true : false; // true

never 是最具体的类型,它不管 extends 什么都是 true

但如果是:

  1. function

  2. 参数是 never

  3. 参数用于 Conditional Types(并且是 naked type)

结果就不同了

type Condition<T> = T extends string ? true : false;
type T2 = Condition<never>; // never

不管 never extends 什么,都会返回 never

提醒:这套机制只有 naked type 才会启动,non-naked type 会失效。(和 Mapped 的 disable distributive concept 雷同)

type T1 = never[] extends string[] ? true : false; // true

type Condition<T> = T[] extends string[] ? true : false;
type T2 = Condition<never>; // true

union types

type T1 = string | number extends string ? true : false; // false
string | number 是一种 string 吗?

不是,因为 number 不是 string,因此最终返回的类型是 boolean literal false

但,如果是 function + 参数 + conditional types 就不同了。

它会有 distributive concept

type Condition<T> = T extends string ? true : false;
type T2 = Condition<string | number>; // true | false

 

 

 

 

 

 

never extends whatever = never

type Func<T> = T extends string ? true : false;
type R = Func<never>; // never

当 T 是 never, 它会直接返回 never

any extends whatever = both result union

type A<T> = T extends string ? 'a' : 'b';
type R = A<any>; // 'a' | 'b'

当 T 是 any, 结果是 2 个 conditional result 的 Union

Distributive Conditional Types (当 T 是 Union)

Union + Naked Type

Union 总是被特殊对待的. 当参数是 Union 时, 会有 Array.map 的效果, 看例子

type Func<T> = T extends string ? number : [T];
type Type = Func<string | number | null>; // number | [null] | [number]

// 用 JS 来描述大概是这样
function func(value: any) {
  if (Array.isArray(value)) {
    return value.map(v => (v === 'string' ? 'number' : [value]));
  } else {
    return value === 'string' ? 'number' : [value];
  }
}

但是这种效果只出现在 Naked Type. 

Union + NotNaked Type

没有了 Array.map 概念, T 就单纯是 Union Types

type Func<T> = T[] extends (string | number | null)[] ? [T] : false;
type Type = Func<string | number | null>; // [string | number | null]

 

 

Infer

这里的 infer 和我们上一篇提到的 类型推断 Type Inference 不是同一个东西.

infer 是一个 keyword 专门运用在 conditional 语法中. 它的作用是提取出某个 part 的类型.

看例子

type GetTypeFromPromise<TPromise> = TPromise extends Promise<infer TResolve> ? TResolve : never;
type Type = GetTypeFromPromise<Promise<string>>; // string

type GetFirst<TArray extends unknown[]> = TArray extends [infer First, ...unknown[]]
  ? First
  : never;
type FirstType = GetFirst<[string, number, boolean]>; // string

type SkipFirst<TArray extends unknown[]> = TArray extends [unknown, ...infer Others]
  ? Others
  : TArray;
type Others1 = SkipFirst<[string, number, boolean]>; // [number, boolean]
type Others2 = SkipFirst<[string]>; // []
type Others3 = SkipFirst<[]>; // []

通过 extends 对比两个类型的同时, 把 infer 关键字放到我们感兴趣的类型上, 在返回时就可以运用它了.

infer 可用于嵌套

type Func<T> = T extends { str: infer R } 
  ? (
    R extends string // 用上一层的 infer R 继续做判断
    ? string 
    : number
  ) 
  : null;

infer with extends

type Func<T> = T extends { str: infer R extends string } 
              ? R
              :string

它是一种连续深层匹配. T 必须是对象 { str } "同时" "里面的" str 类型必须是 string

multiple infer

before we get started, 先了解逆变和协变

当出现 multiple infer 时, TS 会把 infer 出来的类型 group 起来.

group 的方式有 2 种, 一种是 group by Union, 一种是 group by Intersection Types (交叉类型)

当 infer 在协变的位置 (比如, 函数返回), 那么会 group by Union

如果 infer 在逆变的位置 (比如,函数参数), 那么会 group by Intersection (基本上只有参数是逆变的啦)

// Union
type Func<T> = T extends { str: infer R; num: infer R } ? R : never;
type Type = Func<{ str: string; num: number }>; // string | number

// Union
type Func1<T> = T extends { str: () => infer R; num: () => infer R } ? R : never;
type Type1 = Func1<{ str: () => string; num: () => number }>; // string | number

// Intercetion
type Func2<T> = T extends { str: (p: infer R) => void; num: (p: infer R) => void } ? R : never;
type Type2 = Func2<{ str: (p: { name: string }) => void; num: (p: { age: number }) => void }>; // { name: string } & { age: number }

Union extends Infer

和 multiple infer 结果是一样的, 只是它 multiple 体现在 Union 上.

type R1 = (() => string) | (() => number) extends () => infer R ? R : never; // string | number

type R2 = ((p: { str: string }) => void) | ((p : { num: number }) => void) extends (p: infer P) => void ? P : never; // { str: string } & { num: number }

infer 只有一个, 但是 extends 前面是 Union 就变成了 multiple 了.

当 Infer 遇上 Function Overload

参考: Github – Function argument inference only handles one overload

function doSomething(): string
function doSomething(): number
function doSomething(): string | number
function doSomething(): string | number {
    return 5;
}
type R = typeof doSomething extends () => infer R ? R : never; // string | number

infer function overload 只会拿到最后一个 overload 的类型. 如果我把 string | number 换成更抽象的 any, 那么 infer 也会变成 any

function doSomething(): string
function doSomething(): number
function doSomething(): any
function doSomething(): any {
    return 5;
}
type R = typeof doSomething extends () => infer R ? R : never; // any
View Code

intersection function overload 也是相同的结果

type DoSomething1 = () => string;
type DoSomething2 = () => number;
type DoSomething3 = () => string | number;
type DoSomething = DoSomething1 & DoSomething2 & DoSomething3;
type R = DoSomething extends () => infer R ? R : never; // string | number
View Code

 

更多 TypeScript 语法 の Recursive 递归

上面我们介绍了编程语言的 5 大特性, variable, set, function, looping, conditional

递归 recursive 当然也是编程语言不可以缺少的特性.

Type Aliases 递归

type JsonValue = string | number | boolean | null | { [property: string]: JsonValue } | JsonValue[];

这个很好理解, 就是声明变量的同时引用变量自身. 

function 递归

函数递归一定是搭配 conditional 的, 不然就 infinite loop 了嘛...

type GetTypeFromPromise<T> = T extends Promise<infer TResolve> ? GetTypeFromPromise<TResolve> : T;
type Result = GetTypeFromPromise<Promise<Promise<Promise<string>>>>; // string

先检查 T 是不是 Promise, 不是就返回, 是的话用 infer 提取出 Resolve, 进行递归.

这个写法干净, 但是 T 可以是任意类型, 不太理想, 也可以换一个写法

type GetTypeFromPromise<T extends Promise<unknown>> = T extends Promise<infer TResolve>
  ? TResolve extends Promise<unknown>
    ? GetTypeFromPromise<TResolve>
    : TResolve
  : never;

type Result = GetTypeFromPromise<Promise<Promise<Promise<string>>>>; // string

强制参数是 Promise.如果 TResolve 还是 Promise 就递归. 这个代码虽然比较长, 但是表达比较符合直觉.

尾调用

TS 在 4.5 时支持了尾调用 (就是返回递归调用). 不然递归超过 49 层就会报错了

type Func<T extends any[]> = T['length'] extends 49 ? [] : [...Func<[1, ...T]>];
type R = Func<[]>; // Error: Type instantiation is excessively deep and possibly infinite.ts(2589)

加了尾调用就可以到 999 层

type Func<T extends any[]> = T['length'] extends 999 ? [] : Func<[1, ...T]>; // 尾调用指的是直接返回递归调用
type R = Func<[]>; 

民间有一个方法可以让它超过 1000 层不报错, 那就是加一句 0 extends 1 ? never

type Func<T extends any[]> = 0 extends 1 ? never :  T['length'] extends 1000 
? [] 
: Func<[1, ...T]>;

type R = Func<[]>; 

它是一个 Bug 来的.

 

回看 Build-in Utility

至此,我们掌握了许多 TS 编程语法,现在回过头去看那些 Utility 就能明白它们底层是如何写出来的了。

当我们 hover 这些 Utility 时,它会显示底层的实现语法。

另外一点,ThisType 和 Intrinsic String Manipulation Types 是 TS compiler 内部实现的,我们无法用底层语法去实现。

当 hover 到这些 Utility 会显示 intrinsic keyword。

 

TS 的里程碑

我接触 TypeScript 的时候是 v1.8, Angular 2.0 的年代.

开始认真写应该是 TS v2.1 的时候. 我记得 v2.8 有了一次大的提升.

然后整个 3.0 就冷冷清清. 一直到 4.0 又有了一次提升. 但这个时候我已经没有什么写 TS 了.

下面是几个重大语法更新的版本号.

2.1 Keyof, Mapped Types

2.8 Conditional Types, Infer

3.1 Mapped types on tuples and arrays

3.7 Recursive Type Aliases

4.1 Template Literal Types 

Recursive Conditional Types

Mapped types Key Remapping via as

可以看到,我写的那几年,缺失了多少特性啊....但很高心,现在终于比较像样了,而我也准备回来认真写 TypeScript 了。

 

总结

这篇我们介绍了 TS 的编程语法 variable, set, function, looping, conditional, recursive.

至此, 我们算是对 TS 这个编程语言有点了解了. 要再进阶就得灵活运用这些语法知识.

下一篇我们会进入类型体操, 会拿一些 Utility 库来学习. 从各种复杂例子中看出各种语法得运用. 

 

 

 

 

 

set / array / collection

集合也是一门语言重要的概念, 用来表达集合的 TS 语法是 Union.

type MyArrayType = string | number | boolean;

// 用 JS 来描述大概是这样
const myArrayType = ['string', 'number', 'boolean'];

还有一个也是可以用来表达集合的类型是 Tuple, 但是比较常用的是 Union, 两个都常被使用 (不同情况有不同玩法)

Tuple 可以 convert 去 Union (下面会教), 但是反过来就不行. 参考: 这个这个 (主要原因是 Union 是没有顺序概念的, Tuple 却是有的,有去没有 ok,没有去有则不行)

type MyArrayType = [string, number, boolean];

// 用 JS 来描述大概是这样
const myArrayType = ['string', 'number', 'boolean'];

有了集合就少不了迭代语法. 但这里我们先不展开迭代语法. 下一 part 才详细讲.

 

 

当 keyof 遇到 Union

type Obj = { str: string } | { str: string, num : number };
type KeyofObj = keyof Obj; // "str"

最终结果只会有每个对象共同拥有的 Key.

如果希望获取到所以的 Keys, 可以这样写

参考: Stack Overflow – Intersection of mapped types

type MyKeyof<T> = T extends Record<infer K, any> ? K : never;
type MyKyeofObj = MyKeyof<Obj>; // str, num

用到了 Union Map + Infer 技巧 (下面会教)

 

posted @ 2022-10-27 00:32  兴杰  阅读(546)  评论(0)    收藏  举报