skip to content

You maintain a widely used TypeScript library whose helpers must see the caller's literal values precisely. How do you decide between requiring a const assertion at each call site, adding `const` type parameters, and using `NoInfer` — and what does each choice cost?

level: principalimportance: nice to knowfreq 20%

answer

  1. name the failure before the tool
  2. who is responsible, and how it fails
  3. silent widening versus a real error
  4. readonly leaks into your public types
  5. a version floor for consumers

basics

~20 s

Decide by who bears the burden and what it costs them. Caller-side assertions keep signatures simple but fail silently when forgotten; const type parameters guarantee precision at the price of deeply readonly types everywhere; NoInfer only fixes arguments that should conform, not infer.

solid answer

~60 s

They solve different problems, so start by naming the failure. If callers keep losing literal precision, the question is where the fix lives: a call-site const assertion costs nothing in the signature but must be remembered every time, and forgetting it produces no error — just a widened, useless type. A `const` type parameter moves that guarantee into the API, so every call is precise; the price is that inferred types become deeply readonly and literal, which grows hover text, error messages and emitted `.d.ts`, slows checking on large object literals, and can fail to satisfy downstream APIs that want mutable arrays. `NoInfer` addresses a different bug entirely — an argument that should conform to a type instead widening it — and its cost is a floor on the consumer's compiler version, since it resolves from the standard library from TypeScript 5.4. My default: `NoInfer` wherever one argument is the source of truth, `const` type parameters only on the entry points whose value *is* the precision, plain signatures elsewhere.

go deeper

for a junior

Know that TypeScript offers several ways to keep the caller's exact values, and that a library normally solves this in its signatures rather than asking every caller to remember something.

for a middle

Be able to say which tool matches which failure: widening, lost tuple structure, or an argument that widens the type instead of conforming to it.

for a senior

Argue the trade concretely — silent call-site failure versus readonly types leaking into return types, declarations and error messages — and pick per entry point.

for a principal

Own it as API policy: which entry points earn precision, what it does to your emitted declarations and diagnostics, and what minimum compiler version you are committing consumers to.

## Start by naming the failure, not the tool The three knobs answer three different complaints, and mixing them up produces APIs that are precise in the wrong places. - "My literal turned into `string`, so `keyof` gives me nothing useful." That is **widening**, fixed by a primitive constraint or a `const` type parameter. - "My array turned into `T[]`, so positions and length are gone." That is **tuple inference**, fixed by a rest parameter or a `const` type parameter. - "My fallback argument was accepted even though it is not one of the allowed values." That is an **extra inference site**, fixed by `NoInfer`. Only the first two are alternatives to each other. `NoInfer` is orthogonal and frequently belongs alongside one of them. ## Who bears the burden Between the caller-side assertion and the signature-side modifier, the real question is where the responsibility lives — and how it fails. A call-site const assertion keeps the signature minimal, works on any compiler version, and gives the caller local control over how precise they want to be. Its failure mode is the problem: forgetting it is silent. There is no error, no warning, just a type that is less useful than intended, discovered later as missing autocomplete or a `string` where a union was expected. A signature is written once and read thousands of times; a call site is written thousands of times. Precision that depends on every one of those thousands remembering is precision you do not have. A `const` type parameter moves the guarantee into the signature, so it holds for every caller including the ones who have never read your docs. That is worth a lot for the APIs whose entire product is precision: route tables, state machines, schema and column definitions, permission maps, event name registries. ## What the const type parameter actually costs It is not free, and a principal-level answer names the costs concretely. 1. **Deep readonly leaks outward.** Inferred properties become `readonly` and arrays become readonly tuples. That type flows into your return types, your emitted declaration files and consumer code. A consumer who then passes the result into an API expecting `string[]` is forced to spread-copy at the boundary. 2. **Types get large.** A big configuration object infers a large literal type that appears in every hover and every error message that mentions it. Diagnostics on a mismatch inside a deeply literal type are notoriously hard to read. 3. **Check time.** Preserving structure means the checker carries more type information through the call, which is measurable on large literals in a big codebase. 4. **It is a source-breaking change.** Adding the modifier to a published signature narrows what consumers receive; code that relied on the widened form can stop compiling even though the runtime behaviour is identical. Treat it as semver-relevant. 5. **It only helps inline arguments.** A consumer who builds the object in a variable first gets nothing, so the ergonomic win is smaller than it looks in codebases that factor configuration out. ## What `NoInfer` costs Much less on the type side — it changes no assignability rule and produces no extra type surface. Its costs are different in kind: - **A compiler-version floor.** `NoInfer` resolves from the standard library and has been available since TypeScript 5.4. If it appears in a published `.d.ts`, a consumer on an older compiler cannot resolve the name at all. Adopting it raises your supported range, which for a widely used library is a real decision rather than a detail. - **It relocates errors.** Whichever site you block decides which argument gets blamed. Point inference at the argument a reader would call authoritative, or you will produce diagnostics that are correct and still confusing. - **It can be over-applied.** Block every occurrence and inference collapses to the constraint, or to `unknown` — a signature bug that only shows up as a useless return type. ## A defensible policy Apply precision at the boundary where it converts into value, and let everything else widen. - Use `NoInfer` freely on any parameter that should *conform* to a type established elsewhere in the same call. It is cheap and it turns a silent acceptance into a clear error. - Reserve `const` type parameters for the few entry points where the caller's exact keys and values are the reason the API exists — the ones feeding `keyof`, indexed access or template-literal types downstream. - Prefer a primitive constraint (`<T extends string>`) when one literal is all you need. It is the smallest change, produces the smallest types, and adds no readonly modifiers. - Where you decline precision, make it loud: a required annotation, or an overload that forces an explicit type argument, beats a signature that silently widens. - Whatever you choose, remember the layer. Every one of these is erased at compile time; none of them protects a value at runtime, and none of them changes the JavaScript you ship.

  • Why is a caller-side const assertion a weak guarantee for a published API?
    Because its failure mode is silence. Forgetting it does not produce an error — the call still compiles, just with a widened type — so the loss shows up much later as missing autocomplete or a broken `keyof` lookup. A guarantee that depends on every call site remembering an optional annotation is not a guarantee. Signature-side controls hold for callers who never read the docs.
  • When would you deliberately *not* add a const type parameter even though it would improve inference?
    When the precision buys nothing downstream. If the argument is only forwarded or iterated, the readonly literal type just enlarges hovers, error messages and your emitted declarations, and can force consumers to copy arrays at boundaries that want mutable ones. Reserve the modifier for entry points whose value depends on the exact keys and values surviving.
  • How does adopting `NoInfer` affect your library's supported TypeScript range?
    It raises the floor. `NoInfer` comes from the standard library as of TypeScript 5.4, so a consumer on an older compiler cannot resolve the name in a declaration file that uses it. For a widely used package that is a compatibility decision to state in the release notes and the `peerDependencies`/engines story, not an implementation detail.

saying these in an interview costs you the question

  • Treats const type parameters and NoInfer as interchangeable knobs
  • Adds precision everywhere without weighing readonly leakage
  • Ignores that these appear in emitted declaration files
  • Calls a signature change safe because runtime output is identical
  • Assumes callers will reliably remember a const assertion

context