# Typescript 进阶

# 前言

静态类型编程语言都有自己的类型系统,从简单到复杂可以分为 3 类:

  • 简单类型系统
  • 支持泛型的类型系统
  • 支持类型编程的类型系统

TypeScript 就属于这第三种类型系统, 通过给 JavaScript 添加了一套静态类型系统,从动态类型语言变成了静态类型语言,可以在编译期间做类型检查,提前发现一些类型安全问题。同时并没有改变 JS 的语法,只是做了扩展,是 JavaScript 的超集

  • 编译时: 通过 Typescript 来进行静态类型检查
  • 运行时: 使用 Javascript 自己的运行时类型检查

这套类型系统支持泛型,也就是类型参数,有了一些灵活性。而且又进一步支持了对类型参数的各种处理,也就是类型编程,灵活性进一步增强。

现在 TS 的类型系统是图灵完备的,JS 可以写的逻辑,用 TS 类型都可以写。

但是很多类型编程的逻辑写起来比较复杂,因此被戏称为类型体操。

本文将盘点Typescript 的进阶核心知识点

# 类型兼容性检查

# 方式一: 使用 declare

在进行类型比较时,需要使用一个具有具体类型的变量与一个类型进行赋值操作,在“只是想要进行类型比较”的前提下,其实并没有必要真的去声明两个变量,即涉及了值空间的操作。我们完全可以只在类型空间中(你可以理解为用于存放 TypeScript 类型信息的内存空间)比较这些类型,只需要使用 declare 关键字:

interface Foo {
  name: string
  age: number
}
interface Bar {
  name: string
  job: string
}

// 只想获取类型是否兼容, 不需要进行复制操作
let foo: Foo = {
  name: 'hello',
  age: 18
}
let bar: Bar = {
  name: 'hello',
  job: 'fe'
}
foo = bar

// 取而代之
declare let foo: Foo
declare let bar: Bar
foo = bar

# 方式二: 使用 extends

如果返回 1,则说明 'hello'string 的子类型。否则,说明不成立。但注意,不成立并不意味着 string 就是 'hello' 的子类型了

type Result = 'hello' extends string ? 1 : 2

类型兼容性  总结

对于a = b,如果成立,意味着 <b 的类型> extends <a 的类型> 成立,即 b 类型a 类型的子类型

# null、undefined 及 void

TypeScript 中,nullundefined 类型都是有具体意义的类型。这两者在没有开启 strictNullChecks 检查的情况下,会被视作其他类型的子类型,比如 string 类型会被认为包含了 nullundefined 类型:

void 在Javascript会强制将后面的函数声明转化为了表达式, 在 TypeScript 中, void用于描述一个内部没有 return 语句,或者没有显式 return 一个值的函数的返回值

const tmp1: null = null
const tmp2: undefined = undefined

const tmp3: string = null // 仅在关闭 strictNullChecks 时成立,下同
const tmp4: string = undefined

function func1(): void {}
function func2(): void {
  return
}
// 此处用undefined更合适一点
function func3(): undefined {
  return undefined
}

# object、Object 及 { }

为了更好地区分 Object、object 以及{}这三个具有迷惑性的类型, 我们把三者区别罗列出来:

  • 在任何时候都不要,不要,不要使用 Object 以及类似的装箱类型(String、Number、Boolean、Symbol)。

  • 当你不确定某个变量的具体类型,但能确定它不是原始类型(非原始类型的类型,即数组、对象与函数类型),可以使用 object。但我更推荐进一步区分,也就是使用 Record<string, unknown> 或 Record<string, any> 表示对象,unknown[] 或 any[] 表示数组,(...args: any[]) => any 表示函数这样。

  • 我们同样要避免使用{}。{}意味着任何非 null / undefined 的值,从这个层面上看,使用它和使用 any 一样恶劣。

# Symbol 的唯一性

Symbol 在 JavaScript 中代表着一个唯一的值类型,它类似于字符串类型,可以作为对象的属性名,并用于避免错误修改 对象 / Class 内部属性的情况。而在 TypeScript 中,symbol 类型并不具有这一特性,一百个具有 symbol 类型的对象,它们的 symbol 类型指的都是 TypeScript 中的同一个类型。为了实现“独一无二”这个特性,TypeScript 中支持了 unique symbol 这一类型声明,它是 symbol 类型的子类型,每一个 unique symbol 类型都是独一无二的。

const uniqueSymbolFoo: unique symbol = Symbol('hello')

// 类型不兼容
const uniqueSymbolBar: unique symbol = uniqueSymbolFoo

在 TypeScript 中,如果要引用已创建的 unique symbol 类型,则需要使用类型查询操作符 typeof :

declare const uniqueSymbolFoo: unique symbol

const uniqueSymbolBaz: typeof uniqueSymbolFoo = uniqueSymbolFoo

# 函数声明方式

函数声明一般直接在函数中进行参数和返回值的类型声明,要么使用类型别名将函数声明抽离出来, 不建议使用函数类型声明混合箭头函数声明的方式

// 方式一 直接标注返回值(推荐)
const foo = (name: string): number => {
  return name.length
}

// 方式二 函数类型声明混合箭头函数声明(不推荐)
const foo: (name: string) => number = (name) => {
  return name.length
}

// 方式三 类型别名
type FuncFoo = (name: string) => number
const foo: FuncFoo = (name) => {
  return name.length
}

// 方式四 接口
interface FuncFooStruct {
  (name: string): number
}

异步函数、Generator 函数等类型签名 对于异步函数、Generator 函数、异步 Generator 函数的类型签名,其参数签名基本一致,而返回值类型则稍微有些区别:

async function asyncFunc(): Promise<void> {}

function* genFunc(): Iterable<void> {}

async function* asyncGenFunc(): AsyncIterable<void> {}

# 类声明

唯一需要注意的是,setter 方法不允许进行返回值的类型标注,你可以理解为 setter 的返回值并不会被消费,它是一个只关注过程的函数。类的方法同样可以进行函数那样的重载

class Foo {
  prop: string

  constructor(inputProp: string) {
    this.prop = inputProp
  }

  print(addon: string): void {
    console.log(`${this.prop} and ${addon}`)
  }

  set propA(value: string) {
    this.prop = `${value}+A`
  }
}

在派生类中覆盖基类方法时,我们并不能确保派生类的这一方法能覆盖基类方法,万一基类中不存在这个方法呢?所以,TypeScript 4.3 新增了 override 关键字,来确保派生类尝试覆盖的方法一定在基类中存在定义:

class Base {
  printWithLove() {}
}

class Derived extends Base {
  override print() {
    // ...
  }
}

抽象类中的成员需要使用 abstract 关键字才能被视为抽象类成员,我们可以使用implements来实现一个抽象类。对于抽象类, 我们必须完全实现这个抽象类的每一个抽象成员。需要注意的是,在 TypeScript 中无法声明静态的抽象成员。

abstract class AbsFoo {
  abstract absProp: string
  abstract get absGetter(): string
  abstract absMethod(name: string): string
}
class Foo implements AbsFoo {
  absProp: string = 'hello'
  get absGetter() {
    return 'hello'
  }
  absMethod(name: string) {
    return name
  }
}

对于抽象类,它的本质就是描述类的结构, 与 interface 类似。interface 不仅可以声明函数结构,也可以声明类的结构。在这里,我们让类去实现了一个接口。这里接口的作用和抽象类一样,都是描述这个类的结构。除此以外,我们还可以使用 Newable Interface 来描述一个类的结构(类似于描述函数结构的 Callable Interface):

// 接口描述类
interface FooStruct {
  absProp: string
  get absGetter(): string
  absMethod(input: string): string
}

class Foo implements FooStruct {
  absProp: string = 'hello'

  get absGetter() {
    return 'hello'
  }

  absMethod(name: string) {
    return name
  }
}
//  使用Newable Interface描述类
class Foo {}

interface FooStruct {
  new (): Foo
}

declare const NewableFoo: FooStruct

const foo = new NewableFoo()

私有构造函数来实现一个静态类, 把类作为 utils 方法时,此时 Utils 类内部全部都是静态成员,我们也并不希望真的有人去实例化这个类。此时就可以使用私有构造函数来阻止它被错误地实例化:

class Utils {
  public static identifier = 'hello'

  private constructor() {}

  public static makeUHappy() {}
}

# any、unknown、never 及 void

  • never 代表不可达,比如函数抛异常的时候,返回值就是 never, 它属于底层类型(Bottom Type)
  • void 代表空,可以是 undefined 或 never。
  • any 是任意类型,任何类型都可以赋值给它,它也可以赋值给任何类型(除了 never), 它属于顶层类型(Top Type)
  • unknown 是未知类型,任何类型都可以赋值给它,但是它不可以赋值给别的类型, 它属于顶层类型(Top Type)

如果是类型不兼容报错导致你使用 any,考虑用类型断言替代,我们下面就会开始介绍类型断言的作用。

如果是类型太复杂导致你不想全部声明而使用 any,考虑将这一处的类型去断言为你需要的最简类型。如你需要调用 foo.bar.baz(),就可以先将 foo 断言为一个具有 bar 方法的类型。

如果你是想表达一个未知类型,更合理的方式是使用 unknown

越是底层的类型,也就是再少一些类型信息

而内置类型 never 就是这么一个“少的什么都没有”的底层类型, 是整个类型系统层级中最底层的类型

通常我们不会显式地声明一个 never 类型,它主要被类型检查所使用。但在某些情况下使用 never 确实是符合逻辑的,比如一个只负责抛出错误的函数:

function justThrow(): never {
  throw new Error()
}
// 联合类型中, 会直接舍弃掉never
type UnionWithNever = 'hello' | 599 | true | void | never

忽略 never 的场景:

  • 与其他类型组合 A 成联合类型(A| never), 结果类型就是 A
  • never作为索引类型的 key, 该条数据直接被忽略
  • never不触发分布式条件类型, 而是直接返回never

# Interface、 Type

interface 就是描述对象对外暴露的接口,其不应该具有过于复杂的类型逻辑,最多局限于泛型约束与索引类型这个层面。而 type alias 就是用于将一组类型的重命名,或是对类型进行复杂编程, 详细的区别如下:

  • 在对象扩展情况下,interface 使用 extends 关键字,而 type 使用交叉类型(&)
  • 同名的 interface 会自动合并,并且在合并时会要求兼容原接口的结构
  • interfacetype 都可以描述对象类型、函数类型、Class类型,但 interface 无法像 type 那样表达元组、一组联合类型等等
  • interface 无法使用映射类型等类型工具,也就意味着在类型编程场景中我们还是应该使用 type

# as const

TypeScript 默认推导出来的类型并不是字面量类型, 但是类型编程很多时候是需要推导出字面量类型的,这时候就需要用 as const

const o = {
  a: 'hello',
  b: 'world'
}
// 类型提示为: type TypeO = { a: string; b: string; }
type TypeO = typeof o

const b = {
  a: 'hello',
  b: 'world'
} as const
// 类型提示为: type TypeB = { readonly a: "hello"; readonly b: "world"; }
type TypeB = typeof b

# 类型断言

# 使用as

类型断言的正确使用方式是,在 TypeScript 类型分析不正确或不符合预期时,将其断言为此处的正确类型

实际上类型断言的工作原理和类型层级有关,在判断断言是否成立,即差异是否能接受时,实际上判断的即是这两个类型是否能够找到一个公共的父类型。比如 { } 和 { name: string } 其实可以认为拥有公共的父类型 {}(一个新的 {}!你可以理解为这是一个基类,参与断言的 { } 和 { name: string } 其实是它的派生类)。

interface IFoo {
  name: string
}

declare const obj: {
  foo: IFoo
}

const { foo = {} as IFoo } = obj

类型断言还有一种用法是作为代码提示的辅助工具,比如对于以下这个稍微复杂的接口:

interface IStruct {
  foo: string
  bar: {
    barPropA: string
    barPropB: number
    barMethod: () => void
    baz: {
      handler: () => Promise<void>
    }
  }
}

假设你想要基于这个结构随便实现一个对象,你可能会使用类型标注:

const obj: IStruct = {}

这个时候等待你的是一堆类型报错,你必须规规矩矩地实现整个接口结构才可以。但如果使用类型断言,我们可以在保留类型提示的前提下,不那么完整地实现这个结构:

// 这个例子是不会报错的
const obj = <IStruct>{
  bar: {
    baz: {}
  }
}

# 使用satisfies

TypeScript 4.9版本增加了一个新的关键词 satisfies, 新的 satisfies 操作符可以验证表达式的类型是否匹配某种类型,而不更改本身表达式的结果类型

/* 声明一个类型 TypeA */
interface TypeA {
  amount: number | string
}

const record = {
  amount: 20
} satisfies TypeA

const record1 = {
  amount: 20
} as TypeA

// record.amount 的类型为 number, 就不必断言了
record.amount.toFixed(2)
// record1.amount 的类型为 number | string, 此处就需要再次断言为number
(record1 as number).amount.toFixed(2)

上面的写法中, 既能保证 record 是符合 TypeA 类型的,又不影响本身 record 的实际类型 为 { amount : number }

所以在用 record.amount.toFixed(2) 时,record.amount 的类型已经是number, 就不必进行强制类型转换

总的来说, satisfies 可以用来捕获很多潜在的错误,确保一个对象是否符合某种数据类型, 但不会改变对象本身的类型

# 类型层级

  • 最顶级的类型,any 与 unknown
  • 特殊的 Object ,它也包含了所有的类型,但和 Top Type 比还是差了一层
  • String、Boolean、Number 这些装箱类型
  • 原始类型与对象类型
  • 字面量类型,即更精确的原始类型与对象类型嘛,需要注意的是 null 和 undefined 并不是字面量类型的子类型
  • 最底层的 never

# 类型工具

  • 如果按照使用方式来划分, 类型工具可以分成三类:操作符关键字专用语法

  • 按照使用目的来划分,类型工具可以分为 类型创建类型安全保护 两类。

类型创建的类型工具,它们的作用都是基于已有的类型创建新的类型,这些类型工具包括类型别名交叉类型索引类型映射类型

# 类型别名

类型别名可以这么声明自己能够接受泛型(我称之为泛型坑位), 一旦接受了泛型,我们就叫它工具类型, 它的主要意义是基于传入的泛型进行各种类型操作

type Factory<T> = T | number | string

# 交叉类型

  • 交叉类型 &

需要符合定义的所有类型,才能说实现了这个交叉类型,即 A & B,需要同时满足 A 与 B 两个类型才行

  • 联合类型 |

只需要符合成员之一即可(类似于或运算),而交叉类型需要严格符合每一位成员(类似于与运算)

当一堆类联合体类型中,每一个类型都具有一个独一无二的,能让它鹤立鸡群的属性, 可以称这种联合类型为可辨识联合类型

type Struct1 = {
  primitiveProp: string
  objectProp: {
    name: string
  }
}

type Struct2 = {
  primitiveProp: number
  objectProp: {
    age: number
  }
}
type Composed = Struct1 & Struct2
type PrimitivePropType = Composed['primitiveProp'] // never
type ObjectPropType = Composed['objectProp'] // { name: string; age: number; }

// 可辨识联合类型
interface Foo {
  foo: string
  fooOnly: boolean // 可辨识属性
  shared: number
}

interface Bar {
  bar: string
  barOnly: boolean // 可辨识属性
  shared: number
}

# 索引类型

索引类型主要包括三部分:

  • 索引签名类型
  • 索引类型查询
  • 索引类型访问

索引签名类型主要指的是在接口或类型别名中,通过以下语法来快速声明一个键值类型一致的类型结构:

interface AllStringTypes {
  [key: string]: string
}

type AllStringTypes = {
  [key: string]: string
}

索引类型查询,主要利用 keyof 操作符实现。严谨地说,它可以将对象中的所有键转换为对应字面量类型,然后再组合成联合类型; 此处需要特别注意一个常用操作:

keyof any 也就等同于 number | string | symbol

interface Foo {
  hello: 1
  599: 2
}
// 使用伪代码可以理解为: type FooKeys = Object.keys(Foo).join(" | ");
type FooKeys = keyof Foo // "hello" | 599

索引类型访问, 是通过 obj[type] 的方式来动态访问一个一个索引类型的值类型

interface NumberRecord {
  [key: string]: number
}
type PropType = NumberRecord[string] // number

# 映射类型

映射类型的主要作用即是基于键名映射到键值类型, in操作符属于映射类型语法, 用于遍历目标类型的公开属性名

type Clone<T> = {
  [K in keyof T]: T[K]
}
类型工具 创建新类型的方式 常见搭配
类型别名 将一组类型/类型结构封装,作为一个新的类型 联合类型、映射类型
工具类型 在类型别名的基础上,基于泛型去动态创建新类型 基本所有类型工具
联合类型 创建一组类型集合,满足其中一个类型即满足这个联合类型\| 类型别名、工具类型
交叉类型 创建一组类型集合,满足其中所有类型才满足映射联合类型& 类型别名、工具类型
索引签名类型 声明一个拥有任意属性,键值类型一致的接口结构 映射类型
索引类型查询 从一个接口结构,创建一个由其键名字符串字面量组成的联合类型 映射类型
索引类型访问 从一个接口结构,使用键名字符串字面量访问到对应的键值类型 类型别名、映射类型
映射类型 从一个联合类型依次映射到其内部的每一个类型 工具类型

# 类型守卫

对于类型守卫, 个人理解的是解决逻辑中的类型推导问题, 属于类型系统的特性增强

# typeof

typeof 返回的类型就是当你把鼠标悬浮在变量名上时出现的推导后的类型,并且是最窄的推导程度(即到字面量类型的级别)

只能支持在自己的函数体内使用 typeof 进行流程类型保护, 不支持函数抽象

// 只支持typeof就在自己的函数体内
function foo(input: string | number) {
  if (typeof input === 'string') {
    input.replace('hello', 'hello599')
  }
  if (typeof input === 'number') {
    input.toFixed()
  }
}
// 如果进行函数抽象, 那么typeof将无法进行类型保护
function isString(input: unknown): boolean {
  return typeof input === 'string'
}
function foo(input: string | number) {
  if (isString(input)) {
    // 类型“string | number”上不存在属性“replace”。
    input.replace('hello', 'hello599')
  }
  if (typeof input === 'number') {
    input.toFixed()
  }
}

# is

在实际的使用中,将判断逻辑封装起来提取到函数外部进行复用非常常见。为了解决typeof这一类型控制流分析的能力不足, TypeScript 引入了 is 关键字来显式地提供类型信息

// isString 函数称为类型守卫
function isString(input: unknown): input is string {
  return typeof input === 'string'
}
function foo(input: string | number) {
  if (isString(input)) {
    // 正确了
    input.replace('hello', 'hello599')
  }
  if (typeof input === 'number') {
  }
}

# in

in 作为类型保护作用, 用于判断 object 或其原型链上有该属性

interface Foo {
  foo: string
  fooOnly: boolean
  shared: number
}

interface Bar {
  bar: string
  barOnly: boolean
  shared: number
}

function handle(input: Foo | Bar) {
  if ('foo' in input) {
    input.fooOnly
  } else {
    input.barOnly
  }
}

# instanceof

instanceof,它判断的是原型级别的关系

class FooBase {}

class BarBase {}

class Foo extends FooBase {
  fooOnly() {}
}
class Bar extends BarBase {
  barOnly() {}
}

function handle(input: Foo | Bar) {
  if (input instanceof FooBase) {
    input.fooOnly()
  } else {
    input.barOnly()
  }
}

# asserts

TypeScript 3.7 版本专门引入了 asserts 关键字来进行断言场景下的类型守卫; 使用 asserts condition ,而 condition 来自于实际逻辑!这也意味着,我们将 condition 这一逻辑层面的代码,作为了类型层面的判断依据,相当于在返回值类型中使用一个逻辑表达式进行了类型标注。

asserts 是一种类型谓词,告诉编译器将某个变量的类型缩小为更具体的子类型, 以便在后续代码中获得更精确的类型检查

下面例子定义了一个 isNumber 函数,它的返回类型是 asserts value is number。这告诉 TypeScript 编译器,当调用 isNumber 函数时,如果函数执行通过(没有抛出异常),则可以确定参数 valuenumber 类型,因此在 doubleIfNumber 函数中,我们可以放心地进行乘法运算,而不会出现类型错误。

请注意,asserts 不会对传入的参数进行转换或修改,它只是在运行时告诉编译器一个断言,从而使得在该断言范围内可以应用更严格的类型检查。如果断言失败,将会抛出一个异常。因此,在使用 asserts 时,确保你了解代码中的类型约束,并在条件允许的情况下使用它。

function isNumber(value: unknown): asserts value is number {
  if (typeof value !== 'number') {
    throw new Error('Assertion failed: value is not a number');
  }
}

function doubleIfNumber(value: unknown): number | string {
  isNumber(value);
  return value * 2; // 这里可以放心地进行乘法运算,因为编译器认识到 value 是 number 类型
}

console.log(doubleIfNumber(5)); // 输出: 10
console.log(doubleIfNumber('hello')); // 在运行时抛出 Error: Assertion failed: value is not a number

# 泛型约束

在泛型中,我们可以使用 extends 关键字来约束传入的泛型参数必须符合要求。关于 extendsA extends B 意味着 A 是 B 的子类型,也就是说 A 比 B 的类型更精确,或者说更复杂。具体来说,可以分为以下几类。

  • 更精确,如字面量类型是对应原始类型的子类型,即 'hello' extends string,599 extends number 成立。类似的,联合类型子集均为联合类型的子类型,即 1、 1 | 21 | 2 | 3 | 4的子类型。
  • 更复杂,如 { name: string }{} 的子类型,因为在 {} 的基础上增加了额外的类型,基类与派生类(父类与子类)同理。

泛型参数存在默认约束(在下面的函数泛型、Class 泛型中也是)。这个默认约束值在 TS 3.9 版本以前是 any,而在 3.9 版本以后则为 unknown。在 TypeScript ESLint 中,你可以使用 no-unnecessary-type-constraint (opens new window) 规则,来避免代码中声明了与默认约束相同的泛型约束。

提示

一个接口类型可以使用Record<keyof any, any>来约束

在 TypeScript 中,函数的泛型约束可以有多种写法,常见的包括:

  • 将泛型约束写在尖括号中
function identity<T extends string | number>(value: T): T {
  return value
}

在这种写法中,尖括号中的 T 表示一个泛型类型参数,它被约束为 stringnumber 类型。这个约束会对函数参数和返回值类型起到限制作用。

  • 将泛型约束写在函数参数中
function identity<T>(value: T extends string | number ? T : never): T {
  return value
}

在这种写法中,函数参数的类型是一个条件类型 T extends string | number ? T : never,表示如果 T 可以赋值为 stringnumber 类型,则参数类型为 T,否则参数类型为 never,这样可以达到和尖括号中的泛型约束相同的效果。

  • 将泛型约束写在类型别名中
type StringOrNumber = string | number

function identity<T extends StringOrNumber>(value: T): T {
  return value
}

在这种写法中,将泛型约束定义在一个类型别名 StringOrNumber 中,然后使用 extends 关键字将泛型参数 T 约束为 StringOrNumber 类型。

这些写法各有优缺点,选择哪种写法主要取决于具体的情况和个人喜好。

# 类型兼容性系统

按照类型系统特性来分类的话, TypeScript 的类型系统属于结构化类型系统, 还有另一种类型系统叫做标称类型系统

  • 结构化类型系统

  • 标称类型系统

结构化类型又称之为鸭子类型, 鸭子类型中两个类型的关系是通过对象中的属性方法来判断的。它基于类型结构进行判断类型兼容性。结构化类型系统在 C#、Python、Objective-C 等语言中都被广泛使用或支持。

除了基于类型结构进行兼容性判断的结构化类型系统以外,还有一种基于类型名进行兼容性判断的类型系统,标称类型系统。

# 结构化类型系统

TypeScript 比较两个类型并非通过类型的名称,而是比较这两个类型上实际拥有的属性与方法。也就是说,这里实际上是比较 Cat 类型上的属性是否都存在于 Dog 类型上,这就是结构化类型系统的特性

class Cat {
  meow() {}
  eat() {}
}
class Dog {
  eat() {}
}
function feedCat(cat: Cat) {}
// 报错!
feedCat(new Dog())

# 标称类型系统

要在 TypeScript 中模拟实现  标称类型系统,其实我们也只需要为类型额外附加元数据即可,比如 CNY 与 USD,我们分别附加上它们的单位信息即可,但同时又需要保留原本的信息(即原本的 number 类型)

/* 类型层面处理 */
export declare class TagProtector<T extends string> {
  protected __tag__: T
}
export type Nominal<T, U extends string> = T & TagProtector<U>

export type CNY = Nominal<number, 'CNY'>

export type USD = Nominal<number, 'USD'>

const CNYCount = 100 as CNY
const USDCount = 100 as USD
function addCNY(source: CNY, input: CNY) {
  return (source + input) as CNY
}
addCNY(CNYCount, CNYCount)
// 报错了!
addCNY(CNYCount, USDCount)

/* 逻辑层面解决 */
class CNY {
  private __tag!: void
  constructor(public value: number) {}
}
class USD {
  private __tag!: void
  constructor(public value: number) {}
}
const CNYCount = new CNY(100)
const USDCount = new USD(100)
function addCNY(source: CNY, input: CNY) {
  return source.value + input.value
}
addCNY(CNYCount, CNYCount)
// 报错了!
addCNY(CNYCount, USDCount)

综上, 最理想的实现方式:

declare const tag: unique symbol
declare type Tagged<Token> = {
  readonly [tag]: Token
}
export type Opaque<Type, Token = unknown> = Type & Tagged<Token>

# 类型系统层级比较

# 通用情况

粗略来讲, 类型系统的层级如下:

系统类型层级

BottomType < 字面量类型 < 包含此字面量类型的联合类型(同一基础类型: 'hello' | 'world' 或者 2 | 3 ) < 对应的原始类型 < 原始类型对应的装箱类型 < Object 类型 < TopType

此处注意, any 既是顶层类型, 又是底层类型

# 特殊情况

凡事都有例外, 因此特殊情况如下:

type Result16 = {} extends object ? 1 : 2 // 1
type Result18 = object extends {} ? 1 : 2 // 1

type Result17 = object extends Object ? 1 : 2 // 1
type Result20 = Object extends object ? 1 : 2 // 1

type Result19 = Object extends {} ? 1 : 2 // 1
type Result21 = {} extends Object ? 1 : 2 // 1

16-18 和 19-21 这两对,为什么无论如何判断都成立?难道说明 {} 和 object 类型相等,也和 Object 类型一致?

当然不,这里的 {} extends 和 extends {} 实际上是两种完全不同的比较方式。{} extends object 和 {} extends Object 意味着, {} 是 object 和 Object 的字面量类型,是从类型信息的层面出发的,即字面量类型在基础类型之上提供了更详细的类型信息。object extends {} 和 Object extends {} 则是从结构化类型系统的比较出发的,即 {} 作为一个一无所有的空对象,几乎可以被视作是所有类型的基类,万物的起源。如果混淆了这两种类型比较的方式,就可能会得到 string extends object 这样的错误结论。

而 object extends Object 和 Object extends object 这两者的情况就要特殊一些,它们是因为“系统设定”的问题,Object 包含了所有除 Top Type 以外的类型(基础类型、函数类型等),object 包含了所有非原始类型的类型,即数组、对象与函数类型,这就导致了你中有我、我中有你的神奇现象。

type Result40 = [number, number] extends number[] ? 1 : 2 // 1
type Result41 = [number, string] extends number[] ? 1 : 2 // 2
type Result42 = [number, string] extends (number | string)[] ? 1 : 2 // 1
type Result43 = [] extends number[] ? 1 : 2 // 1
type Result44 = [] extends unknown[] ? 1 : 2 // 1
type Result45 = number[] extends (number | string)[] ? 1 : 2 // 1
type Result46 = any[] extends number[] ? 1 : 2 // 1
type Result47 = unknown[] extends number[] ? 1 : 2 // 2
type Result48 = never[] extends number[] ? 1 : 2 // 1
  • 40,这个元组类型实际上能确定其内部成员全部为 number 类型,因此是 number[] 的子类型。而 41 中混入了别的类型元素,因此认为不成立。
  • 42 混入了别的类型,但其判断条件为 (number | string)[] ,即其成员需要为 number 或 string 类型。
  • 43 的成员是未确定的,等价于 never[] extends number[],44 同理。
  • 45 类似于 41,即可能存在的元素类型是符合要求的。
  • 46、47,还记得身化万千的 any 类型和小心谨慎的 unknown 类型嘛?
  • 48,类似于 43、44,由于 never 类型本就位于最下方,这里显然成立。只不过 never[] 类型的数组也就无法再填充值了。

# 条件类型

存在泛型约束和条件类型两个 extends 可能会让你感到疑惑,但它们产生作用的时机完全不同,泛型约束要求你传入符合结构的类型参数,相当于参数校验。而条件类型使用类型参数进行条件判断(就像 if else),相当于实际内部逻辑。

# infer 基于结构提取

// 反转键名与键值
type ReverseKeyValue<T extends Record<string, unknown>> = T extends Record<
  infer K,
  infer V
>
  ? Record<V & string, K>
  : never

交叉类型小技巧

V & string, 此处使用了交叉类型来限定类型的范围只能是 string

# 分布式条件类型

当类型参数为联合类型,并且在条件类型左边直接引用该类型参数的时候,TypeScript 会把每一个元素单独传入来做类型运算,最后再合并成联合类型,这种语法叫做分布式条件类型, 对于这种条件类型可分为两种情况分析:

  • 对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上, 结果是联合类型
type Naked<T> = T extends boolean ? 'Y' : 'N'
/*
 * 将每一个条件分别代入表达式, 得到的结果组成一个联合类型
 * (number extends boolean ? "Y" : "N") | (boolean extends boolean ? "Y" : "N")
 * "N" | "Y"
 */
type Res3 = Naked<number | boolean>
  • 包裹泛型参数可以禁用掉分布式特性, 使其不会分发到联合类型, 而是进行内部的比较
type IsNever<T> = [T] extends [never] ? true : false
type IsNeverRes1 = IsNever<never> // true
type IsNeverRes2 = IsNever<'hello'> // false
  • 特殊情况, any 和 never 在分布式条件类型中表现如下

any 和 never 的特殊之处

  • never 不作为分布式条件类型的泛型参数传入时, 使用never extedns 任意类型, 会触发判断后得到结果
  • never 作为分布式条件类型的泛型参数传入时, 不进行判断直接返回never
  • 使用any extedns 任意类型 ? 1 : 2, 无论是否通过泛型参数传入, 结果也是联合类型1 | 2
  • 使用任意类型 extedns any ? 1 : 2, 无论是否通过泛型参数传入, 都会触发判断再得到非联合类型的结果1
// 直接使用,返回联合类型
type Tmp1 = any extends string ? 1 : 2 // 1 | 2

type Tmp2<T> = T extends string ? 1 : 2
// 通过泛型参数传入,同样返回联合类型
type Tmp2Res = Tmp2<any> // 1 | 2

// 如果判断条件是 any,那么仍然会进行判断
// any 在直接作为判断参数时、作为泛型参数时都会产生这一效果:
type Special1 = any extends any ? 1 : 2 // 1
type Special2<T> = T extends any ? 1 : 2
type Special2Res = Special2<any> // 1

// 直接使用,仍然会进行判断
type Tmp3 = never extends string ? 1 : 2 // 1

type Tmp4<T> = T extends string ? 1 : 2
// 通过泛型参数传入,会跳过判断
type Tmp4Res = Tmp4<never> // never

// 如果判断条件是 never,还是仅在作为泛型参数时才跳过判断
type Special3 = never extends never ? 1 : 2 // 1
type Special4<T> = T extends never ? 1 : 2
type Special4Res = Special4<never> // never

一些常用工具类型:

// A extends A 不是没意义,用于触发联合类型的分布特性
type IsUnion<A, B = A> = A extends A ? ([B] extends [A] ? false : true) : never
// any 类型与任何类型的交叉都是 any,也就是 1 & any 结果是 any。
type IsAny<A> = 'test' extends 1 & A ? true : false
// never 仅在作为泛型参数时会直接返回never
type IsNever<A> = [A] extends [never] ? true : false
// unknown extends T 时,  T 为 any 或 unknown
type IsUnknown<A> = unknown extends A
  ? IsAny<A> extends false
    ? true
    : false
  : false
// 判断两个类型是否一致
type IsEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B
  ? 1
  : 2
  ? true
  : false

# 工具类型分类

内置的工具类型按照类型操作的不同,其实也可以大致划分为这么几类:

  • 属性修饰工具类型: 对属性的修饰,包括对象属性和数组元素的可选/必选、只读/可写

  • 结构工具类型: 对既有类型的裁剪、拼接、转换等,比如使用对一个对象类型裁剪得到一个新的对象类型,将联合类型结构转换到交叉类型结构

  • 集合工具类型: 对集合(即联合类型)的处理,即交集、并集、差集、补集

  • 模式匹配工具类型: 基于 infer 的模式匹配,即对一个既有类型特定位置类型的提取,比如提取函数类型签名中的返回值类型

  • 模板字符串工具类型: 模板字符串专属的工具类型,比如神奇地将一个对象类型中的所有属性名转换为大驼峰的形式

# 属性修饰工具类型

这一部分的工具类型主要使用属性修饰(±?±readonly)、映射类型与索引类型相关实现

// 部分的
type Partial<T> = {
  [P in keyof T]?: T[P]
}
// 必需的
type Required<T> = {
  [P in keyof T]-?: T[P]
}
// 只读的
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}
// 可被修改的
type Mutable<T> = {
  -readonly [P in keyof T]: T[P]
}

# 触发类型展示巧技

征对一些复杂的类型计算, Typescript 并不会直接展示最终的类型结构; 为了让类型结构更加直观, 因此我们需要采取一些方法, 能够将它的类型展平为单层的对象结构

  • 触发深层类型的结构展示

有时候为了展示深层类型的最终结算结果, 可以使用extends any来实现, 例如:

type DeepReadonly<T extends Record<keyof any, any>> = T extends any
  ? {
      readonly [K in keyof T]: T[K] extends Record<keyof any, any>
        ? DeepReadonly<T[K]>
        : T[K]
    }
  : never

最后的效果如图:

1

  • 触发交叉类型的结构展示

下面图中的例子是一个联合类型的类型展示, 可见并不直观 联合类型计算

这种情况, 可以自定义的工具类型, 用来打平该结构

type Flatten<T> = { [K in keyof T]: T[K] }

type MarkPropsAsOptional<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Partial<Pick<T, K>> & Omit<T, K>>

效果图:

类型结构扁平化

# 结构工具类型

主要使用条件类型以及映射类型、索引类型来实现

// 索引工具类型, 通常我们使用这两者来代替 object
type Record<K extends keyof any, V> = {
  [P in K]: V
}

// 从索引类型中, 取出一部分
type Pick<T extends Record<keyof any, any>, K extends keyof T> = {
  [P in K]: T[K]
}

// 从索引类型中, 排除一部分, 注意此处K不能限定在T中, 因为排除的K不一定在T中
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

# 集合工具类型

这一部分的工具类型主要使用条件类型、条件类型分布式特性。

// 交集
type Extract<T, U> = T extends U ? T : never
// 差集, Exclude<T,K> 是从联名类型T中, 删除K的部分
type Exclude<T, U> = T extends U ? never : T
// 并集
type Union<T, U> = T | U
// 补集, 属于特殊的差集(T完全包含了U集合), 会加上泛型约束
type Complement<T, U extends T> = Exclude<T, U>

# 模式匹配工具类型

这一部分的工具类型主要使用条件类型与 infer 关键字实现

示例: 内置工具类型中对 Class 进行模式匹配的工具类型:

type ClassType = abstract new (...args: any) => any

type ConstructorParameters<T extends ClassType> = T extends abstract new (
  ...args: infer P
) => any
  ? P
  : never

type InstanceType<T extends ClassType> = T extends abstract new (
  ...args: any
) => infer R
  ? R
  : any

Class 的通用类型签名可能看起来比较奇怪,但实际上它就是声明了可实例化(new)与可抽象(abstract)罢了。我们也可以使用接口来进行声明:

export interface ClassType<TInstanceType = any> {
  new (...args: any[]): TInstanceType
}

对 Class 的模式匹配思路类似于函数,或者说这是一个通用的思路,即基于放置位置的匹配。放在参数部分,那就是构造函数的参数类型,放在返回值部分,那当然就是 Class 的实例类型了

# 模版字符串工具类型

模版字符串工具类型类似于Javascript中的模版字符串, 通过计算将两个字符串类型值组装在一起返回

同时, 它支持的类型也是有限的, 有效的类型只有 string | number | boolean | null | undefined | bigint

模板字符串类型的主要目的即是增强字符串字面量类型的灵活性,进一步增强类型和逻辑代码的关联

type TStr = 'World'
type THello = `Hello ${TStr}` // hello world

// number会保持原样, 用来约束类型只能为固定格式number
type Tstring = `Hello.${number}`
const test1: Tstring = `Hello.sad` // 报错

如果模版字符串中插槽的类型是联合类型, 那么它就会将所有插槽中的联合类型与剩余的字符串部分进行依次的排列组合

type Words = 'Hello' | 'Hi'
type Names = 'zhangsan' | 'lisi'
// Hello zhangsan | Hello lisi | Hi zhangsan | Hi lisi
type Result = `${Words} ${Names}`

结合索引类型与映射类型修改索引类型的键名, 可以使用 as 新语法实现, 索引类型有专门的语法叫做映射类型,对索引做修改的 as 叫做重映射

type CopyWithRename<T extends object> = {
  [K in keyof T as `modified_${string & K}`]: T[K]
}
interface Foo {
  name: string
  age: number
}
type CopiedFoo = CopyWithRename<Foo>

// ts内置了有关字符串专用类型工具Uppercase、Lowercase、Capitalize
type Heavy<T extends string> = `${Uppercase<T>}`
type Respect<T extends string> = `${Capitalize<T>}`
type HeavyName = Heavy<'hello'> // "HELLO"
type RespectName = Respect<'world'> // "World"

// 使用as简化高级结构类型工具 PickByValueType
type PickByValueType<T, VT> = {
  // never 作为key会被忽略掉该条key
  [K in keyof T as T[K] extends VT ? K : never]: T[K]
}

模板字符串类型与模式匹配

type ReverseName<Str extends string> =
  Str extends `${infer First} ${infer Last}`
    ? `${Capitalize<Last>} ${First}`
    : Str

# infer 约束

TypeScript 4.7 就支持了 infer 约束功能来实现对特定类型地提取,比如想要数组第一个为字符串的成员,如果第一个成员不是字符串,那就不要了

type FirstArrayItemType<T extends any[]> = T extends [
  infer P extends string,
  ...any[]
]
  ? P
  : never

实际上,infer + 约束的场景是非常常见的,尤其是在某些连续嵌套的情况下,一层层的 infer 提取再筛选会严重地影响代码的可读性,而 infer 约束这一功能无疑带来了更简洁直观的类型编程代码。

# 类型推导

TypeScript 主要有两种类型推导:

  • 输入型类型推导: 它们的推导依赖开发者的输入
  • 上下文类型推导: 基于位置的类型推导, 基于已定义的类型来规范开发者的使用
// 自己实现一个函数签名,后续我们实现的表达式可以只使用更少的参数
type CustomHandler = (name: string, age: number) => boolean
// 也推导出了参数类型
const handler: CustomHandler = (arg1, arg2) => true

// 正常
window.onerror = (event) => {}
// 报错
window.onerror = (event, source, line, col, err, extra) => {}

在上下文类型推导中, 允许将返回值非 void 类型的函数(() => list.push())作为返回值类型为 void 类型(arr.forEach)的函数类型参数

const arr: number[] = []
const list: number[] = [1, 2, 3]
// push方法是有返回值(为数组长度), 但是函数的类型返回值为void
list.forEach((item) => arr.push(item))

# 协变、逆变、双向协变、不变

  • 协变(Covariant): 只接受子类但不接受父类, 子类型可以赋值给父类型
  • 逆变(Contravariant): 只接受父类但不接受子类, 父类型可以赋值给子类型
  • 双向协变(Bivariance): 既能接受父类也能接受子类, ts2.x 之前支持这种赋值,也就是父类型可以赋值给子类型,子类型可以赋值给父类型

# 协变

interface IPerson {
  name: string
  age: number
}
interface IWang {
  name: string
  age: number
  hobbies: string[]
}
declare let person: IPerson
declare let wang: IWang
// 协变: 子类可以赋值给父类
person = wang

# 逆变

let printHobbies: (guang: IWang) => void
let printName: (person: IPerson) => void

printHobbies = (wang) => {
  console.log(wang.hobbies)
}
printName = (person) => {
  console.log(person.name)
}
// 逆变: 父类赋值给子类
printHobbies = printName
// 在ts2.6以后开启strictFunctionTypes将报错, 子类赋给父类类型将不安全
printName = printHobbies
  • printHobbies = printName类型是安全的, 因为这个函数调用的时候是按照 IWang 来约束的类型,但实际上函数只用到了父类型 IPerson 的属性和方法,当然不会有问题,依然是类型安全的

  • printName = printHobbies类型不安全, 因为函数声明的时候是按照 IPerson 来约束类型,但是调用的时候是按照 IWang 的类型来访问的属性和方法,那自然类型不安全了,所以就会报错

# 双向协变

在 Typescript2.6 之前函数参数支持双向赋值的,也就是父类型可以赋值给子类型,子类型可以赋值给父类型,既逆变又协变,叫做“双向协变”

如果想要更详细的了解类型变换, 可以参考文章

How I understand Covariance & Contravariance in typescript (opens new window)

在上面的示例中,我们可以证明参数类型的双向协变(Bivariance) 是不安全的,但不要难过,我们可以解决这个问题,这要归功于 Typescript 2.6,您只需要在tsconfig.json配置中开启选项 --strictFunctionTypes即可

从上面几个示例中, 我们最终得出结论:

结论

在 Typescript 中,参数类型必须是逆变(Contravariant),函数返回值类型需要协变(Covariant)

# 不变

逆变和协变都是型变,是针对父子类型而言的,非父子类型自然就不会型变,也就是不变

非父子类型之间不会发生型变,只要类型不一样就会报错

interface IPerson {
  name: string
  age: number
}
interface IWang {
  name: string
  age: number
}
let person: IPerson = {
  name: 'zd',
  age: 30
}
// 报错
let wang: IWang = zhang

let wang1: Iwang = {
  name: 'wang',
  age: 20
}
person = wang1

# 高级工具类型

上文中列举了各种工具类型, 而这些工具类型的组合可以实现真正意义上的类型编程(类型体操)

# 高级属性修饰工具类型

  • 深层的属性修饰: 使用递归进行深层属性修饰
  • 基于已知属性的部分修饰,以及基于属性类型的部分修饰: 将复杂的工具类型,拆解为由基础工具类型、类型工具的组合

# 深层的属性修饰

// 获取深层Promise的返回值
type PromiseValue<T> = T extends Promise<infer V> ? PromiseValue<V> : T

// 对象所有属性变为可选
type DeepPartial<T extends object> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}

// 属性可能为null
type Nullable<T> = T | null

// 筛选含null的属性
type DeepNullable<T extends object> = {
  [K in keyof T]: T[K] extends object ? DeepNullable<T[K]> : Nullable<T[K]>
}

// 从联合类型剔除null | undefined
type NonNullable<T> = T extends null | undefined ? never : T

// 剔除索引类型中, 所有属性值为 null 与 undefined
type DeepNonNullable<T extends Record<keyof any, unkonwn>> = {
  [K in keyof T]: T[K] extends Record<keyof any, unkonwn>
    ? DeepNonNullable<T[K]>
    : NonNullable<T[K]>
}

# 基于已知属性的部分修饰

// 让索引类型的部分已知属性改为可选
type MarkPropsAsOptional<
  T extends object,
  K extends keyof T = keyof T
> = Partial<Pick<T, K>> & Omit<T, K>

// 索引类型的部分已知属性改为必选
type MarkPropsAsRequired<T extends object, K extends keyof T = keyof T> = Omit<
  T,
  K
> &
  Required<Pick<T, K>>

// 部分类型改为null
type MarkPropsAsNullable<T extends object, K extends keyof T = keyof T> = Omit<
  T,
  K
> &
  Nullable<Pick<T, K>>

// 部分类型改为不含null
type MarkPropsAsNonNullable<
  T extends object,
  K extends keyof T = keyof T
> = Flatten<Omit<T, K> & NonNullable<Pick<T, K>>>

# 高级结构工具类型

  • 基于键值类型的 Pick 与 Omit
  • 子结构的互斥处理

# 基于键值类型的 Pick 与 Omit

当索引类型查询中使用了一个联合类型时,它会使用类似分布式条件类型的方式,将这个联合类型的成员依次进行访问,然后再最终组合起来

type TRes = { foo: 'foo'; bar: 'bar'; baz: never }
type TUnion = TRes[keyof TRes] // keyof TRes结果为 'foo' | 'bar' | 'baz'
type TURes = TRes['foo'] | TRes['bar'] | TRes['baz'] // 'foo' | 'bar'

// 基于键值类型查找属性名, 全部改为必选属性
type ExpectedPropKeys<T extends object, ValueType> = {
  [Key in keyof T]-?: T[Key] extends ValueType ? Key : never
}[keyof T]
// 使用Pick获取值类型相等的集合
type PickByValueType<T extends object, ValueType> = Pick<
  T,
  ExpectedPropKeys<T, ValueType>
>

// 基于键值类型排除属性名, 全部改为必选属性
type FilteredPropKeys<T extends object, ValueType> = {
  [Key in keyof T]-?: T[Key] extends ValueType ? never : Key
}[keyof T]
// 使用Omit获取值类型相等的集合
type OmitByValueType<T extends object, ValueType> = Pick<
  T,
  FilteredPropKeys<T, ValueType>
>

/* 下面这个工具类型汇总上面两个的功能 */
// 如果需要双重条件判断, 结果类型也是动态的, 可以采用这个工具类型
type Conditional<V, C, R, S> = [V] extends [C] ? R : S
type ValueTypeFilter<T extends object, ValueType, Positive extends boolean> = {
  [Key in keyof T]-?: T[Key] extends ValueType
    ? Conditional<Positive, true, Key, never>
    : Conditional<Positive, true, never, Key>
}[keyof T]
type PickByValueType<T extends object, ValueType> = Pick<
  T,
  ValueTypeFilter<T, ValueType, true>
>
type OmitByValueType<T extends object, ValueType> = Pick<
  T,
  ValueTypeFilter<T, ValueType, false>
>

在实际使用中, 有时候需要禁用分布式条件类型, 达到全等的效果, 应该包裹上数组括号即可; 因此上面的Condition类型可以优化一下:

// 包裹上就是全等, 不包裹括号可能会存在分布式条件类型导致结果错误
type IsEqual<T, U> = [T] extends [U] ? ([U] extends [T] ? true : false) : false
type StrictConditional<V, C, R, S> = IsEqual<V, C> extends true ? R : S

# 子结构的互斥处理

如果需要表示一个用户信息的类型TUserInfo, 用户总共分为三类: 普通用户CommonUser、VIP 用户VipUser、游客Visitor;

vipExpires 代表 VIP 过期时间,仅属于 VIP 用户,promotionUsed 代表已领取过体验券,属于普通用户,而 refererType 代表跳转来源,属于游客

// 联合类型与交叉类型混用, 会分散到每个联合子类型
type t1 = { a: 'a' } & ({ b: 'b' } | { c: 'c' })
type t2 = ({ a: 'a' } & { b: 'b' }) | ({ a: 'a' } & { c: 'c' })
type t3 = t2 extends t1 ? 1 : 2 // 1
// 描述用户信息的类型, VIP字段vipExpires, 普通用户字段promotionUsed, 两者不互存
interface VIP {
  vipExpires: number
}
interface CommonUser {
  promotionUsed: boolean
}
interface Visitor {
  refererType: RefererType
}
/* Exclude除去T类型和U类型中的公共部分, 留下T类型剩下的部分 */
/* never会被直接忽略, 剩下可选属性undefined, 类似{ vipExpires?: undefined  } */
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
/* 实现 或 类型 */
type XOR<T, U> = (Without<T, U> & U) | (Without<U, T> & T)

/* { vipExpires?: undefined; promotionUsed: boolean; } */
type TCommonUser = Flatten<Without<VIP, CommonUser> & CommonUser>
/* { promotionUsed?: undefined; vipExpires: number; } */
type TVipUser = Flatten<Without<CommonUser, VIP> & VIP>
// VIP和普通用户的互斥类型, 也等于TCommonUser | TVipUser
type TUserInfo = XOR<VIP, CommonUser>

/*
 * VIP、普通用户、游客三者互斥的类型
 * keyof 联合类型, 得到两者共有的key  keyof ({ a: 1, b: 2 } | { a: 2, c: 3 }) ===> 'a'
 */
type XORUser = XOR<VIP, XOR<CommonUser, Visitor>>
type XORUserResult =
  | { vipExpires?: number; promotionUsed: boolean }
  | { vipExpires?: number; promotionUsed?: undefined; refererType: string }
  | { vipExpires: number; refererType?: string; promotionUsed?: boolean }

type Test = XORUser extends XORUserResult ? 1 : 2 // 1

# keyof 小技巧

keyof 一些知识点

  • keyof 索引类型的联合类型, 得到两者共有的 key, 无共用 key 时返回 never, 即keyof( A | B )
  • keyof A & keyof B, 得到该两个索引类型的共同 key, keyof( A | B ) === keyof A & keyof B
  • keyof any, 得到 string | number | symbol
  • keyof A | keyof B, 得到该两个索引类型的所有 key
  • A 和 B 都是索引类型, 那么 A | B 就是一个带有两者共有属性的索引类型
type t1 = keyof ({ a: 1; b: 2 } | { a: 2; c: 3 }) // "a"
type t4 = keyof { a: 1; b: 2 } & keyof { a: 2; c: 3 } // "a"
type t5 = keyof { a: 1; b: 2 } | keyof { a: 1; c: 2; d: 3 } // "a" | "b" | "c" | "d"
type t2 = keyof {} // never
type t3 = keyof object // never
type t6 = { a: 1; b: 2 } | { a: 'zss'; c: 3 } // { a: 1 | 'zss' }

/* keyof巧技实现深度DeepReadonly */
// 只要不是{}、object都应该继续递归
type DeepReadonly<T extends Record<keyof any, any>> = keyof T extends never
  ? T
  : {
      readonly [K in keyof T]: DeepReadonly<T[K]>
    }

# 高级集合工具类型

高级的集合工具类型主要是从一维原始类型集合,扩展二维的对象类型,在对象类型之间进行交并补差集的运算,以及对同名属性的各种处理情况

此处需要用到本文中提到的基础集合工具类型

// 使用更精确的对象类型描述结构
type IndexType = Record<string, any>
// 属性名并集
type ITKeysConcurrence<T extends IndexType, U extends IndexType> =
  | keyof T
  | keyof U
// 属性名交集
type ITKeysIntersection<T extends IndexType, U extends IndexType> = Extract<
  keyof T,
  keyof U
>
// 属性名差集
type ITKeysDifference<T extends IndexType, U extends IndexType> = Exclude<
  keyof T,
  keyof U
>
// 属性名补集
type ITKeysComplement<T extends IndexType, U extends IndexType> = Complement<
  keyof T,
  keyof U
>

/* 交叉补属性名形成的索引类型 */
type IndexTypeIntersection<T extends IndexType, U extends IndexType> = Pick<
  T,
  ITKeysIntersection<T, U>
>
type IndexTypeDifference<T extends IndexType, U extends IndexType> = Pick<
  T,
  ITKeysDifference<T, U>
>
// U属性比T少
type IndexTypeComplement<T extends U, U extends IndexType> = Pick<
  T,
  ITKeysComplement<T, U>
>

而对于并集,就不能简单使用属性名并集版本了,因为使用联合类型实现,我们并不能控制同名属性的优先级,比如我到底是保持原对象属性类型呢,还是使用新对象属性类型?

对于这种类型, 我们采用先局部后整体的思路。比如将一个对象拆分成数个子结构,处理各个子结构,再将它们合并。那么对于合并两个对象的情况,其实就是两个对象各自特有的部分加上同名属性组成的部分

对于 T、U 两个对象,假设以 U 的同名属性类型优先,思路会是这样的:

  • T 比 U 多的部分:T 相对于 U 的差集,ObjectDifference<T, U>
  • U 比 T 多的部分:U 相对于 T 的差集,ObjectDifference<U, T>
  • T 与 U 的交集,由于 U 的优先级更高,在交集处理中将 U 作为原集合, T 作为后传入的集合,ObjectIntersection<U, T>

我们就得到了 Merge:

type Merge<
  T extends PlainObjectType,
  U extends PlainObjectType
  // T 比 U 多的部分,加上 T 与 U 交集的部分(类型不同则以 U 优先级更高,再加上 U 比 T 多的部分即可
> = ObjectDifference<T, U> & ObjectIntersection<U, T> & ObjectDifference<U, T>

// 还可以得到不完全的交集, 即使用对象 U 的属性类型覆盖对象 T 中的同名属性类型,但不会将 U 独特的部分合并过来
type Override<
  T extends PlainObjectType,
  U extends PlainObjectType
> = ObjectDifference<T, U> & ObjectIntersection<U, T>

# 高级模式匹配工具类型

// 提取函数最后一个参数类型则
type LastParams<T extends (...args: any[]) => unknown> = T extends (
  ...args: infer Args
) => unknown
  ? Args extends [...unknown[], infer L]
    ? L
    : Args extends Array<infer R>
    ? R
    : never
  : never

// 提取 Promise 内部值类型
type Awaited<T> = T extends null | undefined
  ? T
  : T extends object & { then(onfulfilled: infer F): any }
  ? F extends (value: infer V, ...args: any) => any
    ? Awaited<V>
    : never
  : T

TypeScript 内部采用的是结构化类型系统,也就意味着由于 { prop: number } 可以视为继承自 {}{} extends { prop: number } 是不满足条件的。但是,如果这里的 prop 是可选的,那就不一样了!由于 { prop?: number } 也可以是一个空的接口结构,那么 {} extends { prop?: number } 就可以认为是满足的

注意

内置工具类型 Pick 不会改变原有类型的可读性/可选性;

索引类型中 -?是为了去掉结果中的 undefined

// 获取一个接口中所有可选或必选的属性
type Tmp1 = {} extends { prop: number } ? 'Y' : 'N' // "N"
type Tmp2 = {} extends { prop?: number } ? 'Y' : 'N' // "Y"

// 还可以使用
// 在一个对象中剔除掉一个可选的属性,还是属于这个对象, 反之不属于
// 如果Omit<T, K(可选属性)> extends T => true,
// 如果Omit<T, K(必选属性)> extends T => false

// 必选属性
type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K
}[keyof T]
// 可选属性
type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never
}[keyof T]

关于获取索引类型的可选或者必选属性组成的新类型, 可以参考GetOptional (opens new window)

如何实现 MutableKeys、ImmutableKeys, 需要使用辅助工具类型IsEquel

注意

如果两个类型全等,那么他们的可读性和可选性也是一致的

// 判断类型全等
type IsEqual<T, U> = (<X>() => X extends T ? 1 : 2) extends <X>() => X extends U
  ? 1
  : 2
  ? true
  : false
// 非只读属性
type MutableKeys<T> = {
  [K in keyof T]-?: IsEqual<
    { readonly [P in K]: T[P] },
    Pick<T, K>
  > extends true
    ? never
    : K
}[keyof T]
// 只读属性
type ImmutableKeys<T> = {
  [K in keyof T]-?: IsEqual<
    { readonly [P in K]: T[P] },
    Pick<T, K>
  > extends true
    ? K
    : never
}[keyof T]

如何实现 JS 中的includes, 也可以使用模式匹配做提取

type Includes<T extends unknown[], U> = T extends [infer F, ...infer R]
  ? IsEqual<F, U> extends true
    ? true
    : Includes<R, U>
  : false

# 高级模版字符串工具类型

// 查找字符串字面量类型中是否含有某个字符串
type Include<Str extends string, Search extends string> = Str extends `${infer Left}${infer Search}${infer Right}` ? true : false

// 上面Include<'', ''>结果不正确, 因此需要做特殊情况处理
type IncludeStrict<Str extends string, Search extends string> = Str extends ''
? ( Search extends ''
  ? true
  : false )
: Include<Str, Search>

// 去除左右的空字符串, 递归多次处理
type TrimL<S extends string> = S extends ` ${infer L}` ? TrimL<L> : S
type TrimR<S extends string> = S extends `${infer R} ` ? TrimR<R> : S
type Trim<S extends string> = TrimL<TrimR<S>>

// 以xx开头
type StartWith<T extends string, S extends string> = T extends `${S}${infer Rest}` ? true : false
type StartWithStrict<T extends string, S extends string> = T extends ''
? ( S extends ''
  ? true
  : false)
: StartWith<T, S>

// 替换
type Replace<
  Str extends string,
  Search extends string,
  Replacement extends string
> = Str extends `${infer Head}${Search}${infer Tail}`
  ? `${Head}${Replacement}${Tail}`
  : Str;

// 全量替换
 type ReplaceAll<
  Str extends string,
  Search extends string,
  Replacement extends string
> = Str extends `${infer Head}${Search}${infer Tail}`
  ? ReplaceAll<`${Head}${Replacement}${Tail}`, Search, Replacement>
  : Str;

// 支持选项控制
type ReplaceByOptions<
  Str extends string,
  Search extends string,
  Replacement extends string,
  RepaceAll extends boolean
> = RepaceAll extends true
  ? ReplaceAll<Str extends string, Search extends string, Replacement extends string>
  : Replace<Str extends string, Search extends string, Replacement extends string>

/*
  * 打散字符串
  * 支持分隔符为多种类型(联合类型), 如 '_' | '-' | '~'
  * Delimiter一次最好只传一种, 因为模版字符串与联合类型的排列组合, 导致结果不一定是我们需要的
  * 可以使用多次Split来按多种字符拆分
  * */
type Split<
  Str extends string,
  Delimiter extends string,
> = Str extends `${infer L}${Delimiter}${infer R}`
  ? [L, ...Split<R, Delimiter>]
  : (Str extends Delimiter
    ? []
    : [Str])

// 字符串长度统计
type StrLength<T extends string> = Split<Trim<T>, ''>['length'];

// 字符串按分隔符组合为字符串 JOIN
type Join<
  List extends Array<string | number>,
  Delimiter extends string
> = List extends [infer L extends string | number, ...infer Rest extends Array<number | string>]
  ? `${L}${Rest['length'] extends 0 ? '' : Delimiter}${Join<Rest, Delimiter>}`
  : ''

// 蛇式字符串转驼峰字符串
type SnakeCase2CamelCase<
  S extends string
> = S extends `${infer L}_${infer R}`
  ? `${Capitalize<L>}${SnakeCase2CamelCase<R>}`
  : (S extends ''
    ? ''
    : S)

// 自定义分隔符转驼峰
type DelimiterCase2CamelCase<
  S extends string,
  Delimiter extends string
> = S extends `${infer L}${Delimiter}${infer R}`
  ? `${Capitalize<L>}${DelimiterCase2CamelCase<R>}`
  : (S extends ''
    ? ''
    : S)

// CamelCase 智能版
type _DelimiterCase2CamelCase<
  S extends string,
  Delimiter extends string
> = S extends `${infer L}${Delimiter}${infer R}`
  ? `${Capitalize<Lowercase<L>>}${DelimiterCase2CamelCase<R, Delimiter>}`
  : (S extends ''
    ? ''
    : Capitalize<Lowercase<S>>)

// 字符串中只能有一种符号
type AutoCamelCase<
  S extends string,
  Delimiter extends string = '_' | '-' | ' '
> = Delimiter extends Delimiter
  ? (S extends `${infer L}${Delimiter}${infer R}`
    ? Uncapitalize<_DelimiterCase2CamelCase<S, Delimiter>>
    : never)
  : never

# 特殊特性

  • any 类型与任何类型的交叉都是 any,也就是 1 & any 结果是 any,可以用这个特性判断 any 类型。
  • boolean 是一个联合类型 true | false,它会触发泛型参数的分布式条件类型
  • 联合类型作为类型参数出现在条件类型左侧时,会分散成单个类型传入,最后合并。
  • never 作为类型参数出现在条件类型左侧时,会直接返回 never。
  • any 作为类型参数出现在条件类型左侧时,会直接返回 trueType 和 falseType 的联合类型。
  • 元组类型也是数组类型,但每个元素都是只读的,并且 length 是数字字面量,而数组的 length 是 number。可以用来判断元组类型。
  • 函数参数处会发生逆变,可以用来实现联合类型转交叉类型。
  • 可选索引的索引可能没有,那 Pick 出来的就可能是 {},可以用来过滤可选索引,反过来也可以过滤非可选索引。
  • 索引类型的索引为字符串字面量类型,而可索引签名不是,可以用这个特性过滤掉可索引签名。
  • keyof 只能拿到 class 的 public 的索引,可以用来过滤出 public 的属性。
  • 默认推导出来的不是字面量类型,加上 as const 可以推导出字面量类型,但带有 readonly 修饰,这样模式匹配的时候也得加上 readonly 才行

# 如何提升

# Type Challenge

Type Challenge (opens new window) 是 antfu (Vue 团队成员,以及 Vite、Vitest、Nuxt 等知名开源项目的团队成员或作者)的作品,其中搜集了许多类型编程的题目,并且贴心地按照难易程度分为了 easy、medium、hard 三个等级, 可以通过大量训练来提升类型编程的能力

# TSD 单元测试

TSD (opens new window)是一个工具类型单元测试库, 用来检查类型定义, 也可以验证一个类型是否符合泛型类型定义

import { expectType } from 'tsd'
type DeepPartialStruct = DeepPartial<{
  foo: string
  nested: {
    nestedFoo: string
    nestedBar: {
      nestedBarFoo: string
    }
  }
}>

expectType<DeepPartialStruct>({
  foo: 'bar',
  nested: {}
})
// 验证是否符合DeepPartialStruct类型定义
expectType<DeepPartialStruct>({
  nested: {
    nestedBar: {}
  }
})

# 版本更新

在 Typescript4.8 的内置类型中, 已经合入了最新的一个关于交叉类型的 PR (opens new window)

the empty object type literal, is a supertype of all types except null and undefined. Thus, {} | null | undefined is effectively equivalent to unknown, and for an arbitrary type T, the intersection T & {} represents the non-nullable form of T

// 最新的NonNullable
type NonNullable<T> = T & {}
type T1 = {} & string // string
type T2 = {} & 'a' // 'a'
type T3 = {} & object // object
type T4 = {} & { x: number } // { x: number }
type T5 = {} & null // never
type T6 = {} & undefined // never
type T7 = undefined & void // undefined

# 其他部分

# 构造器类型

泛型工厂函数类型, 使用new关键字实现

type ConstructType<T> = new (...args: unknown[]) => T
// or 使用泛型接口表示
interface IConstructType<T> {
  new (...args: unknown[]) => T
}

function createFactoryConstruct<U>
  (constructorType: ConstructType<U> /* IConstructType<U> */){
  return new constructorType()
}

# 声明文件

declare let/const // 声明全局变量
declare function  // 声明全局函数
declare class   // 声明全局类
declare enum    // 声明全局枚举类型
declare namespace // 声明(含有子属性的)全局对象
interface/type    // 声明全局类型, 不需要declare关键词

declare namespace里面不能再次使用declare, 而可以使用export将内部属性导出, namespace还是有一定的缺陷, 每次使用都需要带上空间名称, 现在已经不推荐使用, 而更推崇的方式是使用模块声明

declare namespace JQuery{
 export function $(ready: () => void): void
  export namespace ${
    function ajax(url: string, settings?: any): void
  }
}

# 模块声明

declare module 'JQuery' {
  type cssSelector = {
    css: (key: string, value: string) => cssSelector
  }
 export function $(ready: () => void): void
  export namespace ${
    function ajax(url: string, settings?: any): void
  }
  // 只能兼容ESM的模块导出
  // export default $
  // 兼容性更广泛, 还能兼容AMD和Commonjs的导出
  export = $
  export as namespace $
}

# 三斜线指令

用于在一个声明文件中, 包含另一个全局声明文件, ESM之前使用非常普遍, 如今已不推荐使用

/// <reference path="../index.d.ts" />

declare namespace console {
  function log( message?:unknown ): void
}

# 类的二重性

TS中, Class类名具备二重性, 既可以表示类的变量名, 又可以表示类实例的值类型

如果需要拿到class的构造函数, 可以使用typeof class名

class Foo { constructor(...args) { } }
// 此处Foo类型是作为实例名, new Foo()是将类实例化
const ins: Foo = new Foo()
// 此处typeof Foo是作为构造函数名
const createFooInstance = (ctor: typeof Foo, ...args: unknown[]): Foo => new ctor(...args)
const ins1: Foo = createFooInstance(Foo);

Typescript与Javascript 的 typeof 是不同的, 前者作为类型来使用, 而后者作为运行时使用

const name = 'string'
const fo: typeof name = typeof name as 'string'

# 类构造器参数属性

TS中构造函数的参数可以添加修饰符publicprivateprotectedreadonly, 作用是将参数自动变成实例的属性, 优化代码量

// 基础写法
class Foo {
  public name: string // 定义公有实例属性
  protected age: number // 定义受保护的属性
  private gender: string // 定义私有属性
  constructor(n, a, g){
    this.name = n
    this.age = a
    this.gender = g
  }
}

// 使用函数参数属性的精简写法, 跟上面写法等价
class Foo {
  constructor(
    public name: string,
    protected age: number,
    private gender: string
  ) {}
}

# 函数重载优化

重载使用场景

函数返回值为多个联合类型, 使用函数重载

当函数返回值为多个联合类型时, 可以使用函数重载来优化代码, 但是函数重载也有弊端, 会增加大量代码量

// 重载写法
function processInput(input: string): string
function processInput(input: number): number
function processInput(input: string | number): string | number {
  if (typeof input === 'string') {
    return `String: ${input}`
  }
  return input
}

如果input的类型可能有几十个, 或者有多个参数, 使用重载需要实现一堆的重载签名, 代码量会非常大。 在该场景使用下面代码来优化

type ProcessReturnType<T extends number | string > = T extends number ? number : string

function processInput<T extends number | string>( input: T ): ProcessReturnType<T> {
  if (typeof input === 'string') {
    return `String: ${input}` as ProcessReturnType<T>
  }
  return input as ProcessReturnType<T>
}

# 函数泛型约束

泛型使用场景

想要获取输入值, 或者返回值类的参数类型, 使用泛型参数

在 TypeScript 中,函数的泛型约束可以有多种写法,常见的包括:

  1. 将泛型约束写在尖括号中
function identity<T extends string | number>(value: T): T {
  return value;
}

在这种写法中,尖括号中的 T 表示一个泛型类型参数,它被约束为 stringnumber 类型。这个约束会对函数参数和返回值类型起到限制作用。

  1. 将泛型约束写在函数参数中
function identity<T>(value: T extends string | number ? T : never): T {
  return value;
}

在这种写法中,函数参数的类型是一个条件类型 T extends string | number ? T : never,表示如果 T 可以赋值为 stringnumber 类型,则参数类型为 T,否则参数类型为 never,这样可以达到和尖括号中的泛型约束相同的效果。

  1. 将泛型约束写在类型别名中
type StringOrNumber = string | number;

function identity<T extends StringOrNumber>(value: T): T {
  return value;
}

在这种写法中,将泛型约束定义在一个类型别名 StringOrNumber 中,然后使用 extends 关键字将泛型参数 T 约束为 StringOrNumber 类型。

这些写法各有优缺点,选择哪种写法主要取决于具体的情况和个人喜好。

# 类型真题

可串联构造器 (opens new window)

type Chainable<T = {}> = {
  option<K extends string, V>(
    key: K,
    value: V
  ): Chainable<(K extends keyof T ? Omit<T, K> : T) & { [P in K]: V }>
  get(): T
}

Promise.all (opens new window)

// 使用T extends any 触发TS的类型计算展示
type GetPromiseAllType<T extends readonly any[]> = T extends any
  ? Promise<{
      [K in keyof T]: T[K] extends Promise<infer V> ? V : T[K]
    }>
  : never
// 此处使用spread重新得到一个非只读数组
declare function PromiseAll<T extends readonly any[]>(
  values: readonly [...T]
): GetPromiseAllType<T>

Type Lookup (opens new window)

// 方法一
type LookUp<U extends { type: any }, T> = U extends U
  ? U['type'] extends T
    ? U
    : never
  : never
// 方法二
type LookUp<T, U extends string> = T extends { type: U } ? T : never

Trim (opens new window)

type TrimLeft<S extends string> = S extends `${' ' | '\n' | '\t'}${infer R}`
  ? TrimLeft<R>
  : S
type TrimRight<S extends string> = S extends `${infer L}${' ' | '\n' | '\t'}`
  ? TrimRight<L>
  : S
type Trim<S extends string> = `${TrimLeft<TrimRight<S>>}`

ReplaceAll (opens new window)

type ReplaceAll<
  S extends string,
  From extends string,
  To extends string
> = From extends ''
  ? S
  : S extends `${infer L}${From}${infer R}`
  ? `${L}${To}${ReplaceAll<R, From, To>}`
  : S

Permutation (opens new window)

type Permutation<T, A = T> = [T] extends [never]
  ? []
  : T extends T
  ? [T, ...Permutation<Exclude<A, T>>]
  : never

Flatten (opens new window)

type Flatten<T extends Array<unknown>> = T extends [infer F, ...infer R]
  ? F extends any[]
    ? [...Flatten<F>, ...Flatten<R>]
    : [F, ...Flatten<R>]
  : T

Absolute (opens new window)

// 注意如果是bigint数字, 转字符串会自动消除'_', 所以只需要处理负号
type Absolute<T extends number | string | bigint> = `${T}` extends `-${infer N}`
  ? `${N}`
  : `${T}`

String to Union (opens new window)

type StringToUnion<T extends string> = T extends `${infer F}${infer R}`
  ? F | StringToUnion<R>
  : never

Merge (opens new window)

// 方法一
type Flatten<T extends Record<string, unknown>> = {
  [K in keyof T]: T[K]
}
type Merge<F extends Record<string, unknown>, S> = Flatten<Omit<F, keyof (F | S)> & S>
// 方法二
type Merge<F extends Record<string, unknown>, S> = Omit<Omit<F, keyof (F | S)> & S, never>
// 方法三
type Merge<F extends Record<string, unknown>, S extends Record<string, unknown>> = {
  [K in keyof F | keyof S]: K extends keyof S
    ? S[K]
    : F[K extends keyof F ? K : never]

* KebabCase (opens new window)

type FirstLowcase<T extends string> = T extends `${infer F}${infer R}`
  ? F extends Lowercase<F>
    ? T
    : `${Lowercase<F>}${R}`
  : T
type KebabCase<S extends string> = S extends `${infer F}${infer R}`
  ? R extends FirstLowcase<R>
    ? `${FirstLowcase<F>}${KebabCase<R>}`
    : `${FirstLowcase<F>}-${KebabCase<FirstLowcase<R>>}`
  : S

Diff (opens new window)

// 方法一
type Diff<O, O1> = Omit<
  Omit<O, keyof (O | O1)> & Omit<O1, keyof (O | O1)>,
  never
>
// 方法二
type SameKey<T, U> = keyof (T | U)
type AllKey<T, U> = keyof T | keyof U
type Diff<T, U> = {
  [K in Exclude<AllKey<T, U>, SameKey<T, U>>]: K extends keyof T
    ? T[K]
    : K extends keyof U
    ? U[K]
    : never
}
// 方法三
type Diff<T, U> = Omit<T & U, keyof (T | U)>
// 方法四
type Diff<T, U> = {
  [K in Exclude<keyof T | keyof U, keyof T & keyof U>]: (T & U)[K]
}

AnyOf (opens new window)

type AnyOf<T extends unknown[]> = T[number] extends
  | ''
  | 0
  | false
  | []
  | Record<string, never>
  ? false
  : true

IsUnion (opens new window)

type IsUnion<T, U = T> = [T] extends [never]
  ? false
  : T extends T
  ? [U] extends [T]
    ? false
    : true
  : false

ReplaceKeys (opens new window)

type ReplaceKeys<
  U,
  T extends string,
  Y extends Record<keyof any, unknown>
> = U extends U
  ? { [K in keyof U]: K extends T ? (K extends keyof Y ? Y[K] : never) : U[K] }
  : never

Remove Index Signature (opens new window)

// 索引签名不能构造成字符串字面量类型,因为它没有名字,而其他索引可以
// 并不完整, 无法过滤掉[key: number], [key: symbol]类型的
type RemoveIndexSignature<T extends Record<keyof any, unknown>> = {
  [K in keyof T as K extends `${infer STR}` ? STR : never]: T[K]
}
// 完整版
// 只有 [x: string]情况下 string extends string 才成立, 否则 string extends 字面量 不成立
type RemoveIndexSignature<T extends Record<keyof any, unknown>> = {
  [K in keyof T as string extends K
    ? never
    : number extends K
    ? never
    : symbol extends K
    ? never
    : K]: T[K]
}

Percentage Parser (opens new window)

type CheckPrefix<T> = T extends '+' | '-' ? T : never
type CheckSuffix<T> = T extends `${infer P}%` ? [P, '%'] : [T, '']
type PercentageParser<A extends string> = A extends `${CheckPrefix<
  infer L
>}${infer R}`
  ? [L, ...CheckSuffix<R>]
  : ['', ...CheckSuffix<A>]

** MinusOne (opens new window)

type ParseInt<T extends string> = T extends `${infer Digit extends number}`
  ? Digit
  : never
type ReverseString<S extends string> = S extends `${infer First}${infer Rest}`
  ? `${ReverseString<Rest>}${First}`
  : ''
type RemoveLeadingZeros<S extends string> = S extends '0'
  ? S
  : S extends `${'0'}${infer R}`
  ? RemoveLeadingZeros<R>
  : S
type InternalMinusOne<S extends string> =
  S extends `${infer Digit extends number}${infer Rest}`
    ? Digit extends 0
      ? `9${InternalMinusOne<Rest>}`
      : `${[9, 0, 1, 2, 3, 4, 5, 6, 7, 8][Digit]}${Rest}`
    : never

type MinusOne<T extends number> =
  // 字符串转为数字, 如: '99' -> 99
  ParseInt<
    // 删除字符串前面的0, 如: '099' -> '99'
    RemoveLeadingZeros<
      // 翻转为正常数字字符串, 如: '100'-> '099'
      ReverseString<
        // 处理数字减法, 如: '001' -> '990'
        InternalMinusOne<
          // 翻转为字符串, 如: 100 -> '001'
          ReverseString<`${T}`>
        >
      >
    >
  >

* RequiredByKeys (opens new window)

type NoUndefined<T> = T extends undefined ? never : T
type RequiredByKeys<T, K = keyof T> = Omit<
  Omit<T, keyof T & K> & {
    /*
     * 去除值中的 undefined
     *
     * 方式一: T[P] & {}, 也即 NonNullable<T[P]>
     * 方式二: NoUndefined<T[P]>
     * 方式三: T[P] extends infer V ? V extends undefined ? never : V : never
     * 方式四: T[P] extends infer V | undefined ? V : T[P]
     * 方式五: [P in keyof T as P extends K ? P : never]-?: T[P]
     * */
    [P in keyof T & K]-?: T[P] extends infer V | undefined ? V : T[P]
  },
  never
>

type RequiredByKeys1<T, K = keyof T> = Omit<
  Omit<T, keyof T & K> & {
    [P in keyof T as P extends K ? P : never]-?: T[P]
  },
  never
>

Tuple to Nested Object (opens new window)

type TupleToNestedObject<T extends Array<unknown>, U> = T extends [
  infer F,
  ...infer R
]
  ? { [K in F & string]: TupleToNestedObject<R, U> }
  : U

** FlattenDepth (opens new window)

type FlattenArray<
  F,
  D = 1,
  C extends unknown[] = [unknown]
> = F extends unknown[]
  ? C['length'] extends D
    ? F
    : F extends [infer L, ...infer R]
    ? [...FlattenArray<L, D, [...C, unknown]>, ...FlattenArray<R, D, C>]
    : F
  : [F]

type FlattenDepth<T extends Array<unknown>, D = 1> = T extends [
  infer F,
  ...infer R
]
  ? [...FlattenArray<F, D>, ...FlattenDepth<R, D>]
  : T

Flip (opens new window)

type Flip<T extends Record<keyof any, any>> = {
  [K in keyof T as `${T[K]}`]: K
}

# 拓展阅读