skip to content

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%

answer

  1. the key type is not always one key
  2. reads and writes want opposite things
  3. union of value types, not intersection
  4. literal keys close the hole
  5. erased, so nothing re-checks it

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.

solid answer

~40 s

It compiles, and it is unsound. Because `K` captures whatever key type it is given, a union-typed key makes `K` infer as `"a" | "b"`. Indexed access distributes, so the value parameter `T[K]` becomes `T["a"] | T["b"]` = `string | number`, and `1` satisfies that. At runtime `k` may well be `"a"`, so a number lands in a property the type says is a `string`, and every later read of `o.a` is a lie. The pattern is only sound when `K` resolves to a single key: then `T[K]` is that one property's type. In practice you avoid the hole by writing keys as literals, taking one key per call, or accepting a value typed as the intersection of the candidate property types when the key really can vary.

code

typescript · 12 lines
typescript
function set<T, K extends keyof T>(o: T, k: K, v: T[K]): void {
  o[k] = v;
}

const o = { a: "x", b: 1 };

set(o, "a", "safe");

const k: "a" | "b" = Math.random() > 0.5 ? "a" : "b";
set(o, k, 1);

console.log(typeof o.a);

go deeper

for a junior

Know that indexing a type with several keys at once gives you the union of those properties' types, and that a literal key gives just one.

for a middle

Explain how K infers a union when the key argument is union-typed, and that the value parameter widens with it. Be able to show the resulting call that compiles but is wrong.

for a senior

Name the direction problem: writes need the intersection of candidate property types while indexed access yields the union. State the discipline you apply — literal keys, one key per call, or validating before the call.

for a principal

Treat this as one of TypeScript's deliberate soundness trades and set policy: where generic write helpers are worth their risk, whether shared setters should exist at all, and what the codebase does at data boundaries where keys arrive as plain strings.

## The setup ```ts declare function set<T, K extends keyof T>(o: T, k: K, v: T[K]): void; const o = { a: "x", b: 1 }; declare const k: "a" | "b"; set(o, k, 1); // compiles ``` The read-side version of this pattern is famously safe; the write side has a hole, and it is a good senior question precisely because the signature *looks* airtight. ## Why it compiles Inference does not force `K` to be one key. `K` is constrained by `keyof T`, and a union of valid keys is assignable to `keyof T`, so `K` infers as `"a" | "b"`. The value parameter is then evaluated as `T["a" | "b"]`. Indexed access over a union of keys produces the union of the corresponding property types — here `string | number`. The literal `1` is assignable to `string | number`, so the call is accepted. ## Why it is unsound Soundness would require the value to be acceptable *whatever* key `k` turns out to be at runtime. That is the **intersection** of the candidate property types (`string & number`, i.e. effectively nothing writable here), not their union. The checker computes the union because indexed access is defined for reading — reading `o[k]` for an unknown member of the union genuinely can produce either type. Reusing the same operator on the write side flips the direction and loses the guarantee. ```ts set(o, k, 1); // if k === "a" at runtime, o.a is now the number 1 const s: string = o.a; // checker says string; runtime holds 1 o.a.toUpperCase(); // TypeError at runtime ``` Nothing catches this later, because the type annotation on `o` is never re-verified — types are erased. ## When the pattern *is* sound With a literal key, `K` resolves to a single member and `T[K]` is exactly one property type: ```ts set(o, "a", "ok"); // fine set(o, "a", 1); // error: number is not assignable to string ``` That is the overwhelmingly common call shape, which is why the pattern is still the right default. The hole opens only when the key argument's type is itself a union — a key held in a variable, read from configuration, or forwarded from another generic function. ## Defences - **Keep keys literal at the call site.** Most real code does this naturally; `as const` on a key table preserves the literal types instead of widening them to `string`. - **Take one key per call.** If a helper forwards a key it received, give that helper its own single type parameter so the union never forms. - **Ask for the intersection when the key really varies.** If a caller must supply a value valid for every possible key, express the parameter as the intersection of those property types rather than `T[K]`; in the example above that is uninhabited, which correctly says "there is no value that works for both". - **Validate at the boundary.** If the key comes from outside the program, narrow it to a specific literal with a check before calling, which also collapses `K`. ## The bigger point TypeScript deliberately trades some soundness for usability, and this is one of the documented trade sites, alongside assertions, `any`, and non-null `!`. A senior answer names the mechanism (union key, distributive indexed access, union where an intersection is required), shows the runtime consequence, and says which discipline they apply — rather than concluding that the whole `keyof` pattern is unsafe. ## Interview framing "It compiles. `K` infers the union, `T[K]` becomes the union of value types, and the write needs the intersection instead — so a value valid for one key can be stored under another. It is safe when the key is a literal, which is why it is still the right pattern; I keep keys literal or take one key per call."

  • What value type would actually be sound when the key can be any member of a union?
    The intersection of the candidate property types, not their union — the value has to be acceptable under every key the variable might hold. For `{ a: string; b: number }` that intersection is uninhabited, which is the correct answer: no single value is safe for both. TypeScript computes the union instead because indexed access is defined by the read direction.
  • Does the equivalent getter suffer from the same problem?
    No. Reading `o[k]` when `k` may be `"a"` or `"b"` genuinely can yield either property's type, so the union `T[K]` is exactly right. The union is sound for reads and too permissive for writes — same operator, opposite variance.
  • How would you stop a wrapper function from accidentally widening K?
    Give the wrapper its own type parameter constrained the same way and forward it, rather than annotating the key as `keyof T`. If the wrapper declares `key: keyof T`, the union has already formed by the time it calls through, and the inner call inherits the loose value type.

saying these in an interview costs you the question

  • Says it fails to compile because the types differ
  • Thinks T[K] always resolves to a single property type
  • Claims a runtime check inside set would catch it
  • Concludes the whole keyof pattern is unusable
  • Assumes strict mode closes this hole

context