Search code examples
typescriptfunctiontypescript-genericsreact-typescript

Typing first parameter in a typescript function based on the second optional parameter


I'm working on a state manager library for React, I wanted to add a helper function which would allow users to set deeply nested states easily from outside of React with the use of a selector and so far the helper function looks something like this:

type NestedStateSetter<State> = <Selected = State>(
    update: Selected | (() => Selected) | (() => Promise<Selected>),
    selector?: (state: State) => Selected
) => Promise<Selected>;

const initialState = {
    a: {
        b: {
            c: 0,
        },
    },
};

const setState: NestedStateSetter<typeof initialState> = (update, selector) => {
    throw "Yeet"
};

setState(1, (state) => state.a.b.c) // Ok
setState(() => 2, (state) => state.a.b.c) // Ok
setState(async () => 5, (state) => state.a.b.c) // Not ok

But the third call to 'setState' is giving us this error on the (state) => state.a.b.c of the third call above:

Argument of type '(state: { a: { b: { c: number; }; }; }) => number' is not assignable to parameter of type '(state: { a: { b: { c: number; }; }; }) => Promise'. Type 'number' is not assignable to type 'Promise'.(2345)

TypeScript Playground link
Stackblitz link

I tried to use a curried function but that would be a breaking change and I would rather avoid it as much as possible.


Solution

  • You need to order the types in your union from most specific to least specific to prevent TypeScript from matching the first, most generic type.

    So, instead of:

    update: Selected | (() => Selected) | (() => Promise<Selected>)
    

    Use:

    update: (() => Promise<Selected>) | (() => Selected) | Selected
    

    Complete snippet:

    type NestedStateSetter<State> = <Selected = State>(
      update: (() => Promise<Selected>) | (() => Selected) | Selected,
      selector?: (state: State) => Selected
    ) => Promise<Selected>;
    
    const initialState = {
      a: {
        b: {
          c: 0,
        },
      },
    };
    
    const setState: NestedStateSetter<typeof initialState> = (update, selector) => {
      throw 'Yeet';
    };
    
    setState(1, (state) => state.a.b.c); // Ok
    setState(
      () => 2,
      (state) => state.a.b.c
    ); // Ok
    setState(
      async () => 5,
      (state) => state.a.b.c
    ); // Ok
    

    Playground link