| name | currying-inference |
| description | Use when generic types aren't inferred. Use when builder patterns need better types. Use when creating new inference sites. |
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>;
const data = await fetchData('/api/users', { method: 'GET' });
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[]);
The parser function provides an inference site for T.
Solution 2: Curried Functions
function makeRequest<T>(url: string): Promise<T>;
function makeRequest<T>() {
return (url: string): Promise<T> => {
return fetch(url).then(r => r.json());
};
}
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;
}
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();
}
}
const users = await new RequestBuilder()
.setUrl('/api/users')
.withParser((data): User[] => data as User[])
.execute();
Practical Example: Event Emitter
class EventEmitter {
on<T>(event: string, handler: (data: T) => void): void;
emit<T>(event: string, data: T): void;
}
emitter.on('user', (data) => {
});
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) => {
});
Factory Functions
function createStore<T>(initial: T) {
let state = initial;
return {
get: () => state,
set: (newState: T) => { state = newState; }
};
}
const userStore = createStore({ name: 'Alice', age: 30 });
userStore.set({ name: 'Bob', age: 25 });
userStore.set({ name: 'Charlie' });
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();
Generic Constraints for Better Inference
function pluck<T, K>(items: T[], key: K): T[K][];
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
const names = pluck(users, 'name');
The constraint K extends keyof T helps TypeScript infer K from T.
Avoiding Type Parameters in Return Position Only
function parseJson<T>(): T {
return JSON.parse(data);
}
function parseJson<T>(parser: (raw: unknown) => T): T {
return parser(JSON.parse(data));
}
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
function fetch<T>(url: string): Promise<T>;
function fetch<T>(url: string, parse: (raw: unknown) => T): Promise<T>;
function fetch<T>() {
return (url: string): Promise<T> => ...;
}
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.