TypeScript 技巧集锦

编写 TypeScript(后面简称TS)应用是一个与类型斗争的过程,你需要使用 TS 提供的类型工具通过不同的组合来精确描述你的目标。描述越精确,类型约束和提示越准确,潜在错误越少。反之,描述越模糊(如any
一把唆),TS 能提供的类型辅助就越少,潜在的错误也就越多。如何描写精确的类型描述需要掌握 TS 的基础概念,同时掌握常见技巧和类型工具,前者可以阅读官网的 TypeScript 手册 学习,后者可以通过本文学习一二,后续进阶学习就靠多实践多总结了。
常用技巧
这节主要介绍一些基础的类型工具,这是所有高级类型的基石。
typeof T
- 获取 JS 值的类型
typeof
可以获取 JS 变量的类型,它是 JS 值空间向 TS 类型空间转换的桥梁,有了它我们可以从已有的变量中抽取类型进行进一步处理。
const person = {
name: "jim",
age: 99
}
type Person = typeof person
// type Person = {
// name: string;
// age: number;
// }
keyof T
- 获取类型的键
keyof
可获取目标类型的键,返回的是string | number | symbol
的子类型。
interface Person {
name: string
age: number
}
type K = keyof Person
// type K = "name" | "age"
进一步阅读:
T[K]
- 索引类型,获取类型的值
动态获取目标类型属性的类型,类似 JS 中对象取值操作,不过这里取到的是值的类型。
interface Person {
name: string
age: number
}
type T1 = Person['name'] // string
type T2 = Person['age'] // number
type T3 = Person[keyof Person] // string | number
进一步阅读
[P in keyof T]: T[P]
- 类型映射,转换类型
基于旧类型创建新类型,在新类型构造过程中,我们可以对旧类型的属性名、属性值进行重写,从而实现类型转换。其中in
操作符表示遍历目标类型的 key。
interface Person {
name: string
age: number
}
// 将 Person 的属性转换成可选
type PersonPartical = { [P in keyof Person]?: Person[P] }
// type PersonPartical = {
// name?: string | undefined;
// age?: number | undefined;
// }
// 将 Person 的属性转换成只读
type PersonReadonly = { readonly [P in keyof Person]: Person[P] }
// type PersonReadonly = {
// readonly name: string;
// readonly age: number;
// }
// 上面了两个操作实在太常用了,TS 已经内置了相应的类型工具
// 例如上面的可选和只读,可以写成
type PersonPartical = Partial<Person>
type PersonReadonly = Readonly<Person>
进一步阅读
T extends U ? X : Y
- 条件类型
extends
除了用在继承类时会使用,还可以用于判断一个类型是否比另一个类型的父类型,并根据判断结果执行不同的类型分支,其使得 TS 类型具备了一定的编程能力。
extends
条件判断规则如下:如果T
可以赋值给U
返回X
,否则Y
,如果 TS 无法确定T
是否可以赋值给U
,则返回X | Y
。
type isString<T> = T extends string ? true : false
type T1 = isString<number> // false
type T2 = isString<string> // true
进一步阅读
infer T
- 类型推断
在extends
条件类型的子句中,可以使用infer T
来捕获指定位置的类型(该类型由 TS 编译器推断),在infer
后面的子句中可以使用捕获的类型变量。配合extends
条件类型,截取符合条件的目标的某部分类型。
type ParseInt = (n: string) => number
// 如果是类型 T 是函数,则 R 会捕获其返回值类型并返回 R,否则返回 any
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
type R = ReturnType<ParseInt> // number
type GetType<T> = T extends (infer E)[] ? E : never
type E = GetType<['a', 100]>
进一步阅读
never
never
与类型T
(T
是除unknown
外的其他任意类型)union 后结果是类型T
,利用never
的这个特点可以实现类型消除,例如将某个类型先转换成never
,然后再与其他类型 union。
type a = string
type b = number
type c = never
type d = a | b |c
// type d = string | number
type Exclude<T, U> = T extends U ? never : T;
type T = Exclude<string | number, string> // number
进一步阅读
类型工具
TS 内置了一些常用的类型转换工具,熟练掌握这些工具类型不仅可以简化类型定义,而且可以基于此构建更复杂的类型转换。
下面是 TS 内置的所有类型工具,我加了下注释和示例方便理解,你可以先只看示例,测试下能否自行写出对应的类型实现(Playground)。
/**
* 使 T 的所有属性变为为可选的
*
* Partial<{name: string}> // {name?: string | undefined}
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
/**
* 使类型 T 的所有属性变为必需的
*
* Required<{name?: string}> // {name: string}
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
/**
* 使类型 T 的所有属性变为只读的
*
* Readonly<{name: string}> // {readonly name: string}
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
/**
* 从类型 T 中挑出所有属性名出现在类型 K 中的属性
*
* Pick<{name: string, age: number}, 'age'> // {age: number}
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
/**
* 构造一个 key-value 类型,其 key 是类型 K, value 是类型 T
*
* const map: Record<string, number> = {a: 1, b: 2}
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
/**
* 从类型 T 中剔除类型 U
*
* Exclude<'a' | 'b', 'a'> // 'b'
*/
type Exclude<T, U> = T extends U ? never : T;
/**
* 从类型 T 中挑出类型 U
*
* Extract<string | number, number> // number
*/
type Extract<T, U> = T extends U ? T : never;
/**
* 从类型 T 中剔除所有属性名出现在类型 K 中的属性
*
* Omit<{name: string, age: number}, 'age'> // {name: number}
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
/**
* 剔除类型 T 中的 null 和 undefined 子类型
*
* NonNullable<string | null | undefined> // string
*/
type NonNullable<T> = T extends null | undefined ? never : T;
/**
* 获取函数的参数元组(注意是元组不是数组)
*
* Parameters<(name: string, age: number) => void> // [string, number]
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
/**
* 获取构造函数的参数元组
*
* class Person { constructor(name: string, age: number) { } }
* ConstructorParameters<typeof Person> // [string, number]
*
* TS 中类有两个方面:实例面、静态面
* typeof Person 表示类的静态面类型
* Person 表示类的静态面实例,如构造函数、静态方法
* Person 也表示类实例的类型,如成员变量、成员方法
* new Person 表示类的实例
*/
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
/**
* 获取函数的返回类型
*
* ReturnType<() => string> // string
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
/**
* 获取构造函数的返回类型,即类的实例的类型
*
* class Person { constructor(name: string, age: number) { } }
* InstanceType<typeof Person> // Person
*/
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
除了 TS 内置的类型工具外,还有一些第三方开发的类型工具,提供了更多类型转换工具,例如 ts-toolbelt —— TS 版"lodash"库。
下面是从 ts-toolbelt 挑选的部分示例,更多工具类型请查看它的官网。
import type { Object } from 'ts-toolbelt'
// 使对象的部分属性变为可选
type T1 = Object.Optional<{ a: string; b: number }, 'a'>
// type T1 = {
// a?: string | undefined;
// b: number;
// }
// 合并两个对象,前面对象为 undefined 的属性被后面对象对应属性覆盖
type T2 = Object.MergeUp<{ a: 'a1', b?: 'b1' }, { a: 'a2', b: 'b2' }>
// type T2 = {
// a: "a1";
// b: "b1" | "b2";
// }
案例解析
掌握基础概念后,可能依然无法写出精确的类型描述,因为这些概念仅仅停留在单个概念的使用,需要进一步实践练习,才可能融会贯通。下面搜集了一些 TS 的类型转换案例(题目),可以从中学习一些解题思路和代码实现。
No.1
问题
假定对象的所有值都是数组类型,例如:
const data = {
a: ['x', 'y', 'z'],
b: [1, 2, 3]
} as const
要求获取上述对象值中的数组元素的类型,例如:
type TElement = "x" | "y" | "z" | 3 | 1 | 2
解题思路
首先拿到对象的值类型,然后通过数组下标获取数组元素的类型。
参考代码
type GetValueElementType<T extends { [key: string]: ReadonlyArray<any> }> = T[keyof T][number]
type TElement = GetValueElementType<typeof data>
扩展
如果对象的值不都是数组类型呢?
例如下面这样
const data = {
a: ['x', 'y', 'z'],
b: [1, 2, 3],
c: 100
} as const
解题思路:首先依然是拿到对象的值类型,然后过滤出数组类型,最后取数组的元素类型
// 实现1:通过 extends 判断对象的值类型,通过数组下标获取元素类型
type GetValueElementType<T> = { [K in keyof T]: T[K] extends ReadonlyArray<any> ? T[K][number] : never }[keyof T]
// 实现2:通过 extends 判断对象的值类型,通过 infer 推断,获取数组元素类型
type GetValueElementType<T> = { [K in keyof T]: T[K] extends ReadonlyArray<infer E> ? E : never }[keyof T]
No.2
问题
假设有一个EffectModule
类,它包含成员变量和成员方法,代码如下:
interface Action<T> {
payload?: T;
type: string;
}
class EffectModule {
count = 1;
message = "hello!";
delay(input: Promise<number>) {
return input.then(i => ({
payload: `hello ${i}!`,
type: 'delay'
}));
}
setMessage(action: Action<Date>) {
return {
payload: action.payload!.getMilliseconds(),
type: "set-message"
};
}
}
现在有一个叫connect
的函数,它接受EffectModule
实例,将它变成另一个对象,这个对象上只有EffectModule
的同名方法,
type Connected = {
delay(input: number): Action<string>
setMessage(action: Date): Action<number>
}
const effectModule = new EffectModule()
const connected: Connected = connect(effectModule)
即经过connect
函数后,方法的类型签名变成了:
asyncMethod<U, R>(input: Promise<U>): Promise<Action<R>>
// 变成
asyncMethod<U, R>(input: U): Action<R>
syncMethod<U, R>(action: Action<U>): Action<R>
// 变成
syncMethod<U, R>(action: U): Action<R>
要求实现下面的Connect
函数类型,将any
替换成题目的解答后,让编译器可以顺利编译通过,并且返回的类型与Connected
相同。
type Connect = (module: EffectModule) => any
本题来自LeeCode中国区招聘的一道面试题.
解题思路
- 过滤出
EffectModule
实例的成员方法 - 通过
T extends U
判断方法签名 a. 如果方法签名符合条件,使用infer
捕获 Promise 和 Action 中的泛型参数,并返回正确的方法类型签名 b. 否则,返回方法原始的类型签名
参考代码
// 获取对象中value为函数的属性名称
type FilterFunctionNames<T extends {}> = {[P in keyof T]: T[P] extends Function ? P: never}[keyof T]
// 转换函数类型签名
type TransformFunctions<T extends {}> = {
[P in keyof T]: T[P] extends (arg: Promise<infer U>) => Promise<Action<infer R>>
? (arg: U) => Action<R>
: T[P] extends (arg1: Action<infer U>) => Action<infer R>
? (arg: U) => Action<R>
: never
}
// 1. 过滤出 value 为函数类型的实例属性名称
// 2. 通过 Pick 挑出 value 为函数类型的成员组成新对象
// 3. 遍历对象的 key/value,将符合条件的类型签名转换成目标签名
type Connect = (module: EffectModule) => TransformFunctions<Pick<EffectModule, FilterFunctionNames<EffectModule>>>
上面的TransformFunctions
的实现也可以简单点写,例如将返回值类型Promise<Action<infer R>>
改成Promise<infer R>
,却别在于前者判断时更加精准。前者约束返回值必须是 Promise + Action 类型,而后者只约束返回值是 Promise 类型。
type TransformFunctions<T extends {}> = {
[P in keyof T]: T[P] extends (arg: Promise<infer U>) => Promise<infer R>
? (arg: U) => R
: T[P] extends (arg1: Action<infer U>) => infer R
? (arg: U) => R
: never
}
案例部分未完待续。。。
参考
转载自:https://juejin.cn/post/6844904144545775629