Lesson 16 · Module 2.4 — Generics

Inference priority: which position wins when T is pulled two ways

When 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.

The one idea

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.

A two-sided generic

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();
}

The conflict, and who wins

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 wonT = 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.

Now make the parameter flexible — inference flips

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.

The hierarchy, distilled

ranksource of Texample
highestexplicit type annotationinput: (x: string) => {}
mediummost-constraining inference that fitsoutput: () => 42
lowestflexible position that just adaptsinput: (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.

Reading trap

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.

Check yourself

Recall, don’t re-read. (Answers reveal on click.)

With input: (x: string)=>{} and output: ()=>42, what is T?
If you remove the annotation so it reads input: (x)=>{}, then T is…
An error “'number' is not assignable to 'string'” pointing at output tells you…

Primary source

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.

Try it yourself

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.