In TypeScript, given `declare function get<T, K extends keyof T>(obj: T, key: K): T[K]`, what type does `get({ id: 1, name: 'ada' }, 'name')` produce, and what is the `K extends keyof T` constraint doing?
answer
- two type parameters, not one
- the key gets its own parameter
- union of property names
- literal key survives inference
- look the value type back up
basics
~20 sIt produces string. keyof T is the union of T's property names, so K is constrained to those names and infers the literal 'name' at this call; the return type T[K] then resolves to that one property's type.
solid answer
~40 sThe call produces `string`. `T` infers from the object as `{ id: number; name: string }`, so `keyof T` is the union `"id" | "name"`. Because `K` is its own type parameter constrained to that union, it infers the *literal* type `"name"` instead of widening to `string`. The declared return type `T[K]` is an indexed access type, so it resolves to `T["name"]`, which is `string`. The constraint does two jobs: it rejects keys the object does not have — `get({ id: 1 }, 'nope')` is a compile error because `"nope"` is not assignable to `"id"` — and it keeps the key and the value in sync, so the result type always follows whichever key was passed. All of this is compile-time only; the emitted JavaScript is a plain property read.
code
typescript · 9 linesfunction get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "ada", active: true };
const id: number = get(user, "id");
const name: string = get(user, "name");
const active: boolean = get(user, "active");go deeper
Be able to read the signature aloud and say what each piece does: keyof T is the union of key names, K is the one key passed, T[K] is that key's value type. Know that a wrong key is a compile error.
Explain why K stays a literal instead of widening to string, and what happens to T[K] when the key argument is a union rather than a single literal.
Show where the guarantee ends: the compiler trusts the declared shape of obj, so data from the network or an assertion can make T[K] a lie. Pair the pattern with real validation at the boundary.
Judge when a library should expose this shape at all. Precise key-indexed accessors buy safety but leak the object's exact shape into every caller's inferred types, which makes later shape changes source-breaking across consumers.
## The shape of the signature ```ts declare function get<T, K extends keyof T>(obj: T, key: K): T[K]; ``` There are two type parameters and they are **related**: `K` is not free, it is constrained by `T`. That relationship is what makes the return type depend on the argument you actually passed. ## Step 1 — what `T` infers to `get({ id: 1, name: 'ada' }, 'name')` infers `T` from the first argument. Object-literal property types widen, so `T` is `{ id: number; name: string }`, not `{ id: 1; name: "ada" }`. ## Step 2 — what `keyof T` is `keyof T` is the union of the literal types of `T`'s property names: `"id" | "name"`. It is a *type*, not a value — there is no array, no ordering, nothing you can iterate at runtime. Writing `K extends keyof T` says "whatever `K` turns out to be, it must be one of those names (or a union of them)". ## Step 3 — why `K` keeps the literal A string argument would normally widen to `string`. It does not here because `K`'s constraint is a union of string literal types, and TypeScript preserves the literal type when inferring to a type parameter constrained that way. So `K` infers `"name"`. This literal capture is the piece juniors most often miss: without it the whole pattern collapses. ## Step 4 — the indexed access `T[K]` `T[K]` reads the type stored at a property. With `T = { id: number; name: string }` and `K = "name"`, `T[K]` is `T["name"]` = `string`. Indexed access also distributes over a union key: if `K` were `"id" | "name"`, `T[K]` would be `number | string`. ## What the constraint buys you ```ts const user = { id: 1, name: 'ada' }; get(user, 'name'); // string get(user, 'id'); // number get(user, 'nope'); // error: Argument of type '"nope"' is not // assignable to parameter of type '"id" | "name"' ``` Compare it with the untyped alternative, `function get(obj: object, key: string): any`. That version accepts any string and hands back `any`, which silently disables checking on everything downstream. The generic version turns two separate mistakes — a wrong key, and a wrong assumption about the value's type — into compile errors at the call site. ## The setter mirror image The same relationship works for writes, with the value parameter typed as `T[K]`: ```ts function set<T, K extends keyof T>(obj: T, key: K, value: T[K]): void { obj[key] = value; } ``` Now `set(user, 'id', 'oops')` is rejected because `T["id"]` is `number`. ## Erasure None of this survives compilation. `get` emits as an ordinary function that does `obj[key]`; there is no check that the key exists, no reflection, no cost. If the object came from `JSON.parse` and does not actually have that property, the compiler's promise was based on a type you asserted, and you get `undefined` at runtime. The constraint protects code the checker can see; it does not validate data. ## How to say it in an interview Name the three moving parts in order: `keyof T` gives the union of key names, `K extends keyof T` captures the one key the caller passed as a literal, and `T[K]` looks the value type back up. Then add the punchline: the key and the value stay in sync, and it all disappears at runtime.
- What would the return type be if the caller passed a variable typed `'id' | 'name'` rather than a string literal?`K` would infer as the union `"id" | "name"`, and indexed access distributes, so `T[K]` becomes `number | string`. The result is only as precise as the key you pass: a literal gives one property's type, a union gives the union of those properties' types. Callers then have to narrow the result before using it.
- Does this signature stop you reading a property that is missing at runtime?No. The check is purely compile-time against the declared type of `obj`. If the object came from `JSON.parse` and you asserted its type, or a property was deleted, `T[K]` still says `string` while the runtime value is `undefined`. Types are erased, so nothing verifies the key exists — that is what runtime validation is for.
- How does the same pattern extend to a setter?Add a value parameter typed `T[K]`: `function set<T, K extends keyof T>(obj: T, key: K, value: T[K])`. Because both the key and the value are expressed through the same `K`, the compiler enforces that the value matches whatever property the key names, rejecting `set(user, 'id', 'oops')` when `id` is a number.
saying these in an interview costs you the question
- Says keyof T returns an array of key strings at runtime
- Thinks K is just string, so the result is any
- Expects the union of all property types for a literal key
- Believes the constraint is checked when the function runs
- Confuses T[K] with a value lookup rather than a type lookup