-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce mechanism for manipulating function types without losing generic type information #50481
Comments
At the moment many libraries (fp-ts, effect-ts) resolve around an implementation of typeclasses that are really ugly. Here an example of what one would need to do to implement a Functor that handles up to 4 parameters. type Test<A> = $<Kind.Array, [A]>;
// same as
type Test<A> = A[] So here what we have to do today to implement a Functor that fixes the first type, and let the others generic export interface Functor<F extends Kind> {
map: F extends Kind.unary
? <A, B>(f: (a: A) => B) => (fa: $<F, [A]>) => $<F, [B]>
: F extends Kind.binary
? <A, B>(f: (a: A) => B) => <C>(fa: $<F, [A, C]>) => $<F, [B, C]>
: F extends Kind.ternary
? <A, B>(
f: (a: A) => B
) => <C, D>(fa: $<F, [A, C, D]>) => $<F, [B, C, D]>
: F extends Kind.quaternary
? <A, B>(
f: (a: A) => B
) => <C, D, E>(fa: $<F, [A, C, D, E]>) => $<F, [B, C, D, E]>
: never;
} The nightmare begins when you want to compose Functors with other Functors. Handling all uses cases become exponential. export interface Functor<F extends Kind> {
map: <A, B>(f: (a: A) => B) => <...Arity<F>>(fa: $<F, [A, ...Arity<F>]>) => $<F, [B, ...Arity<F>]> |
I'd like to suggest 2 other solutions : Solution 1: Add a type X = <T,R>(e: T) => R;
type Y<T> = AsGeneric<X, T, string> // Y<T> = <T,string>(e: T) => string Solution 2: Allow to use class Base<T> {}
class Child<T> extends Base<T> {}
type X = Child<unknown>
type Y = X satisfies Base<string>; // Y = Child<string> With that, we can then use some workarounds. |
This comment was marked as outdated.
This comment was marked as outdated.
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. {
class ClassA<T> {
fooA() {}
}
class ClassB<T> {
fooB(){}
}
const SYMA = Symbol();
const SYMB = Symbol();
interface TypeMap<T> {
[SYMA]: [ClassA<unknown>, ClassA<T>],
[SYMB]: [ClassB<unknown>, ClassB<T>]
}
type Convert<T, U> = {
[K in keyof TypeMap<U>]: TypeMap<U>[K][0] extends T ? TypeMap<U>[K][1] : never
}[keyof TypeMap<U>];
type TA = ClassA<unknown>;
type TB = ClassB<unknown>;
// we can convert without knowing the real type of TA/TB.
type ResultA = Convert<TA, boolean>; // ClassA<boolean>
type ResultB = Convert<TB, boolean>; // ClassB<boolean>
} Another drawback is that we need to set all generic type. interface InferMap<T> {
[SYMA]: [ClassA<any>, InferStuffA<T>],
[SYMB]: [ClassB<any>, InferStuffB<T>]
} We could then use it to get a kind of tuple of types we could then re-inject using Append<T, U> = [T[0], T[1], U]; // but with recursions. Therefore I think a more complete/generic workaround could be possible ? EDIT: depending on the needs, for a stricter check : type Convert<T, U> = {
[K in keyof TypeMap<U>]: TypeMap<U>[K][0] extends T
? T extends TypeMap<U>[K][0]
? TypeMap<U>[K][1]
: never
: never
}[keyof TypeMap<U>]; If needed, the resulting union can also be transformed into an intersection. |
In some other cases, maybe a workaround like this can also be used: type P<T> = Parameters<F<T>>
type R<T> = ReturnType<F<T>>
type P2<T> = // some manipulations on P<T>
type F2<T> = <T>(...args: P2<T>) : R<T> |
Suggestion
π Search Terms
β Viability Checklist
My suggestion meets these guidelines:
β 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):
extends
as is (probably the prettiest solution but may present challenges when speccing it out):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):π Motivating Example
Consider some kind of API/middleware builder that looks like this when used:
It could be typed as something like this:
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:
But typing it is much harder. Usually we would use module augmentation to allow third parties to type their middleware types:
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.
The text was updated successfully, but these errors were encountered: