Lesson 15 · Module 2.3 — Generics
T from your argumentYou almost never write identity<string>("x"). You write
identity("x") and TS fills T in for you. That filling-in is
inference: TS reads the value you passed and solves for T.
Reading generic code well means knowing which argument drove the solution.
Inference is constraint-solving. Each place T appears in a signature is an
equation; the argument you pass is the known value. TS reads the arguments, writes one equation
per T, and solves for the single type that satisfies them.
function identity<T>(arg: T): T { return arg; }
identity("abc"); // arg: T, you passed "abc" ⇒ T = "abc" ⇒ returns "abc"
One occurrence of T takes the value, so the equation is trivial: T is
whatever you passed. The return type follows because it’s also T.
When T appears in several spots, TS still solves one T — and
here’s a sharp example. An object implementing a Store<T> interface gives TS
multiple methods that mention T; the first method in the interface acts
as the primary constraint:
interface Store<T> {
list(): T[]; // ← FIRST method = primary constraint
get(id: string): T; // ← secondary
save(id: string, item: T): void; // ← secondary
}
function addAndGetItems<T>(newItem: T, id: string, store: Store<T>): T[] {
store.save(id, newItem);
return store.list();
}
Now pass a store whose list() returns Homunculus[] while its other methods
deal in Product:
const productStore = {
list(): Homunculus[] { /* ... */ }, // ← this establishes T = Homunculus
get(id: string): Product { /* ... */ },
save(id: string, item: Product) { /* ... */ },
};
const newStore = addAndGetItems({ name: "Laptop", price: 999 }, "id", productStore);
// Because list() is first, TS solves T = Homunculus.
// Everything else (newItem, get, save) must now conform to Homunculus.
So does this compile? No — and that is the real point. The get/save
lines are valid where they sit (productStore is a fine object on its own); the error appears at
the addAndGetItems(…) call. Once T = Homunculus, the other pieces no longer
fit: { name, price } is not a Homunculus, and get() returns a
Product where a Homunculus is now required — so TS flags the arguments. One subtlety
your eye should catch: a method’s return type must match, so get(): Product is the
strict breaker, while parameter positions like save(item: Product) are checked more loosely.
(Had Product been the same shape as Homunculus, it would all pass — the clash only
bites because they differ.)
The takeaway: TS processes the interface’s methods in declaration order and treats
the first match as primary. Reorder the interface so get() comes first, and a different
method drives T. The list() return type won here simply because it was on top.
When a generic “infers the wrong type,” don’t assume the value is wrong. Check which
occurrence of T the compiler read first — return-typed methods near the top of an
interface quietly anchor the whole solve.
Inference = solve T from the arguments. Most of the time there’s one obvious source
and the answer is boring. The moment T appears in many positions, “boring” ends — and
the next lesson is about which position wins when they disagree.
Recall, don’t re-read. (Answers reveal on click.)
identity("abc") without angle brackets, how is T chosen?Store<T> example, what makes T = Homunculus rather than Product?The official page is
TypeScript Handbook — Generics (working with type variables);
inference is covered under
Type Inference.
Best way to feel it: paste the Store example into the
TS Playground, then swap list() and
get() in the interface and watch the inferred T change.
Got a generic inferring a type you didn’t expect? Ask yourself “which occurrence of T won?”
Next lesson (2.4) builds on this: inference priority — what happens when two positions
disagree about T.