skip to content

In TypeScript, why write `function get<T, K extends keyof T>(obj: T, key: K): T[K]` instead of the simpler `function get<T>(obj: T, key: keyof T): T[keyof T]`?

level: middleimportance: must knowfreq 58%

answer

  1. one parameter forgets, the other remembers
  2. which key versus some key
  3. union of every property type
  4. inference captures the literal
  5. precision only helps on reads

basics

~20 s

Giving the key its own type parameter captures which key was passed. With key: keyof T the compiler only knows it was some key, so the return type T[keyof T] is the union of every property type, and the caller must narrow it back down.

solid answer

~40 s

Both signatures reject invalid keys, so the difference is entirely in the result. `key: keyof T` is a plain parameter of a union type: the checker knows the argument is *one of* the keys but not *which one*, so the best return type it can state is `T[keyof T]`, the union of all property types. Promoting the key to its own type parameter `K extends keyof T` makes inference capture the specific literal at each call site, so `T[K]` collapses to exactly that property's type. For `{ id: number; name: string }` the first version returns `string | number` everywhere and every caller has to narrow; the second returns `number` for `'id'` and `string` for `'name'`. The general lesson is that a type parameter *remembers* what was passed, while a union-typed parameter forgets.

code

typescript · 9 lines
typescript
const user = { id: 1, name: "ada" };

declare function getUnion<T>(o: T, k: keyof T): T[keyof T];
declare function getGeneric<T, K extends keyof T>(o: T, k: K): T[K];

const a = getUnion(user, "name");   // string | number
const b = getGeneric(user, "name"); // string

const upper: string = b.toUpperCase();

go deeper

for a junior

Know that keyof T restricts which key strings are allowed, and that indexing a type with a union of keys gives a union of value types back.

for a middle

Explain that a type parameter is inferred per call and therefore remembers the specific literal, while a union-typed parameter is only checked against and forgets. Give the string | number versus string contrast.

for a senior

Show the write-side consequence: relating the value parameter to the same K is what prevents a value valid for one property being stored under another. Also note precision degrades gracefully when the key is a union variable.

for a principal

Weigh API ergonomics: precise key-indexed signatures make call sites self-checking but couple consumers to the exact object shape and produce long inferred types in editors and .d.ts output. Decide where in a library that tradeoff pays.

## Two signatures that accept the same calls ```ts declare function getA<T>(obj: T, key: keyof T): T[keyof T]; declare function getB<T, K extends keyof T>(obj: T, key: K): T[K]; ``` Both reject `getX(user, 'nope')`. The difference shows up on the way out. ## What a union-typed parameter forgets In `getA`, `key` is an ordinary parameter whose type is `keyof T` — for `{ id: number; name: string }` that is `"id" | "name"`. The signature is a promise about *every* call: the argument is some member of that union. Nothing in the signature can refer to the particular member supplied, so the only honest return type is `T[keyof T]`, i.e. `T["id"] | T["name"]` = `number | string`. ```ts const user = { id: 1, name: "ada" }; const v = getA(user, "name"); // string | number v.toUpperCase(); // error: number has no toUpperCase ``` The caller is pushed into `typeof v === "string"` checks or an assertion — reintroducing exactly the imprecision the annotation was supposed to remove. ## What a type parameter remembers In `getB`, the key argument drives *inference* rather than merely being checked against a union. Because `K`'s constraint is a union of string literal types, TypeScript preserves the literal instead of widening to `string`, so `K` becomes `"name"` for that call. The return type `T[K]` is then evaluated with that binding and yields `string`. ```ts const w = getB(user, "name"); // string w.toUpperCase(); // fine ``` This is the general shape of *relating* type parameters: introduce a parameter for the thing that varies per call, constrain it to the other parameter, and express the result in terms of both. ## Precision is not free — it is only as good as the argument A type parameter captures whatever it is given. Pass a union-typed variable and `K` infers that union, and `T[K]` distributes back into a union: ```ts declare const k: "id" | "name"; const x = getB(user, k); // number | string, same as getA ``` So `getB` is never *worse* than `getA`, and is strictly better whenever the caller knows the key concretely — which is the overwhelmingly common case, since keys are usually written as literals in source. ## When you genuinely want the union form If the function's contract really is "any key, and I will treat the value uniformly", the union parameter is simpler and states that honestly. Two examples: a logger that stringifies whatever it reads, or a function that takes a `keyof T` to use as a sort field and never returns the value at all. Adding a type parameter you do not use in the result buys nothing. ## Why this also matters for writes The read case is about precision; the write case is about safety. With `value: T[keyof T]`, a setter would accept a value valid for *any* property under *any* key — a string could be written into a numeric field with no complaint. Relating the value to the same `K` (`value: T[K]`) is what ties the two arguments together. ## Interview framing Say it as one sentence: a union-typed parameter tells you the key is one of many, a type parameter tells you which one. Then give the concrete consequence — `string | number` versus `string` — and note that the extra parameter costs nothing at runtime, since all of it is erased.

  • Is there any runtime cost to introducing the second type parameter?
    None. Type parameters are erased at compile time — the emitted JavaScript for both signatures is the same function performing `obj[key]`. Generics exist only for the checker, so the choice is purely about how precise the call sites are, never about speed or bundle size.
  • When would you deliberately prefer the `key: keyof T` form?
    When the specific key genuinely does not affect the result: a function that takes a key to sort or group by and never returns the value, or one that stringifies whatever it reads. Adding a type parameter that appears only once in the signature buys no precision and makes the declaration harder to read.
  • If both signatures reject bad keys, what exactly is the extra parameter buying?
    Only the result type. Key validation comes from the constraint `keyof T`, which both forms use. The type parameter adds the ability to *name* the key in the return type, so `T[K]` can resolve to one property instead of the union `T[keyof T]`. That saves every caller a narrowing step.

saying these in an interview costs you the question

  • Thinks key: keyof T lets the caller pass any string
  • Says both forms return the same type
  • Assumes the extra type parameter adds runtime overhead
  • Believes narrowing the result afterwards is just as good
  • Claims T[keyof T] resolves to the key that was passed

context