Navigation Menu

Skip to content
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

Open
metaweta opened this issue Nov 19, 2014 · 179 comments
Open

Allow classes to be parametric in other parametric classes #1213

metaweta opened this issue Nov 19, 2014 · 179 comments
Labels
Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@metaweta
Copy link

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

interface Monad<T<~>> {
  map<A, B>(f: (a: A) => B): T<A> => T<B>;
  lift<A>(a: A): T<A>;
  join<A>(tta: T<T<A>>): T<A>;
}

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

interface Cartesian<T<~>> {
  all<A>(a: Array<T<A>>): T<Array<A>>;
}

Parametric type parameters can take any number of arguments:

interface Foo<T<~,~>> {
  bar<A, B>(f: (a: A) => B): T<A, B>;
}

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:

class ArrayMonad<A> implements Monad<Array> {
  map<A, B>(f: (a:A) => B): Array<A> => Array<B> {
    return (arr: Array<A>) =>  arr.map(f);
  }
  lift<A>(a: A): Array<A> { return [a]; }
  join<A>(tta: Array<Array<A>>): Array<A> {
    return tta.reduce((prev, cur) => prev.concat(cur));
  }
}

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):

typedef Maybe<Array<~>> Composite<~> ;
class Foo implements Monad<Composite<~>> { ... }

The arities of the definition and the alias must match for the typedef to be valid.

@DanielRosenwasser
Copy link
Member

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 any (where your parameter name is A) to B.

Try using the --noImplicitAny flag for better results.

@metaweta
Copy link
Author

Thanks, corrected.

@danquirk danquirk added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Nov 25, 2014
@metaweta
Copy link
Author

I've updated my comment into a proposal.

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Dec 1, 2014
@fdecampredon
Copy link

👍 higher kinded type would be a big bonus for functional programming construct, however before that I would prefer to have correct support for higher order function and generic :p

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this and removed In Discussion Not yet reached consensus labels Apr 28, 2015
@RyanCavanaugh RyanCavanaugh added this to the Community milestone Apr 28, 2015
@RyanCavanaugh
Copy link
Member

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.

@metaweta
Copy link
Author

What are people's opinions on the tilde syntax? An alternative to T~2 would be something like

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>;
}

@DanielRosenwasser
Copy link
Member

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 * in similar contexts instead of ~:

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>;
}

@metaweta
Copy link
Author

I think T<~,~> is clearer than T~2, too. I'll modify the proposal above. I don't care whether we use ~ or *; it just can't be a JS identifier, so we can't use, say, _ . I don't see what benefit the => notation provides; all generics take some input types and return a single output type.

@metaweta
Copy link
Author

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.

@metaweta
Copy link
Author

metaweta commented Jun 1, 2015

I'd be happy to start work on implementing this feature. What's the recommended forum for pestering devs about transpiler implementation details?

@danquirk
Copy link
Member

danquirk commented Jun 1, 2015

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.

@Artazor
Copy link
Contributor

Artazor commented Dec 10, 2015

@metaweta any news? If you need any help/discussion I would be glad to brainstorm on this issue. I really want this feature.

@metaweta
Copy link
Author

No, things at work took over what free time I had to work on it.

@zpdDG4gta8XKpMCd
Copy link

bump: is there a chance to see this feature ever considered?

@RyanCavanaugh
Copy link
Member

#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.

@spion
Copy link

spion commented Apr 18, 2016

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 Promise implementation (constructor) as an argument, e.g. a Database with a pluggable promise implementation:

interface Database<P<~> extends PromiseLike<~>> {   
    query<T>(s:string, args:any[]): P<T> 
}

@bcherny
Copy link

bcherny commented Apr 27, 2016

Would come in handy here too http://stackoverflow.com/questions/36900619/how-do-i-express-this-in-typescript

@ClaudiuCeia
Copy link

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...

@PhilippDehler
Copy link

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"

@Caleb-T-Owens
Copy link

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, Reapply<Target, Args> that reapplied the type arguments of a type.

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'

~ could also be used to indicate leaving a parameter to its default state

type FooToBaz = Reapply<FooToBar, [~, 'baz']> // type 'foo: baz'

@2A5F
Copy link

2A5F commented Aug 30, 2022

@Vovan-VE

"generic arguments forwarding" or "delaying/bubbling generic parameters"

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>>;

@zedryas
Copy link

zedryas commented Sep 2, 2022

Hello,

Any update on an implementation in typescript of the form T<~,~> ? Is it planned ?

@Caleb-T-Owens
Copy link

@zedryas
its reliant on someone making a PR implementing one of the proposed syntaxes so it can be evaluated.

@zedryas
Copy link

zedryas commented Sep 2, 2022

@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 type MyType <T<~,~>> = ... - which can be used in other circumstances than HKT.

@geoffreytools
Copy link

geoffreytools commented Oct 24, 2022

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:

  • the handling of type constraints
  • partial application
  • composition and other combinators
  • higher order types like map/lift/reduce for tuples and objects
  • abstraction over type constructors
  • pattern matching on types and other experiments

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.

@J-Cake
Copy link

J-Cake commented Nov 20, 2022

Alternative syntax proposal: Using the infer keyword outside of a conditional clause:

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.

@exoRift
Copy link

exoRift commented Jan 4, 2023

"2014" 😔

@craigphicks
Copy link

About the syntax - I think arity can be inferred from usage. More important would be

  1. Indicating that the type definition is a meta-type definition
  2. Indicating that a specific parameter is a generic function
  3. Supplying default values to a generic function (which wouldn't need to be a feature in the first version anyway).

For example

type* OneOf<GenFunc><A extends any[], Acc extends Record<number, any>={}> = A extends [] ? Acc :
    A extends [infer H, ... infer Rem] ? OneOf_GenFunc<Rem, Acc & GenFunc<H>> : never ; 

type* rather than metatype or some other alphabetic identifier because metatype is not reserved.

Most of the type checking could be deferred to the instance call

type GenFunc<T> = <T>(a:T,b:T): T;
type FuncTypeInstance = OneOf<GenFunc><[number,string]>

I think it is more clear for the reader and probably more clear to implement.

@dead-claudia
Copy link

@craigphicks You could go one simpler: ditch the type* and just use what's there.

  • Accept a HK type: foo<A<B>>(): ..., type Foo<A<B>> = ...
  • Define a HK type: type Foo<A><B> = ..., interface Foo<A><B> { ... }

The hard part of implementing this isn't the syntax though - it's the semantics. Retrofitting higher-kinded types into a language with type inference almost always requires at least a partial rewrite of the type checker as it mucks with inference a lot (and can rapidly slow it down).

@craigphicks
Copy link

craigphicks commented Dec 16, 2023

@dead-claudia @achinaou

@craigphicks - ... Retrofitting higher-kinded types into a language with type inference almost always requires at least a
partial rewrite of the type checker as it mucks with inference a lot (and can rapidly slow it down).

Note: I originally posted it as a separate proposal but was redirected here as my proposal was claimed to be a duplicate.

What I am proposing imposes no extra logic on the type checker - because the meta type is never evaluated until all it's parameters have been supplied, at which time it rendered to a type.

The original proposal 1213 is a lot more powerful - but also much harder to implement.

I am starting from a very simple simple problem:

type SomeGenericType<T> = Body 1
type MetaTypeResolved<Generic Parameter and Constraints> = Body 2 which references SomeGenericType

For each SomeGenericType, Body 2 has to be rewritten.
This proposal is just to get around that limitation, so that the body can be written once, and applied to many instances of
SomeGenericType X Generic Parameters.

That is the simplest version, the result of which has no unresolved generic parameters, so obviously no inference is involved.

The next level of complexity would be to implement resolving just SomeGenericType

type MetaTypeResolved = MetaType<SomeGenericType>

which would be equivalent to

type MetaTypeResolved<Generic Parameter and Constraints> = Body with VariableGenericName replaced by SomeGenericType

and that would only be a current TypeScript generic, so it would not impose any extra requirements on the inference logic.

@matthew-dean
Copy link

@PhilippDehler I don't see how your solution addresses the problem of passing generics to generics, since the return types are fixed and not generic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.