返回首页

说说你对 TypeScript 中泛型的理解?应用场景?

问题解析

泛型是 TypeScript 最强大的特性之一,考察候选人对泛型概念、语法、约束以及实际应用的理解。需要能够解释泛型如何解决代码复用与类型安全的问题。

核心概念

  • 泛型(Generics):在定义时不指定具体类型,使用时再指定
  • 类型参数<T> 作为占位符,代表任意类型
  • 泛型约束:使用 extends 限制类型参数的范围
  • 泛型默认值:为类型参数提供默认类型
  • 泛型推断:编译器自动推断泛型类型

详细解答

1. 是什么

泛型程序设计(generic programming)是程序设计语言的一种风格或范式。

泛型允许我们在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。

在 TypeScript 中,定义函数、接口或者类的时候,不预先定义好具体的类型,而在使用的时候再指定类型的一种特性。

2. 为什么需要泛型

问题场景

// 接受 number 参数并返回 number
function returnItem(para: number): number {
  return para;
}

// 接受 string 参数并返回 string
function returnItem(para: string): string {
  return para;
}

// 使用 any 失去类型检查
function returnItem(para: any): any {
  return para;
}

使用泛型解决

function returnItem<T>(para: T): T {
  return para;
}

// 使用方式
const num = returnItem<number>(123);      // number
const str = returnItem<string>("hello");  // string
// 类型推断
const bool = returnItem(true);            // boolean,自动推断

3. 泛型的使用方式

泛型通过 <> 的形式进行表述,可以声明:函数、接口、类。

3.1 函数声明

// 单类型参数
function identity<T>(arg: T): T {
  return arg;
}

// 多类型参数
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

const result = swap([7, 'seven']); // ['seven', 7]

// 泛型函数类型
let myIdentity: <T>(arg: T) => T = identity;

3.2 接口声明

interface GenericIdentityFn<T> {
  (arg: T): T;
}

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

3.3 类声明

class Stack<T> {
  private arr: T[] = [];

  public push(item: T) {
    this.arr.push(item);
  }

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

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

// 使用
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
// numberStack.push("3"); // Error

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

4. 泛型约束

4.1 基本约束

interface Lengthwise {
  length: number;
}

// T 必须具有 length 属性
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);  // 现在可以访问 length 属性
  return arg;
}

loggingIdentity("hello");     // ok,字符串有 length
loggingIdentity([1, 2, 3]);   // ok,数组有 length
// loggingIdentity(3);        // Error: number 没有 length

4.2 索引类型约束

function getValue<T extends object, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = { name: "Tom", age: 25 };
const name = getValue(person, "name");  // string
const age = getValue(person, "age");    // number
// const gender = getValue(person, "gender"); // Error: "gender" 不存在

4.3 多类型约束

interface FirstInterface {
  doSomething(): number;
}

interface SecondInterface {
  doSomethingElse(): string;
}

// 创建一个接口继承多个接口
interface ChildInterface extends FirstInterface, SecondInterface {}

class Demo<T extends ChildInterface> {
  private genericProperty: T;

  constructor(genericProperty: T) {
    this.genericProperty = genericProperty;
  }

  useT() {
    this.genericProperty.doSomething();
    this.genericProperty.doSomethingElse();
  }
}

5. 泛型默认值

function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

// 使用默认类型
const strArr = createArray(3, "x");  // string[]

// 显式指定类型
const numArr = createArray<number>(3, 0);  // number[]

深入理解

泛型与 any 的区别

// 使用 any - 失去类型信息
function anyIdentity(arg: any): any {
  return arg;
}
const anyResult = anyIdentity("hello");
// anyResult 是 any 类型,没有智能提示

// 使用泛型 - 保留类型信息
function genericIdentity<T>(arg: T): T {
  return arg;
}
const genericResult = genericIdentity("hello");
// genericResult 是 string 类型,有完整智能提示

泛型推断

function map<T, U>(array: T[], fn: (item: T) => U): U[] {
  return array.map(fn);
}

// 类型推断自动确定 T 和 U
const lengths = map(["hello", "world"], s => s.length);
// T 被推断为 string,U 被推断为 number
// lengths 类型为 number[]

条件类型与泛型

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

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

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

// 使用示例
type T0 = Exclude<"a" | "b" | "c", "a">;  // "b" | "c"
type T1 = Extract<"a" | "b" | "c", "a" | "f">;  // "a"

最佳实践

  1. 优先使用泛型而非 any:保持类型安全
  2. 合理命名类型参数
    • T:Type(类型)
    • K:Key(键)
    • V:Value(值)
    • E:Element(元素)
    • 复杂场景使用描述性名称
  3. 使用泛型约束:限制类型参数的范围,确保类型安全
  4. 提供默认值:当类型参数通常有默认值时,简化使用
  5. 利用类型推断:减少显式类型参数,提高代码可读性

面试要点

  1. 泛型概念:定义时不指定具体类型,使用时指定
  2. 语法<T> 类型参数,可用于函数、接口、类
  3. 泛型约束T extends SomeType 限制类型范围
  4. keyof 约束K extends keyof T 限制键名
  5. 多类型参数<T, U, V> 处理多个类型
  6. 泛型推断:编译器自动推断类型,减少显式声明
  7. 与 any 的区别:泛型保留类型信息,any 失去类型检查
  8. 实际应用
    • 通用数据结构和算法(Stack、Queue、map、filter)
    • API 响应类型封装
    • 高阶组件(React)
    • 状态管理(Redux、Vuex)