currying-inference

📁 marius-townhouse/effective-typescript-skills 📅 10 days ago
1
总安装量
1
周安装量
#47850
全站排名
安装命令
npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill currying-inference

Agent 安装分布

mcpjam 1
openhands 1
replit 1
windsurf 1
zencoder 1

Skill 文档

Use Classes and Currying to Create New Inference Sites

Overview

When TypeScript can’t infer generic types, create new inference opportunities.

TypeScript infers generic type parameters at specific “inference sites.” When it doesn’t have enough information at one site, you can create additional sites using classes, currying, or helper functions.

When to Use This Skill

  • Generic type parameters aren’t being inferred
  • Builder pattern needs type inference
  • Chained methods lose type information
  • Creating APIs that guide type inference

The Iron Rule

Create inference sites where TypeScript needs type information.
Classes and curried functions provide natural inference points.

Remember:

  • Inference happens at function calls and class instantiation
  • More inference sites = better type inference
  • Currying splits inference across multiple calls
  • Classes provide inference at construction

Detection: Missing Inference

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' });

The type parameter T has no inference site.

Solution 1: Add Inference Site with Parameter

declare function fetchData<T>(
  url: string,
  parser: (raw: unknown) => T
): Promise<T>;

const data = await fetchData('/api/users', (raw) => raw as User[]);
// ^? User[]

The parser function provides an inference site for T.

Solution 2: Curried Functions

// 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');

Solution 3: Builder Pattern with Classes

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[]

Practical Example: Event Emitter

// 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 Functions

// 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

Method Chaining with Type Evolution

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; }[]

Generic Constraints for Better Inference

// 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[]

The constraint K extends keyof T helps TypeScript infer K from T.

Avoiding Type Parameters in Return Position Only

// 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>();

Pressure Resistance Protocol

1. “Just Use Type Assertions”

Pressure: “I’ll cast with as T

Response: Assertions bypass type checking. Better to design for inference.

Action: Add inference sites through parameters or currying.

2. “Explicit Type Parameters Work”

Pressure: “Users can just write fn<Type>(...)

Response: Inferred types are less work and less error-prone.

Action: Design APIs where inference works automatically.

Red Flags – STOP and Reconsider

  • Generic function with type parameter only in return type
  • Users always need to specify generic parameters explicitly
  • unknown or any appearing where specific types are expected
  • Type assertions needed to get correct types

Common Rationalizations (All Invalid)

Excuse Reality
“Users can specify the type” Good API design infers types
“It’s too complex” Currying and factories are straightforward
“Type assertions work” They bypass safety; inference is better

Quick Reference

// 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> => ...;
}

The Bottom Line

Design APIs that give TypeScript inference opportunities.

When generic types can’t be inferred, add inference sites: parameters that use the type, curried functions, or class methods. Good API design makes explicit type parameters unnecessary.

Reference

Based on “Effective TypeScript” by Dan Vanderkam, Item 28: Use Classes and Currying to Create New Inference Sites.