Background
I'm trying to replace an existing overloaded function with rest parameters using labeled tuple elements.
Original code
This is a simplified version of the original overloaded function:
function doSomething(arg1: string, arg2: number):void;
function doSomething(arg1: string, arg2: number, arg3: unknown[]):void;
function doSomething(arg1: string, arg2: number, arg3: boolean):void;
function doSomething(arg1: string, arg2: number, arg3: unknown[], arg4: boolean):void {
// ...implementation
}
So the third and fourth arguments are optional but when supplied there order can be:
arg3: unknown[]
arg3: unknown[], arg4: boolean
arg3: boolean
Attempted solution
I decided to create the labeled tuple elements first and then depending on the supplied type variable types either set their type to the supplied type or to never
. Then filter that tuple removing any element that's never
and return the result.
type CalcAdditionArgs<ThirdArgType extends unknown[], FourthArgType extends boolean> = [
myArg3: ThirdArgType extends [] ? never : ThirdArgType,
myArg4 : [FourthArgType] extends [boolean] ? FourthArgType extends true ? true : never : never
]
type GetAdditionalArgs<ThirdArgType extends unknown[], FourthArgType extends boolean> =
FilterType<CalcAdditionArgs<ThirdArgType, FourthArgType>, never>
FilterType
is a modified version of the tuple filtering utility found here https://stackoverflow.com/a/64034671/1798234
type FilterType<T extends unknown[], U = undefined> =
(T extends [] ?
[] :
(T extends [infer H, ...infer R] ?
([H] extends [U] ?
FilterType<R, U> :
[H, ...FilterType<R, U>]) :
T
)
);
Just for clarity, this is the function that uses them
function execute<
ThirdArgType extends unknown[] = [],
FourthArgType extends boolean = false
>(
arg1: string,
arg2: number,
...args:GetAdditionalArgs<ThirdArgType, FourthArgType>
): void {
// do something here
}
Problem
This is the output of the 2 utility types:
type a = CalcAdditionArgs<[], false>; // [myArg3: never, myArg4: never]
type b = CalcAdditionArgs<[], true>; // [myArg3: [string, number], myArg4: never]
type c = CalcAdditionArgs<[string, number], false>; // [myArg3: [string, number], myArg4: never]
type d = CalcAdditionArgs<[string, number, Function], true>; // [myArg3: [string, number, Function], myArg4: true]
type e = GetAdditionalArgs<[], false>; // []
type f = GetAdditionalArgs<[], true>; // [true]
type g = GetAdditionalArgs<[string, number], false>; // [string: number]
type h = GetAdditionalArgs<[string, number, Function], true>; // [[string, number, Function], true]
As you can see, GetAdditionalArgs
(or rather FilterType
) is stripping the tuple element labels.
Question
I'm unable to understand how - if it's actually possible - to go about creating and then manipulating a tuple type within a utility type. i.e create an empty tuple and then add the required types to it. Hence my approach to the solution to create the populated tuple up front and then remove elements instead.
- Does anyone know why the tuple element labels are being stripped by
FilterType
and is there a way to fix this using the existing solution?
Or
- Is there a better/simpler solution to achieve the result i'm looking for?
Solution
Thanks to the response from @captain-yossarian and this SO answer from @ford04 (https://stackoverflow.com/a/64194372/1798234) i was able to rethink my approach to the solution using rest parameters:
type Append<E extends [unknown], A extends unknown[]> = [...A, ...E]
type GetMyArrayArg<T extends unknown[]> = [myArrayArg: T extends [] ? never : T]
type GetMyBooleanArg<T extends boolean> = [myBooleanArg : [T] extends [boolean] ? T extends true ? true : never : never]
type AddParameter<T extends [unknown], U extends unknown[] = []> =
T extends [] ?
U :
T extends [infer H] ?
[H] extends [never] ?
U :
Append<T, U> :
U
type GetAdditionalArgs<ThirdArgType extends unknown[], FourthArgType extends boolean> =
AddParameter<
GetMyBooleanArg<FourthArgType>,
AddParameter<
GetMyArrayArg<ThirdArgType>>
>
type a = GetAdditionalArgs<[], false>; // []
type b = GetAdditionalArgs<[], true>; // [myBooleanArg: true]
type c = GetAdditionalArgs<[string, number], false>; // [myArrayArg: [string, number]]
type d = GetAdditionalArgs<[string, number, Function], true>; // [myArrayArg: [string, number, Function], myBooleanArg: true]
CodePudding user response:
If you are curious why tuple labels are removed from output it might be hard to answer.
First of all, here you can find docs .
According to docs:
They’re purely there for documentation and tooling
This is only my guess.
I think labels are removed because of FilterType
utility type.
FilterType
creates completely new tuple and seems to be that TS does not preserve tuple labels during iteration.
Consider this example:
type Labeled = [name: string];
type Infer<L extends Labeled> = L extends [infer First] ? [First] : never
type Result = Infer<Labeled> // [string], no label
Number indexing does not preserve labels either. [L[0]]
- returns [string]
.
Seems to be that it works with rest parameters:
type Labeled = [name: string];
type Infer<L extends Labeled> = L extends [infer _] ? [...L] : never
type Result = Infer<Labeled> // [name: string], with label
CalcAdditionArgs
does not create new tuple.
I did not find in docs how to preserve the labels.
I assume, that since you create new tuple in FilterType
, the order of elements might not be preserved. Hence it might be unsafe to keep labels. But it is only my guess.
Here you can find PR
However, you can add labels into FilterType
:
type FilterType<T extends unknown[], U = undefined> =
(T extends [] ?
[] :
(T extends [infer H, ...infer R] ?
([H] extends [U] ?
FilterType<R, U> :
[Batman: H, ...Superman: FilterType<R, U>]) : // <----- LABELS
T
)
);
As you might have noticed, I have added Batman
label and Superman
labels and they are exists in the output:
type FilterType<T extends unknown[], U = undefined> =
(T extends [] ?
[] :
(T extends [infer H, ...infer R] ?
([H] extends [U] ?
FilterType<R, U> :
[Batman: H, ...Superman: FilterType<R, U>]) :
T
)
);
type CalcAdditionArgs<ThirdArgType extends unknown[], FourthArgType extends boolean> = [
myArg3: ThirdArgType extends [] ? never : ThirdArgType,
myArg4: [FourthArgType] extends [boolean] ? FourthArgType extends true ? true : never : never
]
type GetAdditionalArgs<ThirdArgType extends unknown[], FourthArgType extends boolean> =
FilterType<CalcAdditionArgs<ThirdArgType, FourthArgType>, never>
type e = GetAdditionalArgs<[], false>; // []
type f = GetAdditionalArgs<[], true>; // [Batman: true]
type g = GetAdditionalArgs<[string, number], false>; // [Batman: [string, number]]
type h = GetAdditionalArgs<[string, number, Function], true>; // [Batman: [string, number, Function], Batman: true]
If this does not meet your expectations, you can just create manually a union of allowed tuples. It will be safest way