一、为什么需要条件类型

在TypeScript中,类型系统是它的核心卖点之一。但有时候,简单的类型定义并不能满足复杂场景的需求。比如,我们可能需要根据某个条件的真假来决定最终的类型。这时候,条件类型(Conditional Types)就派上用场了。

条件类型允许我们基于输入类型进行条件判断,从而动态生成新的类型。它类似于编程中的三元运算符,只不过是在类型层面进行操作。

// 示例1:基础条件类型
type IsString<T> = T extends string ? "Yes" : "No";

type Result1 = IsString<"hello">;  // 类型为 "Yes"
type Result2 = IsString<123>;     // 类型为 "No"

在这个例子中,IsString<T> 是一个条件类型,它会检查 T 是否是 string 的子类型。如果是,则返回 "Yes",否则返回 "No"

二、条件类型的基本语法

条件类型的语法结构非常简单:

T extends U ? X : Y
  • T extends U:判断 T 是否可以赋值给 U
  • ? X : Y:如果条件成立,返回 X,否则返回 Y
// 示例2:嵌套条件类型
type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    "unknown";

type Name1 = TypeName<"hello">;  // "string"
type Name2 = TypeName<42>;       // "number"
type Name3 = TypeName<true>;     // "boolean"
type Name4 = TypeName<{}>;       // "unknown"

这个例子展示了如何通过嵌套条件类型来模拟一个简单的类型判断工具。

三、条件类型的进阶用法

1. 分布式条件类型

当条件类型作用于联合类型时,它会自动分发到每个成员上,这种特性称为“分布式条件类型”。

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

type StrOrNumArray = ToArray<string | number>;  
// 等同于 (string extends any ? string[] : never) | (number extends any ? number[] : never)
// 最终类型为 string[] | number[]

2. 结合 infer 进行类型推断

infer 关键字可以在条件类型中提取类型信息,类似于模式匹配。

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

function greet(): string {
    return "Hello, world!";
}

type GreetReturn = ReturnType<typeof greet>;  // string

3. 条件类型与映射类型结合

我们可以利用条件类型来增强映射类型的功能。

// 示例5:过滤对象类型的某些属性
type FilterProperties<T, U> = {
    [K in keyof T]: T[K] extends U ? K : never;
}[keyof T];

interface User {
    name: string;
    age: number;
    isAdmin: boolean;
}

type StringProps = FilterProperties<User, string>;  // "name"

四、实际应用场景

1. API 响应类型处理

在前端开发中,API 返回的数据结构可能因条件不同而变化。我们可以用条件类型来动态定义响应类型。

// 示例6:API 响应类型
type ApiResponse<T> = {
    success: true;
    data: T;
} | {
    success: false;
    error: string;
};

function handleResponse<T>(response: ApiResponse<T>) {
    if (response.success) {
        console.log(response.data);
    } else {
        console.error(response.error);
    }
}

2. 表单验证

在表单处理中,某些字段可能是可选的,而某些是必填的。我们可以用条件类型来动态生成表单类型。

// 示例7:动态表单类型
type OptionalFields<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface FormData {
    username: string;
    password: string;
    rememberMe?: boolean;
}

type LoginForm = OptionalFields<FormData, "rememberMe">;
// 等同于 { username: string; password: string; rememberMe?: boolean }

五、技术优缺点

优点

  1. 灵活性:可以根据输入类型动态生成新类型。
  2. 减少重复代码:避免手动定义大量相似类型。
  3. 增强类型安全:在编译阶段捕获更多潜在错误。

缺点

  1. 学习曲线较陡:初学者可能需要时间适应条件类型的思维方式。
  2. 调试困难:复杂的条件类型可能导致错误信息难以理解。

六、注意事项

  1. 避免过度嵌套:过多的条件嵌套会让代码难以维护。
  2. 谨慎使用 any:在条件类型中使用 any 可能会绕过类型检查。
  3. 性能考量:极端复杂的条件类型可能会影响编译速度。

七、总结

TypeScript 的条件类型为我们提供了一种强大的工具,可以在类型层面实现动态逻辑。无论是简单的类型判断,还是复杂的类型转换,条件类型都能胜任。合理使用它,可以让我们的代码更加健壮和灵活。