Reputation: 1446
I'm a bit confused on how TypeScript performs type checking when using dynamic keys in object literals. Consider the following two functions returning a copy of an object:
type Foo = {
a: number;
b: number;
};
const INIT_FOO: Foo = { a: 0, b: 0 };
function test1(k: keyof Foo) {
const f: Foo = { ...INIT_FOO, [k]: true };
return f
}
function test2(k: keyof Foo) {
const f: Foo = { ...INIT_FOO };
f[k] = true;
return f
}
The TypeScript compiler will only report errors for function test2
, and not function test1
.
Why doesn't the compiler report an error for function test1
when it is clearly incorrect?
Upvotes: 6
Views: 618
Reputation: 328658
This is a known issue, reported at microsoft/TypeScript#38663. It happens because computed properties where the key is a union of string literals (like keyof Foo
) are widened to a string
index signature. It's not exactly incorrect, but it is not specific enough to be useful. This behavior is either a bug, as reported in microsoft/TypeScript#13948, or possibly a design limitation, as described in microsoft/TypeScript#21030. In any case, this is the current behavior of the language.
Once the computed property is widened to an index signature, it becomes impossible to catch the bug. Index signatures are not considered excess properties; the type {a: number, b: number} & {[k: string]: boolean}
(which is approximately what you get here) is assignable to {a: number, b: number}
, so no bug is reported. But of course, in actuality, what you've done is assign a boolean
to one of the a
or b
properties; it's just that the compiler can't see it because of the index signature widening.
Other than just making a note of this issue and being careful, the other thing you could do is manually implement a function that produces the type that "should" be generated when you have a computed property of a union of key types:
function computedProp<K extends PropertyKey, V>(
key: K, val: V
): K extends any ? { [P in K]: V } : never {
return { [key]: val } as any;
}
The return value is a distributive conditional type that produces a union of object types, like this:
const example = computedProp(Math.random() < 0.5 ? "a" : "b", true);
// const example: { a: boolean; } | { b: boolean; }
The union {a: boolean} | {b: boolean}
is a more accurate representation of the type of {[k]: true}
. If we use that, you get the expected error:
function test1(k: keyof Foo) {
const f: Foo = { ...INIT_FOO, ...computedProp(k, true) }; // error!
// ~
// '{ a: boolean; b: number; } | { b: boolean; a: number; }' is not assignable to 'Foo'.
return f
}
Obviously using a function instead of a direct computed property is not really desirable, but at least you could sort of maintain type safety if it's important to you.
Upvotes: 4