Ok, this is a bit difficult and I'm going to explain it. Go down for TL;DR.
Parameter names in typescript are kept only while spreading original parameters, like:
type OrigParams = Parameters<OrigFunc>;
type AnotherFunc = (...args: OrigParams) => any;
This is due to TS nature where the Parameters
tuple really is a special one, that keep the names as an inner, unaccessible, metadata that gets destroyed while changing the tuple or just creating another one. i.e.:
type F = (a: any, b: any) => void;
type P1 = Parameters<F>;
type P2 = [any, any];
here P1
and P2
are different types even if they are structurally the same.
Citing TS:
Note that when a tuple type is inferred from a sequence of parameters and later expanded into a parameter list, as is the case for U, the original parameter names are used in the expansion (however, the names have no semantic meaning and are not otherwise observable).
However, this can be done using mapped types on the parameters tuple, since mapping the tuple preserves the parameter names, and a bit of post-processing given the "last" item problem.
- In order to "change" the parameter, we can use a Mapped Type. Mapped types are applicable to tuples other than on objects, and Parameters is a tuple, so we came up with:
type ReplaceLastParam<TParams extends readonly any[], TReplace> = {
[K in keyof TParams]: // We should put the replace code here
}
- The problem is that we should not change all the parameters, but only the last one.
K
is a string
while mapping types, i.e. keyof TParams
is "0" | "1" | "2"
in case of a tuple with three elements. So, in the case we have only three parameters, we could easily write:
type ReplaceParam2<TParams extends readonly any[], TReplace> = {
[K in keyof TParams]: K extends "2" ? TReplace : TParams[K]
}
- The problem here is that we don't know the last index, since you are providing functions with dynamic number of arguments. To compute the last one, we can use an helper that tricks TS:
type LastIndex<T extends readonly any[]> =
((...t: T) => void) extends ((x: any, ...r: infer R) => void) ? Exclude<keyof T, keyof R> : never;
This type computes the keys of a tuple R
that has one elements less of a tuple T
and diffs them with Exclude
: so Exclude<"0" | 1" | "2", "0" | "1">
is exactly "2"
, our last item. Note that we are using type inference here and the function rest inference.
- We just put all together, adding a type to pass a function (
ReplaceLast
) to wrap the parameters changing type we just made:
TL;DR:
// Index helper
type LastIndex<T extends readonly any[]> =
((...t: T) => void) extends ((x: any, ...r: infer R) => void) ? Exclude<keyof T, keyof R> : never;
// Replace last parameter with mapped type
type ReplaceLastParam<TParams extends readonly any[], TReplace> = {
[K in keyof TParams]: K extends LastIndex<TParams> ? TReplace : TParams[K]
}
// Replace function
type ReplaceLast<F, TReplace> = F extends (...args: infer T) => infer R
? (...args: ReplaceLastParam<T, TReplace>) => R
: never;
Playground Link
This was fun! Hope this answers your question, however if you really need this in your application, I hope you know what you are doing since the requirement maybe could be solved in a simpler manner.