Home > Back-end >  Typescript make return of another type generic
Typescript make return of another type generic

Time:10-23

I'm using react useReducer and have a problem with generic types.

I want to have a generic dispatch, but typescript makes my dispatch not generic.

Here is my playground

function reducer<T extends keyof State>(
  state: State,
  action: Action<T>
): State {
  return { ...state, [action.type]: action.payload };
}

const [state, dispatch] = useReducer(reducer, {
    state1: "",
    state2: 1,
});

The problem is the type of dispatch, its Dispatch<Action<keyof State>> instead of Dispatch<Action<T>>

Why the return of useReducer is not generic? it's use infer to extract type from reducer parameters but it's not extracting it as generic.

And how to makes dispatch generic?

CodePudding user response:

TypeScript can't easily synthesize the sort of higher-order generic types you would need for useReducer's output to continue to be generic. The compiler gives up and replaces the generic type parameter T with its constraint keyof State, and thus you're dealing with Action<keyof State>, a single object type whose type and payload properties are no longer separated properly.

Luckily, given this example, you don't have to try to get dispatch to be generic, because you can avoid making reducer and Action generic. Instead, you can make Action a union type of your original Action<T> type for every T in keyof State. Like this:

type Action = { [T in keyof State]-?: {
  type: T;
  payload: State[T];
} }[keyof State];

// evaluates to the union:
/* type Action = {
    type: "state1";
    payload: string;
} | {
    type: "state2";
    payload: number;
} */

That technique, where you make a mapped type and then immediately index into it to get a union, is known as making a "distributive object type", as coined in microsoft/TypeScript#47109.

And now reducer() doesn't need to be generic because its action parameter is not generic:

function reducer(
  state: State,
  action: Action
): State {
  return { ...state, [action.type]: action.payload };
}

const [state, dispatch] = useReducer(reducer, {
  state1: "",
  state2: 1,
});

And now dispatch() is a Dispatch<Action>, meaning it only accepts Action arguments, which can no longer accept mixed type and payload properties:

dispatch({ type: "state2", payload: 123 }); // okay
dispatch({ type: "state2", payload: "" }) //error

Playground link to code

  • Related