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

Type aliases requiring type parameters #1616

Closed
danielearwicker opened this issue Jan 8, 2015 · 34 comments
Closed

Type aliases requiring type parameters #1616

danielearwicker opened this issue Jan 8, 2015 · 34 comments
Assignees
Labels
Committed The team has roadmapped this issue Suggestion An idea for TypeScript

Comments

@danielearwicker
Copy link

Sometimes we need to capture a union of types that may be a mixture of primitives, functions, objects, etc. but we want to leave it parameterised (just as we already can with interfaces).

Example

A simple non-recursive example:

type Source<T> = T | (() => T);

Here, a source can either be a plain value or a nullary function that obtains a value.

We can provide uniform access to such sources:

function unwrap<T>(p: Source<T>) {
    return (typeof p === "function") ? p() : p;
}

And then we can specify model interfaces where we we leave open the nature of the source but we tie down the value types:

interface Person {
    name: Source<string>;
    age: Source<number>;
}

e.g. name is a constant, but age depends on when you ask:

var p: Person = {
    name: "John Lennon",
    age: () => ageFromDOB(1940, 10, 9),
}

But we can treat them identically in consuming code:

var n = unwrap(p.name), a = unwrap(p.age);

NB. The above is already possible with union types alone, but the interface Person has to repeat the pattern:

interface Person {
    name: string | (() => string);
    age: number | (() => number);
}

Not so bad for a simple example, but the pattern for a value source might evolve to get more complex and then you have a lot of fiddly updating to do because you "Did Repeat Yourself".

Recursion

The more flexible recursive version:

type Source<T> = T | (() => Source<T>);

A source can either be a plain value or a nullary function that obtains a source (which may be a plain value terminating recursion, or a nullary function that... and so on).

We can again provide uniform access to such sources, either with runtime recursion (risky until tail-call optimisation is widespread):

function unwrap<T>(p: Source<T>) {
    return (typeof p === "function") ? unwrap(p()) : p;
}

Or with a loop:

function unwrap<T>(p: Source<T>) {
    for (;;) {
        if (typeof p !== "function") {
            return p;
        }
        p = p();
    }
}
@zpdDG4gta8XKpMCd
Copy link

I was surprised that the type alias feature that was shipped with 1.4 doesn't support type parameters. This makes it of a very limited use which was hardly worth the effort of adding them the way they are. Please consider adding the type parameters, so we can say TypeScript supports type aliases.

@bluong
Copy link

bluong commented Feb 7, 2015

👍 on please allowing for type parameters

@RamIdeas
Copy link

I would definitely like this. Was there any reason they weren't permitted in the first place?

@mudHOAX
Copy link

mudHOAX commented Feb 12, 2015

👍 definitely a must have.

@Artazor
Copy link
Contributor

Artazor commented Mar 9, 2015

Any point on a roadmap? When this (definitely a must have) feature will be implemented?

@danquirk
Copy link
Member

danquirk commented Mar 9, 2015

Lately we've been quite busy with ES6 alignment and the recently announced Angular 2.0 related features. We will get to (re)evaluating some of these type system specific issues but there's no concrete date for issues like this at the moment. If there are other motivating examples/use cases that you have in mind that you think influence the priority or design do let us know.

@mweststrate
Copy link

👍

type BaseReactClass<P,S> = reactWrapper.BaseReactClass<P,S>;

EDIT in this specific case, import BaseReactClass = reactWrapper.BaseReactClass; works as well

@danihodovic
Copy link

Is this feature underway?

@Artazor
Copy link
Contributor

Artazor commented Apr 2, 2015

Awaiting it eagerly for writing

type P<T> = Promise<T> | T;
type A<T> = P<P<T>[]>;

function resolve<T>(a: A<T>): T[] { ... }

instead of

function resolve<T>(a: Promise<(Promise<T> | T)[]> | (Promise<T> | T)[]): T[] { ... }

@alshain
Copy link

alshain commented Apr 20, 2015

👍

1 similar comment
@jrsnyder
Copy link

👍

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

Approved. Anders suggests this will be non-trivial to implement, essentially requiring a new "kind" of type.

Any PR should have a large set of tests, especially in the area of self-referential type aliases and compatibility there.

@Artazor
Copy link
Contributor

Artazor commented Apr 28, 2015

@RyanCavanaugh can new "kind" of types be avoided if type-aliases will be implemented as higher-level types?

// List :: (* -> *)
type List = Array                               

// Of :: ((* -> *), *) -> *
// or nice but unrealistic:
// Of :: (* -> *) -> * -> *    
type Of<Container,Element> = Container<Element> 

// Numbers :: *
type Numbers = Of<List,Number>                  

Would it be backward compatible to what we have now?

@RyanCavanaugh
Copy link
Member

I don't think so. The issue is that the any use of a type alias is always equivalent to a thing that we already have implemented (object type, primitive type, union type etc.).

A counterexample to that is generic instantiation -- it's never the case today that Foo<string> is anything but an object type, but with generic type parameters in type aliases, it could be a union type. So there's the notion of a new "kind" of thing that is generic, but doesn't always produce an object type.

@Artazor
Copy link
Contributor

Artazor commented Apr 28, 2015

@RyanCavanaugh Oh, I see, what you mean. Of course a new kind of types emerges here.
I'd say that HighOrderType would be an ideal new kind for types that will cover all needs for generic parameters for type aliases, and introduce new powerful and consistent feature.
Here HigherOrderType is a classical Λ-term

τ ::= φ | τ<τ{,τ}> | Λα{,α}.τ   

or closer to the syntax:

τ ::= φ | τ<τ{,τ}> | <α{,α}>τ   

where

  • φ – is one of the types that you have already implemented (let's call it final type);
  • τ<τ{,τ}> — type application (generalisation of generic class instantiation);
  • Λα{,α}.τ or <α{,α}>τ — type abstraction (generalisation of generic class definition) where α is a formal type.

Here we treat all names of generic classes as values from the universe of higher-order types. Union can be established only between types that are β-reducible to final types. The same holds for every usage of a type when the type used to constrain a variable, field, parameter or return value. In these cases only types that are β-reducible to final types should be used. Otherwise Compile-time error should be thrown.

It would be really cool and consistent to have full featured higher-order types in TS

type P<T> = T | Promise<T>

just a syntax sugar to

type P = <T>(T | Promise<T>)

It's very interesting what @ahejlsberg would say about it. Are there practical/pragmatical obstacles to implementing it?

@Artazor
Copy link
Contributor

Artazor commented Apr 28, 2015

UPD. More text added to the previous comment -)

@danielearwicker
Copy link
Author

@Artazor I'm inclined to think this would be a separate feature suggestion. It's not covered by my examples. Specifically, what's not is highlighted by your example:

type Of<Container,Element> = Container<Element> 

i.e. treating the type parameter Container as a generic type rather than a complete type. That's not currently possible anywhere in TS.

Having taken my first look at the tsc code today I can definitely agree with the summary from @RyanCavanaugh and Anders! The code in checkTypeRelatedTo (checker.ts) is organised around the assumption that object types are the only context in which type parameters must be mapped to type arguments.

In my limited understanding I got as far as a function resolveTypeReferenceMembers that sets up a TypeMapper from typeParameters to typeArguments but by that point we're deep into the assumption of structural checking of members of an object. So I guess there would need to be some new if-branches in checkTypeRelatedTo to detect and deal with AliasType, set up the TypeMapper and do the same kind of deep check that is currently done for object types.

So its not a quick change to support my examples above, let alone higher-ordered types in which a type parameter can itself be treated as a generic type.

@Artazor
Copy link
Contributor

Artazor commented May 4, 2015

@danielearwicker surely it can be a separate feature, but in that case I'm afraid that it will not be implemented at all. And it will disappoint me.

I hope you'd catch the following fictional analogy:


Imagine that in early implementation of some language in an alternative reality (where people not yet discovered OOP) you realised, that you need to implement a very special feature: when you extend a class you should be able to change the implementation of the existing method (the method overriding in our world). Meanwhile applications you've considered required only one particular method to be overridable, so you start thinking that every class should have one such method.

You choose a very special magical name to it, and design your language and compiler according to this idea. And, of course, a whole bunch of applications will benefit from this single polymorphic method. Then hacks with switch inside that single overridable method will help to achieve effect of overriding multiple methods, and so on.

Some people can start to ask if it will be worth to allow overriding multiple methods by annotating them as "overridable" ("virtual" in our world), but your compiler is already built on the top of idea, that only one method can be overridden, you implemented special semantics, tricks, optimisations and so on. So if you want to allow multiple methods to be overridable then you have to rollback all these efforts and redesign overriding feature, to make it more general (with high risk to break a backward compatibility). Or you may try to implement this as a separate feature, that duplicates already existing feature but is more general. In that case you will face with a whole matrix of edge cases of interoperability between new feature and old ones, that will probably lead to the nightmare. So the most pragmatic scenario, in that case, is to stitch with single overridable method forever (if you want multiple overridable methods - choose/fork/implement another language).


This sad story is ready to happen with HighOrderTypes in TypeScript. However, it may be already too late to introduce them (by the implementation).

My implicit argument for the HighOrderType is that if you have

class MyVeryLongClassName<T> { ... }

then you can already write

type A = MyVeryLongClassName;

that is a legitimate short form of the future

type A<T> = MyVeryLongClassName<T>;

and both form should be supported (this will be hold automatically if high order types will be implemented)

Also, #2559 will be automatically resolved.

Still want to hear from @ahejlsberg and @RyanCavanaugh. Pleeeease, say that it is not too late to implement it! (?)

@balmychan
Copy link

👍

1 similar comment
@knazeri
Copy link

knazeri commented May 16, 2015

👍

@bluong
Copy link

bluong commented May 26, 2015

Still awaiting on this 👍

@markvandenbrink
Copy link

+1

@mariusschulz
Copy link
Contributor

👍

It would be great to be able to define type aliases like this:

type Predicate<T> = (element: T) => boolean;

@robertpenner
Copy link

👍

1 similar comment
@blobor
Copy link

blobor commented Jun 4, 2015

👍

@flaticols
Copy link

👍
It would be great and helpful feature

@ahejlsberg
Copy link
Member

Since y'all asked: #3397. Enjoy!

@ahejlsberg ahejlsberg modified the milestones: TypeScript 1.6, Community Jun 5, 2015
@ahejlsberg ahejlsberg self-assigned this Jun 5, 2015
@mhegazy mhegazy added Committed The team has roadmapped this issue and removed Help Wanted You can do this labels Jun 5, 2015
@danielearwicker
Copy link
Author

👏 What a legend.

@zpdDG4gta8XKpMCd
Copy link

Awesome!
On Jun 5, 2015 5:59 PM, "Anders Hejlsberg" [email protected] wrote:

Since y'all asked: #3397
#3397. Enjoy!


Reply to this email directly or view it on GitHub
#1616 (comment)
.

@Artazor
Copy link
Contributor

Artazor commented Jun 5, 2015

-)

@tinganho
Copy link
Contributor

tinganho commented Jun 5, 2015

👍

@danihodovic
Copy link

(Y)

@HerringtonDarkholme
Copy link
Contributor

Awesome! My first try on this is type classOf<T> = {new(): T}

@CyrusNajmabadi
Copy link
Contributor

@HerringtonDarkholme That seems like an odd use of generic type aliases. An interface seems like a better choice here:

interface classOf<T> {
    new(): T;
}

@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Committed The team has roadmapped this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests