Featured image of post Strict Type Guarding using TypeScript's NoInfer Utility Featured image of post Strict Type Guarding using TypeScript's NoInfer Utility

Strict Type Guarding using TypeScript's NoInfer Utility

Use the NoInfer utility type to prevent unexpected widening during complex type inferences.

The Widening Problem

When TypeScript infers types from multiple call-site arguments, it often widens the inferred type to accommodate all candidates. This is usually helpful, but in certain generic scenarios it produces types that are too loose, letting invalid states slip through.

Consider a function that accepts a default value alongside a specific key:

function createState<T extends string>(key: T, defaultValue: T) {
  return { key, defaultValue };
}

const state = createState("color", "blue");
// T inferred as "color" | "blue" — not what we want

TypeScript infers T as "color" | "blue" instead of constraining defaultValue to match key. The result type loses precision for both parameters.

Enter NoInfer

Introduced in TypeScript 5.4, NoInfer<T> tells the compiler: do not use this position for type inference. The type is still checked against T, but it does not contribute candidates for T’s resolution.

function createState<T extends string>(
  key: T,
  defaultValue: NoInfer<T>
) {
  return { key, defaultValue };
}

const state = createState("color", "blue");
// T inferred as "color"
// "blue" is checked against "color" — OK

If someone passes a mismatched default, they get a clear error:

createState("color", 42);
// Error: Type 'number' is not assignable to type '"color"'

How NoInfer Works

NoInfer<T> is defined in lib.es5.d.ts as:

type NoInfer<T> = [T][T extends any ? 0 : never];

It uses a conditional type to block inference. The compiler sees [T] inside a distributive condition and treats the position as non-inferrable. The actual type still resolves to T for checking purposes.

Use Case 1 — Function Parameters

A common pattern is an event emitter with typed payloads:

function onEvent<E extends string, P>(
  event: E,
  handler: (payload: NoInfer<P>) => void
): void;

onEvent("click", (p: MouseEvent) => {}); // P = MouseEvent
onEvent("click", (p: number) => {});
// Error: 'number' is not assignable to 'MouseEvent'

Without NoInfer, the handler parameter would influence P’s inference and potentially widen it.

Use Case 2 — Tuple Inference

When inferring tuple types, extra candidates cause widening:

function pair<T extends readonly any[]>(
  first: [...T],
  second: NoInfer<[...T]>
): T;

const p = pair([1, "a"], [2, "b"]);
// T inferred as [number, string] — correct

// Without NoInfer: [number | 2, string | "b"] — too wide

Use Case 3 — Generic Constraints

Preventing inference from constraint types:

function lookup<T, K extends keyof T>(
  obj: T,
  key: K,
  fallback: NoInfer<T[K]>
): T[K];

const val = lookup({ a: 1, b: "hello" }, "a", 0);
// val: number — correct

lookup({ a: 1 }, "a", "wrong");
// Error: 'string' is not assignable to 'number'

Comparison With Alternatives

ApproachBehavior
No NoInferType widens to union of all candidates
Explicit type parameterCaller must manually specify the type
NoInfer<T>Prevents inference from selected positions
Separate type paramsAdds unnecessary complexity

When to Use NoInfer

  • Two+ parameters share a type parameter and one should be the inference source, the other a check target.
  • Return type with inference — prevent call-site arguments from widening the return type.
  • Builder patterns — where chained methods should not pollute each other’s inferred types.
class Builder<T extends Record<string, unknown>> {
  set<K extends string, V>(
    key: K,
    value: NoInfer<V>
  ): Builder<T & Record<K, V>> {
    return this;
  }
}

Summary

NoInfer<T> is a focused tool for tightening generic inference. It prevents unwanted widening by marking specific parameter positions as inference-free, while still enforcing type compatibility. Use it when one argument should anchor the type and others should conform to it, not extend it. The result is more precise types, better IDE autocompletion, and cleaner compiler errors.