You are designing a public TypeScript type whose declaration is shaped like `Query<TData, TError, TKey>`. How do you decide the order of the type parameters and which of them get defaults, and what does that decision commit you to later?
answer
- callers cannot skip an entry
- sort by how often it is specified
- defaults are forced to the tail
- only the tail is a growth area
- fewer parameters, more inference
basics
~20 sOrder type parameters by how often callers will specify them, most-specified first, because a caller cannot skip an earlier one to reach a later one. Default the tail so the common reference stays short, and remember that only a new defaulted parameter appended at the end is non-breaking.
solid answer
~50 sTwo forces drive the order. First, type-argument lists are positional with no way to skip an entry, so a caller who needs the third parameter must spell the first two — put the parameter people actually specify first, and the exotic ones last. Second, defaults must be trailing, so the parameters you expect to be omitted have to sit at the tail anyway; those two pressures point the same way. Choose each default deliberately: `unknown` keeps callers honest, a concrete type like `Error` optimises the common case at the cost of being quietly wrong when it is wrong, and `any` silently disables checking for everyone who omits the argument. The commitment is evolution: appending a new defaulted parameter keeps every existing reference compiling, while inserting one, removing a default, or reordering breaks every call site at once. If the list is getting long, that is usually a signal to infer more from arguments and expose fewer parameters.
code
typescript · 14 lines// v1 of a published generic.
interface Query<TData> {
data: TData;
}
// v2: a new parameter APPENDED with a default.
// Every existing single-argument reference still compiles.
interface QueryV2<TData, TError = Error> {
data: TData;
error: TError | null;
}
type UserQuery = QueryV2<{ id: string }>; // TError = Error
type StrictQuery = QueryV2<{ id: string }, string>; // both suppliedgo deeper
Know that type arguments are positional, so the order in the declaration decides what a caller must write, and that defaults only work on the trailing parameters.
Explain how the no-skipping rule and the trailing-defaults rule together force the ordering, and show which reference forms remain legal when a parameter is defaulted.
Demonstrate the evolution reasoning: appending a defaulted parameter is the one non-breaking change, insertion can silently rebind arguments, and the choice of default type decides what every silent caller gets. Argue concretely for unknown over any on a shared surface.
Own it as policy across a library: a cap on how many type parameters a public generic may expose, a naming convention once the list exceeds two, and a rule that the tail is reserved for growth. Tie it to the release process, since parameter order cannot be revised after publication without a coordinated break.
## Why order is a hard commitment Type-argument lists are positional and admit no placeholder. There is no way to write "use the default for the first, but let me set the third" — a caller who needs the last parameter must write out everything before it. That single fact makes parameter order a real part of your public contract, not a cosmetic choice. The practical rule is to sort by *probability of being specified*, descending. The parameter almost every caller supplies goes first. The parameter a tenth of callers touch goes next. The one that exists for two power users goes last. Any other ordering taxes the common case: users end up writing out types they did not care about just to reach the one they did, and those spelled-out types then become an accidental part of their code that must be maintained. ## The defaults rule pushes in the same direction A type parameter with a default is optional, and optional parameters must be trailing — once you default one, everything after it must be defaulted too. So the parameters you intend to be omitted are forced to the tail regardless. Happily this agrees with the ordering rule: rarely specified means defaulted means last. When the two rules seem to conflict — a frequently specified parameter that also wants a default — the frequency argument wins, and you give the earlier parameter a default as well so the shape stays legal. ## Choosing each default The default type is the contract for every caller who says nothing, which in practice is most of them. - `unknown` is the conservative choice. Callers who omit the argument still have to narrow before using the value, so an omission produces friction rather than silence. Good for data whose shape genuinely varies. - A concrete type — `Error` for a failure channel, `string` for an identifier — reads well and makes the short form pleasant. It is a claim about the real world, so make it when the claim is nearly always true, and accept that a caller with a different type must now supply the argument. - `any` is the choice to argue against. It makes the short form compile everywhere and turns off checking for the values flowing through that position, with no diagnostic and no obvious symptom. Most "TypeScript didn't catch it" stories in a generic-heavy codebase start with a defaulted `any`. - Repeating the constraint as the default — the `<T extends X = X>` shape — is the honest neutral option when the parameter exists so that callers can narrow. It says: supply something more specific if you have it, otherwise you get the widest thing allowed. ```ts interface Query<TData, TError = Error> { data: TData; error: TError | null; } type UserQuery = Query<{ id: string }>; // TError = Error type StrictQuery = Query<{ id: string }, string>; // both supplied ``` ## What the decision commits you to Evolution is where these choices are paid for. - **Appending a new parameter with a default is source-compatible.** Every existing reference that supplied fewer arguments keeps compiling, and the new parameter is opt-in. This is the one safe move, and it is the reason to leave room at the tail. - **Appending without a default breaks everything.** All existing references become arity errors at once. - **Inserting in the middle is worse than breaking — it can silently change meaning.** A reference that used to bind its argument to the first parameter now binds it to the newcomer, and if the types happen to be compatible nothing complains. - **Removing a default is breaking**; adding one to an existing parameter is not, since it only widens what is accepted. - **Reordering is breaking for every call site simultaneously**, with the same silent-rebinding hazard as insertion. So the order you ship is close to permanent, while the tail is your only growth area. ## Naming, because the call site is positional Single letters are fine at one or two parameters. Beyond that the reader of a reference sees only positions and has to count, so descriptive names with a `T` prefix — `TData`, `TError`, `TKey` — pay for themselves in the declaration and in hover tooltips. This is convention rather than a compiler rule, but it is close to universal in libraries with wide generic surfaces. ## The best answer is usually fewer parameters A three-parameter generic is a smell worth interrogating before you ship it. Ask which parameters could be inferred from an argument the caller already passes, which could be derived from another parameter and defaulted to that derivation, and which exist only because one caller once wanted them. Every parameter you remove removes an ordering decision you would otherwise be stuck with, and inference costs the caller nothing at the point of use. And none of it exists at runtime: the whole list is erased at compile time, so this is purely an ergonomics-and-evolution decision about the source people write.
- Which change to a published generic type is safe for existing references, and which are breaking?Appending a new parameter that has a default is safe — every shorter reference keeps compiling and the new one is opt-in. Adding a default to an existing parameter is also safe, since it only widens what is accepted. Breaking: appending without a default, removing a default, reordering, and inserting in the middle — the last of which can silently rebind an argument to the wrong parameter instead of erroring.
- When would you default a type parameter to a concrete type rather than to unknown?When the concrete type is right for the overwhelming majority of uses and being wrong is loud rather than silent — a failure channel defaulting to Error, say. It buys a short, readable common form. Default to unknown instead when the shape genuinely varies, because then an omitted argument should cost the caller a narrowing step rather than handing them a wrong type for free.
- A colleague proposes a fourth type parameter on a widely used generic. What do you push back with?Ask what would infer it instead: an argument the caller already passes, or a derivation from an existing parameter used as its default. Each parameter is an ordering decision you cannot revise later and a position every reader must count past. If it survives that, it goes last with a default, which is the only placement that does not break existing references.
saying these in an interview costs you the question
- Says callers can skip an early type argument to set a later one
- Treats parameter order as cosmetic and reorderable later
- Defaults a parameter to any for convenience
- Assumes inserting a parameter in the middle only causes compile errors
- Adds type parameters instead of letting arguments drive inference