12. TypeScript 高级类型

TypeScript 中的高级类型包括映射类型、条件类型、字面量类型和递归类型等强大结构。这些特性使开发者能够表达类型之间更复杂的关系,从而处理边缘情况,并定义更动态、更灵活的类型系统。

一、映射类型

TypeScript 映射类型(Mapped Types)是一种高级类型工具,它允许我们基于已有的类型创建新的类型。通过遍历已有类型的键(key),并对其进行变换,可以快速构造具有相同结构但属性类型不同的新类型,从而提高代码的灵活性和复用性。

(一) 概念

TypeScript 中的映射类型(Mapped Types)允许你通过转换现有类型的属性来创建新的类型。

  • 它们支持对属性进行修改,例如将属性设为可选(optional)、只读(read-only),或者改变属性的类型。
  • 映射类型有助于减少重复代码,并通过自动化的类型转换提升类型安全性。
  • 它们特别适用于创建现有类型的不同变体,而无需手动重新定义每个属性。

例如下面的代码所示:

type User = {
  id: number;
  name: string;
  email: string;
};

type PartialUser = {
  [P in keyof User]?: User[P];
};

代码解释:

  • PartialUser 是一个新类型,其中 User 类型的每个属性都被标记为可选(optional)。
  • keyof 操作符用于获取 User 类型的所有属性键,而映射类型则会遍历每一个键,并通过 ? 将它们标记为可选属性。

输出:

const user1: PartialUser = { id: 1 }; 
const user2: PartialUser = {}; 
const user3: PartialUser = { id: 2, name: "Alice" }; 

(二) 场景示例

1. 创建只读属性

在 TypeScript 中,可以使用 映射类型(Mapped Types)readonly 关键字创建一个新的类型,使其所有属性变为只读。

type User = {
  id: number;
  name: string;
  email: string;
};

type ReadonlyUser = {
  readonly [P in keyof User]: User[P];
};

const user: ReadonlyUser = { id: 1, name: "Alice", email: "alice@example.com" };
user.id = 2;

代码解释:

  • ReadonlyUser 是一个新类型,其中 User 的所有属性都被标记为只读(readonly)。
  • 尝试修改 user 对象的任何属性都会导致编译时错误。

输出:

Error: Cannot assign to 'id' because it is a read-only property.

2. 创建可为空属性

创建可为空属性(Creating Nullable Properties)指的是将一个类型中的所有属性变为可以为 null 的类型。在 TypeScript 中,可以使用映射类型(Mapped Types)结合联合类型(Union Types)来实现这一点。

type Product = {
  name: string;
  price: number;
  inStock: boolean;
};

type NullableProduct = {
  [P in keyof Product]: Product[P] | null;
};

const product: NullableProduct = { name: "Laptop", price: null, inStock: true };

代码解释:

  • NullableProduct 是一个新的类型,它使得 Product 类型中的每个属性都可以是其原始类型,或者是 null
  • 这允许属性显式地具有 null 值,用于表示该属性当前没有值或值缺失的情况。

输出:

{ name: "Laptop", price: null, inStock: true }

3. 使用模板字面量重命名属性

使用模板字面量重命名属性(Renaming Properties with Template Literals)是 TypeScript 中映射类型的一种高级用法。它允许我们通过字符串模板语法在创建新类型时动态地改变属性名。

type Person = {
  firstName: string;
  lastName: string;
};

type PrefixedPerson = {
  [P in keyof Person as `person${Capitalize<P>}`]: Person[P];
};

const person: PrefixedPerson = { personFirstName: "Felix", personLastName: "Raink" };

代码解释:

  • PrefixedPerson 创建了一个新类型,通过在 Person 类型的每个属性名前加上 "person" 前缀,并将原属性名首字母大写。
  • 这个示例演示了如何结合模板字面量类型和 TypeScript 内置的 Capitalize 工具类型来转换属性名称。

输出:

{ personFirstName: "Felix", personLastName: "Raink" }

(三) TypeScript 映射类型使用最佳实践

  • 保持转换简单:避免过于复杂的嵌套转换,以保持代码的可读性和维护的便捷性。
  • 确保类型安全:利用映射类型强制执行一致的属性转换,提高整个代码库的类型安全性。
  • 结合内置工具类型使用:配合 Partial、Readonly、Pick 和 Omit 等内置工具类型,简化常见的类型转换操作。

二、条件类型

在 TypeScript 中,条件类型使开发者能够根据条件创建类型,从而实现更动态和灵活的类型定义。

它们遵循语法 T extends U ? X : Y,意思是如果类型 T 可赋值给类型 U,则类型解析为 X;否则解析为 Y

(一) 概念

条件类型在创建工具类型和进行高级类型操作时特别有用,能够增强代码的复用性和类型安全性。

比如下面这个例子:

type IsString<T> = T extends string ? 'Yes' : 'No';

type Test1 = IsString<string>;
type Test2 = IsString<number>;

console.log('Test1:', 'Yes');
console.log('Test2:', 'No');
  • 类型别名 IsString 使用条件类型来判断类型 T 是否继承自 string
  • 如果 T 可以赋值给 string,则结果为 'Yes';否则结果为 'No'
  • Test1 被评估为 'Yes',因为 string 继承自 string
  • Test2 被评估为 'No',因为 number 不继承自 string

输出:

Test1: Yes
Test2: No

(二) 场景示例

1. 条件类型约束

条件类型约束允许在条件类型中对泛型类型进行约束,从而实现动态且精确的类型处理。

type CheckNum<T> = T extends number ? T : never;

type NumbersOnly<T extends any[]> = {
  [K in keyof T]: CheckNum<T[K]>;
};

const num: NumbersOnly<[4, 5, 6, 8]> = [4, 5, 6, 8];
const invalid: NumbersOnly<[4, 6, "7"]> = [4, 6, "7"];

代码解释:

  • CheckNum<T> 确保只保留数字类型;其他类型则解析为 never
  • NumbersOnly<T> 对数组中的每个元素应用 CheckNum,过滤掉非数字类型。

输出:

Type '"7"' is not assignable to type 'never'.

2. 条件类型中的类型推断

此特性允许在条件类型定义中提取并使用类型,从而实现精确的类型转换。

type ElementType<T> = T extends (infer U)[] ? U : never;

const numbers: number[] = [1, 2, 3];
const element: ElementType<typeof numbers> = numbers[0];
const invalidElement: ElementType<typeof numbers> = "string";

代码解释:

  • ElementType<T> 使用 infer 关键字从数组中提取元素类型。
  • 变量 element 被正确推断为 number;尝试赋值为字符串则无效。

输出:

Type 'string' is not assignable to type 'number'.

3. 分布式条件类型

分布式条件类型(Distributive Conditional Types)是 TypeScript 条件类型的一种特性,当条件类型作用于联合类型时,会将条件应用到联合类型的每个成员上,然后将结果合并成新的联合类型。

type Colors = 'red' | 'blue' | 'green';

type ColorClassMap = {
  red: 'danger';
  blue: 'primary';
  green: 'success';
};

type MapColorsToClasses<T extends string> = T extends keyof ColorClassMap
  ? { [K in T]: ColorClassMap[T] }
  : never;

const redClass: MapColorsToClasses<Colors> = { red: 'danger' };
const invalidClass: MapColorsToClasses<Colors> = { yellow: 'warning' };

代码解释:

  • MapColorsToClasses<T> 会检查类型 T 是否匹配 ColorClassMap 中的某个键,并将其映射为对应的值。
  • 'yellow' 这样无效的颜色会被拒绝,因为它不在 ColorClassMap 中定义。

输出:

Type '{ yellow: "warning"; }' is not assignable to type 'never'.

(三) TypeScript 条件类型的最佳实践

  • 使用条件类型创建灵活且可复用的类型定义。
  • 将条件类型与泛型结合使用,以增强适应性。
  • 在复杂场景中利用 infer 关键字实现类型推断。

三、字面量类型

TypeScript 的字面量类型允许开发者为变量、函数参数或属性指定精确的值,通过确保变量只能持有预定义的值来增强类型安全性。

  • 允许变量具有特定且精确的值。
  • 通过限制允许的值范围,提高代码的可靠性。

以下是 TypeScript 中字面量类型的几种类型:

(一) 字符串字面量类型

字符串字面量类型允许变量只接受特定的一组字符串值。

type Direction = "Up" | "Down" | "Left" | "Right";

let move: Direction;

move = "Up"; // 有效赋值
// move = "Forward"; // 错误:类型 '"Forward"' 不能赋值给类型 'Direction'
  • Direction 类型只能是指定的字符串字面量之一:“Up”、“Down”、“Left”或“Right”。
  • 赋值为该集合之外的任何值都会导致编译时错误。

(二) 数字字面量类型

数字字面量类型限制变量只能取特定的一组数值。

type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

function rollDice(): DiceRoll {
  return 4; // 有效的返回值
  // return 7; // 错误:类型 '7' 不能赋值给类型 'DiceRoll'
}
  • DiceRoll 类型只能是 1 到 6 之间的数字之一。
  • 返回任何不在该范围内的值都会导致编译错误。

(三) 布尔字面量类型

布尔字面量类型限制变量只能是布尔值 true 或 false。

type Success = true;

function operation(): Success {
  return true; // 合法的返回值
  // return false; // 错误:类型 'false' 不能赋值给类型 'true'
}
  • Success 类型严格限定为 true,返回 false 会导致编译时错误。

(四) TypeScript 字面量类型的最佳实践

  • 使用字面量类型指定精确值:定义变量时使用字面量类型,将其限制为特定的预设值,从而提升代码的可预测性。
  • 结合联合类型使用:利用联合类型让变量能接受有限的多个字面量值,增强类型安全性。
  • 利用类型别名:为复杂的字面量类型组合创建类型别名,简化代码结构并提升可读性。

四、模板字面量类型

TypeScript 中的模板字面量类型允许通过使用模板字面量语法,将已有的字符串字面量类型组合,构造出新的字符串字面量类型。

(一) 概念

它们支持通过在模板字符串中嵌入联合类型或其他字面量类型,创建复杂的字符串模式。
该特性提升了类型安全性,使开发者可以在类型层面定义并强制执行特定的字符串格式。

示例代码:

type Size = "small" | "medium" | "large";
type SizeMessage = `The selected size is ${Size}.`;

let message: SizeMessage;

message = "The selected size is small.";  // 有效
message = "The selected size is extra-large.";  // 报错

代码解释:

  • Size 是一个联合类型,表示可能的尺寸值。
  • SizeMessage 是一个模板字面量类型,通过嵌入 Size,构造出具体的字符串模式。
  • 变量 message 只能被赋值为符合 SizeMessage 模式的字符串。

错误信息示例:

Type '"The selected size is extra-large."' is not assignable to type 'SizeMessage'.

(二) 场景示例

1. 使用 TypeScript 字面量定义路径

type ApiEndpoints = "users" | "posts" | "comments";
type ApiPath = `/api/${ApiEndpoints}`;

const userPath: ApiPath = "/api/users";
const invalidPath: ApiPath = "/api/unknown";
  • ApiEndpoints 是一个联合类型,表示可能的 API 端点名称。
  • ApiPath 是一个模板字面量类型,动态构造出以 /api/ 开头,后接 ApiEndpoints 中任意值的字符串模式。
  • userPath 是有效的,因为它符合构造的模式;而 invalidPath 会报错。

错误信息示例:

Type '"/api/unknown"' is not assignable to type 'ApiPath'.

2. 使用模板字面量格式化消息

type Status = "success" | "error" | "loading";
type StatusMessage = `The operation is ${Status}.`;

const successMessage: StatusMessage = "The operation is success.";
const invalidMessage: StatusMessage = "The operation is pending.";
  • Status 是一个联合类型,表示操作的可能状态。
  • StatusMessage 构造字符串模式,用于描述操作状态。
  • successMessage 是有效的,因为它符合模式;而 invalidMessage 报错,因为 "pending" 不属于 Status 类型。

错误信息示例:

Type '"The operation is pending."' is not assignable to type 'StatusMessage'.

五、递归类型

TypeScript 为 JavaScript 添加了强类型支持。递归类型定义了可以自我引用的类型,适用于树形结构或嵌套对象。实用工具类型(Utility Types)则简化了类型的修改,比如将属性设为可选或只读。这些工具帮助我们写出更清晰、更灵活的代码。

(一) TypeScript 中的递归类型

递归类型是在其定义中引用自身的类型。这使得我们能够建模复杂的数据结构,比如树、链表和嵌套对象,其中类型可以嵌套自身。

  • 允许定义自我引用的类型。
  • 适合表示层级或嵌套数据。
  • 必须使用 TypeScript 的类型别名(type alias)或接口(interface)来定义递归结构。

1. 语法示例

下面是一个表示树结构的简单递归类型:

type TreeNode = {
  value: number;
  children?: TreeNode[];
};
  • TreeNode 类型包含一个 value 属性和一个可选的 children 属性。
  • children 是一个 TreeNode 数组,支持嵌套结构。

2. 使用递归类型示例

const tree: TreeNode = {
  value: 1,
  children: [
    { value: 2 },
    {
      value: 3,
      children: [
        { value: 4 },
        { value: 5 }
      ]
    }
  ]
};

在此示例中,tree 表示一个层级结构,节点可以有子节点,子节点又可以有自己的子节点,依此类推。

3. 递归类型的优点

  • 建模复杂结构:递归类型方便表示层级结构。
  • 类型安全:TypeScript 确保递归结构在每个嵌套层级都符合正确的类型。
  • 类型复用:递归类型可以在多种场景中复用相同的结构定义。

(二) TypeScript 中的实用工具类型(Utility Types)

TypeScript 内置了一些实用工具类型,提供了修改或转换其他类型的现成功能,简化常见的类型操作,提高开发效率。

1. 常见实用工具类型

Partial<T>

将类型 T 的所有属性设为可选。
适用于创建部分属性可缺失的对象。

type Partial<T> = {
  [P in keyof T]?: T[P];
};
Required<T>

将类型 T 的所有属性设为必需。
确保对象中的所有属性必须存在。

type Required<T> = {
  [P in keyof T]?: T[P];
};
Readonly<T>

将类型 T 的所有属性设为只读。
防止初始化后修改对象属性。

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

Pick<T, K>

从类型 T 中挑选出属性 K 的子集。
适用于从复杂类型中选取部分属性。

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

Omit<T, K>

从类型 T 中剔除属性 K。
用于排除不需要的属性。

type Omit<T, K extends keyof T> = {
  [P in Exclude<keyof T, K>]: T[P];
};

Record<K, T>

构造一个对象类型,其属性键为 K,值为 T。
用于定义类似映射的数据结构。

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

Exclude<T, U>

从类型 T 中排除可赋值给 U 的类型。
用于过滤联合类型中的部分类型。

type Exclude<T, U> = T extends U ? never : T;

Extract<T, U>

从类型 T 中提取可赋值给 U 的类型。
用于缩小联合类型范围。

type Extract<T, U> = T extends U ? T : never;

NonNullable<T>

从类型 T 中排除 nullundefined
确保值不是 null 或 undefined。

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

2. 实用工具类型的优点

  • 简化类型转换:内置的工具类型方便对类型结构进行变换(如变成可选、必需等)。
  • 提升代码可读性:使用简洁的类型关键字表达转换意图,使代码更清晰。
  • 提高开发效率:避免重复手写复杂类型定义,减少错误。

通过递归类型和实用工具类型,TypeScript 为复杂数据结构和类型操作提供了强大而灵活的支持,助力开发者编写更安全、更高效的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值