TS 泛型的基本使用

668 阅读6分钟

泛型是 TypeScript 类型系统中一个非常强大和核心的特性,它允许我们编写可重用灵活类型安全的组件(如函数、类、接口、类型别名)。

一、 什么是泛型?为什么需要它?

想象一下,你想编写一个函数,它接收一个参数并返回该参数。

1. 不用泛型的问题:
  • 使用 any 类型:

    function identityAny(arg: any): any {
      return arg;
    }
    let outputAny = identityAny("myString"); // outputAny 的类型是 any
    // 问题:丢失了类型信息,编译器无法提供类型检查和智能提示
    // console.log(outputAny.toFixed()); // 运行时会报错,但编译时不会提示!
        
    

    使用 any 会丢失类型信息,降低了类型安全性。

  • 为每种类型编写特定函数:

    function identityString(arg: string): string { return arg; }
    function identityNumber(arg: number): number { return arg; }
    // ...等等
        
    

    这会导致大量重复代码,难以维护。

2. 泛型的解决方案:

泛型允许我们编写一个“模板”函数(或类、接口),其中的类型是参数化的。你可以把它想象成给类型设置了一个占位符变量,在使用时再指定具体的类型(或者让 TypeScript 自动推断)。

// 定义一个泛型函数 identity
// <T> 声明了一个类型变量 T (Type)
// arg: T 表示参数 arg 的类型是 T
// : T 表示函数返回值的类型也是 T
function identity<T>(arg: T): T {
  return arg;
}

// 如何使用?
// 1. 显式指定类型:
let outputString = identity<string>("myString"); // T 被指定为 string,outputString 类型是 string
let outputNumber = identity<number>(123);       // T 被指定为 number,outputNumber 类型是 number

// 2. 利用类型推断 (更常见):编译器会根据传入的参数自动推断 T 的类型
let outputBool = identity(true); // 传入 boolean,T 被推断为 boolean,outputBool 类型是 boolean
let outputObj = identity({ name: "Alice" }); // T 被推断为 { name: string }

// 类型安全!
// console.log(outputString.toFixed()); // Error: Property 'toFixed' does not exist on type 'string'.
console.log(outputNumber.toFixed(2)); // OK
    

泛型的核心优势:在保证代码可重用性的同时,维持了严格的类型约束和类型安全。

二、 泛型的基本语法和应用

泛型使用尖括号 <> 来声明类型参数 (Type Parameters) ,通常使用单个大写字母(如 T, U, K, V 等)作为类型参数的名称,但这只是约定,你可以使用任何合法的标识符。

1. 泛型函数 (Generic Functions)

如上例所示,类型参数列表放在函数名之后、参数列表之前。

 // 接收一个数组,返回第一个元素
function getFirstElement<T>(arr: T[]): T | undefined {
  return arr.length > 0 ? arr[0] : undefined;
}

let firstNum = getFirstElement([1, 2, 3]);        // T 推断为 number, firstNum 类型是 number | undefined
let firstStr = getFirstElement(["a", "b", "c"]);    // T 推断为 string, firstStr 类型是 string | undefined
let firstEmpty = getFirstElement<boolean>([]); // 显式指定 T 为 boolean, firstEmpty 类型是 boolean | undefined

// 接收两个不同类型的参数
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}
let myPair = pair("hello", 123); // T 推断为 string, U 推断为 number, myPair 类型是 [string, number]
    
2. 泛型接口 (Generic Interfaces)

接口也可以是泛型的,类型参数用于接口内部的成员类型。

// 定义一个泛型接口,表示键值对
interface KeyValuePair<K, V> {
  key: K;
  value: V;
}

let record1: KeyValuePair<string, number> = { key: "age", value: 30 };
let record2: KeyValuePair<number, boolean> = { key: 1, value: true };

// 定义一个泛型接口,表示 API 响应
interface ApiResponse<TData> {
  success: boolean;
  data: TData;
  message?: string;
}

let userResponse: ApiResponse<{ id: number; name: string }> = {
  success: true,
  data: { id: 1, name: "Alice" }
};

let productResponse: ApiResponse<string[]> = {
  success: false,
  data: [],
  message: "Products not found"
};
    
3. 泛型类 (Generic Classes)

类也可以使用泛型,类型参数在类名之后声明。泛型参数不能用于类的静态成员。

class DataStore<T> {
  private data: T[] = [];

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

  getAll(): T[] {
    return [...this.data]; // 返回副本以防外部修改
  }

  // static method cannot use the class type parameter T directly
  // static staticMethod(item: T) {} // Error
}

// 创建一个存储数字的 DataStore 实例
let numberStore = new DataStore<number>();
numberStore.add(1);
numberStore.add(2);
console.log(numberStore.getAll()); // [1, 2]
// numberStore.add("hello"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

// 创建一个存储字符串的 DataStore 实例 (类型推断)
let stringStore = new DataStore<string>();
stringStore.add("apple");
stringStore.add("banana");
console.log(stringStore.getAll()); // ["apple", "banana"]
    
4. 泛型类型别名 (Generic Type Aliases)

类型别名也可以是泛型的。

// 定义一个可能为 null 或 undefined 的泛型类型
type Nullable<T> = T | null | undefined;

let nullableString: Nullable<string>;
nullableString = "hello";
nullableString = null;
nullableString = undefined;
// nullableString = 123; // Error

// 定义一个包含值的容器的泛型类型
type Container<T> = { value: T };

let stringContainer: Container<string> = { value: "container content" };
let numberContainer: Container<number> = { value: 42 };
    

三、 泛型约束 (Generic Constraints)

有时,我们希望泛型函数或类只能处理具有特定结构或能力的类型。例如,你想写一个函数,它接收一个参数并打印其 .length 属性,那么这个参数必须得有 length 属性(比如 string 或 array)。这时就需要泛型约束

使用 extends 关键字来约束类型参数必须符合某个接口或具有某些属性。

// 定义一个接口,表示有 length 属性
interface Lengthwise {
  length: number;
}

// 约束 T 必须符合 Lengthwise 接口(即必须有 number 类型的 length 属性)
function logLength<T extends Lengthwise>(arg: T): void {
  console.log(arg.length);
}

logLength("hello world"); // OK, string 有 length 属性
logLength([1, 2, 3]);   // OK, array 有 length 属性
// logLength(123);       // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
// logLength({ name: "test" }); // Error: Argument of type '{ name: string; }' is not assignable to parameter of type 'Lengthwise'. Property 'length' is missing.

// 约束 T 必须是某个类的子类或实现了某个接口
interface Printable { print(): void; }
class Document implements Printable { print() { console.log("Printing document..."); } }
class Image implements Printable { print() { console.log("Printing image..."); } }

function printItem<T extends Printable>(item: T): void {
  item.print(); // 安全调用 print 方法,因为 T 被约束了
}

printItem(new Document());
printItem(new Image());
// class Report {}
// printItem(new Report()); // Error: Argument of type 'Report' is not assignable to parameter of type 'Printable'. Type 'Report' is missing the following properties from type 'Printable': print
    

四、 在泛型约束中使用类型参数

一个常见的模式是,一个类型参数受另一个类型参数的约束。例如,你想写一个函数来获取对象上某个属性的值,你需要确保这个属性名确实存在于该对象上。

使用 keyof 操作符:keyof T 会产生一个由 T 类型的所有公共属性名组成的联合类型

// K 必须是 T 对象上的一个键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  // T[K] 是索引访问类型 (Indexed Access Type),表示 obj 对象上 key 属性的值的类型
  return obj[key];
}

let car = { make: "Toyota", model: "Camry", year: 2023 };

let make = getProperty(car, "make");   // T = typeof car, K = "make", 返回 string
let year = getProperty(car, "year");   // T = typeof car, K = "year", 返回 number
// let color = getProperty(car, "color"); // Error: Argument of type '"color"' is not assignable to parameter of type '"make" | "model" | "year"'.
    

五、 在泛型中使用类类型

有时你想引用类的构造函数类型。

// ctor 的类型是:一个构造函数,它没有参数,并且返回一个类型为 T 的实例
function create<T>(ctor: new () => T): T {
  return new ctor();
}

class BeeKeeper { honey: boolean = true; }
class ZooKeeper { tigers: number = 2; }

let beeKeeper = create(BeeKeeper); // T 推断为 BeeKeeper
let zooKeeper = create(ZooKeeper); // T 推断为 ZooKeeper
console.log(beeKeeper.honey); // true
console.log(zooKeeper.tigers); // 2
    

六、 泛型参数默认值

可以为泛型参数指定默认类型。当用户没有指定类型(或编译器无法推断)时,将使用默认类型。

interface RequestOptions<T = any> { // 默认类型为 any (谨慎使用)
  method: 'GET' | 'POST';
  headers?: Record<string, string>;
  body?: T;
}

let getReq: RequestOptions = { method: 'GET' }; // T 使用默认的 any
let postReq: RequestOptions<FormData> = { method: 'POST', body: new FormData() }; // T 指定为 FormData
    

总结:

泛型是 TypeScript 中实现代码复用和类型安全的关键工具。通过使用类型参数 ,你可以创建适用于多种类型的函数、类、接口和类型别名。泛型约束 (extends) 允许你对这些类型参数施加限制,确保它们具有必要的结构或能力。掌握泛型对于编写高质量、可维护的 TypeScript 代码至关重要。