返回文章列表
开发技术TypeScript类型系统泛型高级类型

TypeScript 类型体操入门

2025-04-2212 分钟

什么是类型体操

TypeScript 的类型系统是图灵完备的,这意味着我们可以在类型层面进行复杂的逻辑运算。所谓"类型体操",是社区对 TypeScript 高级类型编程的一种戏称。掌握类型体操技巧,可以帮助我们创建更精确的类型定义,从而在编译阶段发现更多潜在错误,减少运行时 Bug。本文将从基础的泛型开始,逐步深入到条件类型和映射类型等高级话题。

泛型基础

泛型(Generics)是 TypeScript 类型体操的基石。它允许我们在定义函数、接口或类时使用类型参数,实现代码的复用与类型安全的兼顾。

// 基本泛型函数
function identity<T>(value: T): T {
  return value;
}

const num = identity<number>(42);       // number
const str = identity('hello');           // string(类型推断)

// 泛型接口
interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

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

type UserResponse = ApiResponse<User>;
type UserListResponse = ApiResponse<User[]>;

// 泛型约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = { id: 1, name: '张三', email: 'zhangsan@example.com' };
const userName = getProperty(user, 'name'); // string
// getProperty(user, 'age'); // 编译错误:'age' 不在 User 的键中

泛型约束(extends)是一个非常重要的概念。通过 K extends keyof T,我们限制了参数 key 只能是对象 obj 的已知属性名,从而在编译时就避免了访问不存在属性的错误。

条件类型

条件类型(Conditional Types)的语法类似三元运算符,根据类型关系做出选择。它的基本形式为 T extends U ? X : Y,含义是:如果 T 可以赋值给 U,则结果类型为 X,否则为 Y。

// 基本条件类型
type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>;  // true
type B = IsString<42>;       // false

// 条件类型与 infer 关键字
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function fetchUser(): User {
  return { id: 1, name: '张三', email: 'zhangsan@example.com' };
}

type FetchUserReturn = ReturnType<typeof fetchUser>; // User

// 提取 Promise 中的值类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type P1 = UnwrapPromise<Promise<string>>; // string
type P2 = UnwrapPromise<number>;           // number

// 递归解包嵌套 Promise
type DeepUnwrapPromise<T> = T extends Promise<infer U>
  ? DeepUnwrapPromise<U>
  : T;

type P3 = DeepUnwrapPromise<Promise<Promise<boolean>>>; // boolean

infer 关键字用于在条件类型中"推断"某个位置的类型。它只能出现在 extends 子句中,是实现复杂类型运算的核心工具。

映射类型

映射类型(Mapped Types)允许我们基于已有类型创建新类型,通过遍历已有类型的属性来进行转换。

// 将所有属性变为可选
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// 将所有属性变为只读
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// 将所有属性变为必需
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

// 选取指定属性
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

// 排除指定属性
type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

// 实际使用示例
interface UserProfile {
  id: number;
  name: string;
  email: string;
  avatar: string;
  bio: string;
}

type UserBasicInfo = MyPick<UserProfile, 'id' | 'name' | 'avatar'>;
type UserUpdateData = MyPartial<MyOmit<UserProfile, 'id'>>;

实用工具类型实现

理解了上述基础概念后,我们可以实现一些更复杂的工具类型,这些在实际项目中非常有用:

// 深度 Partial
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

// 获取对象中值为指定类型的键
type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never;
}[keyof T];

type StringKeys = KeysOfType<UserProfile, string>;
// 'name' | 'email' | 'avatar' | 'bio'

// 将联合类型转为交叉类型
type UnionToIntersection<U> = (
  U extends any ? (arg: U) => void : never
) extends (arg: infer I) => void
  ? I
  : never;

// 模板字面量类型
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>;     // 'onClick'
type ChangeEvent = EventName<'change'>;   // 'onChange'

学习建议

类型体操的学习需要大量练习。推荐通过 type-challenges 项目进行系统化练习,该项目提供了从简单到困难的各种类型挑战。从简单题目开始,逐步理解每个类型工具的用法和组合方式。在实际项目中,不必追求过于复杂的类型定义,在类型安全和代码可读性之间找到平衡才是最重要的。记住:类型是为了帮助开发者,而不是成为开发的障碍。