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
Allow classes to be parametric in other parametric classes #1213
Comments
Not to make any rash assumptions, but I believe you're typing it incorrectly. All parameter types require parameter names, so you probably meant to type map<A, B>(f: (x: A) => B): T<A> => T<B>; whereas right now map is a function that takes a mapper from type Try using the |
Thanks, corrected. |
I've updated my comment into a proposal. |
|
Quasi-approved. We like this idea a lot, but need a working implementation to try out to understand all the implications and potential edge cases. Having a sample PR that at least tackles the 80% use cases of this would be a really helpful next step. |
What are people's opinions on the tilde syntax? An alternative to interface Foo<T<~,~>> {
bar<A, B>(f: (a: A) => B): T<A, B>;
} that allows direct composition of generics instead of needing type aliases: interface Foo<T<~,~,~>, U<~>, V<~, ~>> {
bar<A, B, C, D>(a: A, f: (b: B) => C, d: D): T<U<A>, V<B, C>, D>;
} |
It's odd to have explicit arity since we don't really do that anywhere else, so interface Foo<T<~,~>> {
bar<A, B>(f: (a: A) => B): T<A, B>;
} is a little clearer, though, I know other languages use interface Foo<T<*,*>> {
bar<A, B>(f: (a: A) => B): T<A, B>;
} Though taking that point to an extreme, you might get: interface Foo<T: (*,*) => *> {
bar<A, B>(f: (a: A) => B): T<A, B>;
} |
I think |
A lighter-weight syntax would be leaving off the arity of the generics entirely; the parser would figure it out from the first use and throw an error if the rest weren't consistent with it. |
I'd be happy to start work on implementing this feature. What's the recommended forum for pestering devs about transpiler implementation details? |
You can log many new issues for larger questions with more involved code samples, or make a long running issue with a series of questions as you go. Alternatively you can join the chat room here https://gitter.im/Microsoft/TypeScript and we can talk there. |
@metaweta any news? If you need any help/discussion I would be glad to brainstorm on this issue. I really want this feature. |
No, things at work took over what free time I had to work on it. |
bump: is there a chance to see this feature ever considered? |
#1213 (comment) is still the current state of it. I don't see anything here that would make us change the priority of the feature. |
Seems to me like this is useful in far more situations than just importing category theory abstractions. For example, it would be useful to be able to write module factories that take a interface Database<P<~> extends PromiseLike<~>> {
query<T>(s:string, args:any[]): P<T>
} |
Would come in handy here too http://stackoverflow.com/questions/36900619/how-do-i-express-this-in-typescript |
A useful case for me: // before: we have to repeat many times
type MapValuesWithFoo<O> = {
[K in keyof O]: Foo<O[K]>
}
type MapValuesWithBar<O> = {
[K in keyof O]: Bar<O[K]>
}
// with HKT
type MapValues<O, T<~>> = {
[K in keyof O]: T<O[K]>
}
type MapValuesWithFoo<O> = MapValues<O, Foo>
type MapValuesWithBar<O> = MapValues<O, Bar>
// with high order generics
type MapValues<T<~>> = <O> {
[K in keyof O]: T<O[K]>
}
type MapValuesWithFoo = MapValues<Foo>
type MapValuesWithBar = MapValues<Bar> Also applies for tuples: type MapTuple<T<~>> = <O extends unknown[]> O extends [L, ...infer R] ? [T<L>, MapTuple<T><R>] : []
type MapPromise = MapTuple<Promise>
type X = MapPromise<[number, string]> // [Promise<number>, Promise<string>] |
+1 The lack of higher-kinded types is preventing me from declaring a Visitable interface that accepts a particular type of Visitor-implementing class as a type parameter for a return type T. |
wait.. it's 2040 and this is not implemented in ts yet? |
Hello, does anyone know the current alternative to not having this feature in TS? |
@bombillazo Try fp-ts's HKT. I'm using the same principle in my library deepmerge-ts if you want to check out a real world example. I've also documented how it works for my users here. |
Many search ways lead me here. I have no idea, how this feature should/can be called correct. Say, it is "generic arguments forwarding" or "delaying/bubbling generic parameters". Prerequisitesimport { FC } from 'react';
interface Props<T> {
value: T;
updater?: (value: T) => T;
onChange?: (value: T) => void;
}
const FNumber1: FC<Props<number>> = ({ value, updater, onChange }) => null; Here ProblemTo make generic component, we need to declare like so: const FT1 = <T>({ value, updater, onChange }: Props<T>): ReturnType<FC> => null; Here There is no "legal" direct way to declare type for const FT1: FC<Props<?>> = (...) => ...; Possible solutionNew syntax type FT = FC<Props<*T>>;
const FT1: FT = ({value, updater, onChange}) => null; Here type reference const FT1: FT = ({value, updater, onChange}) => {
const a = value; // a: T
const b: T = value;
return null;
}; Explicit const FT1: FT = <T>({value, updater, onChange}) => null;
const FT1: FT = <U>({value, updater, onChange}) => null; All the same as above without extra const FT1: FC<Props<*T>> = ({value, onChange}) => null;
const FT1: FC<Props<*T>> = <T>({value, onChange}) => null;
const FT1: FC<Props<*T>> = <U>({value, onChange}) => null; And so: const updater = <T>(v: T) => v;
return (
<div>
<FT1 value={42} updater={updater} onChange={(v) => { /* v is number */ }} />
<FT1 value="foo" updater={updater} onChange={(v) => { /* v is string */ }} />
</div>
); |
That seems complicated enough that we might be better off going with true Higher-Kinded Types, which might even be simpler to implement than the proposed new syntax. |
So, err, any hope on this? |
Was there any further thought given to this - is there a blocker that stops this from happening? There seems to be quite a bit of interest... |
So I came up with this solution for my problem. It's working quite well, with some limitations. I hope it belongs here export type ForceWidening<T> = T extends string
? string
: never | T extends number
? number
: never | T extends bigint
? bigint
: never | T extends boolean
? boolean
: never | T extends any[]
? T extends [infer Head, ...infer Tail]
? [ForceWidening<Head>, ...ForceWidening<Tail>]
: []
:
| never
| {
[K in keyof T]: T[K] extends Function ? T[K] : ForceWidening<T[K]>;
};
export declare const lambda: unique symbol;
/**
* Declares basic lambda function with an unique symbol
* to force other interfaces extending from this type
*/
export interface Lambda<Args = unknown, Return = unknown> {
args: Args;
return: Return;
[lambda]: never;
}
/**
* Composes two Lambda type functions and returns a new lambda function
* JS-equivalent:
* const compose = (a,b) => (arg) => a(b(arg))
*
*/
export interface Compose<
A extends Lambda<ForceWidening<Return<B>>>,
B extends Lambda<any, Args<A>>,
I extends Args<B> = Args<B>,
> extends Lambda {
args: I;
intermediate: Call<B, Args<this>>;
return: this["intermediate"] extends Args<A>
? Call<A, this["intermediate"]>
: never;
}
export interface EmptyLambda extends Lambda {}
/**
* Gets return type value from a Lambda type function
*/
export type Call<M extends Lambda, T extends Args<M>> = (M & {
args: T;
})["return"];
/**
* Extracts the argument from a lambda function
*/
export type Args<M extends Lambda> = M["args"];
export type Return<M extends Lambda> = M["return"];
export type Primitve = string | number | bigint | boolean | null | undefined;
interface UpperCase extends Lambda<string> {
return: Uppercase<Args<this>>;
}
interface LowerCase extends Lambda<string> {
return: Lowercase<Args<this>>;
}
type Test = Call<UpperCase, "asd">; // "ASD"
type ComposeTest = Call<Compose<LowerCase, UpperCase>, "asdasd">; // "asdasd" |
This issue could be solved more simply by adding a helper type similar to the common CallHKT pattern that gets used quite a bit in functional libraries, The Args would be an array that replaces the type arguments of the Target. If an item of the arguments array is unknown, it will keep the original provided type argument. If the provided Args array was less than the amount of parameters, the remaining parameters would stay the same also Some basic examples: type KeyValue<Key extends string, Value extends string> = `${Key}: ${Value}`
type FooToBar = KeyValue<'foo', 'bar'> // type 'foo: bar'
type AToB = Reapply<FooToBar, ['a', 'b']> // type 'a: b'
type FooToBaz = Reapply<FooToBar, [unknown, 'baz']> // type 'foo: baz'
type BazToBar = Reapply<FooToBar, ['baz', unknown]> // type 'baz: bar'
type BazToBar = Reapply<FooToBar, ['baz']> // type 'baz: bar'
type Errored = Reapply<FooToBar, [2, unknown]> // Error: Type '2' does not satisfy the constraint 'string'
interface IFunctor<A> {
// IFunctor<A>.pure(value: B): IFunctor<B>
pure<B>(value: B): Reapply<this, [B]>
// IFunctor<A>.map(fn: (value: A) => B): IFunctor<B>
map<B>(fn: (value: A) => B): Reapply<this, [B]>
}
class ImplFunctor<A> implements IFunctor<A> {
constructor(private value: A) {}
// ImplFunctor<A>.map(value: B): ImplFunctor<B>
pure<B>(value: B) {
return new ImplFunctor(value)
}
// ImplFunctor<A>.map(fn: (value: A) => B): ImplFunctor<B>
map<B>(fn: (value: A) => B) {
return this.pure(fn(this.value))
} This also lets us accomplish more complex tasks like passing in a type with arguments to be overridden interface Foo<A extends number> {}
interface Bar<A extends number> extends Foo<A> {}
declare function fn1<A extends number, B extends Foo<number>>(): Reapply<B, [A]>
const val1 = fn1<2, Bar<number>>() // val1: Bar<2>
declare function fn2<A extends string, B extends Foo<number>>(): Reapply<B, [A]> // Error: Type 'string' does not satisfy the constraint 'number' I suggest though that there was some symbol assigned the meaning of fill in the blank interface Foo<A extends number> {}
interface Bar<A extends number> extends Foo<A> {}
type Baz = Foo<~> // Type: Foo<number>
declare function fn1<A extends number, B extends Foo<~>>(): Reapply<B, [A]>
const val1 = fn1<2, Bar<~>>() // val1: Bar<2>
declare function fn2<A extends string, B extends Foo<~>>(): Reapply<B, [A]> // Error: Type 'string' does not satisfy the constraint 'number'
type FooToBaz = Reapply<FooToBar, [~, 'baz']> // type 'foo: baz' |
I called this transitivity and independent in #35816 For example declare function foo<T>(v: T): T
type params = Parameters<typeof foo> // type params = for<T> [v: T]
type rets = ReturnType<typeof foo> // type rets = for<T> T type FT = for<T> FC<Props<T>>; |
Hello, Any update on an implementation in typescript of the form |
@zedryas |
@Caleb-T-Owens yep but it seems if i'm not mistaken that every solution are towards HKT, and non necessarly using parametics type parameters such as |
I made some progress on this issue on the user end of things : free-types I focused my implementation on general-purpose type-level programming, so I included things like:
I didn't do an amazing job at providing an overview of the functionalities in the readme, because there are just so many things you can do with something like that, but there is a good guide which explains how the library works, how it's designed and the limitations it has. I used it in the project type-lenses to enable reaching the values of arbitrary types or manipulating them with arbitrary type-level functions, and in ts-spec to implement equality. I'm also using it in a project that aims at converting a class to a collection of curried functions with all the types correctly wired, which could be a way to easily turn a fantasy-land compliant class into a static-land-like collection of functions, but also any arbitrary class, with some more work from the part of the implementer. I need advice regarding performance: stacking combinators and compositions on top of each other adds up, but I don't have a solid way of evaluating my design decisions to make the building blocks as lightweight as possible. The value of reusing types to an extreme is questionable but I find it to be an interesting question ;) There are also limitations and pain points which may have workarounds or even solutions I don't know about. I don't know if this approach is a dead end but I definitely hit a wall at some point and I don't imagine a full featured and performant solution can be implemented solely on the user end. |
Alternative syntax proposal: Using the type A<X> = X;
type M = [A<infer Intermediate>, A<Intermediate>]; This toy example obviously simplifies to type M<X> = x[]; but that doesn't matter. |
"2014" |
This is a proposal for allowing generics as type parameters. It's currently possible to write specific examples of monads, but in order to write the interface that all monads satisfy, I propose writing
Similarly, it's possible to write specific examples of cartesian functors, but in order to write the interface that all cartesian functors satisfy, I propose writing
Parametric type parameters can take any number of arguments:
That is, when a type parameter is followed by a tilde and a natural arity, the type parameter should be allowed to be used as a generic type with the given arity in the rest of the declaration.
Just as is the case now, when implementing such an interface, the generic type parameters should be filled in:
In addition to directly allowing compositions of generic types in the arguments, I propose that typedefs also support defining generics in this way (see issue 308):
The arities of the definition and the alias must match for the typedef to be valid.
The text was updated successfully, but these errors were encountered: