Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
Roaders opened this issue Feb 14, 2019 · 18 comments · Fixed by #57122
Assignees
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Fix Available A PR has been opened for this issue Suggestion An idea for TypeScript

Comments

@Roaders
Copy link

Roaders commented Feb 14, 2019

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

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Mar 7, 2019
@Andarist
Copy link
Contributor

Andarist commented Mar 8, 2019

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

@Roaders
Copy link
Author

Roaders commented Oct 17, 2019

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)

@Roaders
Copy link
Author

Roaders commented Oct 18, 2019

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

@OxleyS
Copy link

OxleyS commented Dec 17, 2019

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.

@dragomirtitian
Copy link
Contributor

@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.

@RReverser
Copy link
Contributor

RReverser commented Dec 17, 2019

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.

@OxleyS
Copy link

OxleyS commented Dec 18, 2019

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?

@RReverser
Copy link
Contributor

@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).

@levenleven
Copy link

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

@alshdavid
Copy link

alshdavid commented Apr 24, 2021

Here is another failure

type ProxyParameters<T extends (...args: any) => any> = {
  [K in keyof Parameters<T>]: Parameters<T>[K]
} 

const f1 = (foo: string): string => foo
// Works fine:
const f2 = (...args: Parameters<typeof f1>): string => args[0]
// Error:
const f3 = (...args: ProxyParameters<typeof f1>): string => args[0]

Playground

@Semesse
Copy link

Semesse commented Feb 16, 2022

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

@Harpush
Copy link

Harpush commented Jul 13, 2022

Same issue here... Mapped tuple type on ConstructorParameters will result in non array. Trying to assign it to an array variable will fail. While just mapping over a tuple works in that assignment (no infer).

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

const testOne = <T extends any[]>(tuple: MapTupleToObject<T>) => {
  // WORKS!
  const a: any[] = tuple;
}

const testTwo = <T extends new (...args: any[]) => any>(tuple: MapTupleToObject<ConstructorParameters<T>>) => {
  // DOESN'T WORKS!
  const a: any[] = tuple;
}

const testThree = <T extends new (...args: any[]) => any>(tuple: ConstructorParameters<T>) => {
  // WORKS!
  const a: any[] = tuple;
}

@DuBistKomisch
Copy link

Same issue when trying to spread a mapped tuple into another array type:

lib/types/json-schema.ts:213:10 - error TS2574: A rest element type must be an array type.

213       ? [...{ -readonly [K in keyof I]: JSONSchemaDataDef<I[K], D> }, ...JSONSchemaDataDef<A, D>[]]
             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

However it does work by passing it through a Concat recursive conditional type instead: https://stackoverflow.com/a/64631060

Definitely seems like an inconsistency that could be fixed.

@freshgum-bubbles
Copy link

Use-case

Not sure if it helps, but I can provide an explicit use-case for this functionality: mapping an array passed to a function.

Currently, due to this not existing, the only workaround to map arrays passed to functions is to make like 20 overloads.

type Cast<U> = { value: U };

function Y<U1> (args: [U1]): [Cast<U1>];
function Y<U1, U2> (args: [U1, U2])]: [Cast<U1>, Cast<U2>];
function Y<U1, U2, U3> (args: [U1, U2, U3]): [Cast<U1>, Cast<U2>, Cast<U3>];
function Y<U1, U2, U3, U4> (args: [U1, U2, U3, U4]): [Cast<U1>, Cast<U2>, Cast<U3>, Cast<U4>];
function Y<U1, U2, U3, U4, U5> (args: [U1, U2, U3, U4, U5]): [Cast<U1>, Cast<U2>, Cast<U3>, Cast<U4>, Cast<U5>];
function Y (args: unknown[]): Cast<unknown>[] {
  return args.map(x => ({ value: x }));
}

This is exactly the hack I made in TypeDI to type-check service dependencies. The obvious problem is that it's... well, it's horrendous. Sooooo I reverted it, but I'm thinking of undoing that revert as this issue doesn't seem to be gaining any attention.

Once this is implemented, I could simplify the above to...

type Cast<U> = { value: U };
type CastMany<UArgs extends any[]> = {
  [key in keyof UArgs]: Cast<UArgs[key]>;
}

function Y<UArgs extends any[]> (args: UArgs): CastMany<UArgs> {
  return args.map(x => ({ value: x }));
}

@freshgum-bubbles
Copy link

FYI: This seems to have been secretly fixed in the current version of TypeScript.

TS 5.0.4 (presents ts2370)
TS 5.2.2

@Andarist
Copy link
Contributor

Andarist commented Sep 3, 2023

I bisected this particular change that @freshgum-bubbles mentioned to this diff and further down to my own PR: #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

@xixixao
Copy link

xixixao commented Jan 28, 2024

I'm also running into this problem, and here's an example of just how broken this currently is:

Works fine:

type Rec = Record<string, string[]>;

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

function foo<E extends Rec, Key extends keyof Rec>(
  e: E,
  key: Key,
  ...args: M<E[Key]>
) {}

Now add a constraints to M, you get an error:

type Rec = Record<string, string[]>;

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

function foo<E extends Rec, Key extends keyof Rec>(
  e: E,
  key: Key,
  ...args: M<E[Key]> // A rest parameter must be of an array type
) {}

even though it would seem it's even more obvious that the mapped type is an array in this second version!

@bruyeret
Copy link

bruyeret commented Feb 2, 2024

I also encountered this issue.
This is another example of what does and doesn't work:

type MyGeneric<T> = { value: T };

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

// ✅ Use a tuple
declare function f1(...args: WrapInMyGeneric<[string, number]>): void;

// ✅ Use any array
declare function f2(...args: WrapInMyGeneric<any[]>): void;

// ✅ Use the parameters of a function
declare function f3(...args: WrapInMyGeneric<Parameters<(a: string, b: number) => void>>): void;

// ❌ Use the parameters of a generic function
declare function f4<F extends (...args: any[]) => any>(...args: WrapInMyGeneric<Parameters<F>>): void;

The funny thing is that if I replace my generic type with the type itself, it works:

type MyGeneric<T> = T;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Fix Available A PR has been opened for this issue Suggestion An idea for TypeScript
Projects
None yet