In TypeScript, for `declare function make<T>(): T`, what type does `T` get at `const x = make()`, and what changes if the signature is `declare function makeStr<T extends string>(): T`?
answer
- nothing at the call mentions T
- inference needs a source, and finds none
- a default outranks the constraint
- the constraint is also the fallback
- unknown when there is nothing else
basics
~10 sWith no argument mentioning T, inference has nothing to work from: T falls back to its constraint when it has one. So make() gives unknown, while makeStr() with T extends string gives string.
solid answer
~50 s`T` appears only in the return type there, and the return position consumes a type parameter rather than supplying one — so the call has no inference site at all. When the candidate set comes back empty, the compiler falls back: to the type parameter's **constraint** if it declares one, otherwise to `unknown`. That makes `make()` an `unknown` and `makeStr()` a `string`; a `<T extends { id: number }>` version yields `{ id: number }`. A declared default would take precedence over both. The caller can always override the fallback by writing the type argument, `make<Date>()`, which is the whole point of such a signature — it is a request that the caller name the type, since the function cannot discover it. `unknown` rather than `any` is a deliberate choice: the fallback is unusable until you narrow it, so the missing information surfaces at the call rather than leaking silently.
code
typescript · 8 linesdeclare function make<T>(): T;
declare function makeStr<T extends string>(): T;
declare function makeRow<T extends { id: number }>(): T;
export const a = make(); // unknown — no inference site, no constraint
export const b = makeStr(); // string — falls back to the constraint
export const c = makeRow(); // { id: number }
export const d = make<Date>(); // Date — the caller supplies the type argumentgo deeper
Remember that a generic with nothing to infer from lands on unknown, and that writing the type argument yourself — make<Config>() — is how you tell the compiler what you expect back.
Explain the fallback ladder in order — declared default, then constraint, then unknown — and show that a constraint doubles as the compiler's best guess, not just a restriction on callers.
Bring the trust-boundary judgment: a return-only type parameter is a caller assertion with zero runtime checking, so for parsed or fetched data insist on a validator whose return type carries the claim.
Own the policy on such signatures across a codebase. Decide where unknown must be forced through validation, where a default is legitimate ergonomics, and how the team avoids sprinkling unchecked type arguments at every I/O boundary.
## An empty candidate set Inference works by matching arguments against parameter types and collecting **candidates** for each type parameter. Every parameter position that mentions the type parameter is an inference site. `declare function make<T>(): T` has no parameters at all, so there is no inference site, and the candidate set for `T` comes back empty. The return position does not help: it *consumes* `T` to describe the result, it does not supply a type. The compiler still has to produce something — a call expression must have a type — so it falls back. ## The fallback ladder In order: 1. **A declared default.** `function make<T = string>(): T` uses `string` when inference finds nothing. 2. **The constraint.** `<T extends string>` falls back to `string`; `<T extends { id: number }>` falls back to `{ id: number }`. 3. **`unknown`.** With neither a default nor a constraint, this is where an uninferred type parameter lands. ```ts declare function make<T>(): T; declare function makeStr<T extends string>(): T; declare function makeRow<T extends { id: number }>(): T; const a = make(); // unknown const b = makeStr(); // string const c = makeRow(); // { id: number } ``` The constraint doing double duty is worth internalising: a constraint is normally read as a *restriction* on what callers may pass, but it is also the compiler's best guess when callers supply nothing. Adding a constraint to a type parameter therefore improves the failure mode, not just the success case — an uninferred `T` becomes a usable type instead of `unknown`. ## Why `unknown` and not `any` `unknown` is the top type with no capabilities: you may hold it, but you cannot call it, read a property off it, or assign it to anything narrower without narrowing or asserting first. That means an inference failure is *loud* at the first real use. `any` would be silent, disabling checking on everything downstream and letting a genuine modelling gap propagate through a codebase. The fallback is deliberately the safe end of that pair. ## The caller's escape hatch Explicit type arguments override the fallback entirely: ```ts const when = make<Date>(); // Date ``` Supplying them is all-or-nothing: you must provide a type argument for every type parameter unless the trailing ones have defaults. A two-parameter generic called with one type argument is rejected with "Expected 2 type arguments, but got 1", so declaring a default on the second parameter is what makes a partial `f<Something>()` call legal. ## Where this shows up in real code The pattern appears whenever a function produces a value whose shape it cannot know: a parse or deserialise helper, a fetch wrapper, a mock or stub factory, a storage getter keyed by string. All of them have a return-only type parameter, and all of them therefore return `unknown` unless the caller names the type. And that is the honest reading of such a signature: **the type argument is an assertion by the caller, not a check by the function.** Types are erased, so a `make<Config>()` performs no validation whatsoever — the value that comes back is whatever the implementation produced, and the compiler has simply been told to believe it is a `Config`. Falling back to `unknown` when the caller says nothing is the compiler's way of refusing to make that assertion on your behalf. ## Two adjacent cases not to confuse An empty candidate set is not the same as a *conflicting* one: when arguments supply candidates that disagree, you get an assignability error, not a fallback. And an uninferred type parameter is not the same as an implicit `any` parameter: the first is a generic landing on `unknown` and is perfectly legal under `strict`, the second is a missing annotation that `noImplicitAny` reports. Being able to tell those three failure shapes apart on sight — empty, conflicting, un-annotated — is most of what this topic is testing.
- Why does the compiler fall back to `unknown` rather than `any`?`unknown` has no capabilities: you cannot call it, read properties off it, or assign it to a narrower type without narrowing or asserting first. So an inference failure fails loudly at the first real use. `any` would disable checking on everything downstream and let the modelling gap spread silently through the codebase, which is the opposite of what a fallback should do.
- How do a declared default and a constraint interact when inference finds nothing?The default wins. `<T extends string = "a">` falls back to `"a"`, not `string`. The constraint still restricts what a caller may supply explicitly, so the two do different jobs: the constraint bounds the legal type arguments, the default names the one to use when no candidate and no explicit argument exist. Without a default, the constraint doubles as the fallback.
- Does supplying `make<Config>()` give any runtime guarantee about the returned value?None. Type arguments are erased with the rest of the type layer, so the function body runs identically whatever you wrote in the angle brackets and performs no validation. `make<Config>()` is you asserting the result's shape to the compiler. If the value crosses a trust boundary — a network response, storage, user input — validate it at runtime and let the validator's return type carry the claim instead.
saying these in an interview costs you the question
- Says an uninferred type parameter becomes any
- Thinks the return type position drives inference
- Believes the constraint only restricts, never supplies
- Claims the call is an error when T cannot be inferred
- Assumes the explicit type argument is checked at runtime