typescript - Hur tvingar man ett fälttyp att vara samma typ som ett annat fält i en generisk typ i TypeScript?
Låt oss säga att jag har den koden:
type MyStr = "one" | "two" | "three";
const x: MyType<MyStr> = { master: "one", slave: "one" }; // should compile
const y: MyType<MyStr> = { master: "one", slave: "two" }; // should not compile
Mitt mål är att skapa en typ MyType som tvingar slaven att vara samma typ som master.
En lösning är att göra så här:
type MyType<T extends string> = {
[K in T]: { master: K, slave: K }
}[T];
Idén är att skapa alla möjligheter, och ha alla versioner:
type MyType<T extends string> = {
[K in T]: { master: K, slave: K }
};
// MyType<"one" | "two" | "three"> = {
// "one": { master: "one", slave: "one" },
// "two": { master: "two", slave: "two" },
// "three": { master: "three", slave: "three" },
// }
Och att ta den typen på indexet av T ger den vi valde.
Denna metod fungerar, men när man har många fält blir TypeScript-servern överbelastad och kraschar.
Finns det ett bättre alternativ för att uppnå detta?
Självklart är detta bara en demonstration av problemet och mitt verkliga problem är att skapa något som detta:
type ElementFieldType<T, K extends keyof T> = {
name: K;
Element: FC<{ value: T[K], onChange: (value: T[K]) => void }>;
}
Så jag behöver att K tvingas vara samma typ i name och i värdet av Element, vilket betyder att när name har satts till en viss nyckel, tvingas Element att vara den typ som hanterar den nyckeln
Svar #1
Din MyType<T> distribuerar över unioner i T, så att MyType<T1 | T2 | T3> är ekvivalent med MyType<T1> | MyType<T2> | MyType<T3>. Men uppenbarligen har du några användningsfall där detta inte fungerar för dig eftersom unionen i T är för stor. Den specifika formen av MyType<T> är en distributiv objekttyp som myntades i microsoft/TypeScript#47109. Om du vill kan du ändra den till en distributiv konditionell typ istället:
type MyType<T> = T extends unknown ? { master: T, slave: T } : never;
const x: MyType<MyStr> = { master: "one", slave: "one" }; // okay
const y: MyType<MyStr> = { master: "one", slave: "two" }; // error
men om den distributiva objekttypen är för mycket för kompilatorn, är det inte självklart om en distributiv villkorlig typ skulle hjälpa tillräckligt.
Om du inte har råd att förberäkna alla möjligheter för en given T, måste du lägga till ett generiskt typeargument så att endast den relevanta möjligheten beräknas:
type MyType<T, U extends T> = { master: U, slave: U };
Här T motsvarar din ursprungliga union, medan U är medlemmen du vill stödja. Det låter dig skriva
const x: MyType<MyStr, "one"> = { master: "one", slave: "one" }; // okay
const y: MyType<MyStr, "one"> = { master: "one", slave: "two" }; // error
men nu har du "one" skrivet där inne i typen.
Det vore fantastiskt om du kunde få kompilatorn att sluta det åt dig, genom att skriva något som const x: MyType<MyStr, infer>={⋯} eller till och med bara const x: MyType<MyStr>={⋯} men det kan den inte. Det finns en öppen funktionsförfrågan på microsoft/TypeScript#32794 för det, men för tillfället är det inte möjligt och du skulle behöva arbeta runt det. För närvarande infereras generiska typargument bara när du anropar en generisk funktion, så den vanliga lösningen här skulle vara att skapa en hjälpfunktion som bara returnerar sin input, men som ger dig den inferens du vill ha.
Men även för generiska funktioner kan du inte manuellt ange ett typargument som MyType och låta kompilatorn härleda ett annat typargument. Det är allt eller inget. TypeScript skulle behöva partiell typargumentshärledning som efterfrågas i microsoft/TypeScript#26242, men återigen, det är för närvarande inte möjligt och du skulle behöva hitta en lösning. Den vanliga lösningen här är att curry:a funktionen så att istället för <T, U extends T>(m: MyType<T, U>) => MyType<T, U>, har du <T>() => <U extends T>(m: MyType<T, U>) => MyType<T, U>:
const myType = <T,>() => <U extends T>(m: MyType<T, U>) => m;
Och sedan kan du specificera T och spara resultatet till en ny funktion:
const myTypeMyStr = myType<MyStr>();
Och använd det:
const x = myTypeMyStr({ master: "one", slave: "one" }); // okay
const y = myTypeMyStr({ master: "one", slave: "two" }); // no error!! 💥
Vänta, det fungerade inte. Varför inte?
Problemet är att TypeScript mer än gärna härleder unionen "one" | "two" för T inuti myTypeMyStr och därmed inuti myType. Sättet att undvika det är att tala om för kompileraren att du kan härleda T från master men inte från slave. Det enklaste sättet att göra det är med hjälp av utility-typen NoInfer:
type MyType<T, U extends T> = { master: U, slave: NoInfer<U> };
Och nu fungerar allt annat som förväntat:
const myType = <T,>() => <U extends T>(m: MyType<T, U>) => m;
const myTypeMyStr = myType<MyStr>();
const x = myTypeMyStr({ master: "one", slave: "one" }); // okay
const y = myTypeMyStr({ master: "one", slave: "two" }); // error
Så där har du det. Det fungerar, men tyvärr är de funktioner du skulle behöva för att stödja det smidigt inte implementerade, och lösningarna är något klumpiga. Men det är det bästa du kan göra för tillfället.
Playground-länk till kod
