跳到主要内容

泛型

问题

什么是 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: 泛型的作用是什么?

答案

泛型的主要作用:

  1. 代码复用:一套代码支持多种类型
  2. 类型安全:编译时检查类型错误
  3. 保留类型信息:避免使用 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
OmitPick + 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)
  • 两者结合可以构建高度类型安全且可复用的组件库

相关链接