typescript - Hur säkerställer jag att strängar inom ett nästlat objekt refererar till en av objektets nycklar
Hur får jag TypeScript att begränsa en strängtyp till att endast vara nycklar inom samma objekt? Ett förenklat exempel finns nedan. Jag har provat en mängd olika K extends string-typologiker men är fast.
{
players: {
uniquePlayerId1: {
isTargeting: 'uniquePlayerId2' // should only be able to be `uniquePlayerId1` or `uniquePLayerId2` or any other keys of the `players` object
},
uniquePlayerId2: {
isTargeting: 'uniquePlayerId1', // should only be able to be `uniquePlayerId1` or `uniquePLayerId2` or any other keys of the `players` object
},
}
}
Mitt användningsfall är att jag bygger ett spel - exemplet är förenklat, men i princip kan varje spelare rikta in sig på en annan spelare - vilket indikeras av värdet på isTargeting - antingen en sträng, som jag vill säkerställa är en av nycklarna i objektet, t.ex. uniqueId1 eller uniqueId2 i exemplet, eller null - de riktar sig inte mot någon.
Svar #1
Det finns ingen specifik typ i TypeScript som fungerar på det här sättet; om du statiskt kände till listan över nycklar K i players, (säg "uniquePlayerId1" | "uniquePlayerId2") för valfri specifik uppsättning av nycklar i players, då skulle du kunna definiera en specifik typ PlayersType<K> utifrån den på följande sätt
type PlayersType<K extends string> =
{ players: Record<K, { isTargeting: K }> }
Men eftersom du inte känner till K i förväg vill du definiera SomePayersType som är lika med PlayersType<K> för någon K. Denna typ av typ kallas en existentiellt kvantifierad generisk typ och, även om det har efterfrågats (se microsoft/TypeScript#14466), stöder TypeScript inte direkt sådana typer. Liksom de flesta språk med generics har TypeScript bara universellt kvantifierade generiska typer. Om vi hade existentiellt kvantifierade generics kanske du skulle kunna säga:
// not valid TypeScript, don't try this:
type SomePlayersType = <exists K extends string> PlayersType<K>
Men för tillfället kan du inte det. Det finns sätt att emulera existentiella typer med universella typer men de är opraktiska och inte nödvändigtvis motiverade för ditt användningsfall.
Istället kan du ta PlayersType<K> och göra en generell hjälpar-identityfunktion som härleder K åt dig från det passerade värdet. Det kan se ut så här:
const asPlayers = <K extends string>(
obj: PlayersType<K>
) => obj;
Men tyvärr fungerar detta inte riktigt som du vill; kompilatorn härleder faktiskt K från isTargeting-värdena och inte från nycklarna i players, vilket innebär att den kommer att avvisa giltiga argument:
const players = asPlayers({
players: {
uniquePlayerId1: {
isTargeting: 'uniquePlayerId2' // error?!
},
uniquePlayerId2: {
isTargeting: 'uniquePlayerId2'
},
}
})
Vi skulle vilja tala om för kompilatorn att inte härleda K från isTargeting; det vill säga, göra isTargeting till en icke-härledande typ-parameter-användning av K, som efterfrågats i microsoft/TypeScript#14829. Det finns olika sätt att få ett sådant beteende; i det här fallet kan vi helt enkelt lägga till en ny typ-parameter L som är begränsad till K, och använda K för nycklarna och L för isTargeting:
const asPlayers = <K extends string, L extends K>(
obj: { players: Record<K, { isTargeting: L }> }
): PlayersType<K> => obj;
Nu låt oss använda det:
const players = asPlayers({
players: {
uniquePlayerId1: {
isTargeting: 'uniquePlayerId2'
},
uniquePlayerId2: {
isTargeting: 'uniquePlayerId1'
},
}
})
/* const players: PlayersType<"uniquePlayerId1" | "uniquePlayerId2"> */
Det ser bra ut; kompilatorn härleder att players är av typen PlayersTaype<"uniquePlayerId1" | "uniquePlayerId2">. Låt oss se vad som händer om vi skickar in något dåligt:
const badPlayers = asPlayers({
players: {
uniquePlayerId1: {
isTargeting: "oopsie" // error
// Type '"oopsie"' is not assignable to
// type '"uniquePlayerId1" | "uniquePlayerId2"'
},
uniquePlayerId2: {
isTargeting: 'uniquePlayerId1'
}
}
})
Här ser vi det önskade kompileringsfelet; eftersom det inte finns någon nyckel som heter "oopsie", kan inte egenskapen isTargeting namnges "oopsie".
Så där har du det. Istället för att ha en specifik SomePlayersType-typ, har du en PlayersType<K>-typ där du härleder K från värdet. Detta är lite som "halvvägs" till en existentiell typ, och det kan vara tillräckligt för dina behov. Du kan upptäcka att du bär runt på den här extra typparametern, så kanske vill du använda något enklare som PlayersType<string> i koden du kontrollerar, och bara tillämpa härledningen av K för mindre betrodd kod:
function publicFacingCode<K extends string>(players: PlayersType<K>) {
innerLibraryCode(players);
}
function innerLibraryCode(players: PlayersType<string>) {
}
Lekplatslänk till kod
