typescript - Härleda generiska nycklar vid redigering av Record
Detta kodexempel är en funktion för kortfattad oföränderlig redigering av en post för att ställa in boolean-värden.
Funktionen ska acceptera en post med booleska värden och en lista med matchande nycklar. Den ska returnera en ny post som har alla dessa nycklar satta till true (med ett 'oföränderligt' uppdateringsmönster där den ursprungliga posten inte ändras).
De fel jag får är så grundläggande att jag känner att jag behöver ett par extra ögon. Jag måste missa något. Hur kan jag konfigurera generics för att få koden nedan att kompilera och köras på ett rimligt sätt?
function createNextFlags<Key extends string>(
flags: Record<Key, boolean>,
...keys: [Key, ...Key[]]
) {
const nextFlags = {
...flags
}
for (const key of keys) {
nextFlags[key] = true;
}
return nextFlags;
}
createNextFlags({
vanilla:false,
chocolate:true, // this line generates a compiler error because flags constraint is too narrow
}, "vanilla")
Du kan leka med problemet på denna lekplats
Den felaktiga raden antyder att det finns en svårighet med att Typescript härleder Key för snävt. Genom att enbart härleda från keys-arrayen, istället för att försöka härleda även från flags-objektet, slutar det med att klaga på att flags-objektet är ogiltigt om det har några egenskapsnamn som inte finns i keys...
MOTIVERANDE EXEMPEL
Även om detta är ett mycket enkelt exempel, har det mycket mer komplexa exempel jag arbetar med en liknande natur, med ett felvillkor som liknar det för kontroll av överflödiga egenskaper här - med andra ord styr keys den begränsande typen av flags när det borde härledas åt andra hållet - keys bör härledas från egenskaperna hos flags.
LÖSNINGAR
Man skulle tro att lösningen nedan skulle skapa en platshållare för typen av flags-objektet....
// Define Flags explicitly
function createNextFlags<Flags extends Record<Key, boolean>, Key extends string>(
flags: Flags,
...keys: [Key, ...Key[]]
) {
const nextFlags = {
...flags
}
for (const key of keys) {
nextFlags[key] = true; // this assignment is apparently illegal!
}
return nextFlags;
}
createNextFlags({
vanilla:false,
chocolate:true,
}, "vanilla")
Men metoden skapar ett ännu konstigare fel. Att hovra över till synes felaktiga tilldelningen till nextFlags-egenskapen visar de överraskande felraderna (jag skojar inte)...
const nextFlags: Flags extends Record<Key, boolean>
Type 'boolean' is not assignable to type 'Flags[Key]'
Jag har också försökt använda keyof för att härleda nycklarna direkt från typen av flags på det här sättet med identiska resultat, även om det helt eliminerar den generiska Key och gör typen av keys helt härledd från flags.
// use keyof to ensure that the property name aligns
function createNextFlags<Flags extends Record<any, boolean>>(
flags: Flags,
...keys: [keyof Flags, ...(keyof Flags)[]]
)
Men det har precis samma typ av fel
const nextFlags: Flags extends Record<any, boolean>
Type 'boolean' is not assignable to type 'Flags[keyof Flags]'
Jag har också försökt använda infer för att härleda nycklarna direkt från typen av flags på det här sättet...
type InferKey<Flags extends Record<any, boolean>> = Flags extends Record<infer Key, boolean> ? Key: never;
function createNextFlags<Flags extends Record<any, boolean>>(
flags: Flags,
...keys: [InferKey<Flags>, ...InferKey<Flags>[]]
)
Detta leder till ett liknande överraskande fel...
const nextFlags: Flags extends Record<any, boolean>
Type 'boolean' is not assignable to type 'Flags[InferKey<Flags>]'
Vad är det rätta sättet att närma sig detta problem, så att Key-typen kan härledas från flags-objektet, för att begränsa keys-argumentet? Vad missar jag?
Svar #1
När du anropar en generisk funktion använder TypeScript olika heuristiker för att avgöra hur dess typargument ska härledas. Generellt finns det härledningsplatser som kompilatorn kan konsultera för att generera kandidater för det generiska typargumentet (via vissa heuristiker), och sedan, vid flera kandidater, behöver den lista ut hur man väljer mellan dem eller en kombination av dem (via andra heuristiker). Dessa heuristiker fungerar ganska bra över ett brett spektrum av situationer, men naturligtvis finns det fall där kompilatorn gör saker som funktionsdesignern inte vill.
För funktionen med anropssignaturen
function createNextFlags<K extends string>(
flags: Record<K, boolean>,
...keys: [K, ...K[]]
): Record<K, boolean>;
heuristikerna som för närvarande är implementerade prioriterar inferensplatserna i keys och inte den i flags, och därför får du det problematiska beteende som beskrivs i frågan.
Det skulle vara trevligt om du som funktionsdesigner kunde säga till kompilatorn: "försök inte att härleda K från keys. Härled det från flags och kontrollera sedan bara det mot värdet som skickas in för keys". Du skulle säga att K-uttalandena i keys är icke-härledande typparameteranvändningar. Detta är ämnet för microsoft/TypeScript#14829, en begäran om någon NoInfer<T> nyttotyp (förmodligen en intrinsic typ, som beskrivs i microsoft/TypeScript#40580) som utvärderas till T men blockerar härledning.
Om det fanns skulle du skriva
declare function createNextFlags<K extends string>(
flags: Record<K, boolean>,
...keys: [NoInfer<K>, ...NoInfer<K>[]]
): Record<K, boolean>;
och saker skulle börja fungera på det sätt du vill.
Tyvärr finns det för närvarande ingen sådan inbyggd hjälpfunktion som fungerar på det här sättet. Kanske kommer en att introduceras i en TypeScript-version någon dag, men för tillfället finns den inte.
Lyckligtvis finns det olika implementationer på användarnivå som fungerar för åtminstone vissa användningsfall. En visas här, där vi utnyttjar kompilatorns tendens att skjuta upp utvärderingen av generiska villkorstyper till vår fördel:
type NoInfer<T> = [T][T extends any ? 0 : never];
Och nu kan vi prova det:
createNextFlags({
vanilla: false,
chocolate: true,
}, "vanilla"); // okay
createNextFlags({
dog: true,
cat: false
}, "cat", "dog"); // okay
createNextFlags({
red: true,
green: false,
blue: true
}, "green", "purple", "blue"); // error!
// -------> ~~~~~~~~ // okay
Ser bra ut! Kompilatorn härleder K från flags-argumentet och kontrollerar mot de efterföljande argumenten, som önskat. Den ovanstående definitionen av NoInfer<T> fungerar inte nödvändigtvis i alla användningsfall (GitHub-ärendet beskriver några fel) så det är ingen lösning på alla problem. Men om det fungerar för dina ändamål kan det vara tillräckligt bra.
Lekplatslänk till kod
