skip to content

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?

level: juniorimportance: must knowfreq 68%

answer

  1. two type parameters, not one
  2. the key gets its own parameter
  3. union of property names
  4. literal key survives inference
  5. look the value type back up

basics

~20 s

It 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 s

The 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 lines
typescript
function 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context