Home > database >  Typescript function overload with rest parameters
Typescript function overload with rest parameters

Time:10-06

I have a code like this, with a couple of overloaded functions with rest parameters:

    type IAuthSelectors =
      | 'selector1'
      | 'selector2'
      | 'selector3'
      | 'selector4'
      | 'selector5'
      | 'selector6';

    function getAuthState(selector: 'selector1'): string[];
    function getAuthState(selector: 'selector2', param1: string): boolean;
    function getAuthState(selector: 'selector3'): boolean;
    function getAuthState(selector: 'selector4'): string;
    function getAuthState(selector: 'selector5'): MyInterface;
    function getAuthState(selector: 'selector6'): string;
    function getAuthState(selector: IAuthSelectors, ...selectorArguments: unknown[]): unknown
    function getAuthState(selector: IAuthSelectors, ...selectorArguments: unknown[]): unknown {
      // return logic
    }

    function useAuthSelector(selector: 'selector1'): string[];
    function useAuthSelector(selector: 'selector2', param1: string): boolean;
    function useAuthSelector(selector: 'selector3'): boolean;
    function useAuthSelector(selector: 'selector4'): string;
    function useAuthSelector(selector: 'selector5'): MyInterface;
    function useAuthSelector(selector: 'selector6'): string;
    function useAuthSelector(selector: IAuthSelectors, ...selectorArguments: unknown[]): unknown {
      const authState = getAuthState(selector, ...selectorArguments); // ERROR A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556)

      // ...
    }

But I cannot make one to call the other. The error: ERROR A spread argument must either have a tuple type or be passed to a rest parameter.ts(2556) is pretty clear. But if I try to cast it to a tuple it breaks. I have also tried checking the number of rest parameters to call it with parameters or not, but it doesn't work.

Right now, this has only one "selector" with rest parameters, but there could be more in the future. I added the overload functions to have proper types in the components calling them.

Any ideas?

Thank you!


EDIT: If I add the spread overload, it doesn't fail, but I can do something like this:

    type IAuthSelectors =
      | 'selector1'
      | 'selector2';

    function getAuthState(selector: 'selector1'): string[];
    function getAuthState(selector: 'selector2', param1: string): boolean;
    function getAuthState(selector: IAuthSelectors, ...selectorArguments: unknown[]): unknown
    function getAuthState(selector: IAuthSelectors, ...selectorArguments: unknown[]): unknown {
      // return logic
    }

getAuthState('selector1', 'param1', 'param2'); // NO ERROR

Any way to keep the inference of parameters? If I add the spread overload it no longer infers the other prototypes.

CodePudding user response:

I take it the possible arguments for the two functions are identical.

It's not unusual inside an overload implementation for types to be a bit ... loose. :-) You might just suppress the error via ts-expect-error (and not expose the rest arguments versions of the calls), since you know the arguments are correct. I'm not a fan of doing that most of the time, but overloads tend to go this way (but keep reading).

function useAuthSelector(selector: IAuthSelectors, ...selectorArguments: unknown[]): any {
    // @ts-expect-error
    const authState = getAuthState(selector, ...selectorArguments);
    //    ^? const authState: never

    // ...
}

IDE showing parameter name for param1 when "selector2" is used for the selector parameter

caTS points out in a comment that mapped conditionals like that are fairly slow (at compile time) and this one could easily be replaced with a lookup. Quite true! And doing so means we don't have to repeat the selector names, because we can derive IAUthSelectors from that lookup type, like this:

type SelectorCall = {
    selector1: {
        params: [];
        returnType: string[];
    };
    selector2: {
        params: [param1: string];
        returnType: boolean;
    };
    selector3: {
        params: [];
        returnType: boolean;
    };
    selector4: {
        params: [];
        returnType: string;
    };
    selector5: {
        params: [];
        returnType: MyInterface;
    };
    selector6: {
        params: [];
        returnType: string;
    };
};

type IAuthSelectors = keyof SelectorCall;

Then the functions are:

function getAuthState<Selector extends IAuthSelectors, CallType extends SelectorCall[Selector]>(
    selector: Selector,
    ...params: CallType["params"]
): CallType["returnType"] {
    return null as any as CallType["returnType"];
}

function useAuthSelector<Selector extends IAuthSelectors, CallType extends SelectorCall[Selector]>(
    selector: Selector,
    ...params: CallType["params"]
): CallType["returnType"] {
    const authState = getAuthState(selector, ...params);

    return authState;
}

Playground link

Usage is the same, as is the IDE experience (with parameter names and such).

  • Related