Skip to content

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

Description

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?

Activity

  1. mhegazy commented on Sep 23, 2014

    @mhegazy
    Contributor

    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.

  2. mindplay-dk commented on Sep 23, 2014

    @mindplay-dk
    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 ;-)

  3. mindplay-dk commented on Sep 23, 2014

    @mindplay-dk
    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.

  4. danquirk commented on Sep 23, 2014

    @danquirk
    Member

    How is this different from #14 or #186?

  5. mindplay-dk commented on Sep 23, 2014

    @mindplay-dk
    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...

  6. joewood commented on Dec 31, 2014

    @joewood

    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.

  7. benliddicott commented on Jul 14, 2015

    @benliddicott

    Joe Wood (@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;
    }
    
  8. benliddicott commented on Jul 14, 2015

    @benliddicott

    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.

  9. benliddicott commented on Jul 15, 2015

    @benliddicott

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

  10. mhegazy commented on Jul 16, 2015

    @mhegazy
    Contributor

    benliddicott is correct. closing..

  11. added
    FixedA PR has been merged for this issue
    and removed
    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.
    on Jul 16, 2015
  12. locked and limited conversation to collaborators on Jun 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    FixedA PR has been merged for this issueSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions