Typescript-unioner baserade på egenskap inuti nästlat objekt
Jag försöker skapa en Union-typ baserat på en nästlad egenskap i mitt objekt. Se exemplet nedan:
type Foo = {
abilities: {
canManage: boolean
}
}
type Bar = {
abilities: {
canManage: boolean
}
extraProp: number
}
type Condition1 = {
abilities: {
canManage: true
}
} & Bar
type Condition2 = {
abilities: {
canManage: false
}
} & Foo
type TotalData = Condition1 | Condition2
const data: TotalData = {
abilities: {
canManage: false, // if canManage is false, TS should complain when I add the `extraProp` key
},
extraProp: 5
}
Problemet jag har är att TypeScript ignorerar de villkor jag har satt. Jag är intresserad av att endast tillåta vissa egenskaper om värdet på canManage är sant. Detta verkar inte fungera när det är inbäddat. Men om jag helt enkelt hade något som detta utan inbäddning, skulle det vara okej:
type Foo = {
canManage: boolean
}
type Bar = {
canManage: boolean
extraProp: number
}
type Condition1 = {
canManage: true
} & Bar
type Condition2 = {
canManage: false
} & Foo
]
type TotalData = Condition1 | Condition2
const data: TotalData = {
canManage: false,
extraProp: 5 // now typescript complains that this property shouldn't be here because canManage is false
}Hur kan jag gå tillväga för att lösa detta problem när jag försöker sätta en Union baserat på egenskapen inne i ett nästlat objekt?
Svar #1
Kompilatorn förstår inte konceptet med "nästlade diskriminerade unioner". En typ är en diskriminerad union om medlemmarna i unionen delar en gemensam "diskriminant"-egenskap. En diskriminant-egenskap är vanligtvis en singleton/litteraltyp som true eller "hello" eller 123 eller till och med null eller undefined. Du kan inte använda en annan diskriminerad union som diskriminant i sig själv, dock. Det vore trevligt om du kunde, eftersom då skulle diskriminerade unioner kunna propagera upp från nästlade egenskaper på det sätt du gör. Det finns ett förslag på microsoft/TypeScript#18758 för att tillåta detta, men jag ser ingen rörelse där.
Som det är nu, är typen TotalData inte en diskriminerad union. Det är bara en union. Det betyder att kompilatorn inte kommer att försöka behandla ett värde av typen TotalData som utsökt antingen Condition1 eller Condition2. Så du kommer förmodligen att stöta på problem om du skriver kod som testar data.abilities.canManage och förväntar dig att kompilatorn ska förstå konsekvenserna:
function hmm(x: TotalData) {
if (x.abilities.canManage) {
x.extraProp.toFixed(); // error!
// ~~~~~~~~~~~ <--- possibly undefined?!
}
}
Om du vill göra detta kan du upptäcka att du behöver skriva användardefinierade typvaktfunktioner istället:
function isCondition1(x: TotalData): x is Condition1 {
return x.abilities.canManage;
}
function hmm(x: TotalData) {
if (isCondition1(x)) {
x.extraProp.toFixed(); // okay!
}
}
Det specifika problemet du stöter på här, där data ses som ett giltigt TotalData, har att göra med hur kontroll av överskottsattribut utförs. Objekttyper i TypeScript är "öppna"/"utökningsbara", inte "stängda"/"exakta". Du tillåts att lägga till extra egenskaper som inte nämns i en typs definition utan att bryta mot typen. Så kompilatorn kan inte helt förbjuda överskottsattribut; istället använder den heuristik för att försöka avgöra när sådana egenskaper är ett misstag och när de är avsiktliga. Regeln som används är mestadels: om du skapar ett helt nytt objektliteral och någon av dess egenskaper inte nämns i typen av där den används, kommer det att bli ett fel. Annars blir det inte det.
Om TotalData vore en diskriminerad union skulle du få felet du förväntar dig på data eftersom data.abilities.canManage skulle få kompilatorn att smalna data från TotalData till Condition2 som inte nämner extraProp. Men det är den inte, och därför förblir data TotalData som faktiskt nämner extraProp.
Det har föreslagits, i microsoft/TypeScript#20863, att kontrollen av överflödiga egenskaper ska vara striktare för icke-diskriminerade unioner. Jag håller i stort sett med; att blanda och matcha egenskaper från olika unionmedlemmar verkar inte vara ett vanligt användningsfall, så en varning skulle förmodligen vara till hjälp. Men återigen, detta är ett långvarigt problem och jag ser ingen rörelse där.
En sak du kan göra för detta är att vara mer explicit om överflödiga egenskaper som du vill skydda mot. Ett värde av typen {a: string} kan ha en b-egenskap av typen string, men ett värde av typen {a: string, b?: never} kan inte. Så denna senare typ kommer att förhindra egenskaper av typen b utan att förlita sig på kompilatorns heuristik för kontroll av överflödiga egenskaper.
I ditt fall:
type Foo = {
abilities: {
canManage: boolean
};
extraProp?: never
}
kommer att bete sig mycket likt din ursprungliga Foo-definition, men nu får du detta fel:
const data: TotalData = { // error!
// -> ~~~~
// Type '{ abilities: { canManage: false; }; extraProp: number; }'
// is not assignable to type 'TotalData'.
abilities: {
canManage: false,
},
extraProp: 5
}
Kompilatorn kan inte längre förena data med varken Condition1 eller Condition2, så den klagar.
Lekplatslänk till kod
