TS. Typify js_function_decoration and generic narrowing (or "A spread argument must either have a tuple type or be passed to a rest parameter")

35 views Asked by At

I want (in function run) to get one of the known functions and pass corresponding arguments.

type Name = 'f1' | 'f2' | 'f3';
const fns = {
  f1: () => {},
  f2: (a: string) => {},
  f3: (a: string, b: string) => {}
};
const argsArr = {
  f1: [],
  f2: ['str'],
  f3: [1, 2]
};

function run(name: Name) {
  const fn = fns[name];
  const args = argsArr[name];
  fn(...args);   // "A spread argument must either have a tuple type or be passed to a rest parameter"
}

But at fn(...args) that causes the ts-error "A spread argument must either have a tuple type or be passed to a rest parameter". Because, as I think, TS doesnt think there is a guarantee that the args type is corresponding to the function. So, I'm looking for a way to narrow the type of name parameter, so to narrow/infer the type of fn (to make TS understand that although they may vary, they always correspond to each other).


Btw, I tried variants of the following, doesn't work:

function run<N extends Name, F extends Fn>(name: N) {
  const fn= fns[name] as F;
  type Args = Parameters<typeof fn>;
  const args:Args = argsArr[name] as Args;
  fn(...args);
}

Btw2, I didn't find typifying of comon decorators of functions (not TS Decorators) it would help.

2

There are 2 answers

0
Oleksii V On

You can add type for 'fns' object.

type Fns = {
  [key in Name]: (...args: any) => void
}

const fns: Fns = {
  f1: () => {},
  f2: (a: string) => {},
  f3: (a: string, b: string) => {},
};

Then error is gone

0
Murolem On

The problem is — typescript doesn't have any constraints defined for fns and argsArr, so it infers the types of these variables to the best of its ability, which is not enough in this case.

I managed to get it working with this code:

type Name = 'f1' | 'f2' | 'f3';

type Fn = (...args: any[]) => void;
const fns = {
    f1: () => { },
    f2: (a: string) => { },
    f3: (a: string, b: string) => { }
} satisfies Record<Name, Fn>

const argsArr: {
    [Key in Name]: Parameters<typeof fns[Key]>
} = {
    f1: [],
    f2: ['str'],
    f3: [1, 2]
} 

function run(name: Name) {
    const fn: Fn = fns[name];
    const args = argsArr[name];
    fn(...args);   // "A spread argument must either have a tuple type or be passed to a rest parameter"
}


What changed:

  • Function type is extracted to type Fn.
  • fns now has the lower constrain Record<Name, Fn> using satisfies, which allows to properly infer the functions args to use later and constrains the key to only be Name.
  • argsArr keys constrained to Name with the values being constrained to the args of the corresponding functions. This results in a type error for f3 (as expected), since it has number values instead of string ones.
  • in function run, fn variable is typed as Fn because it needs to lose the exact types in favor for more general ones to not give any type errors when calling it. args type remains the same, since now it fits the more general function constraint.