掌握类型编程,让你的TypeScript技能上一个大台阶
为什么需要类型体操?
在上一篇文章中,你学会了如何快速启动TypeScript项目。现在你可能遇到这样的问题:
// 需求:写一个函数,根据传入的参数类型返回对应的处理结果
function processInput(input: unknown) {
// 这里该用什么类型?
// 如何确保返回类型与输入类型匹配?
}
或者看到这样的类型定义时一头雾水:
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
这就是类型体操(Type Gymnastics)的领域。类型体操不是炫技,而是解决实际类型问题的必备技能。
热身:理解类型系统的三个层次
层次1:基础类型(你已经掌握的)
// 1. 基本类型
let name: string = "张三";
let age: number = 25;
let isActive: boolean = true;
// 2. 数组和元组
let numbers: number[] = [1, 2, 3];
let tuple: [string, number] = ["张三", 25];
// 3. 联合类型
let id: string | number = "123";
层次2:泛型(你需要熟悉的)
// 泛型函数
function identity<T>(value: T): T {
return value;
}
// 泛型接口
interface Response<T> {
data: T;
status: number;
}
// 泛型约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
层次3:类型编程(本文要学的)
// 类型编程 - 创造新的类型
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
第一课:理解条件类型
条件类型是类型编程的基础,就像if-else语句一样。
// 基础语法:T extends U ? X : Y
// "如果T可以赋值给U,那么类型是X,否则是Y"
// 例子1:判断是否为数组
type IsArray<T> = T extends any[] ? true : false;
type Test1 = IsArray<number[]>; // true
type Test2 = IsArray<string>; // false
// 例子2:提取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : T;
type Test3 = ElementType<number[]>; // number
type Test4 = ElementType<string>; // string
// 例子3:处理null和undefined
type NonNullable<T> = T extends null | undefined ? never : T;
type Test5 = NonNullable<string | null>; // string
type Test6 = NonNullable<undefined>; // never
实际应用:处理API响应中的可选字段
// API返回的数据中,有些字段可能是null
interface ApiUser {
id: string;
name: string | null;
email: string | null;
age: number;
}
// 我们希望将null转换为可选字段
type NullToOptional<T> = {
[K in keyof T]: T[K] extends null
? (T[K] | undefined) // null变成undefined
: T[K];
} & {
[K in keyof T as T[K] extends null ? K : never]?: never
};
type User = NullToOptional<ApiUser>;
// 等价于:
// type User = {
// id: string;
// name?: string | undefined;
// email?: string | undefined;
// age: number;
// }
第二课:掌握映射类型
映射类型让你能够批量转换对象的属性。
// 基础语法:{ [P in K]: T }
// "对于K中的每个属性P,类型为T"
// TypeScript内置的映射类型
interface User {
id: string;
name: string;
age: number;
}
// 1. Partial - 所有属性变为可选
type PartialUser = Partial<User>;
// 等价于:{ id?: string; name?: string; age?: number; }
// 2. Required - 所有属性变为必填
type RequiredUser = Required<User>;
// 所有的?都被移除
// 3. Readonly - 所有属性变为只读
type ReadonlyUser = Readonly<User>;
// 等价于:{ readonly id: string; readonly name: string; readonly age: number; }
// 4. Pick - 挑选部分属性
type UserNameOnly = Pick<User, 'name'>;
// 等价于:{ name: string; }
// 5. Omit - 排除部分属性
type UserWithoutAge = Omit<User, 'age'>;
// 等价于:{ id: string; name: string; }
实战:创建自己的映射类型
// 1. 创建可选但非undefined的类型
type LoosePartial<T> = {
[P in keyof T]?: T[P] | null;
};
// 2. 创建只读但可修改特定属性的类型
type ReadonlyExcept<T, K extends keyof T> =
Readonly<Pick<T, Exclude<keyof T, K>>> & Pick<T, K>;
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
// 价格和库存可以修改,其他属性只读
type EditableProduct = ReadonlyExcept<Product, 'price' | 'stock'>;
// {
// readonly id: string;
// readonly name: string;
// price: number;
// stock: number;
// }
// 3. 深度只读(递归处理嵌套对象)
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? (T[P] extends Function ? T[P] : DeepReadonly<T[P]>)
: T[P];
};
interface Company {
name: string;
address: {
city: string;
street: string;
};
employees: Array<{
id: string;
name: string;
}>;
}
type ReadonlyCompany = DeepReadonly<Company>;
// address和employees内的所有属性都是只读的
第三课:理解索引类型和keyof
索引类型和keyof是类型体操的核心工具。
// keyof:获取对象所有键的联合类型
interface Person {
name: string;
age: number;
address: string;
}
type PersonKeys = keyof Person; // "name" | "age" | "address"
// 索引访问类型:T[K]
type NameType = Person['name']; // string
type AgeType = Person['age']; // number
// 实战:安全的属性访问函数
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person: Person = {
name: "张三",
age: 25,
address: "北京"
};
const name = getProperty(person, "name"); // string
const age = getProperty(person, "age"); // number
// const invalid = getProperty(person, "email"); // 错误:email不是Person的键
高级应用:类型安全的Redux Action
// 定义Action的类型结构
type ActionType = 'ADD_TODO' | 'REMOVE_TODO' | 'UPDATE_TODO';
// Action的payload类型映射
type ActionPayloads = {
ADD_TODO: { text: string };
REMOVE_TODO: { id: string };
UPDATE_TODO: { id: string; text: string };
};
// 创建类型安全的Action
type Action<T extends ActionType> = {
type: T;
payload: ActionPayloads[T];
};
// 使用
const addTodoAction: Action<'ADD_TODO'> = {
type: 'ADD_TODO',
payload: { text: '学习TypeScript' } // 必须是{ text: string }
};
const removeTodoAction: Action<'REMOVE_TODO'> = {
type: 'REMOVE_TODO',
payload: { id: '123' } // 必须是{ id: string }
};
// 错误示例
// const errorAction: Action<'ADD_TODO'> = {
// type: 'ADD_TODO',
// payload: { id: '123' } // 错误:payload应该是{ text: string }
// };
第四课:实用类型工具集
这些工具类型解决80%的实际问题。
// 1. 提取函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function getUser() {
return { id: '1', name: '张三' };
}
type User = ReturnType<typeof getUser>; // { id: string; name: string }
// 2. 提取函数参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function updateUser(id: string, data: { name: string }) {
// ...
}
type UpdateUserParams = Parameters<typeof updateUser>; // [string, { name: string }]
// 3. 提取构造函数类型
type ConstructorParameters<T> = T extends new (...args: infer P) => any ? P : never;
type InstanceType<T> = T extends new (...args: any[]) => infer R ? R : any;
class UserService {
constructor(public apiUrl: string) {}
}
type UserServiceParams = ConstructorParameters<typeof UserService>; // [string]
type UserServiceInstance = InstanceType<typeof UserService>; // UserService
// 4. 可选属性转必填(但有默认值)
type WithDefaults<T, D extends Partial<T>> = {
[P in keyof T]: P extends keyof D ? (T[P] | D[P]) : T[P];
};
interface Config {
host: string;
port: number;
timeout: number;
}
type DefaultConfig = WithDefaults<Config, { port: 3000, timeout: 5000 }>;
// {
// host: string;
// port: number | 3000; // 可以是number,也可以是默认值3000
// timeout: number | 5000; // 可以是number,也可以是默认值5000
// }
第五课:实战:构建类型安全的表单系统
让我们用类型体操解决一个实际问题:构建类型安全的表单系统。
// 1. 定义表单字段类型
type FieldType = 'text' | 'number' | 'email' | 'password' | 'date';
// 2. 表单配置接口
interface FieldConfig<T = any> {
type: FieldType;
label: string;
required?: boolean;
defaultValue?: T;
validate?: (value: T) => string | null;
}
// 3. 表单配置映射
type FormConfig<T extends Record<string, any>> = {
[K in keyof T]: FieldConfig<T[K]>;
};
// 4. 表单值类型(从配置推导)
type FormValues<T extends Record<string, FieldConfig>> = {
[K in keyof T]: T[K] extends FieldConfig<infer V> ? V : never;
};
// 5. 表单验证结果
type ValidationResult<T> = {
[K in keyof T]?: string;
};
// 示例:用户注册表单
const registerFormConfig = {
username: {
type: 'text' as const,
label: '用户名',
required: true,
validate: (value: string) =>
value.length < 3 ? '用户名至少3个字符' : null
},
email: {
type: 'email' as const,
label: '邮箱',
required: true,
validate: (value: string) =>
!value.includes('@') ? '邮箱格式不正确' : null
},
age: {
type: 'number' as const,
label: '年龄',
defaultValue: 18,
validate: (value: number) =>
value < 0 ? '年龄不能为负数' : null
},
birthDate: {
type: 'date' as const,
label: '出生日期',
required: false
}
};
// 自动推导出的类型
type RegisterFormConfig = typeof registerFormConfig;
type RegisterFormValues = FormValues<RegisterFormConfig>;
// 等价于:
// type RegisterFormValues = {
// username: string;
// email: string;
// age: number;
// birthDate: Date;
// }
// 6. 类型安全的表单类
class TypedForm<T extends Record<string, FieldConfig>> {
private config: T;
private values: FormValues<T>;
constructor(config: T) {
this.config = config;
this.values = {} as FormValues<T>;
// 初始化默认值
for (const key in config) {
const field = config[key];
if (field.defaultValue !== undefined) {
this.values[key] = field.defaultValue;
}
}
}
setValue<K extends keyof FormValues<T>>(
field: K,
value: FormValues<T>[K]
): void {
this.values[field] = value;
}
getValue<K extends keyof FormValues<T>>(field: K): FormValues<T>[K] {
return this.values[field];
}
validate(): ValidationResult<FormValues<T>> {
const errors: ValidationResult<FormValues<T>> = {};
for (const key in this.config) {
const field = this.config[key];
const value = this.values[key];
// 检查必填字段
if (field.required && value === undefined) {
errors[key] = `${field.label}不能为空`;
continue;
}
// 执行自定义验证
if (field.validate && value !== undefined) {
const error = field.validate(value);
if (error) {
errors[key] = error;
}
}
}
return errors;
}
}
// 使用示例
const registerForm = new TypedForm(registerFormConfig);
// 设置值 - 有完整的类型提示
registerForm.setValue('username', '张三'); // ✅
registerForm.setValue('email', 'zhangsan@example.com'); // ✅
registerForm.setValue('age', 25); // ✅
// registerForm.setValue('age', '25'); // ❌ 类型错误
// registerForm.setValue('birthDate', '2023-01-01'); // ❌ 类型错误,应该是Date
// 获取值 - 有正确的类型推断
const username = registerForm.getValue('username'); // string
const age = registerForm.getValue('age'); // number
// 验证
const errors = registerForm.validate();
// errors的类型:{ username?: string; email?: string; age?: string; birthDate?: string; }
类型体操的学习路径
阶段1:看懂(1-2周)
- 理解内置工具类型(Partial、Pick、Omit等)
- 能读懂常见的类型定义
- 在IDE中查看类型推导结果
阶段2:模仿(2-4周)
- 复制并修改现有的类型工具
- 在简单场景中使用条件类型
- 为现有代码添加类型注解
阶段3:创造(1-3个月)
- 为解决实际问题创建新的类型工具
- 设计复杂的类型系统
- 优化现有类型定义
阶段4:精通(持续)
- 理解TypeScript类型系统的底层原理
- 参与开源项目的类型定义贡献
- 为团队建立类型最佳实践
今日挑战:动手练习
练习1:创建一个DeepPartial类型,让嵌套对象的所有属性都可选
interface Company {
name: string;
address: {
city: string;
street: string;
};
}
type DeepPartial<T> = // 你的实现
// 测试
type PartialCompany = DeepPartial<Company>;
// 应该允许:{ address?: { city?: string; street?: string } }
练习2:创建一个类型安全的get函数
// 实现一个函数,可以安全地访问嵌套对象的属性
function get<T, K1 extends keyof T>(
obj: T,
key1: K1
): T[K1];
function get<T, K1 extends keyof T, K2 extends keyof T[K1]>(
obj: T,
key1: K1,
key2: K2
): T[K1][K2];
// 使用示例
const obj = { user: { name: '张三', age: 25 } };
const name = get(obj, 'user', 'name'); // string
学习资源推荐
- 官方文档:TypeScript Handbook
- 类型体操练习:type-challenges
- 实用类型工具:type-fest
- 深入理解:TypeScript Deep Dive
总结:类型体操的价值
类型体操不是炫技,而是让类型系统为你工作的能力。通过掌握类型编程,你可以:
- 减少运行时错误:在编译期发现更多问题
- 提高开发效率:获得更好的IDE支持
- 改善代码质量:强制更严谨的接口设计
- 增强团队协作:明确的类型就是最好的文档
记住:你不必一次掌握所有类型体操技巧。从解决实际问题开始,逐步积累经验。
系列回顾:
- 第一篇:用JSDoc在JavaScript中立即学以致用
- 第二篇:让团队平滑拥抱类型思维
- 第三篇:10个JSDoc实战技巧
- 第四篇:快速启动TypeScript项目
- 第五篇:本文 - TypeScript类型体操入门
你已经完成了从JavaScript到TypeScript类型大师的完整学习路径。接下来就是在实际项目中持续实践。
你在学习TypeScript类型系统时遇到过哪些挑战?或者有有趣的类型体操实践吗?欢迎在评论区分享!
如果这个系列对你有帮助,请点赞收藏,让更多开发者看到类型系统的价值。