第四部分:赋予网页健壮的灵魂 —— TypeScript

发布于:2025-05-13 ⋅ 阅读:(13) ⋅ 点赞:(0)

第八节 - 更深入的类型系统:泛型与高级模式

TypeScript 的类型系统远不止于基本类型和固定结构。泛型等高级特性使得我们可以编写更加灵活、可复用、同时又能保证类型安全的代码。这就像在设计智能家居系统时,我们设计了一套“通用接口”,这套接口可以适用于不同类型的设备(灯、风扇、咖啡机),而无需为每一种设备单独设计接口,但在使用时又能明确知道连接的是哪种设备并进行相应操作。

泛型 (<T>):

泛型允许你编写可以处理多种类型的代码,同时保留类型信息。它们是类型层面的参数化。

泛型函数:

// eighth.ts

// 一个没有泛型的函数,只能处理 string 数组
function getFirstString(arr: string[]): string | undefined {
  return arr.length > 0 ? arr[0] : undefined;
}

// 如果需要获取 number 数组的第一个元素,需要重新写一个函数
// function getFirstNumber(arr: number[]): number | undefined { ... }

// 使用泛型,创建一个可以处理任意类型数组的函数
function getFirstElement<T>(arr: T[]): T | undefined {
  return arr.length > 0 ? arr[0] : undefined;
}

// 调用泛型函数时,TypeScript 会根据传入的参数自动推断 T 的类型
const firstNum = getFirstElement([1, 2, 3]);   // T 被推断为 number
const firstStr = getFirstElement(["a", "b", "c"]); // T 被推断为 string
const firstBool = getFirstElement([true, false]); // T 被推断为 boolean

console.log("第一个数字:", firstNum);
console.log("第一个字符串:", firstStr);
console.log("第一个布尔值:", firstBool);

// 你也可以显式指定泛型类型 (通常不必要,除非 TypeScript 无法正确推断)
const firstElementExplicit = getFirstElement<number>([10, 20]);

泛型接口:

接口也可以使用泛型,来描述包含某种通用类型数据的结构。

// eighth.ts (接着上面的代码)

// 定义一个泛型接口,表示一个包含数据的响应对象
interface ApiResponse<DataType> {
  code: number;
  message: string;
  data: DataType; // data 属性的类型是泛型参数 DataType
}

// 使用泛型接口
// 响应数据是用户列表
interface User { id: number; name: string; }
const userListResponse: ApiResponse<User[]> = {
  code: 200,
  message: "成功",
  data: [{ id: 1, name: "张三" }, { id: 2, name: "李四" }]
};

// 响应数据是单个产品信息
interface Product { id: number; productName: string; price: number; }
const productResponse: ApiResponse<Product> = {
  code: 200,
  message: "成功",
  data: { id: 101, productName: "笔记本电脑", price: 5000 }
};

// 响应数据为空 (使用 null 或 undefined 或特定的空数据类型)
const emptyResponse: ApiResponse<null> = {
  code: 204,
  message: "无内容",
  data: null
};

console.log("用户列表响应:", userListResponse);
console.log("产品响应:", productResponse);

泛型类:

类也可以使用泛型,来创建可以操作泛型数据的组件。

// eighth.ts (接着上面的代码)

// 定义一个泛型栈类 (后进先出)
class Stack<T> {
  private elements: T[] = [];

  push(element: T): void {
    this.elements.push(element);
  }

  pop(): T | undefined {
    return this.elements.pop();
  }

  isEmpty(): boolean {
    return this.elements.length === 0;
  }
}

// 创建一个存储数字的栈
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log("数字栈弹出:", numberStack.pop()); // 返回 number | undefined

// 创建一个存储字符串的栈
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");
console.log("字符串栈弹出:", stringStack.pop()); // 返回 string | undefined

// 尝试向数字栈推入字符串会报错
// numberStack.push("three"); // 编译时报错

泛型约束 (extends):

有时候,你希望泛型参数 T 必须满足某个条件(比如必须包含某个属性,或者必须是某个接口的实现)。可以使用 extends 关键字添加泛型约束。这就像要求连接到通用接口的设备必须具备某些基本功能。

// eighth.ts (接着上面的代码)

interface Lengthwise {
  length: number; // 要求类型 T 必须有 length 属性
}

// 函数泛型约束
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // 现在可以安全地访问 .length 属性
  return arg;
}

// 调用时必须传入具有 length 属性的类型
loggingIdentity("hello"); // string 有 length 属性
loggingIdentity([1, 2, 3]); // array 有 length 属性

// loggingIdentity(10); // 编译时报错:Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
// loggingIdentity({ name: "test" }); // 编译时报错:Argument of type '{ name: string; }' is not assignable to parameter of type 'Lengthwise'.

Utility Types (内置工具类型):

TypeScript 提供了一些非常有用的内置工具类型,它们可以基于现有类型创建新类型,非常方便。这就像智能家居系统提供了一些现成的“转换器”或“适配器”。

  • Partial<T>: 将类型 T 的所有属性变为可选。
  • Readonly<T>: 将类型 T 的所有属性变为只读。
  • Pick<T, K>: 从类型 T 中选取属性 K 创建新类型。
  • Omit<T, K>: 从类型 T 中省略属性 K 创建新类型。
  • Record<K, T>: 创建一个对象类型,其键是 K 类型,值是 T 类型。
// eighth.ts (接着上面的代码)

interface Todo {
  title: string;
  description: string;
  completed: boolean;
  createdAt: Date;
}

// Partial: 所有属性可选
type PartialTodo = Partial<Todo>;
/* 等价于:
type PartialTodo = {
  title?: string;
  description?: string;
  completed?: boolean;
  createdAt?: Date;
};
*/

const partialTask: PartialTodo = {
  title: "学习 TS"
  // 其他属性可选
};

// Readonly: 所有属性只读
type ReadonlyTodo = Readonly<Todo>;
/* 等价于:
type ReadonlyTodo = {
  readonly title: string;
  readonly description: string;
  readonly completed: boolean;
  readonly createdAt: Date;
};
*/

const readonlyTask: ReadonlyTodo = {
    title: "完成报告",
    description: "提交周报",
    completed: false,
    createdAt: new Date()
};

// readonlyTask.completed = true; // 编译时报错:Cannot assign to 'completed' because it is a read-only property.


// Pick: 选取部分属性
type TodoPreview = Pick<Todo, "title" | "completed">;
/* 等价于:
type TodoPreview = {
  title: string;
  completed: boolean;
};
*/

const taskPreview: TodoPreview = {
  title: "购买食材",
  completed: false
};

// Omit: 省略部分属性
type TodoWithoutDates = Omit<Todo, "createdAt">;
/* 等价于:
type TodoWithoutDates = {
  title: string;
  description: string;
  completed: boolean;
};
*/

const taskWithoutDate: TodoWithoutDates = {
    title: "打扫房间",
    description: "彻底清洁",
    completed: true
};


// Record: 创建键值对类型
type Status = "open" | "closed";
type Issue = { title: string; description: string };

type IssueTracker = Record<Status, Issue[]>;
/* 等价于:
type IssueTracker = {
  open: Issue[];
  closed: Issue[];
};
*/

const issueData: IssueTracker = {
  open: [{ title: "Bug 1", description: "..."}, { title: "Feature 2", description: "..."}],
  closed: [{ title: "Task 3", description: "..."}]
};

console.log("部分任务:", partialTask);
console.log("只读任务:", readonlyTask);
console.log("任务预览:", taskPreview);
console.log("不含日期任务:", taskWithoutDate);
console.log("问题追踪器:", issueData);

条件类型 (Conditional Types):

条件类型允许你根据一个类型是否符合某个约束,来决定最终的类型。语法是 T extends U ? X : Y。如果类型 T 可以赋值给类型 U,则结果是 X 类型,否则是 Y 类型。这就像智能家居系统中的一个逻辑判断模块:如果设备是灯,就执行照明操作;如果是风扇,就执行通风操作。

// eighth.ts (接着上面的代码)

// 如果 T 是 string 类型,则结果是 string[],否则是 T
type StringArrayOrT<T> = T extends string ? string[] : T;

let result1: StringArrayOrT<string>;  // result1 的类型是 string[]
result1 = ["hello", "world"];

let result2: StringArrayOrT<number>;  // result2 的类型是 number
result2 = 123;

let result3: StringArrayOrT<boolean>; // result3 的类型是 boolean
result3 = true;


// 一个更实用的例子:提取函数的返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// 如果 T 是一个函数类型 (...args: any[] 代表任意参数的函数),则提取其返回值类型 R,否则是 any
// infer R 是一个关键字,用于在 extends 条件中推断类型变量

function getUser(): User { // User 接口在上面定义过
    return { id: 1, name: "Alice" };
}

function sayHello(): void {
    console.log("hello");
}

type GetUserReturnType = ReturnType<typeof getUser>; // GetUserReturnType 的类型是 User
const userResult: GetUserReturnType = getUser();
console.log("GetUser 返回类型示例:", userResult);

type SayHelloReturnType = ReturnType<typeof sayHello>; // SayHelloReturnType 的类型是 void
const sayHelloResult: SayHelloReturnType = sayHello();
console.log("SayHello 返回类型示例:", sayHelloResult);

映射类型 (Mapped Types):

映射类型允许你遍历一个类型的属性,并为每个属性应用一个转换。这就像智能家居系统中的一个配置面板,可以批量修改所有设备的某个属性(比如将所有灯的亮度增加 10%)。

// eighth.ts (接着上面的代码)

type Properties = 'prop1' | 'prop2' | 'prop3';

// 将 Properties 中的每个属性都转换为 boolean 类型
type MyMappedType = {
  [P in Properties]: boolean;
  // [P in keyof SomeType]: AnotherType; // 更常见的用法是遍历现有类型的属性
};
/* 等价于:
type MyMappedType = {
  prop1: boolean;
  prop2: boolean;
  prop3: boolean;
};
*/

const myFlags: MyMappedType = {
  prop1: true,
  prop2: false,
  prop3: true
};

console.log("映射类型示例:", myFlags);

// 一个常见的映射类型:ReadOnly
type ReadonlyMapped<T> = {
  readonly [P in keyof T]: T[P];
};

interface Point {
  x: number;
  y: number;
}

type ReadonlyPoint = ReadonlyMapped<Point>;
/* 等价于:
type ReadonlyPoint = {
  readonly x: number;
  readonly y: number;
};
*/

const p: ReadonlyPoint = { x: 10, y: 20 };
// p.x = 5; // 编译时报错:Cannot assign to 'x' because it is a read-only property.

编译 eighth.ts

tsc eighth.ts

会生成 eighth.js 文件。然后在 HTML 中引入 eighth.js

小结: 泛型是构建灵活、可复用组件的关键,它使得类型系统能够适应多种数据类型。Utility Types、条件类型和映射类型等高级特性则进一步增强了 TypeScript 类型系统的表达能力,让你能够以声明式的方式操作和转换类型。掌握这些特性可以让你编写出更强大、更安全的 TypeScript 代码。

练习:

  1. 编写一个泛型函数 reverseArray<T>(arr: T[]): T[],它接收一个任意类型的数组,并返回一个新数组,其中元素的顺序是反转的。
  2. 定义一个泛型接口 KeyValuePair<K, V>,表示一个键值对,键的类型是 K,值的类型是 V。
  3. 使用 Partial Utility Type 创建一个新类型 PartialUser,它是基于你之前定义的 User 接口,但所有属性都是可选的。创建一个 PartialUser 对象。
  4. (进阶)尝试编写一个条件类型 ExcludeNullOrUndefined<T>,如果类型 Tnullundefined,则排除它,否则保留 T。例如 ExcludeNullOrUndefined<string | number | null | undefined> 的结果应该是 string | number
  5. (进阶)尝试编写一个映射类型 ToBoolean<T>,它将类型 T 的所有属性值类型都转换为 boolean 类型。

至此,我们已经覆盖了 TypeScript 的大部分核心概念和一些高级特性。从最初的类型注解,到描述复杂数据结构的接口和类,再到灵活强大的泛型和高级类型工具,TypeScript 为 JavaScript 世界带来了前所未有的健壮性和可维护性。掌握了这些知识,你就能更好地构建和管理复杂的 Web 应用,让你的代码“灵魂”不仅活跃,而且坚不可摧!

当然,TypeScript 还有更多细节和高级用法,比如装饰器 (Decorators)、Mixins、声明合并 (Declaration Merging) 等。但以上这些内容已经为你打下了坚实的基础,足以应对绝大多数的开发场景。

下一步:

  • 在实际项目中应用 TypeScript,逐步将你的 JavaScript 代码迁移到 TypeScript。
  • 阅读 TypeScript 官方文档,深入了解更多细节和最新特性。
  • 探索与各种框架和库(如 React, Vue, Angular)集成 TypeScript 的最佳实践。
  • 学习如何编写 .d.ts 声明文件,为你自己或社区提供类型信息。

祝你在 TypeScript 的学习和实践之路上一切顺利!


网站公告

今日签到

点亮在社区的每一天
去签到