mcpField
C
currying-inference

currying-inference

github.com/marius-townhouse开发644 次安装38 周前更新
代码仓库

关于 currying-inference

这项 TypeScript 技能介绍如何设计 API,帮助编译器推断泛型类型。它说明如何通过参数、柯里化、类和工厂函数增加类型推断机会。

主要功能

  1. 01创建类型推断点
  2. 02使用柯里化函数
  3. 03使用类和工厂函数
  4. 04改进泛型约束

适用场景

  1. 01设计可推断类型的构建器 API
  2. 02修复泛型参数被推断为 unknown
  3. 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