关于 currying-inference
这项 TypeScript 技能介绍如何设计 API,帮助编译器推断泛型类型。它说明如何通过参数、柯里化、类和工厂函数增加类型推断机会。
主要功能
- 01创建类型推断点
- 02使用柯里化函数
- 03使用类和工厂函数
- 04改进泛型约束
适用场景
- 01设计可推断类型的构建器 API
- 02修复泛型参数被推断为 unknown
- 03减少显式类型参数和类型断言
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill currying-inference安装后在 Claude Code 中输入/currying-inference确认已加载。
详细介绍
使用类和柯里化创建新的类型推断位置
概述
当 TypeScript 无法推断泛型类型时,创建新的推断机会。
TypeScript 会在特定的“推断位置”推断泛型类型参数。如果某个位置提供的信息不足,可以通过类、柯里化或辅助函数创建其他推断位置。
何时使用此技能
- 无法推断泛型类型参数
- 构建器模式需要类型推断
- 链式方法会丢失类型信息
- 创建能够引导类型推断的 API
铁律
Create inference sites where TypeScript needs type information.
Classes and curried functions provide natural inference points.
请记住:
- 推断发生在函数调用和类实例化时
- 推断位置越多,类型推断效果越好
- 柯里化将推断拆分到多次调用中
- 类在构造时提供推断机会
检测:缺少推断
declare function fetchData<T>(url: string, options: RequestOptions): Promise<T>;
// TypeScript can't infer T
const data = await fetchData('/api/users', { method: 'GET' });
// ^? unknown
// Must specify explicitly
const data = await fetchData<User[]>('/api/users', { method: 'GET' });
类型参数 T 没有推断位置。
解决方案 1:通过参数添加推断位置
declare function fetchData<T>(
url: string,
parser: (raw: unknown) => T
): Promise<T>;
const data = await fetchData('/api/users', (raw) => raw as User[]);
// ^? User[]
parser 函数为 T 提供了推断位置。
解决方案 2:柯里化函数
// Single function: T not inferred
function makeRequest<T>(url: string): Promise<T>;
// Curried: inference at each call
function makeRequest<T>() {
return (url: string): Promise<T> => {
return fetch(url).then(r => r.json());
};
}
// Usage creates inference site
const getUsers = makeRequest<User[]>();
const users = await getUsers('/api/users');
解决方案 3:使用类实现构建器模式
class RequestBuilder<T = unknown> {
private url: string = '';
setUrl(url: string): this {
this.url = url;
return this;
}
// New method creates new inference site
withParser<U>(parser: (data: unknown) => U): RequestBuilder<U> {
return this as unknown as RequestBuilder<U>;
}
async execute(): Promise<T> {
const response = await fetch(this.url);
return response.json();
}
}
// Type inferred from parser
const users = await new RequestBuilder()
.setUrl('/api/users')
.withParser((data): User[] => data as User[])
.execute();
// ^? User[]
实用示例:事件发射器
// Without inference sites
class EventEmitter {
on<T>(event: string, handler: (data: T) => void): void;
emit<T>(event: string, data: T): void;
}
// TypeScript can't connect the T's
emitter.on('user', (data) => {
// ^? unknown
});
// With type map
interface EventMap {
user: User;
message: Message;
}
class TypedEventEmitter<Events extends Record<string, any>> {
on<K extends keyof Events>(
event: K,
handler: (data: Events[K]) => void
): void;
emit<K extends keyof Events>(
event: K,
data: Events[K]
): void;
}
const emitter = new TypedEventEmitter<EventMap>();
emitter.on('user', (data) => {
// ^? User
});
工厂函数
// Factory provides inference site
function createStore<T>(initial: T) {
let state = initial;
return {
get: () => state,
set: (newState: T) => { state = newState; }
};
}
const userStore = createStore({ name: 'Alice', age: 30 });
// T inferred from initial value
userStore.set({ name: 'Bob', age: 25 }); // OK
userStore.set({ name: 'Charlie' }); // Error: missing age
类型演进的链式方法
class QueryBuilder<T = unknown, Selected = T> {
select<K extends keyof T>(...keys: K[]): QueryBuilder<T, Pick<T, K>> {
return this as any;
}
where(predicate: (item: T) => boolean): QueryBuilder<T, Selected> {
return this as any;
}
execute(): Selected[] {
// ...
}
}
interface User { id: number; name: string; email: string; age: number; }
const results = new QueryBuilder<User>()
.select('name', 'email')
.where(u => u.age > 18)
.execute();
// ^? { name: string; email: string; }[]
使用泛型约束改善推断
// Without constraint
function pluck<T, K>(items: T[], key: K): T[K][];
// K not constrained, inference poor
// With constraint
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
const names = pluck(users, 'name');
// ^? string[]
约束 K extends keyof T 有助于 TypeScript 根据 T 推断 K。
避免类型参数只出现在返回类型中
// Bad: T only in return position (no inference)
function parseJson<T>(): T {
return JSON.parse(data);
}
// Good: T has inference site
function parseJson<T>(parser: (raw: unknown) => T): T {
return parser(JSON.parse(data));
}
// Good: Factory pattern
function createParser<T>() {
return {
parse: (data: string): T => JSON.parse(data)
};
}
const userParser = createParser<User>();
抗压应对方案
1. “直接使用类型断言”
压力:“我会用 as T 进行类型转换。”
**回应:**类型断言会绕过类型检查。更好的做法是为类型推断进行设计。
**行动:**通过参数或柯里化添加推断位置。
2. “显式指定类型参数就行”
压力:“用户只需写 fn<Type>(...)。”
**回应:**使用推断类型更省力,也更不容易出错。
**行动:**设计能自动进行类型推断的 API。
危险信号——停止并重新考虑
- 泛型函数的类型参数只出现在返回类型中
- 用户总是需要显式指定泛型参数
- 预期为特定类型的位置出现了
unknown或any - 需要使用类型断言才能获得正确类型
常见借口(均不成立)
| 借口 | 事实 |
|---|---|
| “用户可以指定类型” | 优秀的 API 设计会推断类型 |
| “这太复杂了” | 柯里化和工厂函数都很直观 |
| “类型断言能解决问题” | 它们会绕过安全保障;类型推断更好 |
快速参考
// BAD: No inference site for T
function fetch<T>(url: string): Promise<T>;
// GOOD: Parser provides inference site
function fetch<T>(url: string, parse: (raw: unknown) => T): Promise<T>;
// GOOD: Currying
function fetch<T>() {
return (url: string): Promise<T> => ...;
}
// GOOD: Factory
function createFetcher<T>(parse: (raw: unknown) => T) {
return (url: string): Promise<T> => ...;
}
归根结底
设计能够为 TypeScript 提供类型推断机会的 API。
无法推断泛型类型时,添加推断位置:使用该类型的参数、柯里化函数或类方法。优秀的 API 设计不需要显式指定类型参数。
参考
基于 Dan Vanderkam 所著的《Effective TypeScript》,第 28 条:使用类和柯里化创建新的类型推断位置。
评论
支持 Markdown