TypeScript dependent string literal properties and indexing
Similar to Typescript: Type of a property dependent on another property within the same object I want a type where the properties are dependent.
const attributes = {
physical: {
traits: {
strength: 1,
dexterity: 1,
stamina: 1,
}
},
social: {
traits: {
charisma: 1,
manipulation: 1,
appearance: 1,
}
},
mental: {
traits: {
perception: 1,
intelligence: 1,
wits: 1,
}
}
};
type AttributeTrait =
| {
category: 'physical';
trait: keyof typeof attributes.physical.traits;
}
| {
category: 'social';
trait: keyof typeof attributes.social.traits;
}
| {
category: 'mental';
trait: keyof typeof attributes.mental.traits;
};
const action: AttributeTrait = {
category: 'social',
trait: 'manipulation'
}
function increment(action: AttributeTrait) {
attributes[action.category].traits[action.trait]++; // error 7053
}
In the function, action.trait
is typed as:
(property) trait: "strength" | "dexterity" | "stamina" | "charisma" | "manipulation" | "appearance" | "perception" | "intelligence" | "wits"
so it can't be used to index traits
.
How can I solve this?
Solution 1:
I don't have a non-redundant and type safe solution for you. Your AttributeTrait
is something I've been calling a correlated record type. It's a discriminated union where some bit of code is safe for each member of the union, but the compiler cannot see that it safe for the union as a whole, because it loses track of the correlation between the category
and trait
properties.
If you write redundant code, the error goes away:
function incrementRedundant(action: AttributeTrait) {
switch (action.category) {
case "physical":
attributes[action.category].traits[action.trait]++;
return;
case "social":
attributes[action.category].traits[action.trait]++;
return;
case "mental":
attributes[action.category].traits[action.trait]++;
return;
}
}
But try as you might, you can't collapse those cases into a single line of code and get the compiler to verify safety for you. That's why I filed microsoft/TypeScript#30581, and one reason I had filed microsoft/TypeScript#25051. Since you can't ask the compiler to treat the single line as if it had been written out in a switch
/case
statement, the best thing I can think of is to use a type assertion to tell the compiler that you know better than it does.
One way to do this is to lie a little bit and tell the compiler that attributes
actually has all trait
s on all category
objects:
function increment(action: AttributeTrait) {
(attributes as
Record<AttributeTrait["category"], {
traits: Record<AttributeTrait["trait"], number>
}>
)[action.category].traits[action.trait]++;
}
This is less type safe than the redundant code, but at least it lets you move forward.
Okay, hope that helps; good luck!
Playground link to code