javascript - Kontrollera Typescript-generics mot flera begränsningar?
Till exempel har jag en funktion för att acceptera en array av konfigurationer. Varje arrayelement kan vara en delmängd av konfigurationen. Jag kommer att slå ihop varje arrayelement till en enda konfiguration, som bör matcha Config-typen.
Jag gör det för närvarande på det här sättet:
type Config = {
a: number,
b: number,
c?: number,
};
function fn(arr: Partial<Config>[]) {}
fn([{ a: 1, b: 1, c: 1 }]); // ok
fn([{ a: 1 }, { b: 1 }]); // ok
fn([{ a: 1 }]); // invalid config, but no TS error
Det sista funktionsanropet saknar den obligatoriska b-egenskapen. Jag vill kontrollera hela arrayen mot Config-typen. Jag listade ut denna hack som verkar fungera:
// From https://stackoverflow.com/a/50375286
type UnionToIntersection<U> =
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never;
function fn<T extends Partial<Config>>(
arr: T[] & (UnionToIntersection<T> extends Config ? any : never),
) {}
fn([{ a: 1, b: 1, c: 1 }]); // ok
fn([{ a: 1 }, { b: 1 }]); // ok
fn([{ a: 1 }]); // Type 'number' is not assignable to type 'never'
TS Lekplats
Finns det ett icke-slarvigt sätt att göra detta? Helst skulle felmeddelandet vara tydligare.
Redigera:
Som jcalz påpekade i kommentarerna, fungerar inte min lösning för fn([{ a: 1 }, { c: 3 }]). Detta beror på:
const arr = [{ a: 1 }, { c: 3 }];
type T = (typeof arr)[number];
/* type T = {
a: number;
c?: undefined;
} | {
c: number;
a?: undefined;
} */
type T2 = {
a: number;
c?: undefined;
} & {
c: number;
a?: undefined;
}; // never
Jag trodde att (typeof arr)[number] skulle vara { a: number } | { b: number }. Detta åtgärdar det:
type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T];
type OmitOptional<T> = T extends any ? Omit<T, OptionalKeys<T>> : never;
function fn<T extends Partial<Config>>(
arr: T[] & (UnionToIntersection<OmitOptional<T>> extends Config ? any : Config[]),
) {}
fn([{ a: 1 }, { c: 1 }]); // Property 'b' is missing in type '{ a: number; }' but required in type 'Config'.
Svar #1
Om man antar att anropare av fn() skickar in en array som de redan hade liggande istället för att alltid skicka in ett array-litteral, kanske kompilatorn inte vet de specifika typerna för varje element i arrayen. Det vill säga, den inskickade arrayen kanske inte är av en tuple-typ:
Till exempel:
const arr = [{ a: 1 }, { b: 2 }];
/* const arr: ({
a: number;
b?: undefined;
} | {
b: number;
a?: undefined;
})[] */
Här härleder kompilatorn att arr är av typen Array<{a: number; b?: undefined} | {b: number; a?: undefined}>. Och för allt kompilatorn vet eller bryr sig om, kan det vara en tom array, eller en array med ett värde, eller en array med hundra värden alla av typen, säg, {a: number; b?: undefined}:
if (Math.random() < 0.5) {
arr.pop(); // 🤷♂️
}
Så till viss del är det du vill inte generellt möjligt. Eller åtminstone måste du bestämma om du ska tillåta vissa arrayer som visar sig inte ha alla egenskaper i Config, eller om du ska avvisa vissa arrayer som visar sig ha alla egenskaper i Config. Falska negativa och falska positiva är förmodligen oundvikliga.
Med det sagt, kommer jag att föreslå en "bästa försök" som har en viss risk för falska negativa genom att anta att ett värde av typen Array<X | Y | Z> (en array vars elementtyp är en union) har minst ett element av varje typ X, Y och Z. Detta är liknande att anta att den sammanslagna konfigurationen kommer att ha snittstypen X & Y & Z, vilket är vad du gjorde med UnionToIntersection.
Och en version av UnionToIntersection skulle kunna göras för att fungera när man tar hänsyn till den ofta hjälpsamma men här frånvarande normaliseringen av objektliteral som bidrar till unionstyper. Eftersom [{a: 1}, {c: 2}] producerar typer som {a: number, c?: undefined} | {c: number, a?: undefined}, kommer en enkel intersection att ge never. Du skulle programmässigt kunna bearbeta elementtypen till {a: number} | {c: number} och sedan intersecta det för att få {a: number; c: number}. Men istället kommer jag göra något lite annorlunda:
type AllKeys<T> = T extends unknown ? keyof T : never;
function fn<T extends Partial<Config>>(
arr: T[] & (Pick<Config, AllKeys<T>> extends Config ? unknown : Config[]),
) { }
Så AllKeys<T> kommer att producera en union av alla nycklar som finns i någon unionmedlem av T. Medan keyof ({a: string} | {b: number}) är never, är AllKeys<{a: string} | {b: number}) "a" | "b". Om vi antar att arrayen har minst ett element av varje unionmedlemstyp, och vi antar att varje nyckel definitivt finns i minst ett element, kan vi dra slutsatsen att den sammanslagna konfigurationen kommer att ha alla nycklar i AllKeys<T>. Och eftersom T extends Partial<Config> är sant, kan vi dra slutsatsen att den sammanslagna konfigurationen kommer att vara av typen Pick<Config, AllKeys<T>> (med hjälp av hjälptypen Pick<T, K>).
Typen Pick<Config, AllKeys<T>> bör vara ganska lik UnionToIntersection<T>, förutom att den senare är alltför känslig för de specifika egenskapstyperna hos T, medan den förstnämnda egentligen bara bryr sig om nycklar, med förtroende för att värdena kommer att vara okej eftersom T extends Partial<Config>.
För att göra felmeddelandet "trevligt" (eller åtminstone "inte hemskt"), intersectar vi T[] (den infererade typen av arr) med den villkorliga typen Pick<Config, AllKeys<T>> extends Config ? unknown : Config[]. Om T har alla nödvändiga nycklar i Config, kommer detta resultera i T[] & unknown vilket är T[] (intersectioner med unknown-topptypen absorberar unknown). Om T saknar någon obligatorisk nyckel, kommer detta resultera i T[] & Config[], vilket innebär att kompilatorn plötsligt kommer förvänta sig att varje element i arr ska vara en fullständig Config istället för bara en Partial<Config>. Detta kommer troligen att innebära att varje array-element har ett fel kopplat till sig, vilket är överdrivet, men åtminstone har meddelandet en chans att klaga på att du saknar några nycklar istället för att inte vara never.
Låt oss prova det:
fn([{ a: 1, b: 1, c: 1 }]); // okay
fn([{ a: 1 }, { b: 1 }]); // okay
fn([{ a: 1 }]); // error! Property 'b' is missing
fn([{ a: 1 }, { c: 3 }]) // error! 'b' is missing
Ser bra ut! Kompilatorn accepterar de första två anropen och klagar med rätta på att b saknas i de sista två fallen.
Som påpekats tidigare finns det dock falska negativa i fall där antagandena om att ha åtminstone ett element som innehåller varje nyckel från elementtypen:
const emptyConfigs: Config[] = [];
fn(emptyConfigs); // no error, but problem at runtime
Och om du kastar bort information som kompilatorn behöver för att verifiera nycklarna i arr, kan det också bli falska positiva resultat:
const okay = [{ a: 1, b: 1 }];
const falsePositive: Array<{ a: number }> = okay;
fn(falsePositive); // error, but fine at runtime
Men förhoppningsvis är dessa acceptabla för dig.
Lekplatslänk till kod
