1

I have the following union of types:

export interface GetAll { type: PeopleActionTypes.GET_ALL; }
export type GetAllOk = ApiActionCreator<{ type: PeopleActionTypes.GET_ALL_OK; }>;
export type GetAllFail = ApiActionCreator<{type: PeopleActionTypes.GET_ALL_FAIL}>;
export type GetOne = ApiActionCreator<{type: PeopleActionTypes.GET_ONE}>;
export type GetOneOk = ApiActionCreator<{type: PeopleActionTypes.GET_ONE_OK}>;
export type GetOneFail = ApiActionCreator<{type: PeopleActionTypes.GET_ONE_FAIL}>;
export type Add = ApiActionCreator<{type: PeopleActionTypes.ADD}, Fetchable<Person>>;
export type AddOK = ApiActionCreator<{type: PeopleActionTypes.ADD_OK}, Fetchable<Person>[]>;
export type AddFail = ApiActionCreator<{type: PeopleActionTypes.ADD_FAIL}>;
export type Update = ApiActionCreator<{type: PeopleActionTypes.UPDATE}>;
export type UpdateOK = ApiActionCreator<{type: PeopleActionTypes.UPDATE_OK}, Fetchable<Person>[]>;
export type UpdateFail = ApiActionCreator<{type: PeopleActionTypes.UPDATE_FAIL}>;
export type Remove = ApiActionCreator<{type: PeopleActionTypes.REMOVE}>;
export type RemoveOK = ApiActionCreator<{type: PeopleActionTypes.REMOVE_OK}>;
export type RemoveFail = ApiActionCreator<{type: PeopleActionTypes.REMOVE_FAIL}>;
export type ResetPassword = ApiActionCreator<{type: PeopleActionTypes.RESET_PASSWORD}>;
export type ResetPasswordOK = ApiActionCreator<{type: PeopleActionTypes.RESET_PASSWORD_OK}>;
export type ResetPasswordFail = ApiActionCreator<{type: PeopleActionTypes.RESET_PASSWORD_FAIL}>;

export interface SetCurrent {
  type: PeopleActionTypes.SET_CURRENT;
  id: string;
};

export type PeopleActionCreators =
    SetCurrent
    | GetAll
    | GetAllOk
    | GetAllFail
    | GetOne
    | GetOneOk
    | GetOneFail
    | Add
    | AddOK
    | AddFail
    | Update
    | UpdateOK
    | UpdateFail
    | Remove
    | RemoveOK
    | RemoveFail
    | ResetPassword
    | ResetPasswordOK
    | ResetPasswordFail;

With my ApiActionCreator looking like this:

export type ApiActionCreator<T extends object, Payload = object | any[] | undefined> = T & { payload: Payload, error: ErrorMessage }

For every operation, I have a Xxx, XxxOK and a XxxFail.

Is there anyway in typescript I can somehow generate these types rather than having to create all 3 for ever operataion?

dagda1
  • 26,856
  • 59
  • 237
  • 450

1 Answers1

1

You can avoid having to declare all the unions by using a conditional, conditional types distribute over a type parameter that contains a union. Using this behavior we can apply ApiActionCreator to all members of a union of enum literals.

We can get the union of all enum of literals in PeopleActionTypes excluding SET_CURRENT, which is treated in a different way, using the Exclude conditional type (type PeopleActionTypesKeys = Exclude<PeopleActionTypes, PeopleActionTypes.SET_CURRENT>)

The only problem that remains is the custom payloads for some action types. We can use an object type as a map to keep the relation between the enum member and the payload type.

type GetPayload<TPayloadMap, T extends PropertyKey> = TPayloadMap extends Record<T, infer U> ? U : undefined;

export type StandardActions<TEnumKeys, TPayloadMap> =
    TEnumKeys extends any  ? ApiActionCreator<{type: TEnumKeys }, GetPayload<TPayloadMap, TEnumKeys>> 
    : never ;


export interface SetCurrent {
    type: PeopleActionTypes.SET_CURRENT;
    id: string;
};

type PeopleActionTypesKeys = Exclude<PeopleActionTypes, PeopleActionTypes.SET_CURRENT>

export type PeopleActionCreators = SetCurrent | StandardActions<PeopleActionTypesKeys, {
    [PeopleActionTypes.ADD]: Fetchable<Person>,
    [PeopleActionTypes.UPDATE]: Fetchable<Person>,
    [PeopleActionTypes.ADD_OK]: Fetchable<Person>,
}>;

The solution above has less duplication, unfortunately you lose the nice name for the type aliases, if you hover over PeopleActionCreators you see:

type PeopleActionCreators = SetCurrent | ApiActionCreator<{
    type: PeopleActionTypes.GET_ALL;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.GET_ALL_OK;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.GET_ALL_FAIL;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.GET_ONE;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.GET_ONE_OK;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.GET_ONE_FAIL;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.ADD;
}, Fetchable<Person>> | ApiActionCreator<{
    type: PeopleActionTypes.ADD_OK;
}, Fetchable<Person>> | ApiActionCreator<{
    type: PeopleActionTypes.ADD_FAIL;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.UPDATE;
}, Fetchable<Person>> | ApiActionCreator<{
    type: PeopleActionTypes.UPDATE_OK;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.UPDATE_FAIL;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.REMOVE;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.REMOVE_OK;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.REMOVE_FAIL;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.RESET_PASSWORD;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.RESET_PASSWORD_OK;
}, undefined> | ApiActionCreator<{
    type: PeopleActionTypes.RESET_PASSWORD_FAIL;
}, undefined>

That is considerably less readable, even if it does the same, also the code itself might be more difficult to understand for others.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • This is brilliant even if I don't end up using it :). Very educational. One thing I do not get is `TEnumKeys extends any` as a condition. How is this not always true? – dagda1 Jan 09 '19 at 14:09
  • @dagda1 That is always the question I get when I do this kind of solution :)). It is always true, but we are not interesting in the conditional behavior of conditional types, we are interested in their distributive behavior, the condition does not matter. See docs for the distributive behavior https://www.typescriptlang.org/docs/handbook/advanced-types.html or for my simpler explanation here: https://stackoverflow.com/questions/51651499/typescript-what-is-a-naked-type-parameter/51651684#51651684 – Titian Cernicova-Dragomir Jan 09 '19 at 14:12
  • I see, that makes sense. This is great. Might be too unreadable for others but as I said very educational. – dagda1 Jan 09 '19 at 14:13
  • what is this actually saying `type GetPayload = TPayloadMap extends Record ? U : undefined;` If `TPayloadMap` is a Record, return the generic type passed to Record? – dagda1 Jan 09 '19 at 14:16
  • @dagda1 Everything is basically a `Record` in some way. It says if the key `T` is present in `TPayloadMap` give me the type associated with it in `U `. – Titian Cernicova-Dragomir Jan 09 '19 at 14:18
  • the second type argument to `ApiActionCreator` is always undefined. So this is saying that the key `T` is never found in the `TPayloadMap`? – dagda1 Jan 10 '19 at 07:04
  • @dagda1 I should not always be `undefined` in the expanded type I posted (which I copied form a tooltip, so it's the compiler not me saying it) the second argument is not always undefined, it's just hard to spot the not undefined spots ..`ApiActionCreator<{ type: PeopleActionTypes.UPDATE; }, Fetchable>` – Titian Cernicova-Dragomir Jan 10 '19 at 07:42
  • I'm confused how it would ever be undefined. Thanks for taking the time to answer these questions – dagda1 Jan 10 '19 at 08:23
  • @dagda1 if you want we can chat more on gitter.im. The undefined comes from the conditional type, I chose undefined as a default, you can change that: `TPayloadMap extends Record ? U : undefined /*THIS ONE*/;` – Titian Cernicova-Dragomir Jan 10 '19 at 08:25
  • now I see, because the _FAIL and _OK are not in the keymap. Sir, I salute you – dagda1 Jan 10 '19 at 08:27