declare const GenderChoices: {
readonly Male: "Male";
readonly Female: "Female";
};
type GenderChoice = (typeof GenderChoices)[keyof typeof GenderChoices];
type Person = {
id: number;
name: string;
};
type PersonById = Record<number, Person>;
type PersonByGender = Partial<Record<GenderChoice, PersonById>>;
const gender: GenderChoice = "Male";
const obj = {
[gender]: false
};
const personByGender: PersonByGender = obj; // this does not work as expected
function testFunction(gender: GenderChoice) {
const obj = {
[gender]: false,
};
const personByGender: PersonByGender = obj; // this works although the type of obj on the right side does not match the type of the variable on the left side
}
The line const personByGender: PersonByGender = obj; outside the function does not work as expected but the same code inside the function runs without any error.
I had asked the same question on twitter and two well known typescript experts showered their opinions.
There is an issue about this on the TypeScript Github (Union in a computed property allows any assignment to property value #38663). This answer tries to paraphrase the discussion in the issue and especially the explanation by Wesley Wigham.
Essentially, the problem is the following. You want to add an non-literal string value computed property to an object without loosing type constraints. Non-literal because you're dealing with function arguments.
type Key = "foo" | "bar";
declare const keyValuePair: {
key: Key;
value: unknown;
};
const dict: Partial<Record<Key, string>> = {
[keyValuePair.key]: keyValuePair.value
// ? We would expect an error here because 'unknown' is not assignable to type 'string'.
};
Let's break it down.
Outside the function const gender: GenderChoice = "Male" is defined as a literal string of "Male". TypeScript can infer this computed property on a const object as { Male: boolean; }:
const gender: GenderChoice = "Male";
// type GenderChoice = "Male" | "Female"
const obj = {
[gender]: false
};
const personByGender: PersonByGender = obj;
// ~~~~~~~~~~~~~~ -> ... not assignable (as expected)
However, as soon as we work with non-literals it gets more complicated because computed properties now produce Index Signatures. In the scope of testFunction() obj is inferred as { [x: string]: boolean; } and surprisingly the following does not produces a compiler error:
function testFunction(gender: GenderChoice) {
// type GenderChoice = "Male" | "Female"
// type PersonByGender = Partial<Record<GenderChoice, PersonById>>;
const obj: PersonByGender = {
[gender]: false
};
}
Why is that? If neither the computed property nor the object it's added to have an index signature, TypeScript makes the types evaporate, meaning it doesn't add an index signature to the resulting type. For example:
declare var nonLiteral: string;
const obj = {a: "", b: ""};
const result = {[nonLiteral]: 0, ...obj};
// const result: { a: string; b: string; } <-- No index signature added
In your case the compiler considers the created index signature in obj in testFunction() not to be an Excess Property. This is the case because if the computed property name only actually edits the known names, then there could never be any excess properties. So the issue is that a computed property doesn't edit the types of the properties it may overwrite!
To me, this looks like a flaw in the compiler and will hopefully be considered to be improved.
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