Repository navigation
Introduce mechanism for manipulating function types without losing generic type informationΒ #50481
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
extendsas 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
extendswith 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.
Activity
ecyrbe commentedon Dec 28, 2023
At the moment many libraries (fp-ts, effect-ts) resolve around an implementation of typeclasses that are really ugly.
They need to preemptively add generic parameters to all functions to not loose generic type information when chaining calls.
It would be nice to have a way to manipulate generics functions aliases like we manipulate arrays.
Here an example of what one would need to do to implement a Functor that handles up to 4 parameters.
Don't mind too much about
$, it's just an HKT with a lot of worarounds.$<F,[A]>is an HKT, equivalent if calling a utility typeF<A>.For an array kind it does the following :
So here what we have to do today to implement a Functor that fixes the first type, and let the others generic
The nightmare begins when you want to compose Functors with other Functors. Handling all uses cases become exponential.
So what it could look with some helpers :
denis-migdal commentedon Jan 25, 2024
I'd like to suggest 2 other solutions :
Solution 1:
Add a
AsGeneric<>tool :Solution 2:
Allow to use
satisfieswith types :With that, we can then use some workarounds.
denis-migdal commentedon Feb 3, 2024
denis-migdal commentedon Feb 25, 2024
Hi,
I found an improvement for the previous workaround that may be useful in some cases, by simulating a kind of map of types.
Like the previous workaround, the drawback is that we have to manually register the types/shapes we wish to manipulate.
For methods, maybe there is a way to do something more generic using some kind of recursion ?
Another drawback is that we need to set all generic type.
But maybe there is a way to add some kind of inference ?
We could then use it to get a kind of tuple of types we could then re-inject using
Convert?Manipulating such tuple of type could also be possible with some kind of recursion ?
Therefore I think a more complete/generic workaround could be possible ?
EDIT: depending on the needs, for a stricter check :
If needed, the resulting union can also be transformed into an intersection.
denis-migdal commentedon Feb 26, 2024
In some other cases, maybe a workaround like this can also be used: