TypeScript NoInfer: Controlling Generic Inference
NoInfer blocks type inference from specific positions. Use it when secondary parameters should match the inferred type, not influence it.
TypeScript infers generic type parameters from all positions where they appear. Usually
that's helpful. But sometimes you want a parameter to be inferred from some
arguments but not others. TypeScript 5.4's NoInfer<T> utility type
blocks inference from specific positions, giving you precise control.
The Problem: Unwanted Inference Sites
function createState<T>(initial: T, defaultValue: T): T {
return initial ?? defaultValue;
}
// TypeScript infers T from BOTH arguments
const state = createState("hello", 42);
// T is inferred as: string | number
// We probably wanted T to be just string!
TypeScript considers both initial and defaultValue when
inferring T. If they differ, you get a union. Often you want T
inferred only from the primary argument, with the fallback constrained to match.
NoInfer to the Rescue
function createState<T>(initial: T, defaultValue: NoInfer<T>): T {
return initial ?? defaultValue;
}
// Now T is inferred ONLY from initial
const state = createState("hello", 42);
// ^^
// Error: Argument of type 'number' is not assignable
// to parameter of type 'string'
// ✅ Correct usage
const state = createState("hello", "world"); // T = string
NoInfer<T> tells TypeScript: "Don't use this position to figure out
what T is. Instead, check that this argument matches whatever T
was inferred to be elsewhere."
Common Use Cases
1. Default Values
function getOrDefault<T>(
value: T | undefined,
defaultValue: NoInfer<T>
): T {
return value ?? defaultValue;
}
// T inferred from first argument only
const result = getOrDefault(getData(), "fallback");
// If getData() returns number | undefined, defaultValue must be number2. Discriminated Union Handlers
type Action =
| { type: "increment"; amount: number }
| { type: "decrement"; amount: number }
| { type: "reset" };
function createHandler<T extends Action["type"]>(
type: T,
handler: (action: NoInfer<Extract<Action, { type: T }>>) => void
) {
// handler's parameter is constrained by type, not vice versa
}
createHandler("increment", (action) => {
// action is correctly typed as { type: "increment"; amount: number }
console.log(action.amount);
});
createHandler("reset", (action) => {
// action is { type: "reset" }
// action.amount would error - reset has no amount
});
Without NoInfer, the handler's usage could influence what T
becomes, leading to confusing type widening.
3. Option Validation
type Theme = "light" | "dark" | "system";
function setTheme<T extends Theme>(
theme: T,
fallback: NoInfer<T>
): void {
// ...
}
// T inferred from first arg
setTheme("light", "dark"); // ✅ both are Theme
setTheme("light", "system"); // ✅
// ❌ Error: Can't use NoInfer position to widen T
setTheme("light", "unknown");4. Event Emitter Patterns
type Events = {
click: { x: number; y: number };
keydown: { key: string };
scroll: { offset: number };
};
function emit<K extends keyof Events>(
event: K,
payload: NoInfer<Events[K]>
): void {
// ...
}
// Event name determines payload type
emit("click", { x: 10, y: 20 }); // ✅
emit("keydown", { key: "Enter" }); // ✅
emit("click", { key: "Enter" }); // ❌ Error
The event name is the "source of truth" for K. The payload must match—it
doesn't get to influence what K becomes.
How NoInfer Works
Under the hood, NoInfer<T> is defined simply:
type NoInfer<T> = intrinsic;
It's a compiler intrinsic—the type system treats it specially. When TypeScript
collects inference candidates for T, it skips positions wrapped in
NoInfer. After T is resolved from other positions,
NoInfer<T> evaluates to just T.
NoInfer vs Explicit Type Parameters
// Option 1: NoInfer (DX-friendly)
function example<T>(main: T, fallback: NoInfer<T>): T;
example("hello", "world"); // Just works
// Option 2: Explicit generics (verbose)
function example<T>(main: T, fallback: T): T;
example<string>("hello", "world"); // Must specify T
NoInfer is better DX—callers don't need to specify type parameters
explicitly. The API "just works" with inference from the right positions.
Multiple NoInfer Positions
// T inferred from primary only
function transform<T>(
primary: T,
fallback: NoInfer<T>,
validator: (value: NoInfer<T>) => boolean
): T {
// ...
}
// All inference comes from 'primary'
transform(
{ name: "Alice" },
{ name: "Default" }, // Must match primary's shape
(val) => val.name.length > 0 // val is { name: string }
);
You can wrap multiple parameters in NoInfer. TypeScript will only
use unwrapped positions for inference.
Edge Cases
NoInfer Everywhere = No Inference
// ❌ Bad: nowhere to infer T from
function broken<T>(a: NoInfer<T>, b: NoInfer<T>): T {
// T can't be inferred - must be explicit
}
broken("hello", "world");
// Error: T is unknown because no inference sites existAt least one parameter must allow inference, or callers must provide explicit type arguments.
NoInfer in Return Types
NoInfer in return position doesn't affect inference (return types
don't contribute to inference anyway). It's only meaningful in parameter positions.
NoInfer with Unions
function example<T>(value: T, fallback: NoInfer<T>): T;
// T inferred as string | number from first arg
example<string | number>(Math.random() > 0.5 ? "a" : 1, "fallback");
// fallback must be string | numberReal-World Pattern: State Management
interface Store<T> {
get(): T;
set(value: T): void;
reset(): void;
}
function createStore<T>(
initial: T,
defaultValue: NoInfer<T> = initial as NoInfer<T>
): Store<T> {
let current = initial;
return {
get: () => current,
set: (value) => { current = value; },
reset: () => { current = defaultValue; }
};
}
// T is User, inferred from initial
interface User { name: string; age: number }
const userStore = createStore<User>(
{ name: "Alice", age: 30 },
{ name: "Guest", age: 0 }
);
// Or let it infer
const store = createStore({ count: 0 });
// T = { count: number }Before TypeScript 5.4
Before NoInfer was built-in, developers used workarounds:
// Old hack: conditional type trick
type OldNoInfer<T> = [T][T extends any ? 0 : never];
// This worked but was less readable and occasionally
// had edge cases with complex types
The built-in NoInfer is cleaner, officially supported, and handles
all edge cases correctly.
Key Takeaways
NoInfer<T>blocks inference from specific parameter positions- Use it for default values, fallbacks, and secondary parameters that should match but not influence the type
- Improves API ergonomics—callers don't need explicit type parameters
- Available in TypeScript 5.4+ as a built-in utility type
- At least one non-NoInfer position needed for inference to work
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement