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

前言

上一篇, 我们提到, TypeScript 进阶有 3 个阶段. 

第一阶段是 "把 TypeScript 当强类型语言使用", 我们已经介绍完了. 

第二阶段是 "把 TypeScript 当编程语言使用"

这篇主要就是介绍这个.

 

逻辑类型

C# 是没有办法表达出类型间的逻辑关系的. 你不能表达 "这个变量的类型是那个函数的第一个参数类型".

但 TS 可以!

function doSomething(str: string): void {}

// const str : string = ''; // C# 只能 hardcode 声明类型是 string
const str: Parameters<typeof doSomething>[0] = ''; // TS 可以表达出 "这个变量的类型是那个函数的第一个参数类型"

Parameters<typeof doSomething>[0] 的意思是, 这个类型是 doSomething 函数的第一个参数类型.

我们先不去理解这个语法是怎么回事. 只要知道 TS 可以表达出这个意思就好了.

 

类型 transform

类型 A > transformation > 类型 B

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

这有点像 RxJS stream 中间一堆 pipe operator 那样.

看例子

// 有个 Person Object Literal Type
type Person = {
  str: string;
  num: number;
};

// 我想搞一个 PromisePerson, 它拥有所有 Person 的属性, 只是类型变成 Promise<原本类型>

// hardcode 的方式
type PromisePerson = {
  str: Promise<string>;
  num: Promise<number>;
};

// transform 的方式
type PromisePerson1 = { [key in keyof Person]: Promise<Person[key]> };

我们先不关心它的语法, 只要知道 TS 有这个表达力就好.

 

由浅入深 (TS 基础编程语法)

我们先学几招简单的 TS 编程语法 >  然后再学几招 TS build-in 封装好的 Utility > 最后再把所有语法学全 > 下一篇我们才学如何把语法融会贯通写出自己的 Utility.

define variable

编程语言最基本的功能之一就是定义变量, 定义变量有两个主要目的, 一是为了 code study, 二是为了复用.

上一篇我们介绍过 Type Aliases, 它其实就是 TS 语言中的 define variable (声明变量)

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

TS 整个语言的目的就是去声明/管理 JS 的类型. 所以 TS variable 的 value alwasy is Type, 这个概念我们要记清楚.

上面例子中, 我声明了 2 个 variables, 分别存放了不同的类型, (注: TS 的 variable case style 是 PascalCase 哦).

typeof

typeof 是 JS 的语法, 但同时它也是 TS 的语法.

首先我们要先学会区分 TS 语句 和 JS 语句.

const value = typeof ''; // 这句是 JS
type MyType = typeof value; // 这句是 TS

很简单, 哪一句是 JS 你一定会知道, 其余的就是 TS 咯 (TS 语句基本上都是 starts with 'type' 或者 'declare ' 这类的)

那 typeof 有啥用呢?

它可以从 JS 变量中提取出 TS 类型.

上面例子中 value 的类型是 string, 在 TS 语句中通过 "typeof value" 引入 JS 语句的 value, 它就能把 value 的类型放入到 TS MyType 变量中.

为什么要这样搞呢? 让 TS 语句和 JS 语句这样传来传去不是挺乱的吗? 

之所以需要 typeof 是因为类型推断. 为了少写代码, 许多时候我们是不声明类型的. 而这时如果需要利用到这些类型, 那么就得从 JS 语句中提取了.

在看几个例子感受一下

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

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

// class 有点特别哦, 因为它本身已经是类型了.
class Person {
  str = '';
  static num = 0;
}
type PersonInstanceType = Person; // { str: string } class 是 new 出来对象的类型
type PersonClassType = typeof Person; // { num: number } typeof class 是这个 class 本身身为对象的类型

注意: class 和 typeof class 的区别.

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 才详细讲.

function & call

编程语言除了有变量, 集合概念, 还有一个重要的概念就是函数. TS 的变量用来封装类型, 函数则用来封装 transform 的过程.

它长这样

type MyFunction<T> = T | string;

type MyType = MyFunction<number>; // MyType is number | string

有点像是 Type Aliases + 泛型的味道. 但我不建议你这样去理解它. 把它当函数看就好了.

MyFunction 是函数的名字.

<T> 是参数

= T | string 是 return

MyFunction<number> 就是 call 这个函数并传入参数 number (记住, TS 都是在处理类型, 所以参数值都是类型)

type MyFunction<T> = T | string;
type MyType = MyFunction<number>; // MyType is number | string

// 用 JS 来描述大概是这样
const myFunction = (value) => [value, 'other value'];
const myType = myFunction('value'); // myType = ['value', 'other value']

小结

到这里, 我们介绍了 TS 作为编程语言的 3 大特性, variable 变量, set 集合, function 函数.

利用这些编程手法, 我们就可以表达出逻辑类型和 transform 类型了. 下一 part 我们学一些 TS build-in 的 Utility.

 

TypeScript Build-in Utility

参考: Docs – Utility Types

Utility 是啥? 用过 JS Lodash Library 的就知道, 它就是许许多多小方法的库, 其目的就是让代码复用和干净. 、

TS 的 Utility 也是同样的, 它有许许多多小方法 (TS function), 让我们 transform 类型.

这 part, 我们不去探索这些 utility function 底层语法是怎样的, 我们只学它的功效, 调用就好.

Partial<Type>

Partial 函数的功能是 transform Object Literal / Class / Interface (之后统称 Object) 的所有属性变成 optional property.

type Person = {
  str: string;
  num: number;
};

type PartialPerson = Partial<Person>;

// 等价于
type PartialPerson1 = {
  str?: string; // optional property
  num?: number; // optional property
};

题外话, 学习这类 Utility 库 (e.g. RxJS operators), 具体使用场景不重要, 我们把所有方法过一遍, 有个印象就好. 等项目遇到问题的时候, 回来翻一翻就可以了.

Required<Type>

和 Partial 相反, 它是把 optional property 变成 not optional

type PartialPerson = {
  str?: string;
  num?: number;
};

type Person = Required<PartialPerson>;

// 等价于
type Person1 = {
  str: string;
  num: number;
};
View Code

Readonly<Type>

顾名思义, 就是把 Object / Array 变成 Readonly

type Obj = { str: string; num: number };
type Arr = string[];

type ReadonlyObj = Readonly<Obj>;
type ReadonlyArr = Readonly<Arr>;

// 等价于
type ReadonlyObj1 = {
  readonly str: string;
  readonly num: number;
};

type ReadonlyArr1 = readonly string[];
View Code

Record<Keys, Type>

Record 用来创建 Object Literal, 特色是所有属性拥有相同的类型.

type Obj = Record<'key1' | 'key2' | 'key3', string>;

// 相等于
type Obj1 = {
  key1: string;
  key2: string;
  key3: string;
};

第一个参数是放入所有的属性 keys, 它用一个集合来表示, 也就是 Union + String Literal

第二个参数是属性的 value, 也就是类型 (记住, TS 都是在处理类型, 我重复很多次了)

Record 函数最终会返回 Object Literal 类型, 它拥有所有的 keys 属性, 每一个属性的类型都相同, 就是第二参数传入的类型.

Pick<Type, Keys>

Pick 是从一个 Object 里选出指定的 keys 保留, 去掉其余的属性.

type Obj = {
  str: string;
  num: number;
  bool: boolean;
};

type PickedObj = Pick<Obj, 'str' | 'bool'>; // 只保留 str 和 bool

// 相等于
type PickedObj1 = {
  str: string;
  bool: boolean;
  // 没有 num 了
};

Omit<Type, Keys>

Omit 和 Pick 相反, 它是选择要删除的, 没有选中的则保留下来.

type Obj = {
  str: string;
  num: number;
  bool: boolean;
};

type OmitedObj = Omit<Obj, 'str' | 'bool'>; // 删除 str bool 只留下 num

// 相等于
type OmitedObj1 = {
  num: number;
};
View Code

Exclude<UnionType, ExcludedMembers>

参数 1, 2 都是 Union.

参数 1 是所有类型, 参数 2 是声明不要保留的类型 (和上面 Omit 的概念差不多, 其实 Omit 底层就是用了 Exclude 函数来完成的哦)

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

type Arr = boolean | string | number | null;
type ExcludedArr = Exclude<Arr, boolean | number>; // left string | null

// 相等于
type ExcludedKeys1 = 'key2' | 'key4';
type ExcludedArr1 = string | null;

来一个复杂点的

type Types = 'key1' | 'key2' | 'key3' | number | boolean;
type ExludedTypes = Exclude<Types, string>; // left number | boolean

string 把 'key1' | 'key2' | 'key3' 都给 exclude 掉了, 其实不难理解, 因为 'key1' 是 String Literal, 它是"一种" string, 所以当声明要把 string exclude 掉时, 它自然也需要被 exclude 掉.

还有一个知识点是, 当 Exclude 到一个不剩时, 它会返回 never.

type Types = Exclude<'key1' | 'key2', 'key1' | 'key2'>; // never

Extract<Type, Union>

它是 Exclude 的相反. 参数 2 声明的类型都是要保留的类型.

type Types = 'key1' | 'key2' | number | boolean;
type ExtractedTypes = Extract<Types, string | boolean>; // left boolean | "key1" | "key2"

NonNullable<Type>

参数是一个 Union, 它会把 Union 内的 null 和 undefined 类型过滤掉, 留下其它的

type Types = string | undefined | number | null | boolean;
type NonNullableTypes = NonNullable<Types>; // left: string | number | boolean

Parameters<Type>

获取函数的所有参数类型, 它返回的是 Tuple 哦.

function doSomething(str: string, num: number): void {}

// 1. 这里用了 typeof 把 JS 语句中的 function 类型提出了出来
// 2. Paramters 函数返回的是 Tuple 而不是 Union 哦
type DoSomethingParameters = Parameters<typeof doSomething>; 

// 相等于
type DoSomethingParameters2 =  [str: string, num: number];

ConstructorParameters<Type>

和 Paramters 函数一样, 只是它用在 class 而不是普通函数. 

class Person {
  constructor(str: string, num: number) {}
}

type PersonCtorParametersTypes = ConstructorParameters<typeof Person>; // [str: string, num: number] 要用 typeof 哦

ReturnType<Type>

顾名思义

function doSomething(str: string, num: number): string | null {
  return Math.random() > 0.1 ? '' : null;
}

type DoSomethingReturnType = ReturnType<typeof doSomething>; // string | null

InstanceType<Type>

如果我们只有 class 的类型, 那么可以通过这个类型获取到它的实例类型.

class Person {}

type PersonInstanceType = Person; // 实例类型
type PersonClassType = typeof Person; // class 类型
type PersonInstanceType1 = InstanceType<typeof Person>; // Person 实例类型

分不清楚实力类型和 class 类似的, 请会看上面教的 typeof.

ThisParameterType<Type>

把函数的 this type 提取出来.

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

type thisType = ThisParameterType<typeof doSomething>; // { name: string }

OmitThisParameter<Type>

它的功能是返回一个新的函数类型, 而这个函数类型是除去了 this 类型的.

function doSomething(this: { name: string }) {}

type DoSomethingFuncType = typeof doSomething; // (this: { name: string; }) => void 
type NoThisTypeFunction = OmitThisParameter<typeof doSomething>; // () => void 没有 this 了

ThisType<Type>

ThisType 函数会返回 Object, 这个 Object 的所有方法里头的 this 类型将会是调用 ThisType 函数时传入的参数.

type Obj = ThisType<{ str: string }>;
const obj: Obj = {
  doSomething() {
    console.log(this.str); // this 指向 { str: string }
  },
};

Intrinsic String Manipulation Types

专本针对 String Literal 和 Template Literal 设计的函数.

一共有四个 Uppercase, Lowercase, Capitalize, Uncaitalize<StringType>

type A = Uppercase<'MyLove'>; // 'MYLOVE'
type B = Lowercase<'MyLove'>; // 'mylove'
type C = Capitalize<'myLove'>; // 'MyLove' 它是 firstCharUppercase
type D = Uncapitalize<'MyLove'>; // ''myLove'' 它时 firstCharLowercase

Awaited<Type>

提取出 Promise 的最终返回的类型 (不管 Promise 嵌套多少层)

type PromiseResolved = Awaited<Promise<Promise<string>>>; // string

NoInfer<Type>

NoInfer 是 TypeScript 5.4 阔别多年推出的新 Utility。

我们透过一个例子去了解它的用途。

function doSomething<TValue extends string>(values: TValue[], defaultValue: TValue): TValue {
  return defaultValue;
}

有一个函数,它包含一个 TValue 泛型。

参数一和参数二都和 TValue 泛型有关。

函数返回是 TValue 泛型。

const value = doSomething(['Windows', 'Linux'], 'Android'); // value: "Windows" | "Linux" | "Android"

调用 doSomething 时,我们没有传入泛型,因此 TypeScript 会依据 TValue 和参数一,二的关系去做类型推断,最终 TValue 的类型会是 "Windows" | "Linux" | "Android" (把参数一和参数二的类型加起来)。

好,那如果我希望参数二 defaultValue 的类型是受限于参数一的 values,我该怎么写?

比如参数一是 'Windows' | 'Linux' 那参数二要嘛是 'Windows' 要嘛是 ‘Linux‘

function doSomething<TValues extends string[]>(values: TValues, defaultValue: TValues[number]): TValues[number] {
  return defaultValue;
}

const value = doSomething(['Windows', 'Linux'] as const, 'Android'); // Error: Argument of type '"Android"' is not assignable to parameter of type '"Windows" | "Linux"'

TypeScript 透过参数一做类型推测,TValues 类型是 ['Windows', 'Linux']

参数二的类型是 TValues 里的所有类型,这不是类型推测,而是一个类型限制,所以传入 'Android' 会报错。

上面这个写法虽然可以,但是代码有点乱,使用 NoInfer 就会干净很多。

function doSomething<TValue extends string>(values: TValue[], defaultValue: NoInfer<TValue>): TValue {
  return defaultValue;
}

const value = doSomething(['Windows', 'Linux'], 'Android'); // Argument of type '"Android"' is not assignable to parameter of type '"Windows" | "Linux"'

只要在参数二 wrap 一层 NoInfer 就可以了。

NoInfer 的意思是不让 TypeScript 对参数二的泛型做类型推断,不做推测,它就变成了一种限制,于是传入 'Android' 就报错了。

小结

以上就是 TS 所有 (截至 version 4.8) 的 build-in Utility 函数. 在真实项目中, 以上 Utility 函数是很缺乏的.

虽然社区已经贡献了更多的 Utility 库, 比如 type-festts-toolbelt, 但依然会遇到不够用的情况.

这时我们就得自己实现. 而要实现类似上面这些函数, 我们就必须增长我们的 TS 语法, 至少我们的搞清楚上面这些函数底层是怎样通过 TS 语法写出来的. (虽然它们是 build-in 的, 但其实我们完全可以自己用 TS 语法去实现它们)

好, 进入下一 part !

 

更多 TypeScript 语法 の keyof, Indexed Access, Mapped

keyof

keyof 的用途是提取出 Object 的所有属性 keys. 有点像 JS 的 Object.keys 函数

type Obj = {
  str: string;
  num: number;
  bool: boolean;
};
type Keys = keyof Obj; // 'str' | 'num' | 'bool' 返回的是 Union String Literal


type Obj1 = {
  [key: string]: string;
};
type Keys1 = keyof Obj1; // string | number (注: dynamic property 类型是 string 的话自带 number, number 的话却不会自带 string. 我忘了什么原因了)

它返回的是 Union String Literal

keyof any

keyof any 是一个小技巧, 它返回 "能成为 key 的所有类型", 目前能成为 key 的类型是 string | number | symbol。

不过,更好的方式是使用 built-in 的 PropertyKey。

type p1 = keyof any;   // string | number | symbol
type p2 = PropertyKey; // string | number | symbol

keyof "able"

它是一个小技巧, 当 keyof NotObjectType 的时候它会返回 never. 

type Result = keyof null; // Result = never

通常它会搭配 conditional (if else) 来做一些事情, 关于 conditional 下面会教

当 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 技巧 (下面会教)

Indexed Access Types

indexed access types 是用来从 Object 或 Tuple 中提取 value 类型的语法.

比如下面这个是从 Object 中通过指定 key 获取到对应的 value 类型.

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

type PersonNameType = Person['name']; // string
type PersonNameOrAgeType = Person['name' | 'age']; // string | number 还可以同时 Union 多个 keys 哦
type PersonNameOrAgeType1 = Person['name'] | Person['age']; // string | number 和上一句是等价的.

Tuple 的语法也是很直观的

type Types = [string, number, boolean];
type FirstType = Types[0]; // string
type LastType = Types[2]; // boolean
type Length = Types['length'] // 3 还能拿 length 哦
type AllTypes = Types[number]; // string | number | boolean 这也是把 Tuple 转换成 Union 的方法

最后一句也是 convert Tuple to Union 的方式哦.

Indexed Access Types + keyof

interface Person {
    firstName: string;
    age: number;
}

type valueTypes = Person[keyof Person] // string | number

可以获取所有 value 的类型。

这招还可以用在 Enum

enum Status {
    Processing = 1 << 0,
    Shipping = 1 << 1,
    Completed = 1 << 2,
    Cancelled = 1 << 3,
    Refunded = 1 << 4,
}

type EnumObject = typeof Status;
type EnumValue = EnumObject[keyof EnumObject]; // Status

假如我们只有 Enum 对象类型,可以通过这招获取到 value 类型。

Mapped Types

mapped types 的 ‘map’ 和 JS array.map 的 map 有相同意义. 就是把一个东西 map 成另一个的东西.

但是在 TS, map 主要是指 Map Object, 它虽然也可以 map Tuple 但其实是把 Tuple 当成 Object 来 map.

create object literal by looping Union 

我们学过下面这个语法 

type Obj = {
  [key: string]: string;
};

它是 Object Literal, 拥有一个 dynamic property

而 Mapped Types 的语法是这样的

type Obj = {
  [Name in 'key1' | 'key2' | 'key3']: Name;
};

和上一个有点像, 但它多了 looping 的概念.

首先我们看左边 [Key in 'key1' | 'key2' | 'key3']

in 这个语法是 for loop 的意思. 

Key 是一个 Aliases 或 Variable

所以整句的意思是 for loop Union 然后把 String Literal 放入变量 Key.

用 JS 表达大概就是 

for (const key of ['key1', 'key2', 'key3']) {}

整句看

type Obj = {
  [Name in 'key1' | 'key2' | 'key3']: Name;
};

// 用 JS 来表达就是
const obj = {};
for (const key of ['key1', 'key2', 'key3']) {
  obj[key] = key
}

所以最后生成出来的 Object 类型就是

type Obj = {
  key1: 'key1',
  key2: 'key2',
  key3: 'key3',
};

上面的例子属于创建一个 Object base on Union keys, 这个还不属于 map, 一般上我们说的 map 是封装一个函数, 把一个 Object / Array transform 成不同的 pattern.

map 改变 value 类型

我们做一个 map all key to string 的函数

type MapAllKeysToString<TObject> = {
  [Key in keyof TObject]: string;
};

MapAllKeysToString 是函数, 也是 Mapped Types

TObject 是参数

通过 keyof TObject 提取出 TObject 的所有 keys

value 设置成 string

用 JS 来表达就是

function mapAllKeysToString(obj) {
  const newObj = {};
  for (const key of Object.keys(obj)) {
    newObj[key] = 'string';
  }
}

所以最终效果是

type MapAllKeysToString<TObject> = {
  [Key in keyof TObject]: string;
};

type Obj = {
  num: number;
  bool: boolean;
};
type NewObj = MapAllKeysToString<Obj>;

// result:
type NewObj = {
  num: string;
  bool: string;
}

Optional and Readonly

Utility 函数有介绍过 Partial, Required, Readonly, 它们就是用 Mapped Types 实现的哦.

type MapToOptional<TObject> = {
  // 在 key + 上 ? 就可以了
  [Key in keyof TObject]?: TObject[Key]; // 利用了 Indexed Access Types 获取原本对象的值类型
};

type MapToReadOnly<TObject> = {
  // 在 key 加上 readonly 就可以了
  readonly [Key in keyof TObject]: TObject[Key];
};

注意, 它利用了 Indexed Access Types 获取原本对象的值类型, 这样就做到了原本不动的返回整个对象, 只修改了 optional 和 readonly 而已.

是不是惊叹 TS 语法设计者的巧思 ?! 他总能用最少最抽象的语义, 打出一个组合拳去扩张语言的表达力. 非常的聪明.

那要反过来做 not optional 和 not readonly 呢? 答案是加上 -

type MapToOptional<TObject> = {
  // 加上 -
  [Key in keyof TObject]-?: TObject[Key];  
};

type MapToReadOnly<TObject> = {
  // 加上 -
  -readonly [Key in keyof TObject]: TObject[Key];
};

symbol - (减) 就是去除的意思.

Rename Key

通过 as 关键字, 我们可以在 map 的过程中修改属性名

type MapToGetter<TObject> = {
  [Key in keyof TObject as `get${Capitalize<string & Key>}`]: () => TObject[Key];
};

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

type NewObj = MapToGetter<Obj>;

// result:
type NewObj = {
  getName: () => string;
  getAge: () => number;
};

3 个知识点

1. as

as `get${Capitalize<string & Key>}`

as 后面跟着的就是 new name. 例子中用了 Template Literal 来实现.

它组合了 get 和原本的 key, 同时利用 Capitalize Utility 函数把 Key 的第一个字母变成 Uppercase.

2. why string & Key

为什么要 string & Key 呢? 单单 Key 不行吗? 

确实不行, 因为 Capitalize 参数必须是 string 类型, 而 Key 可能是 string | number | symbol.

而 stirng & Key 利用了交叉类型的特性 "A & B = 双方都满足的类型"

所以最后是 string & string | number | symbol = string. (起到了 filter 的效果)

3. () => TObject[Key]

value 的部分, 偷偷的把所有原本的类型换成了函数返回

Filter Keys by Map + as never

type Obj = {
  [Key in 'key1' | 'key2' as never]: string;
};
// result:
type Obj = {}

as never 表示这个 key 不要生成.

我们可以利用 Utility Exclude 函数来完成这一点.

type Obj = {
  [Key in 'key1' | 'key2' | 'key3' | 'key4' as Exclude<Key, 'key2' | 'key3'>]: string;
};

// result:
type Obj = {
  key1: string;
  key4: string;
};

解释:

首先 Exclude<Key, 'key2' | 'key3'> 会被多次执行, 因为在 for loop, 每一次都会放入一个 Key 

loop 0: Exclude<'key1', 'key2' | 'key3'> = as 'key1'

loop 1: Exclude<'key2', 'key2' | 'key3'> = as never

loop 2: Exclude<'key3', 'key2' | 'key3'> = as never

loop 3: Exclude<'key4', 'key2' | 'key3'> = as ‘key4’

所以, 最后 key3,4 被 as never filter 掉了.

Map Tuple

TS 对 map Tuple 有特殊对待, 它会返回 Tuple 而不是 Object.

除了上面这个特点, 其余的我们可以把 Tuple 当成 Object 看待.

type Arr = [string, number, boolean];
// 把 Tuple 当 Object 看待, 上面相等于下面
type Obj = {
  '1': string;
  '2': number;
  '3': boolean;
};

然后 map 看看

type MapToFunction<TArray> = {
  [Key in keyof TArray]: () => TArray[Key];
};

type newArr = MapToFunction<[string, number, boolean]>;

// result:
type newArr = [() => string, () => number, () => boolean];

注意: TS 是靠 "keyof T" 这个关键字来判断是否返回 Tuple 的. T 一定要是 Tuple 同时一定要搭配 keyof.

下面是一个错误的例子示范.

type Arr = [string, number];
type CreateArr1<TArray> = {
  [Key in keyof TArray]: string; // keyof 参数 TArray
};
type NewArr1 = CreateArr1<Arr>; // [string, string] 最终是 Tuple



type CreateArr2 = {
  [Key in keyof Arr]: string; // 没有使用参数, 直接引用变量 Tuple
};
// results:
// 最终的类型是 Object 而不是 Tuple, 整个乱掉了. 切记不要这样使用.
type CreateArr2 = {
  [x: number]: string;
  0: string;
  1: string;
  // ... and more
};



type CreateArr3<TArray> = {
  [Key in keyof 'key1' | 'key2']: TArray; // 没有使用参数, 直接引用变量 Tuple
};
type NewArr3 = CreateArr3<Arr>;
// results:
// 最终的类型是 Object 而不是 Tuple, 整个乱掉了. 切记不要这样使用.
type NewArr3 = {
  [x: number]: Arr;
  [Symbol.iterator]: Arr;
  length: Arr;
  toString: Arr;
  // ... and more
};
View Code

Map Tuple to Object

上面提到, TS 对 Tuple 有特殊处理, 它会返回 Tuple, 但如果我硬硬想返回 Object 呢? 也可以

type TupleToObject<T extends (keyof any)[]> = {
  [Key in T[number]]: Key;
};

// 等同于 
// type TupleToObject = {
//   [Key in 'key1' | 'key2']: Key;
// };

type Type = TupleToObject<['key1', 'key2']>;

// result:
type Type = {
  key1: 'key1';
  key2: 'key2';
};

首先约束 T 必须是一个 (keyof any)[], 也就是 (string | number | symbol)[]

然后利用 T[number] 把 Tuple convert 成 Union

在利用只要没有 “keyof T” 就不会返回 Tuple

这样就可以了

Map Union

Map Union 和 Map Tuple 完全不同概念.

Map Union 只是语法糖而已, 类似 Indexed Access 使用 Union 也是语法糖而已.

type MapToPromise<TObject> = {
  [Key in keyof TObject]: Promise<TObject[Key]>;
};

type Type1 = MapToPromise<{ str: string } | [string, number]>;
// 等价于
type Type2 = MapToPromise<{ str: string }> | MapToPromise<[string, number]>;

小结

这 part 我们介绍了 

1. keyof = Object.keys

2. Indexed Access Types = Object['key'] and Tuple[number] 

3. Mapped Types = looping, change key, filter key, change value to an Object

 

更多 TypeScript 语法 の Conditional & Infer

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

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

三元运算符 Ternary operation

type Type1 = string extends string ? number : boolean; // number
type Type2 = number extends string ? number : boolean; // boolean

// 用 JS 来描述大概是这样
const type1 = 'string' === 'string' ? 'number' : 'boolean';
const type2 = 'number' === 'string' ? 'number' : 'boolean';

三元运算符我就不多解释了. 如果想实现 if (a && b) 那就不断嵌套三元符 (没有更优雅的写法了).

extends 概念

我们关注在 'extends' 这个 keyword 上.

注意: 它不是 equals 而是 extends.

extends String Literal

type Type = 'string literal' extends string ? true : false; // true

答案是 true, 因为 String Literal 也是 "一种" string

extends Union

type Type = 'a' | 'b' extends 'a' | 'b' | 'c' ? true : false; // true

答案是 true, 因为 'a' | 'b' 是 "一种" 'a' | 'b' | ‘c’

A extends B 表示 B 比较抽象, 能 cover 的 value 比较多

比如 string literal extends string, 明显 string 比较抽象, 能 cover 的 value 比较多

比如 'a' | 'b' extends 'a' | 'b' | 'c', abc 比较抽象能 cover value 比较多

extends Object

type ObjectA = { str: 'string literal'; num: number };
type ObjectB = { str: string };
type Type = ObjectA extends ObjectB ? true : false; // true

答案是 true, A 属性可以多 (多了 num), 类型可以更具体 (string literal).

extends Tuple

type ArrayA = ['string literal', number, boolean];
type ArrayB = [string, number];
type Type = ArrayA extends ArrayB ? true : false; // false

答案是 false, A 的类型可以更具体 (string literal), 但是数量不可以多, 因为 Tuple 是讲究数量的.

除非声明 ...string[], 这表示可以接收更多

type ArrayA = ['string literal', number, boolean];
type ArrayB = [string, number, ...unknown[]]; // 多了 ...unknown[]
type Type = ArrayA extends ArrayB ? true : false; // true

extends function 

extends 函数比较难理解, 因为它涉及到逆变, 协变的概念. 上一篇我们已经解释过了, 这里不再解释, 只看结果.

class GrandParent {
  gValue: string = '';
}
class Parent extends GrandParent {
  pValue: string = '';
}
class Child extends Parent {
  cValue: string = '';
}

type GetName1 = (param: Parent) => Parent;
type GetName2 = (param: GrandParent) => Child;

type Type = GetName2 extends GetName1 ? true : false; // true

背起来: FuncA extends FuncB 要求 FuncA 参数要更抽象(逆变), 返回要更具体(协变).

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)

before we get started, 先了解一个新概念, Naked Type and NotNaked Type

Naked Type vs NotNaked Type

// T extends string                             // 这个是 Naked

// T[] extends string[]                         // 这个是 NotNaked, 因为 T 被 array [] 包装了
// () => T extends () => string                 // 这个是 NotNaked
// (v: T) => void extends (v : string) => void  // 这个是 NotNaked
// [T] extends [string]                         // 这个是 NotNaked

Naked (裸) 就是单独的 T, NotNaked 就是 T 被各种 pattern 包装了, 比如 array, function.

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 库来学习. 从各种复杂例子中看出各种语法得运用. 

 

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