当前位置: 首页 > news >正文

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

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


目录

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

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

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

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

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

使用泛型后:

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

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


2. 基本语法与例子

2.1 泛型函数

// 最基础的泛型函数:identity
function identity<T>(arg: T): T {return arg;
}const s = identity('hello'); // T 被推断为 string
const n = identity<number>(123); // 显示指定泛型

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

2.2 泛型类型别名 / 接口

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 泛型类

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 多个类型参数

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 限制

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 的常见用法

type KeysOfUser = keyof typeof user; // 'id' | 'name'

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

4.1 默认类型

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

// 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 使用示例

// 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 组件代码(简洁、可用)

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 (<selectvalue={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 使用示例

// 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><Selectoptions={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)。

http://www.xdnf.cn/news/1444825.html

相关文章:

  • XSENS VISION NAVIGATOR助力智能城市自动化清洁机器人精确导航
  • TLSF内存算法适配HTOS
  • 【Unity UGUI Canvas(画布)(1)】
  • 【音视频】FMP4 介绍
  • 【正点原子K210连载】第三十一章 音频FFT实验 摘自【正点原子】DNK210使用指南-CanMV版指南
  • 【论文阅读】-《THE JPEG STILL PICTURE COMPRESSION STANDARD》
  • Android 接入deepseek
  • 关于ES中文分词器analysis-ik快速安装
  • k8s使用StatefulSet(有状态)部署单节点 MySQL方案(使用本地存储)
  • 【Bug】Nexus无法正常启动的五种解决方法
  • SuperMap GIS基础产品FAQ集锦(20250901)
  • Elasticsearch 数字字段随机取多值查询缓慢-原理分析与优化方案
  • 504 Gateway Timeout:服务器作为网关或代理时未能及时获得响应如何处理?
  • 揭秘设计模式:优雅地为复杂对象结构增添新功能-访问者模式
  • go语言面试之Goroutine详解
  • Linux使用-Linux系统管理
  • WPF里的几何图形Path绘制
  • 硬件驱动C51单片机——裸机(1)
  • 三、Scala方法与函数
  • 【面试场景题】1GB 大小HashMap在put时遇到扩容的过程
  • 安卓系统中IApplicationThread.aidl对应的是哪个类
  • 智慧交通管理信号灯通信4G工业路由器应用
  • 【小白笔记】移动硬盘为什么总比电脑更容易满?
  • 【LeetCode热题100道笔记】括号生成
  • 系统架构设计师备考第14天——业务处理系统(TPS)
  • WebAppClassLoader(Tomcat)和 LaunchedURLClassLoader(Spring Boot)类加载器详解
  • Llama v3 中的低秩自适应 (LoRA)
  • 51单片机-LED与数码管模块
  • 2024 arXiv Cost-Efficient Prompt Engineering for Unsupervised Entity Resolution
  • JetBrains 2025 全家桶 11合1 Windows直装(含 IDEA PyCharm、WebStorm、DataSpell、DataGrip等)