TypeScript 泛型入门(新手友好、完整详解)

目标读者:刚学 TS 的前端开发者,或希望把泛型用到实际工程(请求封装、组件复用)中的同学。


目录

  1. 为什么需要泛型(直观动机)
  2. 基本语法与例子(函数、接口、类)
  3. 泛型约束(extendskeyof
  4. 进阶语法:默认类型、多个类型参数、泛型推断
  5. 实战一:request<T> 网络请求封装(详细讲解)
  6. 实战二:React 通用下拉组件 <Select<T>>(含使用示例)
  7. 常见坑、调试技巧与最佳实践
  8. 练习题与参考资料

1. 为什么需要泛型(直观动机)

在没有泛型的世界里,如果你写一个工具函数或组件只能处理单一类型,就会出现大量重复代码或丧失类型提示。

举例:写一个返回第一个元素的 first 函数,如果不使用泛型,你可能写成 any,失去类型安全:

ts 复制代码
function firstBad(arr: any[]) { return arr[0]; }
const a = firstBad([1,2,3]); // a 的类型是 any,编辑器不会提示

使用泛型后:

ts 复制代码
function first<T>(arr: T[]): T | undefined { return arr[0]; }
const a = first([1,2,3]); // a 被推断为 number | undefined

泛型能让工具/组件"对所有类型通用",同时保留类型信息,这就是它的价值。


2. 基本语法与例子

2.1 泛型函数

ts 复制代码
// 最基础的泛型函数:identity
function identity<T>(arg: T): T {
  return arg;
}

const s = identity('hello'); // T 被推断为 string
const n = identity<number>(123); // 显示指定泛型

注意 :一般情况下不必显式写 <T>,TypeScript 会根据参数自动推断。

2.2 泛型类型别名 / 接口

ts 复制代码
type Box<T> = { value: T };
const b: Box<number> = { value: 42 };

interface ApiResponse<T> {
  code: number;
  data: T;
}

const r: ApiResponse<string[]> = { code: 0, data: ['a','b'] };

2.3 泛型类

ts 复制代码
class Stack<T> {
  private items: T[] = [];
  push(item: T) { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}

const s = new Stack<number>();
s.push(1);

2.4 多个类型参数

ts 复制代码
function mapArray<T, U>(arr: T[], fn: (t: T) => U): U[] {
  return arr.map(fn);
}

const r = mapArray([1,2,3], x => x.toString()); // r: string[]

3. 泛型约束(extendskeyof

有时候我们要限制泛型的"范围",比如只允许对象类型、必须包含某些属性等。

3.1 extends 限制

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

const user = { id: '1', name: 'Alice' };
pluck(user, 'name'); // OK
// pluck(user, 'notExist'); // Error

解释:K extends keyof T 表示 K 必须是 T 的键之一,防止传入不存在的属性名。

3.2 keyof 的常见用法

ts 复制代码
type KeysOfUser = keyof typeof user; // 'id' | 'name'

4. 进阶语法(默认类型、泛型推断等)

4.1 默认类型

ts 复制代码
function identityDefault<T = string>(arg: T): T { return arg; }
const a = identityDefault('x'); // T 推断为 string

4.2 泛型推断

TypeScript 会根据函数参数自动推断泛型类型,像 identity([1,2,3]) 会推断 Tnumber[] 的元素类型(... 具体依赖签名)。


5. 实战一:封装 request<T>(网络请求)

目的:写一个简单且实用的 request,在调用处能用泛型指定返回类型,从而获得完整的类型提示。

5.1 需求与设计

  • 希望 request<T>(url) 返回 Promise<T>
  • 在大多数场景后端返回的是一个包裹结构,比如 { code: number, data: T },我们也要支持。
  • 稍微封装错误处理与超时(示例化,不追求复杂性)。

5.2 代码实现(utils/request.ts

ts 复制代码
// utils/request.ts
export type ApiResponse<T> = { code: number; data: T; message?: string };

export async function request<T = any>(url: string, init?: RequestInit): Promise<T> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 10_000);

  try {
    const res = await fetch(url, { signal: controller.signal, ...init });
    if (!res.ok) throw new Error(res.statusText);
    const data = await res.json();
    return data as T; // 注意:这是类型断言,运行时不会做检查
  } finally {
    clearTimeout(timeout);
  }
}

5.3 使用示例

ts 复制代码
// types.ts
type User = { id: string; name: string };

// 使用(直接返回数组)
const users = await request<User[]>('/api/users');
users[0].name; // 编辑器会提示 name

// 使用(后端返回包裹结构)
const resp = await request<ApiResponse<User[]>>('/api/users-pkg');
const list = resp.data; // 正常使用

5.4 提醒:类型安全与运行时验证

TypeScript 的类型只存在编译阶段。request<T> 中的 return data as T 是"信任后端返回的结构"。如果需要更严格的保证,请在运行时做校验(使用 zodio-ts 等)。


6. 实战二:React 通用下拉组件 <Select<T>>(简单到常用)

目标:实现一个对数据类型"透明"的下拉组件,使用泛型后,父组件拿到 onChange 的回调类型时能直接获得具体类型提示。

6.1 需求与设计

  • 组件接收 options: T[]
  • 需要 getLabel?: (item: T) => string,用于渲染文本。
  • 需要 keyExtractor?: (item: T, idx: number) => string | number,用于 keyvalue(避免假设数据有 id 字段)。
  • onChange?: (item: T | null) => void

6.2 组件代码(简洁、可用)

tsx 复制代码
import React from 'react';

export interface SelectProps<T> {
  options: T[];
  value?: T | null;
  onChange?: (item: T | null) => void;
  placeholder?: string;
  getLabel?: (item: T) => string;
  keyExtractor?: (item: T, idx: number) => string | number;
}

// 注意箭头函数组件写法:const Select = <T,>(props: SelectProps<T>) => { ... }
export const Select = <T,>({ options, value, onChange, placeholder, getLabel, keyExtractor }: SelectProps<T>) => {
  const labelOf = getLabel ?? ((it: T) => String((it as any)));
  const keyOf = keyExtractor ?? ((_: T, idx: number) => idx);

  return (
    <select
      value={options.indexOf(value as T)}
      onChange={(e) => {
        const idx = Number(e.target.value);
        onChange?.(idx >= 0 ? options[idx] : null);
      }}
    >
      <option value={-1}>{placeholder ?? '请选择'}</option>
      {options.map((it, i) => (
        <option key={String(keyOf(it, i))} value={i}>{labelOf(it)}</option>
      ))}
    </select>
  );
};

说明

  • const Select = <T,>(...) 中的 ,(逗号)是一个常用写法,用来避免 TSX 将 <T> 误解析为 JSX;这是声明泛型函数表达式/箭头函数时的语法技巧。
  • 为了让组件与任意数据结构配合,我们没有假定 itemidlabel 字段,而是通过 keyExtractorgetLabel 注入策略。

6.3 使用示例

tsx 复制代码
// App.tsx
import React, { useState, useEffect } from 'react';
import { Select } from './Select';
import { request } from './utils/request';

type User = { id: string; name: string };

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const [sel, setSel] = useState<User | null>(null);

  useEffect(() => {
    request<User[]>('/api/users').then(setUsers).catch(console.error);
  }, []);

  return (
    <div>
      <Select
        options={users}
        value={sel}
        onChange={(u) => setSel(u)}
        getLabel={(u) => u.name}
        keyExtractor={(u) => u.id}
        placeholder="选择用户"
      />

      <div>当前选中:{sel ? sel.name : '无'}</div>
    </div>
  );
}

类型体验 :当你写 onChange={(u) => setSel(u)} 时,编辑器会推断 u 的类型为 User | null,这给你编辑器级别的保护与提示。

6.4 关于显式泛型(什么时候必须)

通常只要 options 的类型是具体的数组(User[]),TS 能推断出 T,使用时不需要写 <Select<User> />。 如果推断失败(例如 options 类型被擦除为 any[]),你可以:

  • 在数据源处把类型写清楚(推荐);
  • 或在组件使用处做类型断言:<Select options={someAny as User[]} ... />

7. 常见坑、调试技巧与最佳实践

  • 不要滥用 any :泛型的一个目标就是替代 any,保留类型信息。
  • 理解类型与运行时的边界:泛型只是编译期工具,运行时没有类型检查。
  • 在库/公共代码中多写泛型,在应用层用具体类型;库需要更强的泛型设计能力。
  • 避免过度复杂的类型:当类型系统变得难以理解时,权衡是否用运行时校验来代替复杂类型。
  • 在 React 中尽量依赖类型推断 ,不要在 JSX 里频繁显式写 <Component<Type> />(有时会引起解析问题)。

8. 练习题(自测)

  1. 写一个泛型 filterMap<T, U>,它的签名为 (arr: T[], fn: (t: T) => U | null) => U[]
  2. 基于 request<T>,写一个 getJson<T>(url),当后端返回 { code, data } 结构时,自动返回 data
  3. 修改 Select 组件,使它支持 multiple(多选)并确保类型安全。

9. 总结与下一步学习建议

  • 泛型让你的代码既通用类型安全,是编写可复用工具与组件的核心。
  • 推荐掌握:泛型约束(extends)、keyof、条件类型(下一步,可学 infer)、以及常见内置工具类型(Partial/Readonly/Record)。

相关推荐
已读不回1434 小时前
TypeScript 内置工具类型大全(ReturnType、Omit、Pick 等)
typescript
已读不回1434 小时前
实现 TypeScript 内置工具类型(源码解析与实现)
typescript
YaeZed5 小时前
TypeScript6(class类)
前端·typescript
漂流瓶jz14 小时前
解锁Babel核心功能:从转义语法到插件开发
前端·javascript·typescript
一支鱼17 小时前
leetcode-6-正则表达式匹配
算法·leetcode·typescript
拜无忧21 小时前
【教程】vue+vite+ts创建一个最新的高性能后台项目架构
vue.js·typescript·vite
GDAL1 天前
解决 ES 模块与 CommonJS 模块互操作性的关键开关esModuleInterop
typescript
Sui_Network2 天前
Yotta Labs 选择 Walrus 作为去中心化 AI 存储与工作流管理的专用数据层
大数据·javascript·人工智能·typescript·去中心化·区块链
一支鱼2 天前
leetcode-5-最长回文子串
算法·leetcode·typescript