超实用 Typescript 内置类型与自定义类型

2020年01月14日 19:30 1 点赞 0 评论 更新于 2025-11-21 21:29

在使用 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'>>;

通过这种方式,我们可以尽量减少重复的类型定义,复用已有类型,使代码更加优雅且易于维护。

上述代码中使用的 PartialPick 都是 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 的反向操作,用于获取类型 TU 的交集属性。其定义如下:

type Extract<T, U> = T extends U ? T : never;

使用示例

// 'b' | 'c'
type A = Extract<'a' | 'b' | 'c' | 'd', 'b' | 'c' | 'e'>;

这个类型看起来用处不大,可能是因为笔者还没有发掘到其实际用途。

NonNullable

NonNullable 类型用于排除类型 T 中的 nullundefined 属性。其定义如下:

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 内置的类型别名 PartialRecord

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 待推断类型,我们可以探索出各种各样的复杂组合玩法。这里就不再详细介绍了,大家可以自行深入探索。