TypeScript 条件类型

168 阅读5分钟

条件类型示意图

在 TypeScript 的高级类型系统中,条件类型(Conditional Types) 无疑是类型编程领域的"魔法石"。它赋予开发者根据类型关系动态推导新类型的能力,将类型系统提升到了全新的高度。本文将深入探索条件类型的核心概念、使用场景和强大能力。

什么是条件类型?

条件类型允许我们根据类型关系进行类型选择,类似于 JavaScript 的三元表达式,但作用于类型层面:

T extends U ? X : Y

这个表达式表示:如果类型 T 可以赋值给类型 U,则条件类型的结果为 X,否则为 Y

基本示例

type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<"hello">; // "yes"
type B = IsString<42>;      // "no"

在这个例子中,我们创建了一个条件类型 IsString,根据传入类型是否为 string 返回不同的字符串字面量类型。

条件类型的核心特性

1. 分发特性(Distributive Conditional Types)

当条件类型作用于联合类型时,TypeScript 会自动将联合类型"分发"处理:

type ToArray<T> = T extends any ? T[] : never;

type StrOrNumArray = ToArray<string | number>;
// 等效于:string[] | number[]

这种分发行为让条件类型在处理联合类型时表现出强大的能力。但有时我们需要避免分发行为:

// 避免分发的技巧:使用元组包裹
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type Arr = ToArrayNonDist<string | number>; // (string | number)[]

2. 类型推断(infer 关键字)

条件类型中的 infer 关键字让我们能够从类型中提取子类型

type GetReturnType<T> = 
    T extends (...args: any[]) => infer R ? R : never;

// 使用示例
function getUser() { return { id: 1, name: "Alice" }; }
type UserReturn = GetReturnType<typeof getUser>;
// { id: number; name: string }

infer 的强大功能让我们可以轻松获取复杂类型结构的内部类型:

// 提取数组元素的类型
type ArrayElement<T> = T extends (infer U)[] ? U : never;

// 提取Promise的解析类型
type UnpackPromise<T> = T extends Promise<infer U> ? U : never;

内置条件类型工具

TypeScript 提供了多个基于条件类型的实用工具类型:

1. Exclude<T, U>

// 从T中排除可赋值给U的类型
type Exclude<T, U> = T extends U ? never : T;

// 示例
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"

2. Extract<T, U>

// 从T中提取可赋值给U的类型
type Extract<T, U> = T extends U ? T : never;

// 示例
type T1 = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"

3. ReturnType<T>

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

// 示例
type T2 = ReturnType<() => string>; // string

4. Parameters<T>

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

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

条件类型的进阶应用

1. 递归条件类型

条件类型可以递归调用自身,处理深层嵌套结构:

// 展平嵌套数组
type Flatten<T> = 
    T extends any[] 
        ? T extends [infer First, ...infer Rest] 
            ? [...Flatten<First>, ...Flatten<Rest>] 
            : [] 
        : [T];

// 使用示例
type DeepArray = [1, [2, [3, 4], 5], 6];
type FlatArray = Flatten<DeepArray>; // [1, 2, 3, 4, 5, 6]

2. 类型安全的路径访问

实现类似于Lodash的 _.get 函数的类型安全版本:

type Path<T, Keys extends any[]> = 
    Keys extends [infer K, ...infer Rest]
        ? K extends keyof T
            ? Path<T[K], Rest>
            : never
        : T;

function getValue<T, K extends string[]>(obj: T, ...keys: K): Path<T, K> {
    return keys.reduce<any>((acc, key) => acc?.[key], obj);
}

// 使用示例
const user = {
    profile: {
        name: "Alice",
        address: {
            city: "Wonderland"
        }
    }
};

const city = getValue(user, "profile", "address", "city"); // string类型

3. 动态属性类型推导

根据输入参数动态推导返回类型:

type Compute<Action extends 'add' | 'multiply'> = 
    Action extends 'add'
        ? number
        : Action extends 'multiply'
            ? bigint
            : never;

function calculate<Action extends 'add' | 'multiply'>(action: Action): Compute<Action> {
    if (action === 'add') {
        return 42 as Compute<Action>;
    } else {
        return 42n as Compute<Action>;
    }
}

// 使用示例
const addResult = calculate('add'); // number
const multiplyResult = calculate('multiply'); // bigint

4. 状态机类型

使用条件类型实现状态转换的类型安全实现:

type State = "idle" | "loading" | "success" | "error";

type Event = "fetch" | "resolve" | "reject";

type Transition<S extends State, E extends Event> = 
    S extends "idle" 
        ? E extends "fetch" 
            ? "loading" 
            : never
        : S extends "loading" 
            ? E extends "resolve" 
                ? "success" 
                : E extends "reject" 
                    ? "error" 
                    : never
            : S extends "success"
                ? never
                : S extends "error"
                    ? E extends "fetch"
                        ? "loading"
                        : never
                    : never;

function transition<S extends State, E extends Event>(
    current: S, 
    event: E
): Transition<S, E> {
    // 实现状态转换逻辑
    // ...
}

// 使用示例
const nextState = transition("idle", "fetch"); // "loading"
const errorState = transition("loading", "reject"); // "error"

条件类型在框架中的应用

React:处理高阶组件Props

type ExtractComponentProps<TComponent> = 
    TComponent extends React.ComponentType<infer P> ? P : never;

function withLogging<TComponent extends React.ComponentType<any>>(
    Component: TComponent
): React.FC<ExtractComponentProps<TComponent> & { logEnabled: boolean }> {
    return (props) => {
        if (props.logEnabled) {
            console.log('Component rendered:', Component.name);
        }
        return <Component {...props as any} />;
    };
}

// 使用示例
const MyComponent: React.FC<{ name: string }> = ({ name }) => <div>Hello {name}</div>;
const EnhancedComponent = withLogging(MyComponent);

// EnhancedComponent的Props: { name: string; logEnabled: boolean; }

Redux:类型安全的Reducer

type ActionMap = {
    INCREMENT: { type: 'INCREMENT'; payload?: number };
    DECREMENT: { type: 'DECREMENT'; payload?: number };
    RESET: { type: 'RESET' };
};

type Action<Type extends keyof ActionMap> = ActionMap[Type];

type Reducer<S, A extends keyof ActionMap> = 
    A extends any 
        ? (state: S, action: Action<A>) => S 
        : never;

function createReducer<S, A extends keyof ActionMap>(
    initialState: S,
    handlers: {
        [K in A]: (state: S, action: Action<K>) => S
    }
): (state: S | undefined, action: Action<A>) => S {
    // 实现reducer
}

// 使用示例
const counterReducer = createReducer(0, {
    INCREMENT: (state, action) => state + (action.payload ?? 1),
    DECREMENT: (state, action) => state - (action.payload ?? 1),
    RESET: () => 0,
});

条件类型的局限与最佳实践

常见陷阱

  1. 过多的递归深度

    // 递归深度有限制(通常为50层)
    type DeepArray<T> = T | DeepArray<T>[];
    // 可能导致错误:类型实例化过深且可能无限
    
  2. 分发特性导致的意外行为

    type IsNever<T> = T extends never ? true : false;
    type A = IsNever<never>; // never (而不是 true)
    
  3. 复杂的类型推断影响性能

最佳实践

  1. 使用类型约束优化性能

    // 在递归前添加基础情况检查
    type Flatten<T> = 
        T extends any[] ? DeepFlatten<T> : T;
    
    type DeepFlatten<T> = 
        T extends [infer First, ...infer Rest] 
            ? [...DeepFlatten<First>, ...DeepFlatten<Rest>] 
            : [];
    
  2. 使用工具类型简化复杂表达式

    type Primitive = string | number | boolean | symbol | null | undefined;
    
    type Simplify<T> = T extends Primitive 
        ? T 
        : { [K in keyof T]: Simplify<T[K]> };
    
  3. 合理使用类型注释

    /**
     * 提取嵌套对象中指定路径的类型
     * @typeParam T - 根对象类型
     * @typeParam Path - 路径数组,如 ['a', 'b', 'c']
     */
    type DeepPropertyType<T, Path extends PropertyKey[]> = 
        Path extends []
            ? T
            : Path extends [infer K, ...infer Rest]
                ? K extends keyof T
                    ? DeepPropertyType<T[K], Rest>
                    : never
                : never;
    

类型编程的未来

条件类型将 TypeScript 的类型系统提升到了全新的高度,使我们能够:

  • 🔮 动态推导类型:根据输入类型动态决定输出类型
  • 🧩 创建复杂类型工具:构建强大的类型实用工具
  • 🛡️ 增强类型安全性:在复杂场景中提供更强的类型保护
  • 🚀 提升开发效率:减少样板代码,自动化类型推导

通过本文的学习,你应该已经掌握:

  1. 条件类型的基本语法和分发特性
  2. infer 关键字在类型提取中的强大能力
  3. 内置工具类型的实现原理
  4. 条件类型在递归、路径访问等高级场景的应用
  5. 常见框架中条件类型的实际应用案例
  6. 条件类型的最佳实践和性能考量

"条件类型是 TypeScript 类型系统中强大的类型操作工具,它让我们可以在类型空间中表达复杂的逻辑关系。" —— TypeScript 官方文档