TypeScript: When trying to use mapped tuples as rest parameters error 'A rest parameter must be of an array type' given

TypeScript Version: 3.2

Search Terms: mapped tuples rest

Code

type FuncParams<T> = T extends (...args: infer P) => any ? P : never;
type Stringify<T> = {
    [K in keyof T]: string;
};
type Optional<T> = {
    [K in keyof T]?: T[K];
};

type ThreeParamFunc = (paramOne: string, paramTwo: number, paramThree: boolean) => void;

type Params = FuncParams<ThreeParamFunc>; // [string, number, boolean]
type StringParams = Stringify<FuncParams<ThreeParamFunc>>; // [string, string, string]
type OptionalParams = Optional<FuncParams<ThreeParamFunc>>; // [string?, number?, boolean?]

function doStuff<T>(func: T, ...params: FuncParams<T>) { // works fine
}

function doOptionalStuff<T>(func: T, ...params: Optional<FuncParams<T>>) { // A rest parameter must be of an array type.
}

function doStringStuff<T>(func: T, ...params: Stringify<FuncParams<T>>) { // A rest parameter must be of an array type.
}

Expected behavior: I should be able to use a mapped tuple as a rest param in a function

Actual behavior: I get the error A rest parameter must be of an array type.

Playground Link: Link

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 26
  • Comments: 18 (5 by maintainers)

Commits related to this issue

Most upvoted comments

I’d be interested to hear why this is marked as a suggestion rather than a bug. As far as I see it you can usually use a tuple type as a function parameter but in this scenario (and other similarly complicated scenarios) it doesn’t work.

Can anyone explain why it doesn’t work in this case? I have tested my mapped type with function parameters and on it’s own it works fine as a rest param:

type FunctionParams<T> = T extends (...params: infer R) => any ? R : never;

type matchFunc<T> = (value: T) => boolean;

interface IMatcher<T>{
    match: matchFunc<T>;
    toString: () => string;
}

type MatchUnion<T> = matchFunc<T> | IMatcher<T>;

type ToMatchers<T extends any[]> = {
    [P in keyof T]: MatchUnion<T[P]>;
}

interface ISample{
    funcOne(one: string, two: number, three: boolean): true;
}

const sample: ISample = {} as any;

type MappedParams= ToMatchers<FunctionParams<ISample["funcOne"]>>;

function processMatchers(...params: MappedParams) {
}

playground

The issue still exists on typescript 4.5.4. In my case applying generics directly on inferred parameters instead of Parameters<T> will fix this error, and I’ve checked they are actually the same type

type PromisifyTuple<T extends readonly unknown[] | []> =  { [P in keyof T]: Promise<T[P]> }

// it's really an tuple
type test = PromisifyTuple<[string, number, {}, Map<string, number>]>

// will raise ts2370
type Transform1<T extends (...args: any[]) => any> = (...args: PromisifyTuple<Parameters<T>>) => ReturnType<T>

// ok if PromisifyTuple is immediately applied on inferred parameters
type PromisifiedArguments<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? PromisifyTuple<P> : never;
type Transform2<T extends (...args: any[]) => any> = (...args: PromisifiedArguments<T>) => ReturnType<T>

// a is assignable to b and b is assignable to a, they computes to the same type
type TestFunction = (a: string, b: number) => object
let a: PromisifiedArguments<TestFunction> | undefined
let b: PromisifyTuple<Parameters<TestFunction>> | undefined
a = b;
b = a;

playground

Same error when using builtin Parameters and Partial:

function foo<F extends (...args: any[]) => any>(fn: F, ...args: Partial<Parameters<F>>) { }

Error is there, though actual type is resolved properly. Playground

Having the same problem - prepared a simpler repro (with artificial code ofc).

Real world use case would be to cover reselect’s createSelector API in generic manner - https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc

I bisected this particular change that @freshgum-bubbles mentioned to this diff and further down to my own PR: https://github.com/microsoft/TypeScript/pull/49947

…and now I realized that this PR was referencing this exact test case 😅 it’s just that it didn’t fix this issue as a whole

I was about to raise a new issue, but it sounds like the same issue as described in the last comments? Playground

// For some reason a separate helper works as expected, remapping just the tuple items.
type MapParamsHelper<A> = { [K in keyof A]: string };
type MapParams<F extends (...args: number[]) => void> = MapParamsHelper<Parameters<F>>;

let remap: MapParams<(a: number, b: number) => void> = ['a', 'b']; // OK
let x: number = remap.length; // OK

// But inlining same type breaks and iterates over all keys including Array.prototype methods:
type MapParams2<F extends (...args: number[]) => void> = { [K in keyof Parameters<F>]: string };

let remap2: MapParams2<(a: number, b: number) => void> = ['a', 'b']; // fails, because this is now an object and not a tuple
let y: number = remap2.length; // fails, because `length` was remapped to `string` here

@RyanCavanaugh this is marked as a “suggestion” but I think it’s actually a bug.