Lesson 16 · Module 2.4 — Generics
T is pulled two waysWhen the same T shows up in two spots that suggest different types,
there is no tie — there is a winner. The order is well established: an explicit
annotation beats inference, and a constraining position (like a typed parameter) beats a
passive one. Read the conflict, and you can predict the error message.
Not all occurrences of T are equal. An explicitly annotated position
carves T in stone; an inferable position then has to conform to it. The
error always points at the conforming side — never the one that won.
Here T appears both as a parameter (input) and as a return type (output):
interface ConflictTest<T> {
input: (param: T) => void; // T in PARAMETER position
output: () => T; // T in RETURN position
}
function testConflict<T>(options: ConflictTest<T>): T {
return options.output();
}
Give the parameter an explicit type that disagrees with the return value:
const result = testConflict({
input: (x: string) => {}, // explicit ⇒ T = string
output: () => 42 // suggests T = number
});
// Error: Type 'number' is not assignable to type 'string'.
// The expected type comes from property 'output' ... on ConflictTest<string>.
The way to read this message: the explicitly annotated parameter won —
T = string — and the return value 42 was then checked against it and failed.
The error fingers output, the side that had to conform. Explicitness beats inference,
whatever position it sits in.
Drop the explicit annotation on input, and the parameter stops constraining anything.
The only remaining source is the return value, so it drives T:
const ok = testConflict({
input: (x) => {}, // no annotation ⇒ contributes nothing
output: () => 42 // drives T = number
});
// Works. T = number; input is inferred as (param: number) => void.
Same two positions, opposite outcome — because the strongest active source changed.
| rank | source of T | example |
|---|---|---|
| highest | explicit type annotation | input: (x: string) => {} |
| medium | most-constraining inference that fits | output: () => 42 |
| lowest | flexible position that just adapts | input: (x) => {} |
So when two positions disagree, find the most explicit one — that’s T. The
others are validated against it, and any mismatch surfaces as the error, on the conforming side.
Does source order decide it, the way declaration order did in Lesson 2.3? No. That
order tie-break only matters between sources of equal strength (there, every method had an
explicit type, so the first one won). Here the positions are not equal — an explicit annotation
outranks an inferred return — so the hierarchy settles it before order can come into play. Swap the two
properties (output first, input second) and nothing changes:
input: (x: string) => {} still pins T = string, and output: () => 42
still errors. It is only when input carries no annotation —
(x) => {}, not (x: string) => {} — that output becomes the
strongest active source and drives T = number.
The error location lies about blame. “Type 'number' is not assignable to 'string'” pointing at
output does not mean output is wrong — it means input
already pinned T = string and won. Read the winner, then the message makes sense.
Recall, don’t re-read. (Answers reveal on click.)
input: (x: string)=>{} and output: ()=>42, what is T?input: (x)=>{}, then T is…output tells you…The official page is
TypeScript Handbook — Generics;
the underlying mechanics live in
Type Inference — Best Common Type / Contextual Typing.
Best way to feel it: paste testConflict into the
TS Playground and toggle the : string
annotation on input to watch the inferred T flip.
Got a confusing “not assignable” error on a generic? Paste it into the Playground and ask “which
position won the T solve?” Next lesson (2.5) builds on this: rest/tuple parameters —
<TArgs extends any[]> and why it captures a whole signature.