Home > Blockchain >  Is there a better way to tell typescript which type "data" is?
Is there a better way to tell typescript which type "data" is?

Time:09-16

I'm following an action/reducer pattern for React put forth by Kent Dodds and I'm trying to add some type safety to it.

export type Action = 
    { type: "DO_SOMETHING", data: { num: Number } } |
    { type: "DO_SOMETHING_ELSE", data: { nums: Number[] } };

type Actions = {
    [key in Action["type"]]: (state: State, data: Action["data"]) => State;
};

const actions: Actions = {
   DO_SOMETHING: (state, data) => {
       return { nums: [data.num] }; // Type error
   },
   DO_SOMETHING_ELSE: (state, data) => {
       return { nums: data.nums }; // Type error
   }
};

This code is nice because it ensures the actions object contains all the action types listed in the Action union type as well as providing type safety when trying to dispatch an action. The problem comes in when trying to access members of data.

Property 'num' does not exist on type '{ num: Number; } | { nums: Number[]; }'.
  Property 'num' does not exist on type '{ nums: Number[]; }'.

But, if I do this:

export type Action = 
    { type: "DO_SOMETHING", data: { num: Number } } |
    { type: "DO_SOMETHING_ELSE", data: { nums: Number[] } };

type Actions = {
    [key in Action["type"]]: (state: State, action: Action) => State;
};

const actions: Actions = {
   DO_SOMETHING: (state, action) => {
       if (action.type !== "DO_SOMETHING") return state;
       return { nums: [action.data.num] }; // No more type error
   },
   DO_SOMETHING_ELSE: (state, action) => {
       if (action.type !== "DO_SOMETHING_ELSE") return state;
       return { nums: action.data.nums }; // No more type error
   }
};

Now TypeScript knows action.data is the union type that matches the explicit action.type. Is there a cleaner way to do this without having to inline all the actions into a big switch statement?

CodePudding user response:

You were very close.

This line Action['data'] in (state: State, data: Action["data"]) => State; was incorrect.

Action['data'] should have been binded with key property.

See this example:

type State = {
    nums: number[]
}
export type Action =
    | { type: "DO_SOMETHING", data: { num: number } }
    | { type: "DO_SOMETHING_ELSE", data: Pick<State, 'nums'> };

type Actions = {
    [Type in Action["type"]]: (state: State, data: Extract<Action, { type: Type }>['data']) => State;
};

const actions: Actions = {
    DO_SOMETHING: (state, data) => ({ nums: [data.num] }),
    DO_SOMETHING_ELSE: (state, data) => ({ nums: data.nums })
};

Playground

I have used Type instead of key since we are iterating through types property.

Extract - expects two arguments. First - a union, second - type it should match. Treat it as an Array.prototype.filter for unions.

P.S. Please avoid using constructor types like Number, use number instead.

Interface Number corresponds to number as an object and Number as a class corresponds to class constructor:

interface Number {
    toString(radix?: number): string;
    toFixed(fractionDigits?: number): string;
    toExponential(fractionDigits?: number): string;
    toPrecision(precision?: number): string;
    valueOf(): number;
}

interface NumberConstructor {
    new(value?: any): Number;
    (value?: any): number;
    readonly prototype: Number;
    readonly MAX_VALUE: number;
    readonly MIN_VALUE: number;
    readonly NaN: number;
    readonly NEGATIVE_INFINITY: number;
    readonly POSITIVE_INFINITY: number;
}

declare var Number: NumberConstructor;

CodePudding user response:

type ActionDataMap = {
    DO_SOMETHING: { num: Number };
    DO_SOMETHING_ELSE: { nums: Number[] } 
};

type ActionType = keyof ActionDataMap
type ActionsMap = {
  [K in ActionType]: { type: K; data: ActionDataMap[K] }
}

// this will generate the union:
type Action = ActionsMap[ActionType]

// you can index into ActionsMap with K to find the specific Action
type Actions = {
    [K in ActionType]: (state: State, action: ActionsMap[K]) => State;
};
  • Related