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 stringis inferred entirely from theallowed: M[]parameter — that's the only position withoutNoInfer, so it's the only one TypeScript uses to pickM.fallback: NoInfer<M>is still typed as exactlyMfor every purpose except inference — inside the function body it behaves identically toM;NoInferonly changes what the compiler does while resolving the call site.routeMethodcallingisSafeMethod(method)gets back the fullMunion ("GET" | "HEAD" | "OPTIONS"), and the runtime check happens against the sameallowedSetthe guard closed over — the type-level allow-list and the runtime allow-list are the same array.
Gotchas
NoInferrequires 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.NoInferonly suppresses inference — it doesn't validate anything by itself.createMethodGuard(["GET", "HEAD"], "DELETE")becomes a type error only becausefallbackis still checked against the resultingMafter inference finishes;NoInfer's entire job is deciding which positions get to vote on whatMis.- It's tempting to reach for
NoInferon 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.
consttype 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.