返回博客

TypeScript 类型体操指南

TypeScript 的类型系统是图灵完备的,这意味着你可以在类型层面完成复杂的逻辑运算。"类型体操"虽然听起来像炫技,但掌握它能让你写出更安全、更智能的类型定义。

核心价值:编译时捕获错误、智能提示增强、减少运行时检查、API 类型安全

基础工具类型

TypeScript 内置了一些实用工具类型:

工具类型作用示例
Partial<T>所有属性可选Partial<User>
Required<T>所有属性必选Required<User>
Readonly<T>所有属性只读Readonly<Config>
Pick<T, K>选取部分属性Pick<User, 'id' | 'name'>
Omit<T, K>排除部分属性Omit<User, 'password'>
Record<K, V>构造对象类型Record<string, number>

条件类型

基础语法

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

type A = IsString<string>;  // true
type B = IsString<number>;  // false

// 嵌套条件类型
type TypeName<T> = 
    T extends string ? 'string' :
    T extends number ? 'number' :
    T extends boolean ? 'boolean' :
    T extends undefined ? 'undefined' :
    T extends Function ? 'function' :
    'object';

分布式条件类型

当条件类型作用于联合类型时,会自动分配到每个成员:

// 分布式条件类型
type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;  
// string[] | number[],不是 (string | number)[]

// 避免分布式行为
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type Result2 = ToArrayNonDist<string | number>;  
// (string | number)[]
注意:用 [T] 包裹可以阻止分布式行为,这在某些场景下很有用。

infer 关键字

infer 用于在条件类型中推断类型:

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

function getUser() {
    return { id: 1, name: 'Alice' };
}

type UserReturn = ReturnType<typeof getUser>;  
// { id: number; name: string; }

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

type Params = Parameters<(a: string, b: number) => void>;  
// [a: string, b: number]

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

type Result = UnwrapPromise<Promise<string>>;  // string

// 提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : T;

type Item = ElementType<string[]>;  // string

映射类型

基础映射

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

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

// 添加前缀
type Prefixed<T, P extends string> = {
    [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};

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

type ApiUser = Prefixed<User, 'api'>;
// { apiId: number; apiName: string; }

高级映射技巧

// 过滤属性
type OnlyStrings<T> = {
    [K in keyof T as T[K] extends string ? K : never]: T[K];
};

interface Mixed {
    id: number;
    name: string;
    email: string;
    age: number;
}

type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string; }

// 根据 key 过滤
type ExcludeId<T> = {
    [K in keyof T as Exclude<K, 'id'>]: T[K];
};

// 递归映射(深度 readonly)
type DeepReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object 
        ? DeepReadonly<T[K]> 
        : T[K];
};

interface Config {
    server: {
        host: string;
        port: number;
    };
    db: {
        name: string;
        tables: string[];
    };
}

type ReadonlyConfig = DeepReadonly<Config>;
// 所有层级都是 readonly

模板字面量类型

基础用法

// 字符串拼接
type Greeting<T extends string> = `Hello, ${T}!`;

type Message = Greeting<'World'>;  // "Hello, World!"

// 联合类型展开
type Color = 'red' | 'blue' | 'green';
type Size = 'small' | 'medium' | 'large';

type Button = `${Size}-${Color}`;
// "small-red" | "small-blue" | "small-green" | 
// "medium-red" | "medium-blue" | "medium-green" | 
// "large-red" | "large-blue" | "large-green"

内置字符串工具

type Str = 'hello world';

type Upper = Uppercase<Str>;      // "HELLO WORLD"
type Lower = Lowercase<Str>;      // "hello world"
type Cap = Capitalize<Str>;      // "Hello world"
type Uncap = Uncapitalize<Str>;  // "hello world"

// 实际应用:事件处理器类型
type EventName = 'click' | 'focus' | 'blur';
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

高级模式匹配

// 提取字符串前缀
type RemovePrefix<T extends string, P extends string> = 
    T extends `${P}${infer Rest}` ? Rest : T;

type Result = RemovePrefix<'getUserById', 'get'>;  // "UserById"

// 提取字符串后缀
type RemoveSuffix<T extends string, S extends string> =
    T extends `${infer Rest}${S}` ? Rest : T;

type Result2 = RemoveSuffix<'UserController', 'Controller'>;  // "User"

// 驼峰转连字符
type CamelToKebab<S extends string> = 
    S extends `${infer First}${infer Rest}`
        ? First extends Uppercase<First>
            ? `-${Lowercase<First>}${CamelToKebab<Rest>}`
            : `${First}${CamelToKebab<Rest>}`
        : S;

type Kebab = CamelToKebab<'backgroundColor'>;  // "background-color"

实战案例

1. 路由参数推断

// 从路由路径提取参数类型
type ExtractParams<T extends string> = 
    T extends `${string}:${infer Param}/${infer Rest}`
        ? Param | ExtractParams<`/${Rest}`>
        : T extends `${string}:${infer Param}`
            ? Param
            : never;

type Params = ExtractParams<'/users/:id/posts/:postId'>;
// "id" | "postId"

// 构建参数对象类型
type RouteParams<Path extends string> = {
    [K in ExtractParams<Path>]: string;
};

function get<Path extends string>(
    path: Path,
    params: RouteParams<Path>
) {
    // 实现...
}

get('/users/:id/posts/:postId', { id: '1', postId: '2' });  // ✅ 类型安全

2. API 响应类型推导

// 根据 API 路径自动推导响应类型
interface ApiEndpoints {
    '/users': User[];
    '/users/:id': User;
    '/posts': Post[];
    '/posts/:id': Post;
}

type Endpoint = keyof ApiEndpoints;

type ApiResponse<Path extends Endpoint> = ApiEndpoints[Path];

async function fetchApi<Path extends Endpoint>(
    path: Path
): Promise<ApiResponse<Path>> {
    const response = await fetch(path);
    return response.json();
}

const users = await fetchApi('/users');      // User[]
const user = await fetchApi('/users/:id');   // User

3. 深度 Pick

// 支持嵌套路径的 Pick
type DeepPick<T, Path extends string> = 
    Path extends `${infer Key}.${infer Rest}`
        ? Key extends keyof T
            ? { [K in Key]: DeepPick<T[K], Rest> }
            : never
        : Path extends keyof T
            ? Pick<T, Path>
            : never;

interface User {
    id: number;
    profile: {
        name: string;
        email: string;
        address: {
            city: string;
            country: string;
        };
    };
}

type UserName = DeepPick<User, 'profile.name'>;
// { profile: { name: string } }

type UserAddress = DeepPick<User, 'profile.address.city'>;
// { profile: { address: { city: string } } }

性能与最佳实践

性能警告:复杂的递归类型可能显著增加编译时间。TypeScript 有递归深度限制(约 1000 层)。

总结

TypeScript 类型体操的核心技能:

学习资源
TypeScript 官方文档:types-from-types
类型体操练习:type-challenges
utility-types:utility-types