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

Feature: type alternatives (to facilitate traits/mixins/type decorators) #727

Closed
mindplay-dk opened this issue Sep 23, 2014 · 10 comments
Closed
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@mindplay-dk
Copy link

The type system does a great job at mirroring most type patterns in JS, all except mixins/traits, anything that decorates types - which unfortunately is very common in JS.

Without it the type system remains incomplete - that is, we're still one step short of being able to describe JavaScript mixin type patterns.

Duplication and interfaces are not the answer, because this does not reflect what's actually happening in JavaScript - e.g. not duplication, but type decoration of sorts. Not sure if I'm using the correct terminology here.

This small library is an example of a real-world decorator - duplicating all of it's method declarations in every single model type in a view-model hierarchy would be extremely impractical.

The following is a simplified example:

class User {
    public name: string;
}

interface Events {
    on(event: string, callback: {():void});
    // other methods here...
}

function addEvents<T>(object: T): T {
    // decorate the object:
    object['on'] = function() {
        // ...
    }
    return <T> {};
}

var user = addEvents(new User());

The drawback here is you have to choose which is more important - that User is still a User after being decorated by addEvents(), or that it is now also Events. There is no direct way to document this type pattern - your only option currently is to document the shape of the resulting type fully, duplicating all the member declarations contributed by the decorator.

I propose syntax along the lines of the following, to support type alternatives:

function addEvents<T>(object: T): T|Events {
    // decorate the object:
    object['on'] = function() {
        // ...
    }
    return <T|Events> {};
}

var user = addEvents(new User());

The return type of the call is now User|Events which is a distinct composite type composed members of User and Events.

The resulting composite type extends User and implements Events, and should pass type-checks for both.

This should facilitate type-safe traits/mixins and probably other arbitrary type-patterns possible in JS.

Static type-hints with composite types are implemented in this way in e.g. PhpStorm using php-doc annotations, and it works well.

I think this may be a simpler and more flexible way to support mixins/traits, without explicitly adding support for those patterns specifically.

Thoughts?

@mhegazy
Copy link
Contributor

mhegazy commented Sep 23, 2014

A comment about the proposed syntax, T|Events to me reads as "T" or "Events", meaning that it is either; using a value decorated with this type, I would want to only use the intersection of the properties of both, and not the union. I would suggest calling it T&Events.

@mindplay-dk
Copy link
Author

I perceive e.g. User|Observable as meaning, "you can treat this object as User or Observable".

Interestingly, you can use the | operator in e.g. php-doc as meaning, "this type or that type", or "this type and that type" - since there is only one possible type of static operation, which is a union, which really is neither "and" or "or", but is both (or neither) depending on the context.

To give a concrete example, if I type an argument as File|Folder, you can take that as meaning File or Folder - when checking property access or method calls statically, you have to permit File and Folder properties and methods, since the argument could be either.

Compared with an argument typed as User|Observable, which you can take as meaning User and Observable, or "can be treated as either User or Observable" - there's no difference, since, likewise, when checking property access or method calls statically, you have to consider both User and Observable properties and methods to be valid, since the argument could be either or both.

In cases where the | type operator is used to specify type alternatives, you will often see further type-checking and type-casting in the implementation - e.g.:

if (item is File) { ... } else if (item is Folder) { ... }

In other words, the difference is a conceptual (or in some languages run-time) aspect, not something you can distinguish at design-time or compile-time, and it doesn't matter whether you think of the operator as meaning "and" or "or", because, as far as static analysis is concerned, there's no difference.

My guess is they picked | over & for the operator, because & is often taken as meaning address-of or reference-to in various other languages - it's meaning is already more overloaded than that of |.

It's possible they picked it simply for aesthetics too - since it doesn't really mean "and" anymore than it means "or", they may have picked it just because it's easier on the eyes ;-)

@mindplay-dk
Copy link
Author

If you wanted to avoid ambiguity by overloading, you could also choose a literal keyword operator, e.g. union(User, Observable) though it might start to look rather strange in complex type expressions such as List<union(User, Observable)> which might look more natural as simply List<User|Observable>.

Come to think of it, it's probably premature to debate the syntax until we hear from the TypeScript team whether this is a feature they're even willing to consider.

@danquirk
Copy link
Member

How is this different from #14 or #186?

@mindplay-dk
Copy link
Author

Appears to be similar to #14 but different from #186 which proposes sum types.

I would prefer something simple and declarative, which does not leave a run-time footprint, to ensure semantic compatibility with JavaScript, in and out - especially in... there are tons of JS libraries already which decorate variables or prototypes with new members...

@RyanCavanaugh RyanCavanaugh 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 26, 2014
@joewood
Copy link

joewood commented Dec 31, 2014

Hitting this issue while using React mixins. I was wondering if Multiple Inheritance would work, which is how C++ does mixins. A sum type could be expressed as an anonymous type extending other types:

function addEvents<T>(object: T): extends T,Events {
    // decorate the object:
    object['on'] = function() {
        // ...
    }
    return <extends T,Events>object;
}

var user = addEvents(new User());

Unlike C++, order would matter here, with later entries overriding slots of previous entries.

@benliddicott
Copy link

@joewood, they don't need to be extends, only implements.

function addEvents<T>(o: T): implements T, Events {
  o["on"] = function(){}
  return <implements T, Events>o;
}

If an interface could be extend a type parameter, this would provide an alternative solution, more verbose, but also possible to be more type-safe:

class C { c(){}}

interface HasEvents<T> extends T { on(name, fn: (o: T, n:number);}

function addEvents<T>(object: T) {
  object["on"]=function(){};
  return <HasEvents<T>>object;
}

@benliddicott
Copy link

OK this is not like #14 or #805, it's kind of the opposite. Here we need to say, in a generic way, "if you give me any object I will give you back the same object but now supporting a new interface", i.e. of both types so it is an intersection type not a union type.

If interfaces could extend a type parameter this would solve the issue and enable expressing the traits pattern properly, as above.

In the a real-world example we would want something like this (standard Emitter pattern):

interface Emitter{ 
  on(event, fn);
  once(event, fn);
  off(event, fn);
  emit(event);
}
interface EmitterTrait<T> extends T, Emitter{}

function MakeEmitter<T> : EmitterTrait<T>(t: T){
  // add the functions to the object
  require('emitter-library').mixin(t);
  return <EmitterTrait<T>>t;
}

Presumably it would also allow writing a type-safe .d.ts which did the same.

@benliddicott
Copy link

@mindplay-dk , I think this is resolved by Intersection types #3622 committed to master 21 days ago.

@mhegazy
Copy link
Contributor

mhegazy commented Jul 16, 2015

@benliddicott is correct. closing..

@mhegazy mhegazy closed this as completed Jul 16, 2015
@mhegazy mhegazy added Fixed A PR has been merged for this issue and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Jul 16, 2015
@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
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants