GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Suggestion: sum types / tagged union · Issue #186 · microsoft/TypeScript

Suggestion: sum types / tagged union #186

New issue
Copy link
New issue
Copy link
Closed
Closed
Suggestion: sum types / tagged union#186
Copy link
Assignees
ahejlsberg
Labels
CommittedThe team has roadmapped this issueThe team has roadmapped this issueFixedA PR has been merged for this issueA PR has been merged for this issueSuggestionAn idea for TypeScriptAn idea for TypeScript
Milestone
TypeScript 2.0

Description

@alainfrisch
Alain Frisch (alainfrisch)
opened on Jul 22, 2014
Issue body actions

A very nice addition to TypeScript's type system would be sum types in the spirit of ML-like languages. This is one of basic and simple programming constructs from functional programming which you really miss once you get used to it, but which seem to have a hard time being included in new modern languages (contrary to other features from functional programming such as first-class functions, structural types, generics).

I guess the most natural way to integrate sum types in the current language syntax would be to extend enum variants with extra parameters (similarly to what Rust does: http://doc.rust-lang.org/tutorial.html#enums ) and upgrade the switch statement to a more powerful structural pattern matching (although in a first step, simply discriminating on the toplevel variant and capturing its parameters would be already quite good).

This is quite different from other the proposal about "union types" (#14), which would mostly be useful to capture types in existing Javascript APIs. Sum types are rather used to describe algebraic data structures. They would be particularly useful for any kind of symbolic processing (including for implementing the TypeScript compiler).

Reacted by Milán Nagy, Adi Dahiya, Michael Messer, Arpad Borsos, Nikita, Thor Adam, Tom Crockett, Oliver Uvman, Tingan Ho, Linda_pp and 8 more

Activity

Next
  1. fdecampredon commented on Jul 22, 2014

    @fdecampredon
    François de Campredon (fdecampredon)
    on Jul 22, 2014
    More actions

    👍

  2. Ryan Cavanaugh (RyanCavanaugh)
    added
    Needs Proposal
    on Jul 22, 2014
  3. RyanCavanaugh commented on Jul 22, 2014

    @RyanCavanaugh
    Ryan Cavanaugh (RyanCavanaugh)
    on Jul 22, 2014
    Member
    More actions

    +Suggestion, Needs Proposal

  4. fdecampredon commented on Jul 22, 2014

    @fdecampredon
    François de Campredon (fdecampredon)
    on Jul 22, 2014
    More actions

    I would love such feature.
    However from javascript perspective I think complex pattern matching could be really hard to achieve.
    Even for simple example like the following one, it's hard to find javascript corresponding :

    enum Color {
      Red,
      Green,
      Blue,
      Rgb(r: number, g:  number, b:  number)
    }
    
    
    function logColor(color: Color) {
      switch(color) {
        case Color.RGB(r, g, b):
          console.log('rgb(' + r + ', ' + g + ', ' + b + ')');
          break
        default:
          console.log(Color[color])
          break;
      }
    }
    
    var myColor: Color = Color.Rgb(255, 0, 255); 
    var red: Color = Color.Red;
    
    logColor(myColor); //should ouput 'rgb(255, 0, 255)'
    logColor(red);  //should ouput 'Red'
    
    console.log(Color[myColor]); // should output 'Rgb'
    console.log(Color[3]); // should output 'Rgb';
    console.log(Color[Color.Rgb]); // should ouput 'Rgb'
    console.log(Color['Rgb']); // should ouput '3'
    

    I tried to hack a little something, it seems to work but i'm not sure if it worth the outputted JS

    var Color;
    (function (Color) {
        Color[Color["Red"] = 0] = "Red";
        Color[Color["Green"] = 1] = "Green";
        Color[Color["Blue"] = 2] = "Blue";
        Color.Rgb = function (r, g, b) {
          var result = [r, g, b];
          result.valueOf = result.toString = function () {  return 3; };
          return result;
        };
        Color.Rgb.toString = Color.Rgb.valueOf = function () { return 3; };
        Color[3] = "Rgb";
    })(Color || (Color = {}));
    
    function logColor(color) {
      switch(+color) {
        case 3:
          var r = color[0];
          var g = color[1];
          var b = color[2];
          console.log('rgb(' + r + ', ' + g + ', ' + b + ')');
          break;
        default:
          console.log(Color[color])
          break;
      }
    }
    
    var myColor = Color.Rgb(255, 0, 255); 
    var red = Color.Red;
    
    logColor(myColor); //should ouput 'rgb(255, 0, 255)'
    logColor(red);  //should ouput 'Red'
    
    console.log(Color[myColor]); // should output 'Rgb'
    console.log(Color[3]); // should output 'Rgb';
    console.log(Color[Color.Rgb]); // should ouput 'Rgb'
    console.log(Color['Rgb']); // should ouput '3'

    note the + before the switch statement value to force the transformation trough 'valueOf'. I hope there is a better way to do that but I don't see how without breaking retrocompatibility.

  5. iislucas commented on Jul 23, 2014

    @iislucas
    iislucas (iislucas)
    on Jul 23, 2014
    More actions

    I suggest an implementation that leverages the existing lookup power of objects (not using switch/case but instead using parameter names). Here's what I'm thinking of...

    [Note: I've changed the example to avoid using colour as that's a bit confusing as colours are made of RGB. Example now uses Animals.]

    type Animal {
      | Dog;
      | Cat;
      | Bird;
      | Monster { scaryness :number, size :number };
    }
    

    That would approximately correspond to a JS object that is assumed to have exactly one of the following parameters, i.e. be treated a bit like:

    interface Animal {
      kind_ :string;
      Dog ?:void;
      Cat ?:void;
      Bird ?: void;
      Monster ?: { scaryness :number, size :number, b :number }
    }
    

    When you define a variable of this type, e.g.

    var monster = Monster {scaryness: 3, size: 2};
    

    it can be compiled like so:

    var monster = { kind_: 'Monster', Monster: {scaryness: 3, size: 2} };
    

    Then you can match in a more standard functional programming style of syntax like so:

    function logAnimal(animal: Animal) {
      case (animal) {
        | Dog => { console.log('Dog (barks!)'); }
        | Monster(m) => { console.log('Monster is ' + m.scaryness + ' scary'); }
        | _ => { console.log(animal.kind_); }
      };
    }
    
    var myMonster :Animal = Animal.Monster { scaryness: 100, size: 5 };
    var dog :Animal = Animal.Dog;
    
    logAnimal(myMonster); //should ouput 'Monster is 100 scary'
    logAnimal(dog);  //should ouput 'Dog (barks!)'
    

    Which would be compiled to JS like so:

    function logAnimal(animal) {
      if(animal.kind_ in animal) {
        var caseSelector_ = {
          'Dog': function() { console.log('Dog (barks!)'); },
          'Monster': function(m) {
              console.log('Monster is ' + m.scaryness + ' scary');
            }
        }
        caseSelector_[animal.kind_](animal[animal.kind_]);
      } else {
        // Default
        console.log(animal.kind_);
      }
    }
    
    var myMonster = { kind_: 'Monster', Monster: {scaryness: 100, size: 5} };
    var dog = { kind_: 'Dog', Dog: null };
    
    logAnimal(myMonster); //should ouput 'Monster is 100 scary'
    logAnimal(dog);  //should ouput 'Dog (barks!)'
    

    That seems to provide reasonable balance of conciseness, readablity and efficiency.

    Reacted by Michael Messer, Daniel Ferreira Monteiro Alves and andretshurotshka
  6. RyanCavanaugh commented on Jul 28, 2014

    @RyanCavanaugh
    Ryan Cavanaugh (RyanCavanaugh)
    on Jul 28, 2014
    Member
    More actions

    Can we close this as a duplicate of #14?

  7. fdecampredon commented on Jul 29, 2014

    @fdecampredon
    François de Campredon (fdecampredon)
    on Jul 29, 2014
    More actions

    Ryan Cavanaugh (@RyanCavanaugh) for me it's something quite different than union type more an extension of enum.

  8. iislucas commented on Jul 29, 2014

    @iislucas
    iislucas (iislucas)
    on Jul 29, 2014
    More actions

    Maybe worth thinking of this more like an algebraic data type?

    Reacted by Balázs Édes, Michael Messer and Roman Melnikov
  9. fsoikin commented on Jul 29, 2014

    @fsoikin
    Fyodor Soikin (fsoikin)
    on Jul 29, 2014
    More actions

    I second that. Unions and discriminated unions are very different both in implementation and semantics. Plus, non-discriminated unions are already used a lot in actual JavaScript code out there, which gives them a much higher priority.

  10. danquirk commented on Jul 29, 2014

    @danquirk
    Dan Quirk (danquirk)
    on Jul 29, 2014
    Member
    More actions

    A major concern I have is how to effectively use types like this without adding a lot of expression level syntax for operating over them. How useful will it be if there's no 'match' type operator to nicely decompose these in a type safe manner?

  11. alainfrisch commented on Jul 30, 2014

    @alainfrisch
    Alain Frisch (alainfrisch)
    on Jul 30, 2014
    Author
    More actions

    Union types and sum types (i.e. disjoint tagged unions) are really different concepts, with different use cases.

    Union types are certainly very important to represent faithfully the API of existing Javascript libraries, although a full support for them is tricky since you cannot in general determine type-information based on runtime values.

    Sum types are more important, I'd say, to write high-level programs, not necessarily related to existing Javascript code bases, particularly for everything which pertains to symbolic processing. Having them in the language would make it a great choice for a whole new class of problems.

    Dan Quirk (@danquirk) Extending the existing switch statement to allow capturing "enum arguments" would be a natural first step. See François de Campredon (@fdecampredon)'s first example. This would not introduce a lot of new expression level syntax.

    Reacted by Xor
  12. alainfrisch commented on Sep 9, 2014

    @alainfrisch
    Alain Frisch (alainfrisch)
    on Sep 9, 2014
    Author
    More actions

    Another possible view of sum types in TypeScript would be as an extension of regular interfaces with an "exclusive-or" modality instead of an "and".

    interface MySumType {
    | name: string
    | id: number
    }

    This would classify objects that contains exactly one of the mentioned fields (with the corresponding type).

    This might be closer to how people would naturally encode sum types in Javascript.

  13. iislucas commented on Sep 9, 2014

    @iislucas
    iislucas (iislucas)
    on Sep 9, 2014
    More actions

    Alain Frisch (@alainfrisch) 👍 that's the way I was coding it up in my suggestion above :)

  14. beloglazov commented on Sep 10, 2014

    @beloglazov
    Anton Beloglazov (beloglazov)
    on Sep 10, 2014
    More actions

    👍

  15. 29 remaining items

  16. Artazor commented on Jan 21, 2016

    @Artazor
    Anatoly Ressin (Artazor)
    on Jan 21, 2016
    Contributor
    More actions

    Since we have string literal types, I would propose to interpret the following declaration

    interface Action[type] {   // the name "type" will be used as discriminator tag
         "REQUEST": {}
         "SUCCESS": { data: string }
         "FAILURE": { error: any } 
    }

    as fully equivalent shorthand to

    type Action = { type: "REQUEST" } 
                | { type: "SUCCESS", data: string } 
                | { type: "FAILURE", error: any }

    and detect such situation in if and switch in the following manner

    if all conditions are met:

    • expression of form expr.prop is compared against expression S for equality or not-equality;
    • syntactically expr is simple identifier (no compound expressions);
    • the type of the S expression is singleton string literal;
    • expression expr itself has type, which in every union branch has the same property named prop with singleton string literal type (not necessarily disjoint across the union branches)

    then in the proper following context the type of expr is narrowed to the union of the branches where type of the prop field exactly matches the type of S (or doesn't match if the check was for non-equality), or type: { prop: S } if no alternatives found, or {prop: string} in switch/default alternative.

    Thus, we will be able to write

    var a: Action;
    // ...
    if (a.type === "SUCCESS") {
         console.log(a.data) // type of a is narrowed to {type: "SUCCESS", data: string}
    }

    or even

    function reducer(s: MyImmutableState, a: Action): MyImmutableState {
        switch (a.type) {
            case "REQUEST": // narrow a: { type: "REQUEST" }
                return s.merge({spinner: true});
            case "SUCCESS": // narrow a: { type: "SUCCESS", data: string }
                return s.merge({spinner: false, data: a.data, error: null});  
            case "FAILURE": // narrow a: { type: "FAILURE", error: any }
                return s.merge({spinner: false, data: null, error: a.error});
            default: // widen a: { type: string }
                return s; 
        }
    }

    This proposal is less powerfull than full algebraic types (it lacks recursion), nevertheless it's rather pragmatic as it helps to assign the correct types to commonly used patterns in JavaScript especially for React + (Flux/Redux).

    I know that narrowing is working only on simple variables, but I think this case is somewhat ideologically equivalent to the type-checking for expressions assigned to the simple variable.

    Dan Quirk (@danquirk), what is your opinion?

    Reacted by Simon Buchan, Sean Vieira, Punya Biswal, Sunny64 and Patrick Lienau
  17. weswigham commented on Jan 21, 2016

    @weswigham
    Wesley Wigham (weswigham)
    on Jan 21, 2016
    Member
    More actions

    Anatoly Ressin (@Artazor) I've already done some investigation into equality -based type narrowing - It is pretty cool to use.

  18. DanielRosenwasser commented on Jan 21, 2016

    @DanielRosenwasser
    Daniel Rosenwasser (DanielRosenwasser)
    on Jan 21, 2016
    Member
    More actions

    Anatoly Ressin (@Artazor) with Wesley Wigham (@weswigham)'s type narrowing branch, and some recent changes I've made, something like that works. It needs to happen one step at a time though.

  19. simonbuchan commented on Mar 15, 2016

    @simonbuchan
    Simon Buchan (simonbuchan)
    on Mar 15, 2016
    More actions

    If you're looking for matching sugar for using existing types, maybe allow destructuring in cases?:

    function reducer(s: MyImmutableState, a: Action): MyImmutableState {
        switch (a) {
            case {type: "REQUEST"}:
                return s.merge({spinner: true});
            case {type: "SUCCESS", data}:
                return s.merge({spinner: false, data, error: null});  
            case {type: "FAILURE", error}:
                return s.merge({spinner: false, data: null, error});
            default:
                return s;
        }
    }
    

    This may already have a (nonsense) meaning of check if a is the same instance of this new literal. Other options:

    • case {type is "FAILURE", error}:, suggesting type guarding is happening, but those are not emitted.
    • case let {type: "FAILURE", error}:, though I'm concerned about it being too easy to miss the let.
    • case {type == "FAILURE", error}:, which would allow other destructurings like {opCount == 2, op1, op2}, {opCount > 3, opList}, but type = "FAILURE" (meaning default missing type in existing ES6) would, again be too close.

    Alternatively: Perhaps depend on adding C++-style condition declarations (if (let foo = getFooOrNull()) foo.bar();, and allow:

    if (let {type == "FAILURE", error} = a) ...
    

    I think this is closer to how languages with pattern matching work in a way that feels like where ES is going, but it seems like (something like) it should be proposed as an ES feature first.

    Reacted by Pauan
  20. basarat commented on Mar 15, 2016

    @basarat
    Basarat Ali Syed (basarat)
    on Mar 15, 2016
    Contributor
    More actions

    FWIW Anatoly Ressin (@Artazor)'s proposal #186 (comment) is what flow does (disjoint unions) : http://flowtype.org/docs/disjoint-unions.html#_

    type Action = { type: "REQUEST" } 
                | { type: "SUCCESS", data: string } 
                | { type: "FAILURE", error: any }

    Conditional checks on type allow you to narrow down the other members of Action 🌹

  21. weswigham commented on Mar 16, 2016

    @weswigham
    Wesley Wigham (weswigham)
    on Mar 16, 2016
    Member
    More actions

    Basarat Ali Syed (@basarat) I have a version of our narrowing code which allows this behavior
    with existing types (#6062) - but it still has some open questions.

    On Tue, Mar 15, 2016, 7:44 PM Basarat Ali Syed [email protected]
    wrote:

    FWIW Anatoly Ressin (@Artazor) https://github.com/Artazor's proposal #186 (comment)
    #186 (comment)
    is what flow does (disjoint unions) :
    http://flowtype.org/docs/disjoint-unions.html#_

    type Action = { type: "REQUEST" }
    | { type: "SUCCESS", data: string }
    | { type: "FAILURE", error: any }

    Conditional checks on type allow you to narrow down the other members of
    Action [image: 🌹]

    —
    You are receiving this because you were mentioned.
    Reply to this email directly or view it on GitHub
    #186 (comment)

    Reacted by Richard Simpson, Claudia Meadows, Sean Vieira, Ilya Rezvov, Michael Messer and Stefano Pigozzi
    Reacted by Basarat Ali Syed, Sean Vieira, Michael Messer, Sunny64 and Stefano Pigozzi
  22. ahejlsberg commented on Jun 14, 2016

    @ahejlsberg
    Anders Hejlsberg (ahejlsberg)
    on Jun 14, 2016
    Member
    More actions

    Implementation of discriminated union types using string literal type tags is now available in #9163.

    Reacted by ZpdDG4gta, Troy Gerwien, Anatoly Ressin, Linda_pp and Christoph Herzog
    Reacted by Josh Abernathy, Troy Gerwien, Yegor Roganov, Lennart, Claudia Meadows, ZpdDG4gta, Sean Vieira, Ting-gian LUA, Christoph Herzog, Pauan and 1 more
    Reacted by ZpdDG4gta and Troy Gerwien
  23. Mohamed Hegazy (mhegazy)
    added this to the TypeScript 2.0 milestone on Jun 15, 2016
  24. Mohamed Hegazy (mhegazy)
    added
    CommittedThe team has roadmapped this issueThe team has roadmapped this issue
    and removed
    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.This issue needs a plan that clarifies the finer details of how it could be implemented.
    on Jun 15, 2016
  25. Mohamed Hegazy (mhegazy)
    assigned
    Anders Hejlsberg (ahejlsberg)
    on Jun 15, 2016
  26. Mohamed Hegazy (mhegazy)
    closed this as completedon Jun 17, 2016
  27. Mohamed Hegazy (mhegazy)
    added
    FixedA PR has been merged for this issueA PR has been merged for this issue
    on Jun 17, 2016
  28. shelby3 commented on Sep 10, 2016

    @shelby3
    shelby3
    on Sep 10, 2016
    More actions

    Aleksey-Bykov wrote:

    Another thing I truly dont understand about type guards is when I do a
    check against an interface _rather than_ class (function constructor), say:

    Use an abstract class instead of interface.

    Dan Quirk (@danquirk) wrote:

    Well that wouldn't do what you want in JavaScript either if x was some object literal. Your type guard would either be a set of property checks

    if(x.length) { ... } 
    if(x.aPropertyInA) { ... }
    

    That is a structural guard which although may be a useful feature but may not always be applicable, thus my suggestion above to use abstract class for a nominal guard instead of:

    or we could do something like #1007

    Which appears to me to be undesirable.

    Mike Stay (@metaweta) wrote:

    TypeScript already has some support for ADTs using standard class inheritance. I propose a mild desugaring:

    Which is a nominal sum type (aka ADT) and employs nominal guards.

    Andy Hanson (@andy-hanson) wrote:

    Yegor Roganov (@roganov) wrote:

    Andy Hanson (@andy-hanson) wrote:

    Ivo Gabe de Wolff (@ivogabe) wrote:

    Andy Hanson (@andy-hanson) wrote:

    It would be nice to have a sealed keyword on an abstract class to indicate that the only subclasses intended to exist are those defined in the same project.

    You can do that with a union type:

    The idea is that one should be able to define the class (with methods) and the type as the same thing, rather than having separate class Super and type SuperT = Sub1 | Sub2.

    Can't you just override required methods on Sub1 and Sub2 (in other words, dynamic dispatch)? Why would you want to check types?

    Many people prefer a style of programming where functions are defined only once, as opposed to once per subclass. That's why this issue here exists.

    Without the class Super then the only way to compile-time type that some member properties of the types in the union are equivalent is structural typing, thus it loses some of nominal typing capability.

    So sealed is required for exhaustive checking where we want nominal typing and want to follow the software engineering principles of Single-Point-Of-Truth (SPOT) and DNRY, so that we don't have to declare a separate class Super and type SuperT = Sub1 | Sub2.

    However, if ever nominal typeclasses were supported (a la Rust or Haskell), then the entire point of typeclasses (versus class subtyping) is to not conflate the definition and implementations of new interfaces on data type in order to have more degrees-of-freedom in compile-time extensibility. Thus, the relationship between interface structures becomes nominal and orthogonal to the definition of the data type or nominal sum type, and the extra declaration of trait Super (and implementations of Sub1 and Sub2 for typeclass Super) isn't a violation of SPOT and DNRY. So with typeclasses and if we don't formalize nominal class subtyping, then afaics sealed would be unnecessary.

    Basarat Ali Syed (@basarat) wrote:

    type Action = { type: "REQUEST" } 
                | { type: "SUCCESS", data: string } 
                | { type: "FAILURE", error: any }
    

    Conditional checks on type allow you to narrow down the other members of Action

    That is structural (not nominal) sum typing.

    Anatoly Ressin (@Artazor) wrote:

    This proposal is less powerfull than full algebraic types (it lacks recursion)

    And it isn't compatible with nominal typeclasses; thus conflates interface (declaration and implemention) with data type, i.e. the interfaces (e.g. data: string) that each of the members of the sum type (e.g. SUCCESS) implement is not orthogonal to the declaration of the sum type Action (and such conflation doesn't exist in Rust and Haskell). Afaics, structural typing isn't compatible with maximum compile-time extensible (nor complete solutions to Wadler's Expression Problem). Merged #9163 gives us extensibility of declaring classes and interfaces which implement the discriminated union types, but it doesn't enable us to compile-time extend _existing_ classes (without editing their dependent code, e.g. a function returning a type of existing class) by providing orthogonal implementation of a typeclass.

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

  • @ahejlsberg
    Anders Hejlsberg (ahejlsberg)

Labels

CommittedThe team has roadmapped this issueThe team has roadmapped this issueFixedA PR has been merged for this issueA PR has been merged for this issueSuggestionAn idea for TypeScriptAn idea for TypeScript

Type

No type

Projects

No projects

    Milestone

    • TypeScript 2.0

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.