最新文章
泰课新年学课蛇来运转欢度春节活动
02-01 20:25
共庆2024圣诞、元旦泰课双蛋活动
12-16 10:21
泰课共庆75周年国庆活动!
10-05 21:24
暑假双月联动学习计划 7月15 - 8月21日
07-14 23:09
泰课在线劳动光荣,勤学快乐之五月勤学季活动
04-30 21:19
2024年青春绽放开学季活动
03-11 13:01
超实用 Typescript 内置类型与自定义类型
在使用 Typescript 时,我们常常需要提前声明类型,再将其赋予变量。例如,在业务开发中渲染表格时,通常需要定义表格行的数据类型:
interface Row {
user: string;
email: string;
id: number;
vip: boolean;
// ...
}
const tableDatas: Row[] = [];
// ...
有时,我们还需要为表格对应的搜索表单定义类型,只包含一两个搜索项。刚接触 Typescript 的开发者可能会这样写:
interface SearchModel {
user?: string;
id?: number;
}
const model: SearchModel = {
user: '',
id: undefined
};
这种写法存在一个问题,如果后续 id 的类型需要改为 string,我们需要在两个地方进行修改,稍不留意就可能遗漏一处。因此,有些开发者会这样做:
interface SearchModel {
user?: Row['user'];
id?: Row['id'];
}
这确实是一种解决方案,但实际上,我们之前已经定义了 Row 类型,完全可以更优雅地复用它:
const model: Partial<Row> = {
user: '',
id: undefined
};
// 或者需要明确指定 key 的情况
const model2: Partial<Pick<Row, 'user' | 'id'>>;
通过这种方式,我们可以尽量减少重复的类型定义,复用已有类型,使代码更加优雅且易于维护。
上述代码中使用的 Partial 和 Pick 都是 Typescript 内置的类型别名。接下来,我们将详细介绍 Typescript 常用的内置类型以及自定义类型。
Typescript 内置类型
Partial
Partial 类型可以将类型 T 的所有属性标记为可选属性。其定义如下:
type Partial<T> = {
[P in keyof T]?: T[P];
};
使用场景:
// 账号属性
interface AccountInfo {
name: string;
email: string;
age: number;
vip: 0 | 1; // 1 是 vip ,0 是非 vip
}
// 当我们需要渲染一个账号表格时,需要定义
const accountList: AccountInfo[] = [];
// 但当我们需要查询过滤账号信息,通过表单进行查询时
// 显然我们不一定需要使用所有属性进行搜索,此时可以定义
const model: Partial<AccountInfo> = {
name: '',
vip: undefined
};
Required
与 Partial 相反,Required 类型将类型 T 的所有属性标记为必选属性。其定义如下:
type Required<T> = {
[P in keyof T]-?: T[P];
};
Readonly
Readonly 类型将类型 T 的所有属性标记为只读属性,即这些属性不能被修改。其定义如下:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Pick<T, K>
Pick<T, K> 类型用于从类型 T 中选取属性 K。其定义如下:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
使用场景:
interface AccountInfo {
name: string;
email: string;
age: number;
vip?: 0 | 1; // 1 是 vip ,0 是非 vip
}
type CoreInfo = Pick<AccountInfo, 'name' | 'email'>;
/*
{
name: string;
email: string;
}
*/
Record<K, T>
Record<K, T> 类型用于标记对象的键值类型。其定义如下:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
使用场景:
// 定义 学号(key)-账号信息(value) 的对象
const accountMap: Record<number, AccountInfo> = {
10001: {
name: 'xx',
email: 'xxxxx',
// ...
}
};
const user: Record<'name' | 'email', string> = {
name: '',
email: ''
};
// 复杂点的类型推断
function mapObject<K extends string | number, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>
const names = { foo: "hello", bar: "world", baz: "bye" };
// 此处推断 K, T 值为 string , U 为 number
const lengths = mapObject(names, s => s.length); // { foo: number, bar: number, baz: number }
Exclude<T, U>,Omit<T, K>
Exclude<T, U> 类型用于移除类型 T 中包含的类型 U。其定义如下:
type Exclude<T, U> = T extends U ? never : T;
使用场景:
// 'a' | 'd'
type A = Exclude<'a' | 'b' | 'c' | 'd', 'b' | 'c' | 'e'>;
乍一看,Exclude 类型似乎没什么用处,但通过一些操作,我们可以实现 Pick 的反向操作:
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type NonCoreInfo = Omit<AccountInfo, 'name' | 'email'>;
/*
{
age: number;
vip: 0 | 1;
}
*/
Extract<T, U>
Extract<T, U> 是 Exclude 的反向操作,用于获取类型 T 和 U 的交集属性。其定义如下:
type Extract<T, U> = T extends U ? T : never;
使用示例:
// 'b' | 'c'
type A = Extract<'a' | 'b' | 'c' | 'd', 'b' | 'c' | 'e'>;
这个类型看起来用处不大,可能是因为笔者还没有发掘到其实际用途。
NonNullable
NonNullable 类型用于排除类型 T 中的 null 和 undefined 属性。其定义如下:
type NonNullable<T> = T extends null | undefined ? never : T;
使用示例:
type A = string | number | undefined;
type B = NonNullable<A>; // string | number
function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
let s1: string = x; // Error, x 可能为 undefined
let s2: string = y; // Ok
}
Parameters
Parameters 类型用于获取一个函数的所有参数类型。其定义如下:
// 此处使用 infer P 将参数定为待推断类型
// T 符合函数特征时,返回参数类型,否则返回 never
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
使用示例:
interface IFunc {
(person: IPerson, count: number): boolean;
}
type P = Parameters<IFunc>; // [IPerson, number]
const person01: P[0] = {
// ...
};
另一种使用场景是快速获取未知函数的参数类型:
import { somefun } from 'somelib';
// 从其他库导入的一个函数,获取其参数类型
type SomeFuncParams = Parameters<typeof somefun>;
// 内置函数
// [any, number?, number?]
type FillParams = Parameters<typeof Array.prototype.fill>;
ConstructorParameters
ConstructorParameters 类型类似于 Parameters<T>,用于获取一个类的构造函数参数。其定义如下:
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
使用示例:
// string | number | Date
type DateConstrParams = ConstructorParameters<typeof Date>;
ReturnType
ReturnType 类型用于获取函数类型 T 的返回类型。其定义如下:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
其使用方式与 Parameters<T> 类似,这里不再赘述。
InstanceType
InstanceType 类型用于获取一个类的实例类型。其定义如下:
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
其使用方式与 ConstructorParameters<T> 类似,这里不再赘述。
自定义常用类型
Weaken
在使用 Typescript 时,有时我们需要重写一个库提供的 interface 的某个属性,但直接重写可能会导致冲突。例如:
interface Test {
name: string;
say(word: string): string;
}
interface Test2 extends Test {
name: Test['name'] | number;
}
// error: Type 'string | number' is not assignable to type 'string'.
我们可以通过 Weaken 类型来实现这个需求:
// 原理是,将 类型 T 的所有 K 属性置为 any,
// 然后自定义 K 属性的类型,
// 由于任何类型都可以赋予 any,所以不会产生冲突
type Weaken<T, K extends keyof T> = {
[P in keyof T]: P extends K ? any : T[P];
};
interface Test2 extends Weaken<Test, 'name'> {
name: Test['name'] | number;
}
// ok
数组转换为 union
有时候,我们需要将数组转换为联合类型:
const ALL_SUITS = ['hearts', 'diamonds', 'spades', 'clubs'] as const; // TS 3.4
type SuitTuple = typeof ALL_SUITS; // readonly ['hearts', 'diamonds', 'spades', 'clubs']
type Suit = SuitTuple[number]; // union type : 'hearts' | 'diamonds' | 'spades' | 'clubs'
根据 enum 生成 union
enum 的 key 值 union
enum Weekday {
Mon = 1,
Tue = 2,
Wed = 3
}
type WeekdayName = keyof typeof Weekday; // 'Mon' | 'Tue' | 'Wed'
enum 无法实现 value-union ,但可以使用 object 的 value 值 union
const lit = <V extends keyof any>(v: V) => v;
const Weekday = {
MONDAY: lit(1),
TUESDAY: lit(2),
WEDNESDAY: lit(3)
};
type Weekday = (typeof Weekday)[keyof typeof Weekday]; // 1 | 2 | 3
PartialRecord
前面我们介绍了 Record 类型,在实际使用中可能会遇到一些问题。例如:
interface Model {
name: string;
email: string;
id: number;
age: number;
}
// 定义表单的校验规则
const validateRules: Record<keyof Model, Validator> = {
name: { required: true, trigger: `blur` },
id: { required: true, trigger: `blur` },
email: { required: true, message: `...` },
// error: Property age is missing in type...
};
这里的问题是,validateRules 的键值必须与 Model 完全匹配,缺一不可。但实际上,我们的表单可能只包含其中的一两项。这时,我们可以使用 PartialRecord 类型:
type PartialRecord<K extends keyof any, T> = Partial<Record<K, T>>;
const validateRules: PartialRecord<keyof Model, Validator> = {
name: { required: true, trigger: `blur` }
};
这个例子组合使用了 Typescript 内置的类型别名 Partial 和 Record。
Unpacked
Unpacked 类型用于解压抽离关键类型。其定义如下:
type Unpacked<T> =
T extends (infer U)[] ? U :
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T;
type T0 = Unpacked<string>; // string
type T1 = Unpacked<string[]>; // string
type T2 = Unpacked<() => string>; // string
type T3 = Unpacked<Promise<string>>; // string
type T4 = Unpacked<Promise<string>[]>; // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
总结
实际上,基于已有的类型别名和新推出的 infer 待推断类型,我们可以探索出各种各样的复杂组合玩法。这里就不再详细介绍了,大家可以自行深入探索。