跳到主要内容

工具类型(Utility Types)

问题

TypeScript 内置了哪些工具类型?如何使用和实现它们?

答案

工具类型是 TypeScript 内置的类型转换工具,用于基于现有类型创建新类型。它们利用映射类型、条件类型等高级特性实现。


属性修饰类型

Partial<T> - 全部可选

将类型的所有属性变为可选:

interface User {
id: number;
name: string;
email: string;
}

type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string }

// 使用场景:更新操作
function updateUser(id: number, updates: Partial<User>) {
// 只需要传部分字段
}

updateUser(1, { name: 'Alice' }); // OK

实现原理

type MyPartial<T> = {
[P in keyof T]?: T[P];
};

Required<T> - 全部必选

将类型的所有属性变为必选(去除 ?):

interface Config {
host?: string;
port?: number;
ssl?: boolean;
}

type RequiredConfig = Required<Config>;
// { host: string; port: number; ssl: boolean }

// 使用场景:配置验证后使用
function initServer(config: RequiredConfig) {
// 所有配置都必须存在
}

实现原理

type MyRequired<T> = {
[P in keyof T]-?: T[P]; // -? 移除可选标记
};

Readonly<T> - 全部只读

将类型的所有属性变为只读:

interface State {
count: number;
items: string[];
}

type ReadonlyState = Readonly<State>;
// { readonly count: number; readonly items: string[] }

const state: ReadonlyState = { count: 0, items: [] };
// state.count = 1; // 错误:只读属性

// 注意:只是浅只读
state.items.push('item'); // OK,数组内容可修改

实现原理

type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};

属性选择类型

Pick<T, K> - 选择属性

从类型中选择指定属性:

interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}

type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string }

// 使用场景:API 返回的公开数据
function getPublicProfile(user: User): PublicUser {
const { id, name, email } = user;
return { id, name, email };
}

实现原理

type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};

Omit<T, K> - 排除属性

从类型中排除指定属性:

type UserWithoutPassword = Omit<User, 'password'>;
// { id: number; name: string; email: string; createdAt: Date }

// 排除多个
type BasicUser = Omit<User, 'password' | 'createdAt'>;
// { id: number; name: string; email: string }

// 使用场景:创建用户时不需要 id
type CreateUserDto = Omit<User, 'id' | 'createdAt'>;

实现原理

type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

联合类型操作

Exclude<T, U> - 排除联合成员

从联合类型 T 中排除可赋值给 U 的类型:

type AllEvents = 'click' | 'focus' | 'blur' | 'scroll' | 'resize';
type MouseEvents = 'click' | 'scroll';

type NonMouseEvents = Exclude<AllEvents, MouseEvents>;
// 'focus' | 'blur' | 'resize'

// 排除 null 和 undefined
type Primitive = string | number | boolean | null | undefined;
type NonNullPrimitive = Exclude<Primitive, null | undefined>;
// string | number | boolean

实现原理

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

Extract<T, U> - 提取联合成员

从联合类型 T 中提取可赋值给 U 的类型:

type AllTypes = string | number | boolean | (() => void);
type Functions = Extract<AllTypes, Function>;
// () => void

type Primitives = Extract<AllTypes, string | number>;
// string | number

实现原理

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

NonNullable<T> - 移除 null 和 undefined

type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string

type Data = { name: string } | null | undefined;
type ValidData = NonNullable<Data>;
// { name: string }

实现原理

type MyNonNullable<T> = T extends null | undefined ? never : T;

函数类型操作

Parameters<T> - 获取函数参数类型

function fetchData(url: string, options: RequestInit, retry: number) {
// ...
}

type FetchParams = Parameters<typeof fetchData>;
// [url: string, options: RequestInit, retry: number]

// 获取单个参数
type UrlType = Parameters<typeof fetchData>[0]; // string
type OptionsType = Parameters<typeof fetchData>[1]; // RequestInit

实现原理

type MyParameters<T extends (...args: any) => any> = 
T extends (...args: infer P) => any ? P : never;

ReturnType<T> - 获取函数返回类型

function createUser() {
return { id: 1, name: 'Alice', createdAt: new Date() };
}

type UserResult = ReturnType<typeof createUser>;
// { id: number; name: string; createdAt: Date }

// 异步函数
async function fetchUser(): Promise<User> {
return {} as User;
}

type FetchResult = ReturnType<typeof fetchUser>;
// Promise<User>

// 获取 Promise 内部类型
type AwaitedUser = Awaited<ReturnType<typeof fetchUser>>;
// User

实现原理

type MyReturnType<T extends (...args: any) => any> = 
T extends (...args: any) => infer R ? R : any;

ConstructorParameters<T> - 构造函数参数类型

class UserService {
constructor(private apiUrl: string, private timeout: number) {}
}

type ServiceParams = ConstructorParameters<typeof UserService>;
// [apiUrl: string, timeout: number]

InstanceType<T> - 实例类型

class User {
constructor(public name: string) {}
}

type UserInstance = InstanceType<typeof User>;
// User

// 用于工厂函数
function createInstance<T extends new (...args: any[]) => any>(
Ctor: T,
...args: ConstructorParameters<T>
): InstanceType<T> {
return new Ctor(...args);
}

const user = createInstance(User, 'Alice'); // User 类型

字符串类型操作

type Greeting = 'hello world';

type Upper = Uppercase<Greeting>; // 'HELLO WORLD'
type Lower = Lowercase<Greeting>; // 'hello world'
type Capital = Capitalize<Greeting>; // 'Hello world'
type Uncap = Uncapitalize<'Hello World'>; // 'hello World'

实际应用

// 属性名转换
type EventMap = {
click: MouseEvent;
focus: FocusEvent;
keydown: KeyboardEvent;
};

type OnEvent = {
[K in keyof EventMap as `on${Capitalize<K>}`]: (event: EventMap[K]) => void;
};
// { onClick: (event: MouseEvent) => void; onFocus: ...; onKeydown: ... }

其他常用工具类型

Record<K, T> - 构造对象类型

type PageName = 'home' | 'about' | 'contact';
type PageInfo = { title: string; url: string };

const pages: Record<PageName, PageInfo> = {
home: { title: 'Home', url: '/' },
about: { title: 'About', url: '/about' },
contact: { title: 'Contact', url: '/contact' }
};

// 动态键
type StringMap = Record<string, any>;
type NumberIndex = Record<number, string>;

实现原理

type MyRecord<K extends keyof any, T> = {
[P in K]: T;
};

Awaited<T> - 解包 Promise

type P1 = Awaited<Promise<string>>;        // string
type P2 = Awaited<Promise<Promise<number>>>; // number(递归解包)
type P3 = Awaited<string | Promise<boolean>>; // string | boolean

ThisParameterType<T> 和 OmitThisParameter<T>

function greet(this: { name: string }, greeting: string) {
return `${greeting}, ${this.name}`;
}

type ThisType = ThisParameterType<typeof greet>;
// { name: string }

type GreetFn = OmitThisParameter<typeof greet>;
// (greeting: string) => string

组合使用

interface User {
id: number;
name: string;
email: string;
password: string;
isAdmin: boolean;
createdAt: Date;
updatedAt: Date;
}

// 创建 DTO:排除 id 和时间戳,密码可选
type CreateUserDto = Partial<Pick<User, 'password'>> &
Omit<User, 'id' | 'password' | 'createdAt' | 'updatedAt'>;
// { name: string; email: string; isAdmin: boolean; password?: string }

// 更新 DTO:除 id 外全部可选
type UpdateUserDto = Partial<Omit<User, 'id'>>;

// 只读视图
type ReadonlyUser = Readonly<Omit<User, 'password'>>;

常见面试问题

Q1: 实现 Required<T>

答案

// 内置实现
type Required<T> = {
[P in keyof T]-?: T[P];
};

// -? 表示移除可选标记
// 同理,-readonly 可以移除只读标记

type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};

Q2: 实现 Pick<T, K> 和 Omit<T, K>

答案

// Pick:从 T 中选择 K 中的属性
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};

// Omit:从 T 中排除 K 中的属性
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// 或者直接实现
type MyOmit2<T, K extends keyof any> = {
[P in keyof T as P extends K ? never : P]: T[P];
};

// 测试
interface Example {
a: string;
b: number;
c: boolean;
}

type Picked = MyPick<Example, 'a' | 'b'>; // { a: string; b: number }
type Omitted = MyOmit<Example, 'c'>; // { a: string; b: number }

Q3: 实现 Parameters<T> 和 ReturnType<T>

答案

// Parameters:获取函数参数类型元组
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;

// ReturnType:获取函数返回类型
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;

// 使用 infer 在条件类型中推断类型

// 测试
type Fn = (a: string, b: number) => boolean;
type Params = MyParameters<Fn>; // [a: string, b: number]
type Return = MyReturnType<Fn>; // boolean

Q4: Partial 是深层的吗?如何实现深层 Partial?

答案

// Partial 是浅层的
interface Nested {
a: {
b: {
c: string;
};
};
}

type ShallowPartial = Partial<Nested>;
// { a?: { b: { c: string } } }
// a 可选,但 b, c 仍然必需

// DeepPartial 实现
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;

type DeepPartialNested = DeepPartial<Nested>;
// { a?: { b?: { c?: string } } }

// 更完善的实现(处理数组、函数等)
type DeepPartial2<T> = T extends (...args: any[]) => any
? T
: T extends Array<infer U>
? DeepPartialArray<U>
: T extends object
? { [P in keyof T]?: DeepPartial2<T[P]> }
: T;

interface DeepPartialArray<T> extends Array<DeepPartial2<T>> {}

Q5: Record 和 索引签名的区别?

答案

// 索引签名
interface StringMap {
[key: string]: number;
}

// Record
type RecordStringNumber = Record<string, number>;

// 主要区别:

// 1. Record 可以使用联合类型作为键
type Status = 'active' | 'inactive' | 'pending';
type StatusCodes = Record<Status, number>;
// 必须包含所有 Status 成员

// 索引签名无法限制具体的键
interface StatusCodesInterface {
[key: string]: number;
// 无法限制只能是 'active' | 'inactive' | 'pending'
}

// 2. Record 可以使用 symbol 和 number
type NumberKeyed = Record<number, string>;
type SymbolKeyed = Record<symbol, any>;

// 3. 类型检查
const statusCodes: StatusCodes = {
active: 1,
inactive: 0,
pending: 2,
// other: 3, // 错误:不存在于 Status
};

// 索引签名允许任意键
const statusCodes2: StatusCodesInterface = {
active: 1,
inactive: 0,
anything: 999, // OK
};

相关链接