typescript - Härleda generisk typ i ordboksattribut
Jag har ett generiskt gränssnitt där jag vill att en egenskap endast ska inkludera nycklar från en annan egenskap, och ha samma typ också....
interface IMyInterface<T> {
propA: T;
propB: { [key in keyof T]?: T[key] };
}
Det här är okej, jag har gjort liknande saker många gånger tidigare.... I princip vill jag att det ska härleda vad T är baserat på vad jag skickar till propA.
Men här är mitt problem, jag har en ordbok med många IMyInterface-typer....
interface IMyDictionary {
[key: string]: IMyInterface;
}
Här är var mitt problem är, den ovanstående definitionen kräver en generisk typ... men vilken typ jag än använder skulle tillämpas på varje egenskap. Om jag använder any så motverkar det syftet, eftersom när jag deklarerar min ordbok antar den att allt är any.
interface IInterfaceA {
name: string;
age: number;
}
const objA: IInterfaceA = {
name: 'John',
age: 18
}
const myDictionary: IMyDictionary {
test1: {
propA: objA,
propB: { age: 40 }
}
}
Så i princip, när jag tilldelar värdet till myDictionary ovan, vill jag att det ska härleda att propA är av typen IInterfaceA, och i propB endast tillåta egenskaperna name med typen string och age med typen number.
Jag skulle enkelt kunna göra detta genom att inte definiera typen IMyDictionary, och skapa varje objekt ett i taget, och sedan tilldela dem till ett nytt objekt (myDictionary)….
const test1: IMyInterface = {
propA: objA,
propB: { age: 40 }
}
const myDictionary = { test1 }
Detta skulle härleda den generiska typen för test1, och sedan härleda typen för myDictionary, genom att använda exakta egenskapsnamn, istället för en array av strängar. Men jag skulle hellre använda vanlig objekt-assignering om det är möjligt.
Jag kan inte lista ut ett sätt att deklarera typen av IMyDictionary för att tvinga den att härleda den generiska typen för varje instans av IMyInterface. Alternativt är jag öppen för idéer om olika sätt att definiera IMyInterface för att härleda egenskaperna och typerna för propB.
Tack i förväg!
Svar #1
Den typ du letar efter är ett uppslagsverk där varje post är någon IMyInterface<T>-typ, men inte någon specifik T. Detta uttrycks förmodligen bäst som en existentiell typ, som (eventuellt) {[k: string]: IMyInterface<exists T>}, vilket för närvarande inte stöds nativt i TypeScript (eller i de flesta andra språk med generics). TypeScript (och de flesta andra språk med generics) har bara universella typer: någon som vill ha ett värde av typen X<T> kan specificera vilken typ som helst för T som de vill, och den som tillhandahåller värdet måste kunna uppfylla detta. En existentiell typ är motsatsen: någon som vill tillhandahålla ett värde av en typ som X<exists T> kan välja vilken specifik typ som helst för T som de vill, och mottagaren av det värdet måste bara följa detta. Existentiella typer låter dig "dölja" den generiska typen inuti en icke-generisk typ.
Men TypeScript har inte existentiella typer, så vi måste göra något annat. (Tja, det har inte inbyggda existentiella typer. Du kan emulera dem genom att använda generiska funktioner och omvänd styrning via återuppringningar, men det är ännu mer komplicerat att använda än lösningen jag kommer att föreslå härnäst. Om du fortfarande är intresserad av existentiella typer kan du läsa den länkade artikeln om det)
Det näst bästa vi kan göra är att beskriva formen av IMyDictionary som en generisk typ, där vi egentligen inte bryr oss om det, och låta kompilatorn härleda det åt oss så mycket som möjligt. Här är ett sätt:
type IMyDictionary<T> = { [K in keyof T]: IMyInterface<T[K]> }
const asIMyDictionary = <T>(d: IMyDictionary<T>) => d;
Idén är att istället för att deklarera en variabel som typen IMyDictionary, använder du hjälpfunktionen asIMyDictionary() för att ta värdet och producera en lämplig typ IMyDictionary<T>. Detta fungerar bara, förresten, eftersom typen IMyDictionary<T> är en homomorfisk mappad typ, så T kan härledas från ett värde av typen IMyDictionary<T>. Låt oss se det i praktiken:
const myDictionary = asIMyDictionary({
test1: {
propA: { name: "John", age: 18 },
propB: { age: 40 }
},
test2: {
propA: { size: 10, color: "blue" },
propB: { color: "purple" }
},
test3: {
propA: { problem: true },
propB: { oops: false } // error!
// ~~~~~~~~~~~~
// 'oops' does not exist in type '{ problem?: boolean | undefined; }'
}
});
Det här fungerar och ger dig fel där du förväntar dig dem. Hurra!
Här är varningen att du fortfarande måste hantera denna T-typ som du inte bryr dig om. Alla funktioner eller typer som du hoppades bara skulle hantera ett konkret IMyDictionary måste bli generiska. Kanske är det inte alltför besvärligt för dig. Om det är det kan du fundera på den "emulerade" existentiella typen. Men jag har redan skrivit mycket här, så förhoppningsvis hjälper det ovanstående dig... om du behöver att jag skriver ut den emulerade existentiella versionen kan jag göra det.
Lycka till!
