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?
To dynamically access an object's property: Use keyof typeof obj as the type of the dynamic key, e.g. type ObjectKey = keyof typeof obj; . Use bracket notation to access the object's property, e.g. obj[myVar] .
Use the keyof typeof syntax to create a type from an object's keys, e.g. type Keys = keyof typeof person . The keyof typeof syntax returns a type that represents all of the object's keys as strings.
keyof is a keyword in TypeScript which is used to extract the key type from an object type.
Define a Type for Object with Dynamic keys in TypeScript# Use an index signature to define a type for an object with dynamic keys, e.g. [key: string]: string;. Index signatures are used when we don't know all of the names of a type's properties ahead of time, but know the shape of the values.
In JavaScript, the fundamental way that we group and pass around data is through objects. In TypeScript, we represent those through object types. As we’ve seen, they can be anonymous: or a type alias.
In TypeScript, we represent those through object types. As we’ve seen, they can be anonymous: or a type alias. In all three examples above, we’ve written functions that take objects that contain the property name (which must be a string) and age (which must be a number ).
Dynamic type validation allows a type to generate a validator from its definition. Now they are related — a validator depends entirely on a type, preventing any mismatch between structures. To generate these validators, I found an amazing open-source project called typescript-json-validator, made by @ForbesLindesay.
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.
Playground link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With