Vad är skillnaden mellan en generisk typ som utökar en typ och en vanlig typ i Typescript?
Hej Typescript-experter,
kan någon förklara varför följande kod ger mig ett fel på rad 16 men inte på 13. Är detta avsiktligt eller en saknad funktion?
Kod
interface Config {
// There need to be different types in here for the error to occur
A: number
B: string
}
type union = "A" | "B"
var Global = {A: 1, B: "Hello"} as Config
function foo<K extends union>(key: K, x: union) {
if (x === "A"){
Global[x].toFixed()
}
if (key === "A"){
Global[key].toFixed()
}
}
Lekplatslänk
Svar #1
Det är förmodligen en saknad funktion.
När du kontrollerar (x === "A") fungerar det som en typvakt för typen av värdet x, vilket får det att smalna från unionstypen "A"|"B" till endast "A" genom kontrollflödesanalys.
Tyvärr blir generiska typparametrar i TypeScript inte avsmalnade via kontrollflödesanalys; se microsoft/TypeScript#24085 för mer information. Så, till exempel, att kontrollera (key === "A") kommer inte att avsmalna typen av K till "A". Denna typ av begränsning är logisk när du har flera värden av samma generiska typ:
function foo<K extends Union>(key: K, x: Union, key2: K) {
if (key2 === "A") {
Global[key].toFixed(); // error!
}
}
Självklart borde kontrollen av värdet på key2 inte påverka typen av key, så kompilatorn antar konservativt att K aldrig bör begränsas. Detta är det grundläggande problemet i microsoft/TypeScript#13995 och flera relaterade problem har tagits upp med förslag om hur man kan hantera det i de fall där det borde vara säkert att göra sådan begränsning. Hittills har dock inget införts i språket.
Det här är dock egentligen inte hela historien; man skulle kunna invända med: okej, kanske kan du inte begränsa typ-parametern K från K extends Union till K extends "A", men säkert kan du begränsa typen av värdet key från K till "A" eller K & "A" (en intersection-typ), vilket skulle göra att Global[key].toFixed() lyckas:
if (key === "A") {
Global[key as (K & "A")].toFixed(); // okay
}
Och jag har inget bra svar på det just nu. De flesta frågor jag har sett om detta slutar till slut med att hänvisas till microsoft/TypeScript#13995. 🤷♂️
Det närmaste jag kan komma till ett fullständigt svar är att det verkar som att användning av inbyggda typkontroller som a === b eller typeof a === "string" eller a instanceof B eller a in b endast slutar med att filtrera unioner eller eventuellt begränsa string eller number till sträng- eller siffra-litteraler, men aldrig producerar en intersections-typ. Jag har frågat tidigare, se microsoft/TypeScript#21732, om a in b typkontrollen kan producera några intersections, men det har inte implementerats. Så detta kan vara två saknade funktioner:
- inga avsmalnande generiska typparametrar, och
- ingen inbyggd typvakt som avgränsar till snitt.
Så, lösningar: Självklart är den enklaste för detta exempel att helt enkelt tilldela värdet på den generiska typade variabeln till en unionstypad variabel:
const k: Union = key;
if (k === "A") {
Global[k].toFixed();
}
Eller, du kan använda en typkonstaterande som i ovanstående as (K & "A") eller bara as "A":
if (key === "A") {
Global[key as (K & "A")].toFixed(); // okay
Global[key as "A"].toFixed(); // okay
}
Eller, om detta händer ofta, kan du skriva din egen användardefinierade typvaktfunktion, eftersom användardefinierade typvakter faktiskt producerar snitt i true-grenen av den efterföljande kontrollflödet:
const isEq =
<T extends string | number | boolean>(v: any, c: T): v is T => v === c;
if (isEq(key, "A")) {
Global[key].toFixed(); // okay, key is now of type K & "A";
}
Playground-länk till kod
Svar #2
Jag är inte säker på varför det felet uppstår, men jag är säker på att någon har en bra förklaring. Här är ett alternativt sätt att uppnå samma sak genom att använda intersection-typer med generics:
interface Config {
// There need to be different types in here for the error to occur
A: number
B: string
}
type Union = keyof Config;
const Global: Config = { A: 1, B: "Hello" };
function foo<K>(key: K & Union, x: Union) { // Use intersection type instead of inheritance
if (x === "A"){
Global[x].toFixed();
}
if (key === 'A'){
Global[key].toFixed()
}
}
Lekplatslänk
Se även denna fråga: Skillnaden mellan att utöka och korsa gränssnitt i TypeScript?
