skip to content

What problem does TypeScript's built-in `NoInfer<T>` utility type solve, and how would you use it in a generic function signature?

level: seniorimportance: should knowfreq 34%

answer

  1. check here, do not infer here
  2. one argument should decide the type
  3. the typo widens the union instead of erroring
  4. a one-way valve on an inference site
  5. block them all and you get the constraint

basics

~20 s

NoInfer marks a parameter position as check-only: it stops that position from contributing an inference candidate, so the type parameter is fixed by the other arguments and a stray default or fallback argument is reported as an error instead of widening the type.

solid answer

~50 s

`NoInfer<T>` is built into the standard library from TypeScript 5.4 and means "check against `T` here, but do not infer `T` from here". Normally every occurrence of a type parameter contributes an inference candidate, so a fallback or default argument can quietly drag the parameter wider than intended: with `declare function createStreetLight<C extends string>(colors: C[], defaultColor: C)`, the call `createStreetLight(["red", "yellow"], "blue")` infers `C = "red" | "yellow" | "blue"` and the typo is accepted. Changing the second parameter to `NoInfer<C>` removes that inference site, so `C` is fixed by `colors` alone and `"blue"` is reported as not assignable to `"red" | "yellow"`. It affects inference only — never assignability, never emit. If every occurrence is wrapped there is nothing left to infer from, and the type parameter falls back to its constraint, or `unknown` when unconstrained.

go deeper

for a junior

Know only that TypeScript can be told not to infer a type parameter from a particular argument, and that NoInfer is the built-in way to say it.

for a middle

Explain that every occurrence of a type parameter normally contributes an inference candidate, and show a signature where a fallback argument widens the parameter until NoInfer blocks it.

for a senior

Diagnose the widening-instead-of-erroring failure in a real API, choose which site to block so the error lands on the wrong argument, and know the all-sites-blocked fallback behaviour.

for a principal

Own the consequences for a published API: adopting it raises the minimum TypeScript your declaration files require, and it changes which argument consumers see errors on.

## The failure mode TypeScript infers a type argument by collecting candidates from **every** position where the type parameter appears, then combining them. That is usually what you want, but it silently defeats a very common API shape: one parameter defines the allowed set, another must be a member of it. ```ts declare function createStreetLight<C extends string>( colors: C[], defaultColor: C, ): C; createStreetLight(["red", "yellow", "green"], "blue"); // no error! ``` The intent was "`defaultColor` must be one of `colors`". What actually happens is that `"blue"` contributes its own candidate, so `C` is inferred as `"red" | "yellow" | "green" | "blue"` and every argument satisfies it. The check the signature was written to perform never runs. ## What `NoInfer` does `NoInfer<T>` is an intrinsic utility type in the standard library, added in TypeScript 5.4. Wrapping a type parameter in it blocks that occurrence from producing an inference candidate, while leaving it fully usable for checking: ```ts declare function createStreetLight<C extends string>( colors: C[], defaultColor: NoInfer<C>, ): C; createStreetLight(["red", "yellow"], "blue"); // Argument of type '"blue"' is not assignable to // parameter of type '"red" | "yellow"'. ``` Inference now runs only over `colors`, fixing `C` as `"red" | "yellow"`. The second argument is then checked against that fixed type, and the error appears where the mistake actually is. The mental model is a one-way valve: the type flows *into* the position for checking, but no information flows *out* of it into inference. ## Direction matters Because `NoInfer` removes a site rather than reordering priorities, which site you block determines the outcome: ```ts declare function pair<T>(a: T, b: NoInfer<T>): T; // a decides T declare function order<T>(a: NoInfer<T>, b: T): T; // b decides T ``` Both reject a mismatched pair, but the error lands on a different argument and the resulting type differs. Choose the site that represents the source of truth — the array of allowed values, the schema, the config object — and block the rest. ## What happens if you block everything If no occurrence of the type parameter can produce a candidate, inference has nothing to work with and falls back: ```ts declare function only<T>(x: NoInfer<T>): T; const a = only("red"); // T = unknown declare function onlyC<T extends string>(x: NoInfer<T>): T; const b = onlyC("red"); // T = string — the constraint ``` That is a signature bug, not a clever trick: the caller either gets `unknown` or the bare constraint. Always leave at least one live inference site, or require an explicit type argument. ## What it is not - It is **not** a check, a brand, or a narrowing device. Assignability is unchanged: whatever was assignable to `C` is still assignable to `NoInfer<C>`. - It has **no runtime meaning**. Like every other type, it is erased; the emitted JavaScript is identical. - It is **not** a constraint. It does not restrict what `T` may be; it only changes where `T` comes from. ## Before 5.4, and the compatibility cost The same effect used to be hand-rolled with a conditional-type indirection that made the position non-inferable, and those tricks are still found in older library typings. The built-in version is faster for the checker and, more importantly, readable. The trade is a floor on the compiler version: `NoInfer` resolves from the standard library, so a consumer whose TypeScript predates 5.4 cannot resolve the name in a declaration file that uses it. If you publish `.d.ts` files, treat adopting it as raising your supported TypeScript range. ## Where it earns its place Typical uses are a default or fallback value validated against a set (`createStreetLight`), an initial value checked against a schema-derived type, an assertion helper where `expected` must not widen `actual`, and any builder where one argument should define the type and the rest should merely conform. In each case the win is the same: the error is reported at the argument that is wrong, instead of the type quietly growing to accommodate it.

  • What happens if every occurrence of the type parameter is wrapped in `NoInfer`?
    Inference has no candidates left, so the type parameter falls back to its constraint — or to `unknown` when it is unconstrained. `declare function only<T>(x: NoInfer<T>): T` called with a string yields `T = unknown`, and with `T extends string` it yields `string`. That is a broken signature: keep at least one live inference site, or make callers pass an explicit type argument.
  • Does `NoInfer<C>` change what is assignable to that parameter?
    No. It affects only inference; the parameter still accepts exactly what `C` accepts. The apparent behaviour change comes entirely from `C` being resolved differently — once the other arguments fix it to a narrower type, a previously accepted argument is now checked against that narrower type and can fail. Assignability rules themselves are untouched, as is the emitted JavaScript.
  • If two parameters must agree, which one should you wrap?
    Wrap the one that should merely conform, and leave the source of truth inferable. In a fallback-plus-allowed-values API the array of values defines the type, so the fallback gets `NoInfer`. The choice also decides where the error is reported, which matters for diagnosability: point inference at the argument a reader would call authoritative.

saying these in an interview costs you the question

  • Thinks NoInfer is a runtime or validation check
  • Says it constrains what the type parameter may be
  • Wraps every occurrence and expects inference to still work
  • Believes it changes assignability rules rather than inference
  • Assumes older consumer compilers can read it from a .d.ts

context