中文

一份关于 TypeScript 强大的映射类型和条件类型的综合指南,包含实用示例和高级用例,用于创建健壮且类型安全的应用程序。

精通 TypeScript 的映射类型与条件类型

TypeScript 作为 JavaScript 的一个超集,提供了强大的功能来创建健壮且可维护的应用程序。在这些功能中,映射类型 (Mapped Types)条件类型 (Conditional Types) 作为高级类型操作的基本工具脱颖而出。本指南全面概述了这些概念,探讨了它们的语法、实际应用和高级用例。无论您是经验丰富的 TypeScript 开发者还是刚刚起步,本文都将为您提供有效利用这些功能的知识。

什么是映射类型?

映射类型允许您通过转换现有类型来创建新类型。它们会遍历现有类型的属性,并对每个属性应用转换。这对于创建现有类型的变体特别有用,例如将所有属性变为可选或只读。

基本语法

映射类型的语法如下:

type NewType<T> = {
  [K in keyof T]: Transformation;
};

实用示例

将属性设为只读

假设您有一个表示用户个人资料的接口:

interface UserProfile {
  name: string;
  age: number;
  email: string;
}

您可以创建一个所有属性均为只读的新类型:

type ReadOnlyUserProfile = {
  readonly [K in keyof UserProfile]: UserProfile[K];
};

现在,ReadOnlyUserProfile 将拥有与 UserProfile 相同的属性,但它们都将是只读的。

将属性设为可选

同样,您可以将所有属性设为可选:

type OptionalUserProfile = {
  [K in keyof UserProfile]?: UserProfile[K];
};

OptionalUserProfile 将拥有 UserProfile 的所有属性,但每个属性都将是可选的。

修改属性类型

您还可以修改每个属性的类型。例如,您可以将所有属性都转换为字符串:

type StringifiedUserProfile = {
  [K in keyof UserProfile]: string;
};

在这种情况下,StringifiedUserProfile 中的所有属性都将是 string 类型。

什么是条件类型?

条件类型允许您根据条件定义类型。它们提供了一种根据类型是否满足特定约束来表达类型关系的方式。这类似于 JavaScript 中的三元运算符,但作用于类型。

基本语法

条件类型的语法如下:

T extends U ? X : Y

实用示例

判断一个类型是否为字符串

让我们创建一个类型,如果输入类型是字符串,则返回 string,否则返回 number

type StringOrNumber<T> = T extends string ? string : number;

type Result1 = StringOrNumber<string>;  // string
type Result2 = StringOrNumber<number>;  // number
type Result3 = StringOrNumber<boolean>; // number

从联合类型中提取类型

您可以使用条件类型从联合类型中提取特定类型。例如,提取非空类型:

type NonNullable<T> = T extends null | undefined ? never : T;

type Result4 = NonNullable<string | null | undefined>; // string

在这里,如果 Tnullundefined,类型将变为 never,然后 TypeScript 的联合类型简化会将其过滤掉。

推断类型

条件类型也可以与 infer 关键字一起使用来推断类型。这允许您从更复杂的类型结构中提取类型。

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function myFunction(x: number): string {
  return x.toString();
}

type Result5 = ReturnType<typeof myFunction>; // string

在此示例中,ReturnType 提取了函数的返回类型。它检查 T 是否是一个接受任何参数并返回类型 R 的函数。如果是,它返回 R;否则,它返回 any

结合映射类型和条件类型

映射类型和条件类型的真正威力在于将它们结合起来。这使您能够创建高度灵活和富有表现力的类型转换。

示例:深度只读 (Deep Readonly)

一个常见的用例是创建一个类型,使对象的所有属性(包括嵌套属性)都变为只读。这可以通过递归条件类型来实现。

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
  };
}

type ReadonlyCompany = DeepReadonly<Company>;

在这里,DeepReadonly 递归地将 readonly 修饰符应用于所有属性及其嵌套属性。如果一个属性是对象,它会递归地对该对象调用 DeepReadonly。否则,它只是将 readonly 修饰符应用于该属性。

示例:按类型过滤属性

假设您想创建一个只包含特定类型属性的类型。您可以结合使用映射类型和条件类型来实现这一点。

type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Person {
  name: string;
  age: number;
  isEmployed: boolean;
}

type StringProperties = FilterByType<Person, string>; // { name: string; }

type NonStringProperties = Omit<Person, keyof StringProperties>;

在此示例中,FilterByType 遍历 T 的属性,并检查每个属性的类型是否扩展了 U。如果是,它就在结果类型中包含该属性;否则,它通过将键映射到 never 来排除它。注意使用 "as" 来重映射键。然后我们使用 `Omit` 和 `keyof StringProperties` 从原始接口中移除字符串属性。

高级用例和模式

除了基本示例外,映射类型和条件类型还可以用于更高级的场景,以创建高度可定制和类型安全的应用程序。

分布式条件类型

当被检查的类型是联合类型时,条件类型是分布式的。这意味着条件会分别应用于联合的每个成员,然后将结果组合成一个新的联合类型。

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

type Result6 = ToArray<string | number>; // string[] | number[]

在此示例中,ToArray 分别应用于联合类型 string | number 的每个成员,结果为 string[] | number[]。如果条件不是分布式的,结果将是 (string | number)[]

使用工具类型

TypeScript 提供了几个内置的工具类型,它们利用了映射类型和条件类型。这些工具类型可以作为更复杂类型转换的构建块。

这些工具类型是强大的工具,可以简化复杂的类型操作。例如,您可以结合使用 PickPartial 来创建一个只使某些属性可选的类型:

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}

type OptionalDescriptionProduct = Optional<Product, "description">;

在此示例中,OptionalDescriptionProduct 拥有 Product 的所有属性,但 description 属性是可选的。

使用模板字面量类型

模板字面量类型允许您基于字符串字面量创建类型。它们可以与映射类型和条件类型结合使用,以创建动态和富有表现力的类型转换。例如,您可以创建一个类型,为所有属性名称添加特定前缀:

type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${string & K}`]: T[K];
};

interface Settings {
  apiUrl: string;
  timeout: number;
}

type PrefixedSettings = Prefix<Settings, "data_">;

在此示例中,PrefixedSettings 将拥有属性 data_apiUrldata_timeout

最佳实践与注意事项

结论

映射类型条件类型 是 TypeScript 中强大的功能,使您能够创建高度灵活和富有表现力的类型转换。通过掌握这些概念,您可以提高 TypeScript 应用程序的类型安全性、可维护性和整体质量。从简单的转换(如使属性可选或只读)到复杂的递归转换和条件逻辑,这些功能为您提供了构建健壮和可扩展应用程序所需的工具。不断探索和试验这些功能,以释放它们的全部潜力,成为一名更熟练的 TypeScript 开发人员。

在您继续 TypeScript 之旅时,请记得利用丰富的可用资源,包括官方 TypeScript 文档、在线社区和开源项目。拥抱映射类型和条件类型的力量,您将能够从容应对最棘手的类型相关问题。