DevToolBox免费
博客

TypeScript 泛型详解:从入门到实战的完整指南

14 分钟阅读作者 DevToolBox

TypeScript 泛型是类型系统中最强大的特性之一。它允许你通过参数化类型来编写灵活、可复用且类型安全的代码。泛型让你无需为每种数据类型重复编写函数或类,只需编写一次即可与任何类型一起使用,同时保持完整的类型安全。本指南涵盖从基础到高级模式的所有内容。

你的第一个泛型函数

经典的起点是身份函数——一个返回其接收内容的函数。如果不使用泛型,你要么使用 any 丢失类型信息,要么为每种类型编写单独的函数。

使用泛型,你定义一个类型参数(通常命名为 T),作为占位符。调用函数时,TypeScript 会自动推断实际类型。

// Without generics — loses type info
function identityAny(value: any): any {
  return value;
}
const result1 = identityAny("hello"); // type: any (not string!)

// With generics — preserves type info
function identity<T>(value: T): T {
  return value;
}
const result2 = identity("hello");    // type: string ✓
const result3 = identity(42);         // type: number ✓
const result4 = identity(true);       // type: boolean ✓

// Explicit type argument (rarely needed)
const result5 = identity<string>("hello"); // type: string

泛型函数

泛型函数可以接受多个类型参数,甚至可以有默认类型。这使它们在构建工具函数时非常灵活。

// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}
const p = pair("hello", 42); // type: [string, number]

// Swap function with two type params
function swap<A, B>(tuple: [A, B]): [B, A] {
  return [tuple[1], tuple[0]];
}
const swapped = swap(["hello", 42]); // type: [number, string]

// Default type parameters
function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}
const strings = createArray(3, "x");   // T inferred as string
const numbers = createArray(3, 0);     // T inferred as number
const defaults = createArray<>(3, ""); // T defaults to string

默认类型参数的工作方式与默认函数参数相同——如果调用者未指定类型,则使用默认类型。

泛型接口和类型

泛型不仅限于函数。你可以参数化接口类型别名,创建适用于任何类型的灵活数据结构。

// Generic interface
interface Box<T> {
  value: T;
  label: string;
}

const stringBox: Box<string> = { value: "hello", label: "greeting" };
const numberBox: Box<number> = { value: 42, label: "answer" };

// Generic interface with multiple params
interface KeyValuePair<K, V> {
  key: K;
  value: V;
}

// Generic type alias
type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

const ok: Result<string> = { success: true, data: "hello" };
const fail: Result<string> = { success: false, error: new Error("oops") };

// Generic type for callbacks
type AsyncCallback<T> = (error: Error | null, data: T | null) => void;

// Generic type for nullable values
type Nullable<T> = T | null | undefined;

泛型接口在定义 API 响应、集合和数据容器的结构时特别有用。

泛型类

类也可以是泛型的。这对于构建栈、队列、链表等数据结构或任何需要与多种类型一起工作的容器非常有用。

// Generic Stack class
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];
  }

  get size(): number {
    return this.items.length;
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // type: number | undefined

const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.peek(); // type: string | undefined

// Generic class with multiple type params
class HashMap<K, V> {
  private map = new Map<K, V>();

  set(key: K, value: V): void {
    this.map.set(key, value);
  }

  get(key: K): V | undefined {
    return this.map.get(key);
  }

  has(key: K): boolean {
    return this.map.has(key);
  }
}

类型参数在整个类中都可用——在属性、方法参数和返回类型中。

使用 extends 的泛型约束

不受约束的泛型接受任何类型,但有时你需要缩小允许的类型范围。extends 关键字让你可以添加约束。

使用 keyof 约束对象键

一个非常常见的模式是将类型参数约束为另一个类型的有效键。这确保了类型安全的属性访问。

// getProperty with keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30, email: "alice@example.com" };

getProperty(user, "name");  // type: string ✓
getProperty(user, "age");   // type: number ✓
// getProperty(user, "foo"); // Error: "foo" is not assignable to "name" | "age" | "email"

// Pick specific keys from an object
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(key => { result[key] = obj[key]; });
  return result;
}

const subset = pick(user, ["name", "email"]);
// type: { name: string; email: string }

约束到特定结构

你可以通过扩展接口或对象类型来要求类型参数具有某些属性。

// Constraint to objects with a length property
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(value: T): T {
  console.log(`Length: ${value.length}`);
  return value;
}

logLength("hello");     // OK — string has length
logLength([1, 2, 3]);   // OK — array has length
logLength({ length: 5, name: "test" }); // OK — has length
// logLength(42);       // Error — number has no length property

// Constraint to objects with an id
interface HasId {
  id: number | string;
}

function findById<T extends HasId>(items: T[], id: T["id"]): T | undefined {
  return items.find(item => item.id === id);
}

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

const found = findById(users, 1);
// type: { id: number; name: string } | undefined

条件类型

条件类型允许你根据条件在两种类型之间选择,使用 T extends U ? X : Y 语法。它们是许多高级类型计算的基础。

// Basic conditional type
type IsString<T> = T extends string ? true : false;

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

// Flatten arrays, leave other types alone
type Flatten<T> = T extends Array<infer Item> ? Item : T;

type D = Flatten<string[]>;    // string
type E = Flatten<number[][]>;  // number[]
type F = Flatten<string>;      // string (not an array, returned as-is)

// Extract return type (simplified ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type G = MyReturnType<() => string>;           // string
type H = MyReturnType<(x: number) => boolean>; // boolean

// Extract promise value
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type I = UnwrapPromise<Promise<string>>; // string
type J = UnwrapPromise<number>;          // number

条件类型与 infer 结合时变得特别强大,后者允许你从其他类型内部提取类型。

映射类型

映射类型允许你转换现有类型的属性来创建新类型。它们使用 [K in keyof T] 语法遍历键。

// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties required (like Required<T>)
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Make all properties nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

// Transform property types
type Stringify<T> = {
  [K in keyof T]: string;
};

interface User {
  name: string;
  age: number;
  active: boolean;
}

type StringifiedUser = Stringify<User>;
// { name: string; age: string; active: string }

// Mapped type with conditional
type OptionalNullable<T> = {
  [K in keyof T]: undefined extends T[K] ? T[K] | null : T[K];
};

你可以在映射类型中添加或删除 readonly?(可选)等修饰符。

内置工具类型

TypeScript 附带了许多使用泛型构建的工具类型。以下是最常用的:

工具类型描述示例
Partial<T>使所有属性可选Partial<User>
Required<T>使所有属性必需Required<Partial<User>>
Pick<T, K>选取指定属性Pick<User, "name" | "email">
Omit<T, K>排除指定属性Omit<User, "password">
Record<K, V>创建键值类型映射Record<string, number>
Readonly<T>使所有属性只读Readonly<Config>
ReturnType<T>提取函数返回类型ReturnType<typeof fn>
Parameters<T>提取函数参数类型元组Parameters<typeof fn>
// Utility types in action
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// For update forms — all fields optional
type UpdateUser = Partial<User>;

// Public user — omit password
type PublicUser = Omit<User, "password">;

// User creation — omit auto-generated fields
type CreateUser = Omit<User, "id" | "createdAt">;

// Lookup table
type UserLookup = Record<number, User>;

// Immutable user from API
type FrozenUser = Readonly<User>;

// Extract function types
function fetchUser(id: number): Promise<User> { /* ... */ }
type FetchReturn = ReturnType<typeof fetchUser>;     // Promise<User>
type FetchParams = Parameters<typeof fetchUser>;     // [number]

真实世界的模式

泛型 API 响应包装器

泛型最常见的用途之一是用一致的结构包装 API 响应。这确保每个端点都以相同的格式返回数据,同时保留特定的数据类型。

// Generic API response wrapper
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: string;
}

interface PaginatedResponse<T> extends ApiResponse<T[]> {
  pagination: {
    page: number;
    pageSize: number;
    totalItems: number;
    totalPages: number;
  };
}

// Usage with specific types
interface User {
  id: number;
  name: string;
  email: string;
}

interface Product {
  id: number;
  title: string;
  price: number;
}

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

// TypeScript knows the exact data type
const userRes = await fetchApi<User>("/api/users/1");
console.log(userRes.data.name); // type: string ✓

const productsRes = await fetchApi<Product[]>("/api/products");
console.log(productsRes.data[0].price); // type: number ✓

泛型 React 组件 Props

泛型让你构建适用于不同数据类型的 React 组件,同时保持 props 的类型安全。

// Generic list component props
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
  onItemClick?: (item: T) => void;
  emptyMessage?: string;
}

function GenericList<T>({
  items,
  renderItem,
  keyExtractor,
  onItemClick,
  emptyMessage = "No items",
}: ListProps<T>) {
  if (items.length === 0) return <p>{emptyMessage}</p>;
  return (
    <ul>
      {items.map((item, index) => (
        <li
          key={keyExtractor(item)}
          onClick={() => onItemClick?.(item)}
        >
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
}

// Usage — TypeScript infers T from items
<GenericList
  items={users}
  keyExtractor={(user) => user.id}
  renderItem={(user) => <span>{user.name}</span>}
  onItemClick={(user) => console.log(user.email)}
/>

泛型仓储模式

仓储模式将数据访问抽象在泛型接口背后。这允许你在不更改业务逻辑的情况下切换数据源。

// Generic repository interface
interface Entity {
  id: number | string;
}

interface Repository<T extends Entity> {
  findAll(): Promise<T[]>;
  findById(id: T["id"]): Promise<T | null>;
  create(data: Omit<T, "id">): Promise<T>;
  update(id: T["id"], data: Partial<T>): Promise<T>;
  delete(id: T["id"]): Promise<boolean>;
}

// Concrete implementation
class ApiRepository<T extends Entity> implements Repository<T> {
  constructor(private baseUrl: string) {}

  async findAll(): Promise<T[]> {
    const res = await fetch(this.baseUrl);
    return res.json();
  }

  async findById(id: T["id"]): Promise<T | null> {
    const res = await fetch(`${this.baseUrl}/${id}`);
    if (!res.ok) return null;
    return res.json();
  }

  async create(data: Omit<T, "id">): Promise<T> {
    const res = await fetch(this.baseUrl, {
      method: "POST",
      body: JSON.stringify(data),
    });
    return res.json();
  }

  async update(id: T["id"], data: Partial<T>): Promise<T> {
    const res = await fetch(`${this.baseUrl}/${id}`, {
      method: "PATCH",
      body: JSON.stringify(data),
    });
    return res.json();
  }

  async delete(id: T["id"]): Promise<boolean> {
    const res = await fetch(`${this.baseUrl}/${id}`, { method: "DELETE" });
    return res.ok;
  }
}

// Usage
interface User extends Entity { id: number; name: string; email: string; }
const userRepo = new ApiRepository<User>("/api/users");
const users = await userRepo.findAll(); // type: User[]

泛型事件发射器

类型安全的事件发射器确保事件名称及其负载被正确类型化。不再有未知数据的字符串事件。

// Type-safe event emitter
type EventMap = Record<string, any>;
type EventCallback<T> = (payload: T) => void;

class TypedEmitter<Events extends EventMap> {
  private listeners: {
    [K in keyof Events]?: EventCallback<Events[K]>[];
  } = {};

  on<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(callback);
  }

  off<K extends keyof Events>(event: K, callback: EventCallback<Events[K]>): void {
    const cbs = this.listeners[event];
    if (cbs) {
      this.listeners[event] = cbs.filter(cb => cb !== callback);
    }
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    const cbs = this.listeners[event];
    if (cbs) {
      cbs.forEach(cb => cb(payload));
    }
  }
}

// Define your event types
interface AppEvents {
  "user:login": { userId: string; timestamp: number };
  "user:logout": { userId: string };
  "item:added": { itemId: string; quantity: number };
  "error": { message: string; code: number };
}

const emitter = new TypedEmitter<AppEvents>();

// Fully typed — payload type is inferred
emitter.on("user:login", (payload) => {
  console.log(payload.userId);    // type: string ✓
  console.log(payload.timestamp); // type: number ✓
});

// Error: "unknown:event" is not assignable to keyof AppEvents
// emitter.on("unknown:event", () => {});

常见错误

以下是开发者在使用泛型时最常犯的错误及其修正:

错误问题修正
使用 any 代替泛型丢失所有类型信息使用类型参数 T 代替 any
过度约束类型参数使泛型对有效类型不可用只约束你实际需要的部分
不必要的类型参数添加只使用一次的 T 增加了复杂性如果 T 只出现在返回类型中,直接使用具体类型
访问属性时不使用约束TypeScript 错误:类型 T 上不存在属性添加 extends 约束:T extends { prop: type }
混淆泛型默认值和约束默认类型不限制——调用者仍可传递其他类型用 extends 限制,用 = 设置默认值
没有在可能时进行推断在 TS 可以推断时显式传递类型尽可能让 TypeScript 从参数推断类型
使用 T[] 而不是 readonly T[]允许修改应为只读的数组对来自 API 的不可变数据使用 readonly T[]
// ❌ Mistake: Using any
function badParse(json: string): any {
  return JSON.parse(json);
}
const data = badParse('{"name":"Alice"}');
data.whatever; // No error — but might crash at runtime!

// ✓ Correction: Using generics with validation
function safeParse<T>(json: string, validator: (data: unknown) => data is T): T | null {
  try {
    const parsed = JSON.parse(json);
    return validator(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

// ❌ Mistake: Unnecessary type parameter
function badLength<T>(arr: T[]): number {
  return arr.length; // T is never meaningfully used
}

// ✓ Correction: Just use the concrete type
function goodLength(arr: unknown[]): number {
  return arr.length;
}

// ❌ Mistake: Missing constraint
function badGetName<T>(obj: T): string {
  return obj.name; // Error: Property 'name' does not exist on type 'T'
}

// ✓ Correction: Add constraint
function goodGetName<T extends { name: string }>(obj: T): string {
  return obj.name; // OK ✓
}

常见问题

泛型和 any 类型有什么区别?

泛型在整个函数或类中保留类型信息,而 any 会丢弃它。使用泛型,如果传入字符串,TypeScript 知道返回类型也是字符串。使用 any,所有类型信息都会丢失。

什么时候应该使用泛型而不是函数重载?

当逻辑对所有类型相同且你只需要保留类型关系时使用泛型。当实现因输入类型而异,或需要将特定输入类型映射到特定输出类型时使用重载。

在 TSX 文件中可以将泛型用于箭头函数吗?

可以,但需要添加尾随逗号以避免 JSX 歧义:const fn = <T,>(arg: T): T => arg。逗号告诉解析器这是泛型,不是 JSX 标签。

泛型应该有多少个类型参数?

越少越好。一到两个很常见。三个有时是必要的。如果需要三个以上,考虑重构——你的抽象可能过于复杂。每个类型参数都应有明确的用途。

泛型会影响运行时性能吗?

不会。泛型纯粹是编译时特性。它们在编译过程中被完全擦除,不会产生运行时开销。生成的 JavaScript 中没有泛型类型参数的痕迹。

类型参数的命名约定是什么?

单个大写字母是惯例:T 表示类型,K 表示键,V 表示值,E 表示元素,R 表示返回类型。对于有多个参数的复杂泛型,TData、TError 等描述性名称也可以接受,并能提高可读性。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

TSJSON to TypeScriptJSTypeScript to JavaScriptGTGraphQL to TypeScript

相关文章

JSON 转 TypeScript:完整指南与示例

学习如何自动将 JSON 数据转换为 TypeScript 接口。涵盖嵌套对象、数组、可选字段和最佳实践。

TypeScript vs JavaScript:何时以及如何转换

关于何时将 TypeScript 转换为 JavaScript 或反向转换的实用指南。涵盖迁移策略、工具链、包体积影响和团队考量。

JSON 转 Zod Schema:TypeScript 中的类型安全运行时验证

学习如何将 JSON 转换为 Zod schema,实现 TypeScript 中的类型安全运行时验证。涵盖基本类型、对象、数组、联合类型、z.infer 以及与 JSON Schema 的对比。

GraphQL 转 TypeScript:代码生成与类型安全开发指南

从 GraphQL schema 自动生成 TypeScript 类型。