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?
answer
- the key type is not always one key
- reads and writes want opposite things
- union of value types, not intersection
- literal keys close the hole
- erased, so nothing re-checks it
basics
~20 sIt 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 sIt 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 linesfunction 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
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.
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.
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.
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