10LOC
TypeScriptadvanced

Use NoInfer to keep generic inference predictable

Published August 3, 2027

export const createMethodGuard = <M extends string>(allowed: M[], fallback: NoInfer<M>) => {
  const allowedSet = new Set<string>(allowed);
  return (method: string): M => (allowedSet.has(method) ? (method as M) : fallback);
};

const isSafeMethod = createMethodGuard(["GET", "HEAD", "OPTIONS"], "GET");

export const routeMethod = (method: string) => {
  const safe = isSafeMethod(method);
  return safe === method ? `handling ${safe}` : `rejected ${method}, fell back to ${safe}`;
};

What

createMethodGuard(allowed, fallback) builds a function that checks a string against an allow-list and falls back to a default when it doesn't match. NoInfer<M> around the fallback parameter's type keeps M pinned to exactly the literal types in allowed, so an invalid fallback is a compile error instead of silently joining the allowed set.

Why it matters

TypeScript infers a generic's type from every position it appears in — including argument positions that are meant to just consume the type, not define it. Without NoInfer, fallback: M would let TypeScript infer M from whichever argument gives the best fit across both allowed and fallback. Call createMethodGuard(["GET", "HEAD"], "DELETE") and instead of an error, M widens to "GET" | "HEAD" | "DELETE" — the entire point of the allow-list, that "DELETE" isn't one of the allowed values, silently disappears at the type level. NoInfer<M> tells the compiler "infer M from allowed only; don't let this position contribute candidates," so fallback is checked against the already-inferred type instead of participating in inference.

How it works

  • M extends string is inferred entirely from the allowed: M[] parameter — that's the only position without NoInfer, so it's the only one TypeScript uses to pick M.
  • fallback: NoInfer<M> is still typed as exactly M for every purpose except inference — inside the function body it behaves identically to M; NoInfer only changes what the compiler does while resolving the call site.
  • routeMethod calling isSafeMethod(method) gets back the full M union ("GET" | "HEAD" | "OPTIONS"), and the runtime check happens against the same allowedSet the guard closed over — the type-level allow-list and the runtime allow-list are the same array.

Gotchas

  • NoInfer requires TypeScript 5.4 or newer. It's a compiler intrinsic (type NoInfer<T> = intrinsic), not something expressible in userland TypeScript, so there's no polyfill for older compiler versions the way there is for most utility types.
  • NoInfer only suppresses inference — it doesn't validate anything by itself. createMethodGuard(["GET", "HEAD"], "DELETE") becomes a type error only because fallback is still checked against the resulting M after inference finishes; NoInfer's entire job is deciding which positions get to vote on what M is.
  • It's tempting to reach for NoInfer on every generic parameter with more than one use site. It only matters when a "consuming" position (like a default or fallback) would otherwise widen inference away from a "defining" position (like an allow-list). Most generic functions don't have that asymmetry.

Related

  • Function overloads vs. a single generic signature — an older, clunkier way to get similar control by splitting a function into non-generic overloads instead of shaping inference within one generic signature.
  • const type parameters (<const M extends string>, TypeScript 5.0) — a different inference control: it stops literal types from widening at all, rather than controlling which parameter positions participate in inference. The two are complementary and sometimes combined on the same function.