EdgeCases Logo
Feb 2026
TypeScript
Deep
7 min read

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
noinfer
generics
type-inference
utility-types
typescript-5.4

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 number

2. 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 exist

At 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 | number

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

Related Insights

Explore related edge cases and patterns

TypeScript
Expert
TypeScript infer: Advanced Extraction Patterns
8 min
TypeScript
Expert
TypeScript Distributive Conditional Types: The Union Distribution Rule
8 min

Advertisement