i@yujinyan.me

Blog

TypeScript 类型编程指南

两个视角:

  • 类型系统是集合
  • 类型系统是编程语言

类型是集合

元素交集与并集

type S1 = 1 | 2 | 3
type S2 = 3 | 4 | 5
type S3 = 6 | 7 | 8
type S4 = 1 | 2

type R1 = S1 | S2 // 1 | 2 | 3 | 4 | 5
type R2 = S1 & S2 // 3
type R4 = Exclude<S1, S2> // 1 | 2
type R5 = S4 extends S1 ? "Yes" : "No"
type R6 = S3 extends S1 ? "Yes" : "No"

全集与空集

空集:never

(1 | 2 | 3) & (4 | 5 | 6) // never
keyof {}                  // never

全集:unknown

// In an intersection everything absorbs unknown
type T00 = unknown & null;              // null
type T01 = unknown & undefined;         // undefined
type T02 = unknown & null & undefined;  // null & undefined (which becomes never in union)
type T03 = unknown & string;            // string
type T04 = unknown & string[];          // string[]
type T05 = unknown & unknown;           // unknown
type T06 = unknown & any;               // any

// In a union an unknown absorbs everything
type T10 = unknown | null;              // unknown
type T11 = unknown | undefined;         // unknown
type T12 = unknown | null | undefined;  // unknown
type T13 = unknown | string;            // unknown
type T14 = unknown | string[];          // unknown
type T15 = unknown | unknown;           // unknown
type T16 = unknown | any;               // any

unknownany 的区别

any 相当于关闭了类型检查,可以对 any 做任何任何操作,改成 unknown 后都是编译错误:

function f1(a: any) {
  a.foo
  a["foo"]
  a()
}

function f2(a: unknown) {
  a.foo    // error
  a["foo"] // error
  a()      // error
}

必须对 unknown 的值做出相应的类型检查:

function f2(a: unknown) {
  if (typeof a == 'function') {
    a()
  }

  if (typeof a == 'object' && a != null && "foo" in a) {
    a["foo"]
  }
}

https://github.com/microsoft/TypeScript/pull/24439

类型是编程语言

TypeScript = JavaScript + 类型语言:

JavaScript类型语言
作用于集合
存在于运行时编译时

JavaScript 和类型语言之间的交互:

  • JavaScript → 类型语言:foo: Foo
  • 类型语言 → JavaScript:typeOf foo

绑定声明

const obj = {name: 'foo'}
type Obj = {name: string}

条件判断

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html

SomeType extends OtherType ? TrueType : FalseType;

https://github.com/microsoft/TypeScript/pull/21316

遍历

type User = {
  name: string
  gender: "M" | "F"
}

type User1 = {
  [F in keyof User as Capitalize<F>]: User[F] 
}
  • User 类型中每一个键 F 变换成 Capitalize<F>
  • 这个键对应的值类型是 User[F],即保持不变

函数

可以将范型类比函数:

function union(x, y) {
  return [...new Set([...x, ...y])]
}
type Union<X, Y> = X | Y
  • Union: 函数名
  • <X, Y>: 声明函数入参
  • 等号右侧 X | Y: 函数返回值
🖋

Type Challenge: 尝试自己实现 ts 内置的辅助类:Omit<T, K>,其作用是从 T 中移除所有 K 里列出来的属性,例如:

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyOmit<Todo, 'description' | 'title'>

const todo: TodoPreview = {
  completed: false
}
查看解答
type MyOmit<T, O> = {
  [P in keyof T as P extends O ? never : P]: T[P]
}

递归

练习:

DeepReadonly

模板字符串

type Events = 'Created' | 'Approved' | 'Terminated'
type EventHandler = `on${Events}`

https://github.com/microsoft/TypeScript/pull/40336

属性、元素访问

访问对象属性:

type Config = { key: string, value: string }
type Key = Config['key'] // string

访问数组内元素:ArrayType[number]

const MyArray = [
  { name: "Alice", age: 15 },
  { name: "Bob", age: 23 },
  { name: "Eve", age: 38 },
];

type Person = typeof MyArray[number];
🖋

假设有这样一个路由配置和跳转方法:

const routes = {
  index: {
    path: "/index",
    payload: {} as { page: number }
  },
  search: {
    path: "/search",
    payload: {} as { query: string }
  }
}

declare function navigateTo(
  route: string,
  payload: Record<string, string>
)
navigateTo("index", { page: 1 })

请重构 navigateTo 的类型标注,使得以下调用都编译错误:

navigateTo("blah", { page: 1 }) // 不存在的路由
navigateTo("search", { page: 1 }) // 不匹配的路由参数配置
查看解答
declare function myNavigateTo<T extends keyof typeof routes>(
  route: T,
  payload: (typeof routes)[T]['payload']
): void

参考资料