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?
answer
- the body knows less than the caller
- unresolved parameter, unevaluated type
- only the constraint is available
- turn the relationship around
- state the value requirement up front
basics
~20 sInside 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.
solid answer
~40 sInside the body, `T` and `K` are unresolved type parameters, so `T[K]` is a *deferred* indexed access: the compiler knows only that it is the type at some property of some `T`, which could be anything. Arithmetic requires `number` (or `bigint`), so `obj[key] + 1` is an error, and the assignment back into `obj[key]` is rejected too because `number` is not known to be assignable to `T[K]`. The fix is to constrain in the opposite direction — declare the key parameter first and require the object to have a numeric property under it: `function bump<K extends string, T extends Record<K, number>>(obj: T, key: K)`. Now the constraint on `T` mentions `K`, so `obj[key]` is known to be `number` inside the body, and `bump({ hits: 'x' }, 'hits')` is rejected at the call site.
code
typescript · 10 linesfunction bump<K extends string, T extends Record<K, number>>(
obj: T,
key: K,
): void {
obj[key] = (obj[key] + 1) as T[K];
}
const stats = { hits: 0, label: "page" };
bump(stats, "hits");
console.log(stats.hits);go deeper
Know that inside a generic function the type parameters have no concrete value yet, so the compiler can only rely on what the constraint says about them.
Explain that T[K] is a deferred indexed access bounded by its constraint, which is why arithmetic and writes are rejected, and show the Record<K, number> reformulation that supplies the missing information.
Contrast fixing the contract with asserting past it, and point out that the assertion leaves bad call sites compiling. Show that the constraint direction is a design choice about who supplies what.
Set the house rule for generic helpers: constraints express requirements, assertions hide them. Decide how much type-level machinery a shared utility earns before its inferred types and error messages start costing the team more than the safety is worth.
## The failing version ```ts function bump<T, K extends keyof T>(obj: T, key: K) { obj[key] = obj[key] + 1; // error: The left-hand side of an arithmetic operation must be of // type 'any', 'number', 'bigint' or an enum type. } ``` The signature is fine for *callers*. It fails for the *implementation*, and understanding why is the point of the question. ## Deferred types Inside a generic function body the type parameters are not yet bound to anything. `T[K]` cannot be evaluated, so the checker keeps it as a symbolic, **deferred** indexed access. All it knows about that type is its constraint: `T[K]` is bounded by the union of `T`'s property types, and since `T` is unconstrained here, that bound is effectively `unknown`. Nothing lets it conclude `number`. The same reasoning blocks the write. Assigning a concrete `number` into `obj[key]` needs `number` assignable to `T[K]`, and for an arbitrary `T` it is not — the caller might have instantiated `T` with `{ hits: string }`. ## Constrain in the other direction The pattern `<T, K extends keyof T>` derives the key from the object. When you need a *requirement on the value type*, relate them the other way round: derive the object's constraint from the key. ```ts function bump<K extends string, T extends Record<K, number>>( obj: T, key: K, ): void { obj[key] = obj[key] + 1; } const stats = { hits: 0, misses: 0 }; bump(stats, "hits"); // ok bump({ name: "a" }, "name"); // error: string is not assignable to number ``` `K` infers the literal `"hits"` because its constraint is `string`, then `T` must satisfy `Record<"hits", number>` — a type with a `hits` property of type `number`. `Record` is a built-in utility type, so nothing custom is needed. Inside the body the constraint tells the checker that reading `obj[key]` yields `number`, and that a `number` may be written back. Either ordering of the two type parameters works; what matters is that one constraint mentions the other parameter. ## Why not just cast? A common but weaker answer is `(obj[key] as number) + 1` and `obj[key] = value as T[K]`. That silences the checker at exactly the point where it was telling you something true: the signature admits objects whose property is not a number. The assertion moves the failure to runtime, and it does not stop `bump({ name: 'a' }, 'name')` from compiling. Constraints fix the contract; assertions hide the mismatch. ## Related friction to expect Deferred types cause similar complaints elsewhere. Calling a method on `T[K]`, spreading it, or comparing it with `===` against a concrete literal all fail for the same reason: without a constraint the checker has no information. The remedy is always the same shape — say more in the constraint, so the body has something to reason from. Note also that the object's constraint here does not forbid extra properties; `Record<K, number>` is a minimum requirement, and `{ hits: 0, label: 'x' }` still satisfies it. ## Erasure, as always None of this affects the emitted JavaScript, which is a read, an addition and a write. The constraint exists only so the compiler can reject the call sites that would break that code. ## Interview framing "`T[K]` is deferred while `T` is a type parameter, so the checker only knows its constraint, which here is nothing useful — hence the arithmetic error. If I need the value to be a number, I put that in the constraint by requiring the object to be a `Record<K, number>` rather than deriving the key from an unconstrained object."
- Why is an assertion like `(obj[key] as number)` a worse fix than changing the constraint?It suppresses the error without changing the contract, so `bump({ name: 'a' }, 'name')` still compiles and then produces `'a1'` or `NaN` at runtime. Assertions are unchecked promises — they move a compile-time failure to production. A constraint fixes the signature so bad call sites are rejected where the mistake is made.
- Does `T extends Record<K, number>` prevent the object from having other properties?No. It states a minimum requirement, so `{ hits: 0, label: 'x' }` satisfies `Record<'hits', number>` and stays fully typed as itself, because `T` is inferred from the argument. Only the named key is constrained; the rest of the object's shape is untouched and still visible to the caller.
- What if the helper should accept any key whose value is a number, chosen from a known object type?Take the key parameter first and constrain the object through it, as above — the caller then supplies the key and the object, and the compiler verifies the pairing. Deriving the set of numeric keys from an object type instead is a mapped-type filtering exercise, which is a different tool from a plain constraint.
saying these in an interview costs you the question
- Says the body should know T[K] is a number
- Reaches for as number instead of a constraint
- Thinks enabling strict would fix the error
- Believes the two type parameters must be declared in a fixed order
- Claims Record<K, number> forbids other properties