typer - Hur kan TypeScript acceptera denna felaktiga tilldelning till en karta med enum-nycklar?
Hur kan Typescript acceptera/transpilera detta uppenbart felaktiga utdrag:
enum Test {
T1 = 'T1',
T2 = 'T2',
}
const arg: { member: Test } = { member: Test.T1 };
const map: { [t in Test]?: { index: number; title: string; isActive?: boolean } } = {
[arg.member]: 100, // ok, shouldn't be a number!
};
TypeScript härleder korrekt arg.member till en medlem av enum Test men tillåter det ändå. Följande utdrag transpileras korrekt inte:
enum Test {
T1 = 'T1',
T2 = 'T2',
}
const arg: Test = Test.T1;
const map: { [t in Test]?: { index: number; title: string; isActive?: boolean } } = {
[arg]: 100 // error
};
Om vi däremot tilldelar värdet av arg vid tilldelning, är det återigen tillåtet:
enum Test {
T1 = 'T1',
T2 = 'T2',
}
const arg: Test = Test.T1 as Test; // cast here
const map: { [t in Test]?: { index: number; title: string; isActive?: boolean } } = {
[arg]: 100 // ok
};
Att ersätta indextypen Test med string löser problemet och TypeScript kastar korrekt ett fel
const arg: { member: string } = { member: 'hello' };
const map: { [t in string]?: { index: number; title: string; isActive?: boolean } } = {
[arg.member]: 100, // error
};
Några ledtrådar?
Svar #1
Detta orsakas av två samverkande problem, var och en av dem betraktas som en bugg i TypeScript, och ingen av dem visar några tecken på att åtgärdas inom en snar framtid.
Den första är microsoft/TypeScript#13948: när du använder en beräknad egenskap vars typ inte är en enda literal, så får objektet till slut en indexsignatur. Det är som om typen på den beräknade egenskapen har vidgats.
När du skriver följande rad är typen av arg Test, ett unions-enum:
const arg: Test = Test.T1 as Test; // assert here
Det betyder att arg är en union av literaltyper och inte en enskild literal. (Observera att om du inte använder as Test, typulsäkring, kommer kompilatorn att använda tilldelningen const arg: Test=Test.T1 för att begränsa den uppenbara typen av arg till endast den enskilda literal Test.T1, vilket förändrar saker.) Så om vi använder arg som en beräknad egenskapsnyckel, får vi den överraskande vidgningen:
const v = { [arg]: 100 };
/* const v: {
[x: string]: number;
} */
Denna utvidgning är egentligen inte felaktig, men det är inte vad de flesta förväntar sig att se. Förmodligen skulle du förvänta dig något som Partial<Record<Test, number>>, eller kanske { T1: number } | { T2: number }, eller kanske { T1: number, T2?: never } | { T1?: never, T2: number }. Men inget av detta händer, mest för att det i allmänhet skulle vara för kostsamt att syntetisera nya typer på det sättet... åtminstone enligt denna kommentar på ett relaterat problem microsoft/TypeScript#48569.
Observera att även om detta klassas som en bugg, när jag har sett TS-teamet nämna det (som i kommentaren som länkats ovan) tenderar de att behandla det som det bästa av dåliga alternativ, så det fungerar som en designbegränsning, och inte något vi kan förvänta oss att bli "fixat".
Den andra är microsoft/TypeScript#27144. Indexsignaturer är tydligen tilldelningsbara till svaga objekttyper (de vars egenskaper alla är valfria), även om egenskapsvärdestyperna från varje inte är kompatibla.
Så givet objektet v ovan av typen {[k: string]: number}, ger följande tilldelning ingen felmeddelande, även om du potentiellt tilldelar ett number till en boolean:
const w: { a?: boolean, b?: boolean } = v; // no error!
Detta är samma allmänna situation som problemet i ditt exempel:
type MyType = { index: number; title: string; isActive?: boolean };
const m: { T1?: MyType; T2?: MyType } = v; // no error!
Det gjordes ett försök att åtgärda detta i microsoft/TypeScript#27591 men det bröt tydligen viss verklig kod och övergavs så småningom.
Så det är därför det händer. Två lågprioriterade buggar eller begränsningar i språket. Det enda sättet att "fixa" det är att arbeta runt det.
Så hur arbetar vi runt det?
Det beror verkligen på användningsfall; mitt vanliga tillvägagångssätt är att använda en hjälpfunktion som fungerar som ett alternativt typer för beräknade egenskaper, som nämnts i den här kommentaren:
function kv<K extends PropertyKey, V>(
k: K, v: V
): { [P in K]: { [Q in P]: V } }[K] {
return { [k]: v } as any
}
Jag har uttryckligen beskrivit hur jag vill att beräknade egenskaper ska bete sig där, genom att distribuera vilken union som helst i K över outputtypen, och inneslutit det beteendet i hjälpfunktionen kv(). Om jag istället använder det, då beter sig typerna mer rimligt:
const v2 = kv(arg, 100);
// const v2: { T1: number; } | { T2: number; }
const mapBad: { [K in Test]?: MyType } = kv(arg, 100); // error!
const mapGood: { [K in Test]?: number } = kv(arg, 100); // okay!
Ja, det är klumpigt, men tills och om inte beräknade egenskaper naturligt beter sig som kv() (eller någon mer lämplig utdata typ), är detta det bästa jag kan göra, i den meningen att osäkerheten i typförklaringen är begränsad till implementeringen av kv() och jag behöver inte upprepa den för varje användning.
Lekplatslänk till kod
