-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Description
Suggestion
π Search Terms
- generic function add parameter
- parameters of generic function
- prepend parameter to generic function
β Viability Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
β Suggestion
Being able to manipulate generic function types without losing the generic type parameters would be very useful (see under motivating example). Coming up with a good syntax for it is not easy, but I see a few possible approaches (precise syntax/names subject to change):
- A bunch of utility types (inelegant but gets the job done)
type F = <T, R>(t: T) => R;
type X1 = WithReturnType<F, number>; // X1 = <T, R>(t: T) => number;
type X2 = WithParameters<F, [x: number]>; // X1 = <T, R>(x: number) => R;
type X3 = PrependParameter<F, number>; // X1 = <T, R>(arg0: number, t: T) => R;
type X4 = AppendParameter<F, number>; // X1 = <T, R>(t: T, arg0: number) => R;
type X5 = ReplaceParameter<F, 0, number>; // X1 = <T, R>(t: number) => R;
- Enhancement of
extends
as is (probably the prettiest solution but may present challenges when speccing it out):
type F = <T, R>(t: T) => R;
type X1 = F extends (...args: infer Args) => infer R
? (x: number, ...args: Args) => R
: never;
// ...
- Enhancement of
extends
with new syntax to declare intent (this is much more general but would only make sense when combined with a broader "generic generics" or HKT feature):
type F = <T, R>(t: T) => R;
type X1 = F extends <...infer TypeArgs>(...args: infer Args) => infer R
? (x: number, ...args: Args<...TypeArgs>) => R<...TypeArgs>
: never;
// ...
// or maybe
type X1 = F extends <...infer TypeArgs>(...args: infer Args<...TypeArgs>) => infer R<...TypeArgs>
? (x: number, ...args: Args<...TypeArgs>) => R<...TypeArgs>
: never;
// ...
- Alternative extension to existing syntax (in general, but here's one possibility):
type F = <T, R>(t: T) => R;
type X1 = <...Args>(x: number, ...args: Parameters<F<...Args>>) => ReturnType<F<...Args>>;
π Motivating Example
Consider some kind of API/middleware builder that looks like this when used:
builder
.add('slow', { rate: 500 })
.add('get', { query: ['abc'] as const }, ({ query: { abc } }) => {
// ...
};
It could be typed as something like this:
interface Builder {
add(type: 'slow', options: { rate: number }): Builder;
add<Q extends readonly string[]>(
type: 'get',
options: { query: Q },
callback: (data: { query: { [K in Q[number]]: string } }) => void
): Builder;
// ...
}
However, now we want third parties to be able to add their own middleware types to the builder. Making an API for adding them is fairly easy:
BuilderFactory.register('merge', (options, callback) => {
// ...
callback(...);
// ...
} );
But typing it is much harder. Usually we would use module augmentation to allow third parties to type their middleware types:
interface Builder {
add<T extends keyof BuilderMethods>(type: T, ...args: Parameters<BuilderMethods[T]>): ReturnType<BuilderMethods[T]>
}
// third party code
declare module '...' {
interface BuilderMethods {
merge: <Q extends readonly string[]>(
options: { query: Q },
callback: (data: { query: { [K in Q[number]]: string } }) => void
) => void;
}
}
But if we do this, we lose all of the information encoded in the type parameters. This feature would allow the add
method to retain the generic type information of the function type it's derived from.
π» Use Cases
My use case would be relatively similar to the motivating example above. Currently my workaround is to just only have a good typing experience for first-party methods, but that's not ideal.