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

Created on 14 Feb 2019  路  9Comments  路  Source: microsoft/TypeScript


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

Awaiting More Feedback Suggestion

Most helpful comment

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

All 9 comments

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 also get this issues here:

playground

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;

function returnMatcher<T, K extends keyof T, P extends any[]>(source: T, name: K){
    return {
        proxyFunction: (...params: FunctionParams<T[K]>) => true,
        doMatch: (...params: ToMatchers<FunctionParams<T[K]>>) => true // A rest parameter must be of an array type.
    }
}

returnMatcher(sample, "funcOne").proxyFunction("", 4, true)

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

Problem still exists in Typescript 3.7.2. Looks like a bug. Simple reproduction: Playground

If you mouse over P, it shows up as [string, number] as expected. If you mouse over M, it looks like it's expanded the entire definition of Array as an interface:

type M = {
    [x: number]: string | number;
    0: string;
    1: number;
    length: 2;
    toString: () => string;
    toLocaleString: () => string;
    pop: () => string | number | undefined;
    push: (...items: (string | number)[]) => number;
    concat: {
        ...;
    };
    ... 22 more ...;
    values: {
        ...;
    };
}

While this is compatible in many ways, it is ultimately not a tuple anymore, so when you try to use it as a rest parameter it complains as such.

@OxleyS Actually the original problem was solved, if you go through a generic mapping type the result will be a tuple:

type P = [string, number];


type M<T> = { [K in keyof T]: T[K] };

const f1 = (...params: P) => params[0]; // OK
const f2 = (...params: M<P>) => params[0]; // OK

Playground Link

Not sure why mapping directly over a tuple is not supported though.

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.

I don't think it's really solved, I think it's just inconsistent in its behavior. Here's another example that uses a generic mapping type on a tuple and gives the same error: Playground

I think the common ground between all the examples that people have posted as not working is the use of a type such as Parameters. Could the problem be that the type system is considering the case where Parameters expands to never and raising an error based on that?

@OxleyS I don't think that's the case; at least it doesn't explain why a workaround with an intermediate generic type works (see my example).

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

Was this page helpful?
0 / 5 - 0 ratings