skip to content

keyof Constraints & Relating Type Parameters

The pattern behind every typed get/set/pick helper: constrain K to keyof T so the key and the returned value stay in sync. More generally, constraints let one type parameter be defined in terms of another.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

5

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

open as a page

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%

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.

open as a page

In TypeScript, why does `obj[key] = obj[key] + 1` fail to compile inside `function bump<T, K extends keyof T>(obj: T, key: K)`, and how do you type a helper that only accepts keys whose property is a number?

level: middleimportance: should knowfreq 42%

basics

~20 s

Inside the function T is still unresolved, so T[K] is a deferred type the checker cannot prove is number, and arithmetic on it is rejected. Fix it by relating the parameters the other way: constrain the object to a Record whose value at the key is number.

open as a page

In TypeScript, `declare function set<T, K extends keyof T>(o: T, k: K, v: T[K]): void`. If `o` is `{ a: 'x', b: 1 }` and `k` is a variable declared as `'a' | 'b'`, does `set(o, k, 1)` compile, and is it safe?

level: seniorimportance: should knowfreq 30%

basics

~20 s

It compiles but is unsafe. K infers as the union 'a' | 'b', so T[K] widens to string | number and the number 1 is accepted — even though at runtime k may be 'a', writing a number into a property typed string.

open as a page

In TypeScript, given `interface Comparable<U> { compareTo(other: U): number }`, what does the self-referential constraint in `function max<T extends Comparable<T>>(a: T, b: T): T` express, and when do you need it rather than `T extends Comparable<unknown>`?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

It requires T to be comparable against itself: any type used here must declare compareTo taking that same type. That is what makes max(a, b) meaningful, since both arguments are T and each must accept the other.

open as a page