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
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:
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) {
}
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
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
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