TypeScript 从零基础到精通(五):高级类型与泛型

摘要:泛型是 TypeScript 最强大、最核心的高级特性之一。它允许我们编写"适用于广泛类型"的代码,而不是为每个类型重复编写逻辑。本文将从泛型的动机讲起,逐步深入到泛型函数、泛型接口、泛型类、泛型约束,再到映射类型、条件类型以及 TS 内置的工具类型(Partial、Required、Pick、Omit、Readonly、Record 等)。


一、前言

在前四篇文章中,我们已经掌握了 TypeScript 的基础类型、函数、接口、类以及面向对象编程。现在我们可以给大部分代码加上类型,让编译器帮我们检查错误。

然而,你会遇到这样的场景:编写一个通用函数,比如"获取数组中第一个元素"。如果是数字数组,返回 number;字符串数组,返回 string;用户对象数组,返回 User。不使用泛型的话,我们只能写多个重载或用 any(丢失类型信息)。

TypeScript 复制代码
// 使用 any 类型不安全
function firstElement(arr: any[]): any {
  return arr[0];
}
​
const num = firstElement([1, 2, 3]);  // num 类型是 any,无法享受后续类型检查

泛型就是解决这个问题的:把"类型"也作为参数,在调用时再确定具体类型。


二、泛型的动机:让"类型参数化"

想象一下,你写了一个函数 identity,它返回传入的参数本身。在 JavaScript 中很简单:

TypeScript 复制代码
function identity(arg) {
  return arg;
}

但在 TypeScript 中,如果要求类型安全,你可能想为每个类型写一个版本:

TypeScript 复制代码
function identityNumber(arg: number): number { return arg; }
function identityString(arg: string): string { return arg; }
// 不可能为所有类型都写一遍

泛型允许我们定义一个类型变量(Type Variable),在调用时才填充:

TypeScript 复制代码
function identity<T>(arg: T): T {
  return arg;
}
​
// 调用时自动推导类型
let output1 = identity("hello");   // 类型为 string
let output2 = identity(42);        // 类型为 number

<T> 表示声明一个类型变量 T,它会在函数调用时被具体的类型(如 stringnumber)替换。


三、泛型函数

3.1 基本语法与使用

泛型函数在参数列表前使用 <T>(可以用任何标识符,通常用 TUKV)。

TypeScript 复制代码
function getArrayLength<T>(arr: T[]): number {
  return arr.length;
}
​
console.log(getArrayLength([1, 2, 3]));        // T 被推导为 number
console.log(getArrayLength(["a", "b", "c"]));  // T 被推导为 string

3.2 类型推导与显式指定

大多数情况下,TypeScript 能根据参数自动推导类型变量。你也可以手动指定:

TypeScript 复制代码
let result = identity<string>("hello");   // 显式指定 T = string

手动指定在参数不足以推导时很有用(例如没有参数,或类型需要精确控制)。

3.3 多个类型参数

可以同时使用多个类型变量:

TypeScript 复制代码
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}
​
const merged = merge({ name: "Tom" }, { age: 25 });
// merged 类型为 { name: string } & { age: number } => { name: string; age: number }
console.log(merged.name, merged.age);

四、泛型接口与泛型类

4.1 泛型接口

接口也可以使用泛型,使其更灵活。

TypeScript 复制代码
interface Box<T> {
  value: T;
  getValue(): T;
}
​
const stringBox: Box<string> = {
  value: "hello",
  getValue() {
    return this.value;
  }
};
​
const numberBox: Box<number> = {
  value: 100,
  getValue() {
    return this.value;
  }
};

泛型接口也常用于定义函数类型:

TypeScript 复制代码
interface Comparator<T> {
  (a: T, b: T): number;
}

const compareNumbers: Comparator<number> = (a, b) => a - b;

4.2 泛型类

类和接口类似,可以在类名后加上 <T>

TypeScript 复制代码
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

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

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop());  // 2 (类型为 number | undefined)

const stringStack = new Stack<string>();
stringStack.push("a");

静态成员不能引用类的类型参数,因为静态成员属于类本身,而非实例。


五、泛型约束(Constraints)

有时候我们希望类型变量必须满足某些条件(比如必须有 length 属性)。这时可以使用 extends 关键字来约束。

5.1 基本约束

TypeScript 复制代码
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength("hello");    // ✅ 字符串有 length
logLength([1, 2, 3]);  // ✅ 数组有 length
// logLength(123);     // ❌ 数字没有 length 属性

5.2 使用 keyof 约束属性名

当你需要确保传入的键(key)确实存在于某个对象中时,可以使用 keyof 操作符。

TypeScript 复制代码
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = { name: "Alice", age: 30 };
const nameValue = getProperty(person, "name"); // string
const ageValue = getProperty(person, "age");   // number
// const invalid = getProperty(person, "gender"); // ❌ 参数 "gender" 不能赋给 "name" | "age"

keyof T 是一个联合类型,包含 T 的所有公共属性名。


六、泛型默认类型

我们可以为泛型参数指定默认类型,类似函数参数的默认值。当调用者不指定时,使用默认类型。

TypeScript 复制代码
interface ApiResponse<T = any> {
  code: number;
  data: T;
  message: string;
}

// 使用默认类型 any
const res1: ApiResponse = { code: 200, data: "ok", message: "success" };

// 指定具体类型
const res2: ApiResponse<{ id: number }> = { code: 200, data: { id: 1 }, message: "success" };

默认类型在有可选参数或复杂层级时非常有用。


七、映射类型(Mapped Types)

映射类型允许你基于旧类型创建新类型,通过对旧类型的每个属性进行转换。

7.1 基础语法

映射类型的语法是 { [P in K]: T },其中 K 是一个联合类型(通常是 keyof T)。

TypeScript 复制代码
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

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

其实 TypeScript 内置了这些工具类型(见后文)。我们可以自己实现一个简单的映射类型,把所有属性变成 nullundefined

TypeScript 复制代码
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

interface User {
  id: number;
  name: string;
}

type NullableUser = Nullable<User>;
// 等价于 { id: number | null; name: string | null; }

7.2 映射修饰符

readonly? 是映射类型中的修饰符。我们可以通过前缀 +- 来添加或移除修饰符(+ 是默认的)。

TypeScript 复制代码
// 移除所有属性的 readonly
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// 移除所有属性的可选修饰符(变成必选)
type Required<T> = {
  [P in keyof T]-?: T[P];
};

八、条件类型(Conditional Types)

条件类型类似于 JavaScript 的三元运算符:T extends U ? X : Y。它根据类型关系选择不同的类型。

8.1 基本语法

TypeScript 复制代码
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;  // true
type B = IsString<number>;   // false

8.2 分布式条件类型

当条件类型作用于泛型 且该泛型是联合类型时,TS 会将联合类型的每个成员分别代入条件,最后合并结果。这称为分布式条件类型。

TypeScript 复制代码
type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// 等价于 (string extends any ? string[] : never) | (number extends any ? number[] : never)
// 结果: string[] | number[]

防止分布式:用方括号包裹 [T]

TypeScript 复制代码
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>; // (string | number)[]

8.3 infer 关键字

infer 允许我们在条件类型中声明一个待推断的类型变量,常用于提取类型的内部结构。

TypeScript 复制代码
// 获取函数返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function foo(): boolean { return true; }
type FooReturn = ReturnType<typeof foo>;  // boolean

// 获取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : T;

type E1 = ElementType<number[]>;   // number
type E2 = ElementType<string>;     // string (不变)

infer 也可以用于元组和 Promise 等。


九、内置工具类型详解

TypeScript 内置了许多常用的类型工具,极大提升了开发效率。下面逐一介绍。

9.1 Partial<T> ------ 所有属性变为可选

TypeScript 复制代码
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>): Todo {
  return { ...todo, ...fieldsToUpdate };
}

const todo1: Todo = { title: "Learn TS", description: "Study", completed: false };
const todo2 = updateTodo(todo1, { description: "Study advanced" });

9.2 Required<T> ------ 所有属性变为必选

TypeScript 复制代码
interface Props {
  a?: number;
  b?: string;
}

const obj: Required<Props> = { a: 5, b: "hello" };  // 必须提供 a 和 b

9.3 Readonly<T> ------ 所有属性变为只读

TypeScript 复制代码
const frozen: Readonly<Todo> = { title: "Freeze", description: "Immutable", completed: false };
// frozen.title = "Changed";  // ❌

9.4 Pick<T, K> ------ 从 T 中挑选部分属性

TypeScript 复制代码
type TodoPreview = Pick<Todo, "title" | "completed">;
// { title: string; completed: boolean; }

9.5 Omit<T, K> ------ 从 T 中排除部分属性

TypeScript 复制代码
type TodoInfo = Omit<Todo, "completed">;
// { title: string; description: string; }

9.6 Record<K, T> ------ 构造一个对象类型,键为 K,值为 T

TypeScript 复制代码
type PageInfo = {
  title: string;
  url: string;
};

type Page = "home" | "about" | "contact";

const pages: Record<Page, PageInfo> = {
  home: { title: "Home", url: "/" },
  about: { title: "About", url: "/about" },
  contact: { title: "Contact", url: "/contact" }
};

9.7 Exclude<T, U> ------ 从 T 中排除可赋值给 U 的类型

TypeScript 复制代码
type T = Exclude<"a" | "b" | "c", "a" | "b">;  // "c"

9.8 Extract<T, U> ------ 提取 T 中可赋值给 U 的类型

TypeScript 复制代码
type T = Extract<"a" | "b" | "c", "a" | "d">;  // "a"

9.9 NonNullable<T> ------ 排除 null 和 undefined

TypeScript 复制代码
type T = NonNullable<string | number | null | undefined>;  // string | number

9.10 ReturnType<T> ------ 获取函数返回值类型

TypeScript 复制代码
function getString(): string { return "hello"; }
type R = ReturnType<typeof getString>;  // string

9.11 Parameters<T> ------ 获取函数参数类型(元组)

TypeScript 复制代码
function greet(name: string, age: number): void {}
type Params = Parameters<typeof greet>;  // [string, number]

十、总结

本文深入讲解了 TypeScript 的高级类型特性:

泛型

  • 让类型变量化,编写可复用的组件

  • 泛型函数、泛型接口、泛型类

  • 泛型约束(extends + keyof

  • 泛型默认类型

映射类型

  • 基于旧类型通过 [P in keyof T] 生成新类型

  • 修饰符 readonly? 及加减操作

条件类型

  • T extends U ? X : Y

  • 分布式条件类型(联合类型自动分发)

  • infer 提取类型

内置工具类型

  • PartialRequiredReadonlyPickOmitRecordExcludeExtractNonNullableReturnTypeParameters

这些高级特性是 TypeScript 区别于普通类型检查器的核心优势,也是写出健壮、灵活、可维护代码的关键。


如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。

相关推荐
counterxing1 小时前
vibe coding 之后,我更不想打字了
前端·agent·ai编程
云水一下2 小时前
TypeScript 从零基础到精通(六):类型声明与模块化
javascript·typescript
copyer_xyf2 小时前
Python 模块与包的导入导出
前端·后端·python
研☆香2 小时前
es6新特性功能介绍(四)
前端·ecmascript·es6
微扬嘴角2 小时前
React篇1--JSX语法规则、组件、组件实例的3大特性
前端·react.js·前端框架
copyer_xyf2 小时前
Python venv 虚拟环境
前端·后端·python
无聊的老谢2 小时前
Vue 3 + TypeScript 构建大型电信运维平台的前端架构设计
前端·vue.js·typescript
xiaofeichaichai2 小时前
Map / Set / WeakMap / WeakSet
前端·javascript
李可以量化2 小时前
成交量的终极量化策略:价量共振指标完整实现(下篇)
前端·数据库·人工智能