TypeScript的泛型工具类型是类型编程的核心能力。BeyondPartial、Required这些内置工具类型,实际项目中大量场景需要自定义工具类型来保证类型安全。这篇文章从需求出发,逐步实现常用的自定义工具类型并给出应用场景。
内置工具类型的盲区
TypeScript内置了Partial、Required、Readonly、Pick、Omit、Record等工具类型,但它们有局限:
Partial<T>只做一层浅Partial,嵌套对象不会递归处理Omit<T, K>对联合类型的Key无法做条件分发- 没有内置的DeepPartial、DeepReadonly、ValueOf等
这些场景在实际业务中频繁出现,需要自定义。
递归工具类型:DeepPartial与DeepReadonly
表单场景经常需要DeepPartial——一个嵌套3层的配置对象,每一层都可选:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? T[P] extends Function
? T[P]
: DeepPartial<T[P]>
: T[P];
};
// 使用示例
interface AppConfig {
database: {
host: string;
port: number;
pool: {
min: number;
max: number;
};
};
cache: {
ttl: number;
prefix: string;
};
}
// 所有嵌套属性都变为可选
type PartialConfig = DeepPartial<AppConfig>;
// 合法
const config: PartialConfig = {
database: {
pool: { max: 10 }
}
};
DeepReadonly用于冻结配置对象,防止运行时意外修改:
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P];
};
条件类型:提取与变换
ExtractRouteParams从路由字符串中提取参数类型:
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
// /users/:id/posts/:postId → { id: string; postId: string }
type Params = ExtractRouteParams<"/users/:id/posts/:postId">;
function navigate<T extends string>(path: T, params: ExtractRouteParams<T>) {
// 类型安全的路由跳转
}
navigate("/users/:id/posts/:postId", { id: "1", postId: "42" }); // OK
navigate("/users/:id", { id: "1" }); // OK
navigate("/users/:id", {}); // Error: missing id
键值变换工具类型
API返回的字段名和前端使用的命名规范不同时,需要类型层面的映射:
type CamelToSnake<S extends string> =
S extends `${infer First}${infer Rest}`
? First extends Uppercase<First>
? `_${Lowercase<First>}${CamelToSnake<Rest>}`
: `${First}${CamelToSnake<Rest>}`
: S;
type SnakeToCamel<S extends string> =
S extends `${infer First}_${infer Rest}`
? `${First}${Capitalize<SnakeToCamel<Rest>>}`
: S;
// 整个对象的键名变换
type TransformKeys<T, Transformer> = {
[K in keyof T as K extends string
? Transformer extends (s: K) => string
? ReturnType<Transformer>
: K
: K]: T[K];
};
// 实际应用:API响应转驼峰
type SnakeToCamelKeys<T> = {
[K in keyof T as K extends string ? SnakeToCamel<K> : K]:
T[K] extends object ? SnakeToCamelKeys<T[K]> : T[K];
};
interface ApiUser {
user_name: string;
created_at: string;
email_address: string;
}
type FrontendUser = SnakeToCamelKeys<ApiUser>;
// { userName: string; createdAt: string; emailAddress: string }
类型安全的事件系统
一个常见需求:EventEmitter的类型安全。用映射类型约束事件名和payload:
interface EventMap {
"user:login": { userId: string; timestamp: number };
"user:logout": { userId: string };
"page:view": { path: string; referrer?: string };
}
class TypedEventEmitter<T extends Record<string, unknown>> {
private listeners = new Map<string, Function[]>();
on<K extends keyof T & string>(event: K, fn: (payload: T[K]) => void) {
const existing = this.listeners.get(event) || [];
this.listeners.set(event, [...existing, fn]);
}
emit<K extends keyof T & string>(event: K, payload: T[K]) {
(this.listeners.get(event) || []).forEach(fn => fn(payload));
}
}
const emitter = new TypedEventEmitter<EventMap>();
emitter.on("user:login", (payload) => {
// payload自动推断为 { userId: string; timestamp: number }
console.log(payload.userId, payload.timestamp);
});
emitter.emit("user:login", { userId: "1", timestamp: Date.now() }); // OK
emitter.emit("user:login", { userId: "1" }); // Error: missing timestamp
运行时类型校验:TypeBox方案
TypeScript类型只在编译期生效,运行时数据(API响应、用户输入)需要校验。TypeBox同时提供TypeScript类型和JSON Schema:
import { Type, Static } from "@sinclair/typebox";
const UserSchema = Type.Object({
id: Type.String(),
name: Type.String({ minLength: 1, maxLength: 100 }),
email: Type.String({ format: "email" }),
role: Type.Union([Type.Literal("admin"), Type.Literal("user")]),
});
type User = Static<typeof UserSchema>;
// 等价于 { id: string; name: string; email: string; role: "admin" | "user" }
// 运行时校验
import { Value } from "@sinclair/typebox/value";
const input = JSON.parse(responseBody);
if (Value.Check(UserSchema, input)) {
// 这里input的类型是User
saveUser(input);
} else {
const errors = [...Value.Errors(UserSchema, input)];
throw new ValidationError(errors);
}
类型调试技巧
复杂类型推断结果看不清时,用Expect断言:
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
// 测试类型是否正确
type _test = Expect<Equal<SnakeToCamel<"user_name">, "userName">>;
// 如果类型正确,编译通过;否则报类型错误
泛型工具类型的核心不是炫技,而是用类型系统在编译期捕获运行时可能出现的错误。业务代码中每多一个精确的类型约束,线上就少一个潜在的bug。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/typescript-shi-zhan-fan-xing-gong-ju-lei-xing-de-gao-ji/