工具类型(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
};