泛型
问题
什么是 TypeScript 泛型?如何使用泛型实现类型安全的可复用代码?
答案
泛型(Generics)是 TypeScript 最强大的特性之一,允许我们编写可重用、类型安全的代码。泛型就像是类型的"参数",让类型可以像变量一样传递。
为什么需要泛型
不使用泛型的问题:
// 问题 1:失去类型信息
function identity(value: any): any {
return value;
}
const result = identity('hello'); // result 类型是 any,失去了类型提示
// 问题 2:为每种类型写重复代码
function identityString(value: string): string {
return value;
}
function identityNumber(value: number): number {
return value;
}
// 需要无限多个函数...
使用泛型解决:
// 泛型函数:T 是类型参数
function identity<T>(value: T): T {
return value;
}
// 调用时自动推断类型
const str = identity('hello'); // string
const num = identity(42); // number
const arr = identity([1, 2, 3]); // number[]
// 也可以显式指定类型
const explicit = identity<string>('hello');
泛型函数
基本语法
// 单个类型参数
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// 多个类型参数
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
// 箭头函数泛型
const last = <T>(arr: T[]): T | undefined => arr[arr.length - 1];
// 注意:在 JSX 中箭头函数泛型需要加逗号或 extends
const lastJSX = <T,>(arr: T[]): T | undefined => arr[arr.length - 1];
const lastJSX2 = <T extends unknown>(arr: T[]): T | undefined => arr[arr.length - 1];
泛型约束(extends)
// 约束 T 必须有 length 属性
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): T {
console.log(value.length);
return value;
}
logLength('hello'); // OK,string 有 length
logLength([1, 2, 3]); // OK,array 有 length
logLength({ length: 10, value: 'test' }); // OK
// logLength(123); // 错误:number 没有 length
// 约束 T 必须是某类型的子类型
function clone<T extends object>(obj: T): T {
return { ...obj };
}
// 约束 K 必须是 T 的键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Alice', age: 25 };
getProperty(user, 'name'); // string
getProperty(user, 'age'); // number
// getProperty(user, 'email'); // 错误:'email' 不是 user 的键
默认类型参数
// 默认类型
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const strings = createArray(3, 'a'); // string[]
const numbers = createArray<number>(3, 0); // number[]
// 多个类型参数带默认值
interface ApiResponse<T = any, E = Error> {
data: T;
error: E | null;
}
泛型接口
// 泛型接口
interface Box<T> {
value: T;
getValue(): T;
setValue(value: T): void;
}
// 实现泛型接口
class NumberBox implements Box<number> {
constructor(public value: number) {}
getValue(): number {
return this.value;
}
setValue(value: number): void {
this.value = value;
}
}
// 泛型函数接口
interface SearchFunc<T> {
(items: T[], predicate: (item: T) => boolean): T | undefined;
}
const findNumber: SearchFunc<number> = (items, predicate) => {
return items.find(predicate);
};
泛型类
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];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
// 使用
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const top = numberStack.pop(); // number | undefined
const stringStack = new Stack<string>();
stringStack.push('hello');
// 泛型类继承
class LoggingStack<T> extends Stack<T> {
push(item: T): void {
console.log('Pushing:', item);
super.push(item);
}
}
泛型类型别名
// 泛型类型别名
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
// 复杂泛型类型
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return { success: false, error: '除数不能为零' };
}
return { success: true, data: a / b };
}
// 泛型工具类型
type ReadonlyArray2<T> = readonly T[];
type PromiseValue<T> = T extends Promise<infer U> ? U : T;
泛型与映射类型
// 映射所有属性为可选
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 映射所有属性为必选
type Required<T> = {
[P in keyof T]-?: T[P];
};
// 映射所有属性为只读
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 实际使用
interface User {
name: string;
age: number;
email?: string;
}
type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string; }
type RequiredUser = Required<User>;
// { name: string; age: number; email: string; }
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email?: string; }
泛型与条件类型
// 基本条件类型
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// 提取类型
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type C = ArrayElement<string[]>; // string
type D = ArrayElement<number[]>; // number
// 函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet(): string {
return 'hello';
}
type GreetReturn = ReturnType<typeof greet>; // string
// 排除类型
type Exclude<T, U> = T extends U ? never : T;
type E = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
实用泛型模式
1. 创建类型安全的 API 响应
interface ApiResponse<T> {
code: number;
message: string;
data: T;
timestamp: number;
}
interface User {
id: number;
name: string;
}
interface Post {
id: number;
title: string;
content: string;
}
// 类型安全的 API 调用
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
return response.json();
}
// 使用时指定返回类型
const userResponse = await fetchApi<User>('/api/user/1');
console.log(userResponse.data.name); // 有类型提示
const postsResponse = await fetchApi<Post[]>('/api/posts');
console.log(postsResponse.data[0].title); // 有类型提示
2. 类型安全的事件发射器
type EventMap = {
click: { x: number; y: number };
keypress: { key: string; code: number };
load: undefined;
};
class TypedEventEmitter<T extends Record<string, any>> {
private listeners: {
[K in keyof T]?: Array<(data: T[K]) => void>;
} = {};
on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
emit<K extends keyof T>(event: K, data: T[K]): void {
this.listeners[event]?.forEach((cb) => cb(data));
}
}
const emitter = new TypedEventEmitter<EventMap>();
// 类型安全的事件监听
emitter.on('click', (data) => {
console.log(data.x, data.y); // 有类型提示
});
emitter.on('keypress', (data) => {
console.log(data.key, data.code);
});
// 类型安全的事件触发
emitter.emit('click', { x: 100, y: 200 });
// emitter.emit('click', { x: 100 }); // 错误:缺少 y
3. Builder 模式
class QueryBuilder<T extends Record<string, any> = {}> {
private query: Partial<T> = {};
where<K extends string, V>(
key: K,
value: V
): QueryBuilder<T & Record<K, V>> {
(this.query as any)[key] = value;
return this as any;
}
build(): T {
return this.query as T;
}
}
const query = new QueryBuilder()
.where('name', 'Alice')
.where('age', 25)
.where('active', true)
.build();
// query 类型: { name: string; age: number; active: boolean }
4. 类型安全的 Redux Action
// Action 类型定义
type Action<T extends string, P = void> = P extends void
? { type: T }
: { type: T; payload: P };
// Action 创建器
function createAction<T extends string>(type: T): () => Action<T>;
function createAction<T extends string, P>(
type: T
): (payload: P) => Action<T, P>;
function createAction<T extends string, P>(type: T) {
return (payload?: P) =>
payload === undefined ? { type } : { type, payload };
}
// 使用
const increment = createAction<'INCREMENT'>('INCREMENT');
const setCount = createAction<'SET_COUNT', number>('SET_COUNT');
const action1 = increment(); // { type: 'INCREMENT' }
const action2 = setCount(10); // { type: 'SET_COUNT', payload: number }
常见面试问题
Q1: 泛型的作用是什么?
答案:
泛型的主要作用:
- 代码复用:一套代码支持多种类型
- 类型安全:编译时检查类型错误
- 保留类型信息:避免使用 any 丢失类型
// 没有泛型:要么失去类型,要么重复代码
function getFirstAny(arr: any[]): any {
return arr[0];
}
function getFirstString(arr: string[]): string {
return arr[0];
}
// 有泛型:一个函数,保持类型安全
function getFirst<T>(arr: T[]): T {
return arr[0];
}
const str = getFirst(['a', 'b']); // string
const num = getFirst([1, 2]); // number
Q2: keyof 和 typeof 在泛型中的应用?
答案:
// keyof - 获取对象的键类型
interface Person {
name: string;
age: number;
}
type PersonKeys = keyof Person; // 'name' | 'age'
// 结合泛型使用
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person: Person = { name: 'Alice', age: 25 };
const name = getValue(person, 'name'); // string
const age = getValue(person, 'age'); // number
// typeof - 获取值的类型
const config = {
api: 'https://api.example.com',
timeout: 5000
};
type Config = typeof config;
// { api: string; timeout: number }
// 结合使用
function getConfigValue<K extends keyof typeof config>(
key: K
): typeof config[K] {
return config[key];
}
Q3: 如何约束泛型为特定类型?
答案:
使用 extends 关键字约束泛型:
// 约束为特定接口
interface HasId {
id: number;
}
function updateEntity<T extends HasId>(entity: T): T {
entity.id = entity.id + 1;
return entity;
}
// 约束为对象类型
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
// 约束为函数
function call<T extends (...args: any[]) => any>(
fn: T,
...args: Parameters<T>
): ReturnType<T> {
return fn(...args);
}
// 约束为类构造函数
function create<T>(Constructor: new () => T): T {
return new Constructor();
}
Q4: infer 关键字的作用?
答案:
infer 用于在条件类型中推断类型:
// 推断数组元素类型
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type A = ArrayElement<string[]>; // string
// 推断函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// 推断 Promise 的解析类型
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type B = Awaited<Promise<Promise<number>>>; // number
// 推断函数参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number): string {
return `Hello, ${name}(${age})`;
}
type GreetParams = Parameters<typeof greet>; // [string, number]
// 推断对象值类型
type ValueOf<T> = T extends { [K: string]: infer V } ? V : never;
type C = ValueOf<{ a: number; b: string }>; // number | string
Q5: 泛型默认值和可选泛型参数?
答案:
// 泛型默认值
interface Response<T = any> {
data: T;
status: number;
}
const res1: Response = { data: 'hello', status: 200 }; // T = any
const res2: Response<number> = { data: 42, status: 200 }; // T = number
// 多个泛型参数带默认值
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
// 必须按顺序提供,或者全部提供
type R1 = Result<string>; // E = Error
type R2 = Result<string, string>; // E = string
// 条件默认值
type DefaultIfNever<T, D> = [T] extends [never] ? D : T;
type X = DefaultIfNever<never, string>; // string
type Y = DefaultIfNever<number, string>; // number
Q6: 如何用泛型约束实现类型安全的事件系统?
答案:
核心思路是用一个事件映射类型(EventMap)作为泛型约束,让事件名和回调参数建立严格的对应关系,做到:发错事件名会报错,回调参数自动推断。
typed-event-emitter.ts
// 1. 定义事件映射:事件名 -> 回调参数类型
interface AppEvents {
login: { userId: string; timestamp: number };
logout: { userId: string };
'page:view': { path: string; referrer?: string };
error: { code: number; message: string };
}
// 2. 泛型事件发射器
class TypedEventEmitter<TEvents extends Record<string, unknown>> {
private listeners = new Map<keyof TEvents, Set<Function>>();
// K 被约束为 TEvents 的键,callback 的参数类型自动推断
on<K extends keyof TEvents>(
event: K,
callback: (data: TEvents[K]) => void
): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
// 返回取消订阅函数
return () => {
this.listeners.get(event)?.delete(callback);
};
}
emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
this.listeners.get(event)?.forEach((cb) => cb(data));
}
// once: 只监听一次
once<K extends keyof TEvents>(
event: K,
callback: (data: TEvents[K]) => void
): void {
const unsubscribe = this.on(event, (data) => {
callback(data);
unsubscribe();
});
}
}
// 3. 使用
const emitter = new TypedEventEmitter<AppEvents>();
// ✅ 类型安全:参数自动推断
emitter.on('login', (data) => {
console.log(data.userId); // string - 有提示
console.log(data.timestamp); // number - 有提示
});
// ✅ 发送事件也是类型安全的
emitter.emit('login', { userId: '123', timestamp: Date.now() });
// ❌ 编译错误:缺少 timestamp
// emitter.emit('login', { userId: '123' });
// ❌ 编译错误:事件名不存在
// emitter.on('unknown', () => {});
进阶:支持无参数事件和可选参数事件:
// 用 void 表示无参数事件
interface GameEvents {
start: void;
score: { points: number; player: string };
end: { winner: string } | void; // 可选参数
}
class AdvancedEmitter<TEvents extends Record<string, unknown>> {
private listeners = new Map<keyof TEvents, Set<Function>>();
// 条件类型:如果事件参数是 void,emit 不需要传 data
emit<K extends keyof TEvents>(
...args: TEvents[K] extends void
? [event: K]
: [event: K, data: TEvents[K]]
): void {
const [event, data] = args;
this.listeners.get(event)?.forEach((cb) => cb(data));
}
on<K extends keyof TEvents>(
event: K,
callback: (data: TEvents[K]) => void
): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
}
}
const game = new AdvancedEmitter<GameEvents>();
game.emit('start'); // ✅ 无需参数
game.emit('score', { points: 100, player: 'Alice' }); // ✅ 需要参数
// game.emit('start', {}); // ❌ start 不接受参数
// game.emit('score'); // ❌ score 需要参数
Q7: 泛型工具类型是怎么实现的?手写 Partial/Required/Pick
答案:
TypeScript 内置的工具类型本质上都是通过泛型和映射类型实现的。理解它们的实现原理,对掌握 TypeScript 类型编程至关重要。
1. Partial<T> - 所有属性变为可选
// 实现原理:遍历 T 的所有键,给每个属性加上 ?
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// 验证
interface User {
name: string;
age: number;
email: string;
}
type PartialUser = MyPartial<User>;
// { name?: string; age?: number; email?: string }
2. Required<T> - 所有属性变为必选
// 实现原理:-? 去掉可选修饰符
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
interface Config {
url: string;
timeout?: number;
retries?: number;
}
type RequiredConfig = MyRequired<Config>;
// { url: string; timeout: number; retries: number }
3. Readonly<T> - 所有属性变为只读
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
4. Pick<T, K> - 从 T 中挑选指定属性
// K 被约束为 T 的键的子集
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
type UserBasic = MyPick<User, 'name' | 'email'>;
// { name: string; email: string }
5. Omit<T, K> - 从 T 中排除指定属性
// 先用 Exclude 过滤掉不要的键,再用 Pick 挑选剩余的键
type MyOmit<T, K extends keyof any> = MyPick<T, Exclude<keyof T, K>>;
// Exclude 的实现:分发条件类型
type MyExclude<T, U> = T extends U ? never : T;
type UserWithoutEmail = MyOmit<User, 'email'>;
// { name: string; age: number }
6. Record<K, V> - 构造键值对类型
type MyRecord<K extends keyof any, V> = {
[P in K]: V;
};
type UserRoles = MyRecord<'admin' | 'user' | 'guest', string[]>;
// { admin: string[]; user: string[]; guest: string[] }
实现原理总结
| 工具类型 | 核心技术 | 关键语法 |
|---|---|---|
Partial | 映射类型 | ? 添加可选 |
Required | 映射类型 | -? 去除可选 |
Readonly | 映射类型 | readonly 修饰符 |
Pick | 映射类型 + 约束 | K extends keyof T |
Omit | Pick + Exclude | 条件类型 + 映射类型 |
Record | 映射类型 | K extends keyof any |
Q8: 泛型在 React 组件中的应用(泛型组件、泛型 Hook)
答案:
泛型在 React 中非常常用,主要体现在泛型组件和泛型 Hook 两个场景,让组件和 Hook 在保持类型安全的同时具备高度复用性。
1. 泛型组件:通用列表组件
GenericList.tsx
import { ReactNode } from 'react';
// 泛型 Props:T 代表列表项的类型
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => ReactNode;
keyExtractor: (item: T) => string;
onItemClick?: (item: T) => void;
}
function GenericList<T>(props: ListProps<T>): ReactNode {
const { items, renderItem, keyExtractor, onItemClick } = props;
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)} onClick={() => onItemClick?.(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// 使用:类型自动推断
interface User {
id: string;
name: string;
age: number;
}
function App() {
const users: User[] = [
{ id: '1', name: 'Alice', age: 25 },
{ id: '2', name: 'Bob', age: 30 },
];
return (
<GenericList
items={users}
keyExtractor={(user) => user.id} // user 自动推断为 User
renderItem={(user) => <span>{user.name}</span>}
onItemClick={(user) => console.log(user.age)} // user.age 有提示
/>
);
}
2. 泛型 Select 组件
GenericSelect.tsx
interface SelectProps<T> {
options: T[];
value: T | null;
onChange: (value: T) => void;
getLabel: (option: T) => string;
getValue: (option: T) => string;
}
function Select<T>(props: SelectProps<T>): ReactNode {
const { options, value, onChange, getLabel, getValue } = props;
return (
<select
value={value ? getValue(value) : ''}
onChange={(e) => {
const selected = options.find(
(opt) => getValue(opt) === e.target.value
);
if (selected) onChange(selected);
}}
>
{options.map((option) => (
<option key={getValue(option)} value={getValue(option)}>
{getLabel(option)}
</option>
))}
</select>
);
}
// 使用
interface Country {
code: string;
name: string;
population: number;
}
const countries: Country[] = [
{ code: 'CN', name: '中国', population: 1400000000 },
{ code: 'US', name: '美国', population: 330000000 },
];
<Select
options={countries}
value={null}
onChange={(country) => console.log(country.population)} // 自动推断
getLabel={(c) => c.name}
getValue={(c) => c.code}
/>;
3. 泛型 Hook:useLocalStorage
useLocalStorage.ts
import { useState, useEffect } from 'react';
function useLocalStorage<T>(key: string, initialValue: T) {
// 惰性初始化:从 localStorage 读取
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
// 值变化时同步到 localStorage
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error('Failed to save to localStorage:', error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue] as const;
}
// 使用:类型自动推断
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
// theme: 'light' | 'dark'
// setTheme: Dispatch<SetStateAction<'light' | 'dark'>>
const [user, setUser] = useLocalStorage('user', { name: '', age: 0 });
// user: { name: string; age: number } - 从初始值推断
4. 泛型 Hook:useFetch
useFetch.ts
import { useState, useEffect } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
const controller = new AbortController();
async function fetchData(): Promise<void> {
setState((prev) => ({ ...prev, loading: true, error: null }));
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = (await response.json()) as T;
setState({ data, loading: false, error: null });
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') return;
setState({ data: null, loading: false, error: error as Error });
}
}
fetchData();
return () => controller.abort();
}, [url]);
return state;
}
// 使用:显式指定泛型
interface Post {
id: number;
title: string;
body: string;
}
function PostList() {
const { data: posts, loading, error } = useFetch<Post[]>('/api/posts');
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
return (
<ul>
{posts?.map((post) => (
// post 自动推断为 Post,有完整的类型提示
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
泛型组件 vs 泛型 Hook
- 泛型组件:让同一个 UI 组件适配不同数据类型(List、Select、Table)
- 泛型 Hook:让同一个逻辑 Hook 适配不同数据类型(useFetch、useLocalStorage、useForm)
- 两者结合可以构建高度类型安全且可复用的组件库