TypeScript 泛型

40 阅读4分钟

理解泛型:类型系统中的"变量"

graph TD
    A[泛型概念] --> B[基本语法]
    A --> C[核心价值]
    C --> D[代码重用]
    C --> E[类型安全]
    C --> F[灵活性]
    
    B --> G[泛型函数]
    B --> H[泛型接口]
    B --> I[泛型类]

想象你正在开发一个函数,既可以处理数字,又可以处理字符串。没有泛型时,你可能会写出:

// 非泛型实现 - 冗余代码
function identityNumber(arg: number): number {
  return arg;
}

function identityString(arg: string): string {
  return arg;
}

泛型提供了类型变量,让你可以创建可重用的组件:

// 泛型实现 - 单一函数处理多种类型
function identity<T>(arg: T): T {
  return arg;
}

const str = identity<string>("TypeScript"); // string 类型
const num = identity<number>(42);           // number 类型
const arr = identity<number[]>([1, 2, 3]);  // number[] 类型

这里T就是一个类型变量,它捕获用户传入的类型,允许我们在函数内部使用这个类型。

泛型的核心价值

1. 代码复用性

  • 消除重复代码逻辑
  • 单一实现服务多种类型

2. 类型安全性

  • 编译时类型检查
  • 避免运行时类型错误

3. 灵活性

  • 保留类型信息
  • 支持复杂类型操作

泛型的应用场景

1. 泛型函数

// 返回数组第一个元素
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

const numbers = firstElement([1, 2, 3]);        // number | undefined
const strings = firstElement(["a", "b", "c"]);  // string | undefined

2. 泛型接口

interface KeyValuePair<K, V> {
  key: K;
  value: V;
}

const stringPair: KeyValuePair<string, string> = {
  key: "username", 
  value: "tsUser"
};

const numberPair: KeyValuePair<number, boolean> = {
  key: 1, 
  value: true
};

3. 泛型类

class Box<T> {
  private content: T;
  
  constructor(initialValue: T) {
    this.content = initialValue;
  }
  
  getValue(): T {
    return this.content;
  }
  
  setValue(newValue: T): void {
    this.content = newValue;
  }
}

const stringBox = new Box<string>("Hello Generics");
console.log(stringBox.getValue().toUpperCase()); // "HELLO GENERICS"

const numberBox = new Box<number>(42);
console.log(numberBox.getValue().toFixed(2)); // "42.00"

高级泛型技巧

graph TD
J[高级技巧] --> K[泛型约束]
    J --> L[泛型参数默认值]
    J --> M[条件类型]
    J --> N[映射类型]

1. 泛型约束(Generic Constraints)

使用extends关键字约束泛型类型:

// 约束为具有length属性的类型
interface Lengthwise {
  length: number;
}

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

logLength("TS");      // 正确 - 字符串有length
logLength([1, 2, 3]); // 正确 - 数组有length
logLength(42);        // 错误 - 数字没有length

2. 在泛型约束中使用类型参数

// 确保key存在于对象中
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const user = { name: "Alice", age: 30 };

getProperty(user, "name"); // 正确
getProperty(user, "email"); // 错误 - 'email' 不在 user 的属性中

3. 泛型参数默认值

// 设置默认的容器类型为 HTMLElement
function createContainer<T = HTMLElement>(tagName: string): T {
  return document.createElement(tagName) as T;
}

const div = createContainer("div");            // HTMLElement
const myDiv = createContainer<HTMLDivElement>("div"); // 指定为更具体的类型

4. 类型推断与泛型

TypeScript可以自动推断泛型类型:

function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

// 类型自动推断为 [string, number]
const result = pair("TypeScript", 4.2);

实用泛型工具类型

TypeScript内置了强大的泛型工具类型:

// Partial: 所有属性变为可选
type PartialUser = Partial<{ name: string; age: number }>;
// { name?: string; age?: number }

// Readonly: 所有属性变为只读
type ReadonlyUser = Readonly<{ name: string }>;
// { readonly name: string }

// Pick: 选择对象类型的子集
type NameOnly = Pick<{ name: string; age: number }, "name">;
// { name: string }

// Omit: 移除对象类型的部分属性
type WithoutAge = Omit<{ name: string; age: number }, "age">;
// { name: string }

// Record: 创建键值映射
type UserRoles = Record<"admin" | "user", boolean>;
// { admin: boolean; user: boolean }

// ReturnType: 获取函数返回类型
type FetchResponse = ReturnType<typeof fetch>;
// Promise<Response>

条件类型与映射类型

条件类型

根据条件选择类型:

// 如果 T 是数组,返回数组元素类型,否则返回 T 本身
type Flatten<T> = T extends Array<infer U> ? U : T;

// string 类型
type StringType = Flatten<string>;
// number 类型
type NumberType = Flatten<number[]>;

映射类型

对现有类型进行变换:

// 将所有属性改为可空
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

// 为所有属性添加 getter 函数
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

type UserInfo = {
  name: string;
  age: number;
};

type UserGetters = Getters<UserInfo>;
// { getName: () => string; getAge: () => number }

实际应用场景

1. API响应处理

interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
  timestamp: Date;
}

// 用户数据响应
type UserApiResponse = ApiResponse<{
  id: number;
  email: string;
  role: "user" | "admin";
}>

// 产品数据响应
type ProductApiResponse = ApiResponse<{
  id: number;
  name: string;
  price: number;
  stock: number;
}>

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  return await response.json();
}

// 使用时明确指定返回类型
const userData = await fetchData<UserApiResponse["data"]>("/api/user");
console.log(userData.email); // 类型安全访问

2. 高级函数组合

// 函数组合类型
type Compose<A, B, C> = (f: (b: B) => C, g: (a: A) => B) => (a: A) => C;

// 实现组合函数
const compose: Compose<unknown, unknown, unknown> = 
  (f, g) => (a) => f(g(a));

// 使用组合函数
const toUpperCase = (s: string) => s.toUpperCase();
const addExclamation = (s: string) => s + "!";

const shout = compose(toUpperCase, addExclamation);
console.log(shout("hello")); // "HELLO!"

3. 类型安全的Redux Reducer

// 定义Action基础结构
interface Action<T extends string> {
  type: T;
}

// 带有payload的Action
interface PayloadedAction<T extends string, P> extends Action<T> {
  payload: P;
}

// 创建Action Creator
function createAction<T extends string>(type: T): Action<T>;
function createAction<T extends string, P>(type: T, payload: P): PayloadedAction<T, P>;
function createAction<T extends string, P>(type: T, payload?: P) {
  return payload ? { type, payload } : { type };
}

// 使用示例
const loginSuccess = createAction("LOGIN_SUCCESS", { username: "tsMaster" });
const logout = createAction("LOGOUT");

泛型最佳实践

  1. 使用描述性类型变量名

    // 优先使用
    function save<Entity>(entity: Entity): Promise<Entity>
    
    // 避免
    function save<T>(entity: T): Promise<T>
    
  2. 合理约束泛型参数

    // 添加约束提供类型安全
    function getById<Model extends { id: string }>(id: string): Model
    
  3. 避免过度使用泛型

    • 当具体类型足够时不要使用泛型
    • 避免过度复杂的类型关系
  4. 利用类型推断减少类型参数

    // 让TypeScript自动推断
    const users = new Array<User>();
    
  5. 优先使用内置工具类型

    • 避免重复造轮子
    • 使用Partial、Pick、Omit等简化代码

泛型与性能

在TypeScript中使用泛型对运行时性能没有影响,因为泛型是编译时特性。编译后所有类型信息都会被擦除:

// TypeScript 代码
function identity<T>(arg: T): T {
  return arg;
}

// 编译后的JavaScript
function identity(arg) {
  return arg;
}

泛型是TypeScript最强大的特性之一,它提供了:

  • 类型抽象能力:创建灵活可复用的代码组件
  • 编译时类型安全:在开发阶段捕捉潜在错误
  • 开发效率提升:减少重复代码并增强IDE支持
  • 类型表达能力:精确描述复杂的数据结构和算法

掌握泛型将使你能够:

  1. 构建类型安全的API和框架
  2. 创建复杂的类型转换和操作
  3. 提高代码的可维护性和可扩展性
  4. 深入理解TypeScript的类型系统

随着TypeScript版本迭代,泛型的能力不断增强(如条件类型、模板字面量类型等),成为现代TypeScript开发不可或缺的核心技术。