typescript - Typekontroll createAction Redux-hjelper
Jeg har Action-typen definert slik:
type Action =
{ type: 'DO_X' }
|
{ type: 'DO_Y', payload: string }
|
{ type: 'DO_Z', payload: number }
Det er en unionstype hvor hvert medlem er en gyldig handling.
Nå vil jeg lage en funksjon createAction som tar type og returnerer en ny funksjon som tar payload.
const doZ = createAction('DO_Z')
console.log(doZ(42)) // { type: 'DO_Z', payload: 42 }
Her er min nåværende implementering:
const createAction = (type: Action['type']) =>
(payload?: any) =>
({ type, payload })
Det typekontrollerer type slik jeg vil. Hvordan kan jeg også typekontrollere payload? Jeg vil at payload skal samsvare med typen til korrekt handling basert på type. For eksempel skal doZ feile når den kalles med en string fordi payload indikerer at den kun godtar number.
Svar #1
Det kanoniske svaret på dette spørsmålet avhenger av ditt eksakte bruksområde. Jeg antar at du trenger at Action evalueres nøyaktig til typen du skrev; det vil si at et objekt av type: "DO_X" ikke har en payload-egenskap av noe slag. Dette innebærer at createAction("DO_X") skal være en funksjon uten argumenter, mens createAction("DO_Y") skal være en funksjon med ett string-argument. Jeg antar også at du vil at eventuelle typeparametere på createAction() skal bli automatisk utledet, slik at du for eksempel ikke trenger å spesifisere createAction<Blah>("DO_Z") for noen verdi av Blah. Hvis noen av disse begrensningene fjernes, kan du forenkle løsningen til noe lignende det som er gitt av @Arnavion.
TypeScript liker ikke å mappe typer fra egenskaps verdier, men det er glad for å gjøre det fra egenskaps nøkler. Så la oss bygge Action-typen på en måte som gir oss typer kompilatoren kan bruke for å hjelpe oss. Først beskriver vi payloadene for hver handlingstype slik:
type ActionPayloads = {
DO_Y: string;
DO_Z: number;
}
La oss også introdusere alle Action-typer uten nyttelast:
type PayloadlessActionTypes = "DO_X" | "DO_W";
(Jeg har lagt til en 'DO_W' type bare for å vise hvordan det fungerer, men du kan fjerne den).
Nå er vi endelig i stand til å uttrykke Action:
type ActionMap = {[K in keyof ActionPayloads]: { type: K; payload: ActionPayloads[K] }} & {[K in PayloadlessActionTypes]: { type: K }};
type Action = ActionMap[keyof ActionMap];
Typen ActionMap er et objekt der nøklene er type for hver Action, og verdiene er de tilsvarende elementene i Action-unionen. Det er skjæringspunktet mellom Action-ene med payload og Action uten payload. Og Action er bare verditypen til ActionMap. Bekreft at Action er det du forventer.
Vi kan bruke ActionMap for å hjelpe oss med å skrive createAction()-funksjonen. Her er den:
function createAction<T extends PayloadlessActionTypes>(type: T): () => ActionMap[T];
function createAction<T extends keyof ActionPayloads>(type: T): (payload: ActionPayloads[T]) => ActionMap[T];
function createAction(type: string) {
return (payload?: any) => (typeof payload === 'undefined' ? { type } : { type, payload });
}
Det er en overbelastet funksjon med en typeparameter T som tilsvarer type av Action du oppretter. De to øverste deklarasjonene beskriver de to tilfellene: Hvis T er type av en Action uten payload, er returtypen en null-argument-funksjon som returnerer riktig type Action. Ellers er det en én-argument funksjon som tar riktig type payload og returnerer riktig type Action. Implementasjonen (den tredje signaturen og kroppen) ligner din, bortsett fra at den ikke legger til payload i resultatet hvis det ikke er sendt inn payload.
Alt ferdig! Vi kan se at det fungerer som ønsket:
var x = createAction("DO_X")(); // x: { type: "DO_X"; }
var y = createAction("DO_Y")("foo"); // y: { type: "DO_Y"; payload: string; }
var z = createAction("DO_Z")(5); // z: { type: "DO_Z"; payload: number; }
createAction("DO_X")('foo'); // too many arguments
createAction("DO_X")(undefined); // still too many arguments
createAction("DO_Y")(5); // 5 is not a string
createAction("DO_Z")(); // too few arguments
createAction("DO_Z")(5, 5); // too many arguments
Du kan se alt dette i aksjon på TypeScript-lekeplassen. Håper det fungerer for deg. Lykke til!
Svar #2
Ordrik, men dette fungerer:
type XAction = { type: 'DO_X', payload: undefined };
type YAction = { type: 'DO_Y', payload: string };
type ZAction = { type: 'DO_Z', payload: number };
type Action = XAction | YAction | ZAction;
const createAction = <T extends Action>(type: T['type']) =>
(payload: T['payload']) =>
({ type, payload });
// Do compile:
createAction<XAction>("DO_X")(undefined);
createAction<YAction>("DO_Y")("foo");
createAction<ZAction>("DO_Z")(5);
// Don't compile:
createAction<XAction>("DO_X")(5); // Expected `undefined`, got number
createAction<YAction>("DO_Y")(5); // Expected string, got number
createAction<ZAction>("DO_X")(5); // Expected `"DO_Z"`, got `"DO_X"`
Den enklere måten (uten å tvinge typens parameter for createAction):
type Action = { type: 'DO_X', payload: undefined } | { type: 'DO_Y', payload: string } | { type: 'DO_Z', payload: number };
createAction("DO_Y")("foo");
lar dessverre createAction<YAction>("DO_Y")(5) osv. kompilere, siden T alltid blir tolket som Action og dermed er payload-parameteren string|number|undefined
