microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.47k stars 12.43k forks source link

Suggestion: sum types / tagged union #186

Closed alainfrisch closed 8 years ago

alainfrisch commented 10 years ago

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

fdecampredon commented 10 years ago

:+1:

RyanCavanaugh commented 10 years ago

+Suggestion, Needs Proposal

fdecampredon commented 10 years ago

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.

iislucas commented 10 years ago

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.

RyanCavanaugh commented 10 years ago

Can we close this as a duplicate of #14?

fdecampredon commented 10 years ago

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

iislucas commented 10 years ago

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

fsoikin commented 10 years ago

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.

danquirk commented 10 years ago

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?

alainfrisch commented 10 years ago

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.

@danquirk Extending the existing switch statement to allow capturing "enum arguments" would be a natural first step. See @fdecampredon's first example. This would not introduce a lot of new expression level syntax.

alainfrisch commented 10 years ago

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.

iislucas commented 10 years ago

@alainfrisch :+1: that's the way I was coding it up in my suggestion above :)

beloglazov commented 10 years ago

:+1:

metaweta commented 9 years ago

Note that union and sum are two different operations: {1,2,3} U {2,3,4} = {1,2,3,4} // four elements {1,2,3} + {2,3,4} = {(L, 1), (L, 2), (L, 3), (R, 2), (R, 3), (R, 4)} // six elements, needs tag L/R,

Union is the coproduct in the category of sets and inclusions. string | string = string Sum is the coproduct in the category of sets and functions. L string + R string = two tagged copies of string.

Sum is what's used in algebraic data types. In Haskell, the pipe | does not mean union, it means sum:

  data twoString = L string | R string
zpdDG4gta8XKpMCd commented 9 years ago

@danquirk, do you think the following implementation is too heavy as far as expression syntax?

// sum-type declaration syntax example

data Optional<a> = None | Some a

// initializing a sum-type variable
var optValue = Math.random() > 0.5 ? None : Some('10')

// destructuring (syntax TBD)
var text = optValue {
    None => 'There is nothing there.';
    Some(value) => 'Got something: ' + value;
    // or Some(value) { return 'Got something: ' + value; }
};

generated javascript

// initializing a sum-type variable 
var optValue = Math.random() > 0.5 ? ({ tag: 'None' }) : ({tag: 'Some', value: '10'});

// destructuring (one possible implementation)
var text = (function(optValue) {
    switch(optValue.tag) {
        case 'None': return 'There is nothing there.';
        case 'Some': return 'Got something: ' + optValue.value;
        default: throw Error('Unexpected tag \'' + optValue.tag + '\.');
    }
})(optValue);
zpdDG4gta8XKpMCd commented 9 years ago

or something like this which I like better

// sum-type declaration syntax example

data Optional<a> = none | some a

// initializing a sum-type variable
var optValue = Math.random() > 0.5 ? none() : some('10');

// destructuring (syntax TBD)
var text = optValue {
    none: () => 'There is nothing there.',
    some: (value) => 'Got something: ' + value
};

generated javascript

// initializing a sum-type variable 
var optValue = Math.random() > 0.5 ? ({ 'none': {} }) : ({ 'some': '10'});

// destructuring (one possible implementation)
var text = (function(optValue) {
    switch(___keyof(optValue)) {
        case 'none': return 'There is nothing there.';
        case 'some': return 'Got something: ' + optValue.some;
        default: throw Error('Unexpected tag.');
    }
})(optValue);

function ____keyOf(value) { for (var key in value) return key; }
metaweta commented 9 years ago

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

data Tree<A> {
  Node(a: A, left: Tree<A>, right: Tree <A>);
  Leaf();
}

var myTree = Node(Leaf(), Leaf());

match (myTree) {
  Node(v, l, r): foo(l);
  Leaf(): bar;
}

should desugar to the following TypeScript:

interface Tree<A> {}
class Node<A> implements Tree<A> {
    constructor (public a: A, public left: Tree<A>, public right: Tree<A>) {}
}
class Leaf<A> implements Tree<A> {
    constructor () {}
}

var myTree = Node(Leaf(), Leaf());

switch (myTree.constructor.name) {
  case 'Node': ((v,l,r) => foo(l)) (myTree.a, myTree.left, myTree.right); break;
  case 'Leaf': (() => bar) (); break;
}

In the code above, I've used the name property of functions; a more type-guard-style approach would desugar to the expression

(myTree instanceof Node) ? ((v,l,r) => foo(l)) (myTree.a, myTree.left, myTree.right)
: (myTree instanceof Leaf) ? (() => bar) () 
: void 0;
danquirk commented 9 years ago

@aleksey-bykov as much as I love the syntax used for this concept in functional languages I think our current type guard concept is a very elegant solution for the problem in TypeScript. It allows us to infer and narrow union cases using existing JavaScript patterns which means more people will benefit from this than if we required people to write new TypeScript specific syntax. Hopefully we can extend type guards further (there are a number of suggestions for that already).

zpdDG4gta8XKpMCd commented 9 years ago

The bigest benefit of sum types is their propery of exhaustiveness or totality if you will. Type guards are not going to give it, are they? On Jan 5, 2015 9:12 PM, "Dan Quirk" notifications@github.com wrote:

@aleksey-bykov https://github.com/aleksey-bykov as much as I love the syntax used for this concept in functional languages I think our current type guard concept is a very elegant solution for the problem in TypeScript. It allows us to infer and narrow union cases using existing JavaScript patterns which means more people will benefit from this than if we required people to write new TypeScript specific syntax. Hopefully we can extend type guards further (there are a number of suggestions for that already).

Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/186#issuecomment-68816231 .

danquirk commented 9 years ago

That's definitely a big part of their value. It's something I would like to look at but haven't filed a formal issue for yet. Certainly you could imagine an error for something like this:

function foo(x: string|number) {
    if(typeof x === "string") { ... }
    // error, unhandled case: typeof x === "number"
}

the question is whether it could be robust enough to handle more complicated cases.

zpdDG4gta8XKpMCd commented 9 years ago

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

interface A {}
type T = string | A;
var x : T = undefined;
if (x instanceof A) then alert('Hey!');

How is this supposed to work? On Jan 5, 2015 9:27 PM, "Dan Quirk" notifications@github.com wrote:

That's definitely a big part of their value. It's something I would like to look at but haven't filed a formal issue for yet. Certainly you could imagine an error for something like this:

function foo(x: string|number) { if(typeof x === "string") { ... } // error, unhandled case: typeof x === "number" }

the question is whether it could be robust enough to handle more complicated cases.

Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/186#issuecomment-68817309 .

danquirk commented 9 years ago

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) { ... }

or we could do something like #1007

zpdDG4gta8XKpMCd commented 9 years ago

Is checking for property considered a type guard? I mean I know I can sniff properties just like I would do in plain JavaScript. What my question is if a check for a property like you put it would give me any tyoe safety in then block after such check. On Jan 5, 2015 9:50 PM, "Dan Quirk" notifications@github.com 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) { ... }

or we could do something like #1007 https://github.com/Microsoft/TypeScript/issues/1007

Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/186#issuecomment-68818961 .

danquirk commented 9 years ago

Not at the moment, but I implemented it over here to test it out https://github.com/Microsoft/TypeScript/issues/1260. So you can see why I might think there's enough room to extend this functionality far enough to get the full breadth of checking you're imagining without needing new syntax (which means more people benefit and we make life easier on ourselves if that syntax is needed by ES later).

zpdDG4gta8XKpMCd commented 9 years ago

I am all up for keeping as less new syntax as possible. If you can fit a new feature in old syntax it's good for everyone. Then I will withdraw my proposal as needless. On Jan 5, 2015 9:59 PM, "Dan Quirk" notifications@github.com wrote:

Not at the moment, but I implemented it over here to test it out #1260 https://github.com/Microsoft/TypeScript/issues/1260. So you can see why I might think there's enough room to extend this functionality far enough to get the full breadth of checking you're imagining without needing new syntax (which means more people benefit and we make life easier on ourselves if that syntax is needed by ES later).

Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/186#issuecomment-68819561 .

alainfrisch commented 9 years ago

The bigest benefit of sum types is their propery of exhaustiveness or totality if you will. Type guards are not going to give it, are they?

Nevor's proposal for implementing sum types through literal singleton types (https://github.com/Microsoft/TypeScript/issues/1003) supports checking exhaustiveness. After exhausting all cases through type guards, the type is reduced to the empty type (void), and you can ask the compiler to check for exhaustiveness by adding a final call to the "impossible" function defined as function impossible(v : void) { throw "impossible"; }

zpdDG4gta8XKpMCd commented 9 years ago

I cannot see how exhaustiveness can be enforced this way. What if I simply missed a case in a switch? Will the compiler give me an error? For example:

type Ready = { value : "ready" };
type NotReady = { value : "notready" };
type Value = Ready | NotReady;
var v : Value;
switch (v.value) {
   case "ready":
     break;
   // TODO: Don't forget to uncomment the following!
   // case "notready":
   //  break;
   default:
      impossible(v); // okay as long as the type is not extended
}

An error from a compiler about the missing case of "notready" is required in order to call it "exhaustiveness".

But even in case there was a compiler error it would be a breaking change, because the switch operator has never been exhaustive. So to avoid a breaking change the switch operator has to know somehow when to be become exhaustive and when be just a switch in a conventional way.

With all this said it doesn't seem to be that exhaustiveness can be enforced without additional syntax.

alainfrisch commented 9 years ago

Yes, you get exhaustiveness check. If you forget the "notready" case, then the default branch is type-checked asssuming that v has type NotReady (which is what remains from Ready|NotReady after you matched on "ready"), and type-checking of that default branch fails, because impossible expects that v has type void (and NotReady is not a subtype of void).

And it's not a breaking change, because the error is only triggerred by the call to the impossible function. If you don't call it, you don't get the error (and you don't get the exhaustiveness check). This is the only weakness here: you need to think about adding the default branch and call impossible in it or you don't get the exhaustiveness check.

zpdDG4gta8XKpMCd commented 9 years ago

Oh, now I see what you are saying. The part I didn't realize was "impossible expects that v has type void"

fdecampredon commented 9 years ago

Honestly even if we have property type-guard the syntax will be somewhat very unnatural and verbose

enum ColorEnum {
  Red,
  Green,
  Blue,
  Rgb,
  Rgba,
}
type Color = 
  { value: ColorEnum.RED } | 
  { value: ColorEnum.GREEN } | 
  { value: ColorEnum.Blue} | 
  { value: ColorEnum.Rgb , r: number; g : number, b: number } |
  { value:  ColorEnum.Rgba, r: number; g : number, b: number, a: number }

function logColor(color: Color) {
  switch(color.value) {
    case ColorEnum.Rgb : 
      var {r, g, b} = color;
      console.log(....);
      break;
    case ColorEnum.Rgba:
      var {r, g, b, a} = color;
      console.log(....);
      break;
    default: 
      ....
}

Also in this kind of construct, we can seriously greatly feel that the language could provide us shorhand ..

Something like:

type Color = ?keyword? {
  Red,
  Green,
  Blue,
  Rgb(r: number, g:  number, b:  number ),
  Rgba(r: number, g:  number, b:  number, a: number)
}

function logColor(color) {
  match(color) {
    ColorEnum.Rgb(r, g, b):
      console.log(....);
      break;
    ColorEnum.Rgba(r, g, b, a):
      console.log(....);
      break;
    default: 
      ....
}

would make more sense fore me, but sure it creates a lot of new syntax

jbondc commented 9 years ago

Didn't think was possible unless you'd add metadata. But after playing with type guards and #1003 would be pretty cool.

e.g.

enum ColorEnum {
  Red,
  Green,
  Blue
}

// This will have to emit 'Rgb' and 'Rgba' as JS ~classes, better naming?
type class ColorApi {
  Rgb(r: number, g:  number, b:  number )
  Rgba(r: number, g:  number, b:  number )
}

type red = { red : number };
type blue = {  blue : number };
type yellow = { yellow: number };
type ColorStruct = red | blue | yellow
type ColorNames = "red" | "blue" | "yellow"

// sum type, forces use of type guards to exhaust all possible cases
type Color = ColorEnum + ColorApi + ColorStruct + ColorNames + string

function logColor(color: Color) {
  if(color == null) {
    // null or undefined, narrows to void type
  } elseif(color instanceof ColorEnum) {
  } elseif(color instanceof Rgb) {
  } elseif(color instanceof Rgba) {
  } elseif(typeof color.red === "number" || typeof color.blue === "number" || typeof color.yellow === "number") {
  } elseif(color === "red" || color === "blue" || color === 'yellow') {
  } elseif(typeof color === 'string') {
      console.log("convert this from hex?")
  } else {
     throw ?
  }
}

Makes good candidate for a test case :)

jbondc commented 9 years ago

Actually the 'ColorApi' part probably may not work well , probably the hardest :/

donaldpipowitch commented 9 years ago

Sum types/tagged union like Rusts would be a really great addition. I hope this is something which can land in the foreseeable future, so it maybe can influence other discussions about types in JavaScript (like the recent SoundScript).

roganov commented 9 years ago

I would really love this feature. For as long as it's not implemented, what is the recommended way to emulate the following Elm snippet?

type Action
    = NoOp
    | UpdateField String
    | EditingTask Int Bool
    | UpdateTask Int String

@DanielRosenwasser has kindly shown me a pretty cool way (comment #4190) to achieve what I want, but unfortunately it requires quite a lot of boilerplate code. In a mid-sized flux application there might be hundreds of actions so it isn't very convenient.

acjay commented 9 years ago

I'm really hoping we get this. Javascript is already a very functional language, and some sort of support for ADTs would really give it much of the additional power that all the typed-functional programming languages possess with their pattern matching. I noticed that some of the proposals involved storing tags on objects. This could be made less brittle with ES6 symbols.

Zorgatone commented 8 years ago

You could set the prototype's valueOf method, and then use Math operators on it in JavaScript.

The question is if TypeScript will allow that too, or you get an error (with/without classes)

andy-hanson commented 8 years ago

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.

That way you could do this:

sealed abstract class Super {}
class Sub1 extends Super {}
class Sub2 extends Super {}

function f(s: Super) {
    if (s instanceof Sub1) {

    } else {
        // s is a Sub2
    }
}
ivogabe commented 8 years ago

@andy-hanson You can do that with a union type:

type Super = Sub1 | Sub2;
class Sub1 { a: string; }
class Sub2 { b: string; }
function (s: Super) {
  if (s instanceof Sub1) {
    // s: Sub1
  } else {
    // s: Sub2
  }
}
andy-hanson commented 8 years ago

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

roganov commented 8 years ago

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

opensrcken commented 8 years ago

If I'm not mistaken, ScalaJs has to handle a similar concept.

abstract class Exp

case class Fun(e: Exp) extends Exp
case class Number(n: Int) extends Exp
case class Sum(exp1: Exp, exp2: Exp) extends Exp
case class Product(exp1: Exp, exp2: Exp) extends Exp

def print(e: Exp): String = e match {
    case Number(1) => "1"
    case Number(x) => x.toString
    case Sum(Number(1), Number(2)) => "(1 + 2)"
    case Sum(e1, e2) => "(+ " + print(e1) + " " + print(e2) + ")"
    case Product(e1, e2) => "(* " + print(e1) + " " + print(e2) + ")"
    case Fun(e) => "(fn [] " + print(e) + ")"
}

compiles to

$c_Ltutorial_webapp_TutorialApp$.prototype.print__Ltutorial_webapp_TutorialApp$Exp__T = (function(e) {
  var rc19 = false;
  var x2 = null;
  var rc20 = false;
  var x5 = null;
  if ($is_Ltutorial_webapp_TutorialApp$Number(e)) {
    rc19 = true;
    x2 = $as_Ltutorial_webapp_TutorialApp$Number(e);
    var p3 = x2.n$2;
    if ((p3 === 1)) {
      return "1"
    }
  };
  if (rc19) {
    var x = x2.n$2;
    return ("" + x)
  };
  if ($is_Ltutorial_webapp_TutorialApp$Sum(e)) {
    rc20 = true;
    x5 = $as_Ltutorial_webapp_TutorialApp$Sum(e);
    var p6 = x5.exp1$2;
    var p7 = x5.exp2$2;
    if ($is_Ltutorial_webapp_TutorialApp$Number(p6)) {
      var x8 = $as_Ltutorial_webapp_TutorialApp$Number(p6);
      var p9 = x8.n$2;
      if ((p9 === 1)) {
        if ($is_Ltutorial_webapp_TutorialApp$Number(p7)) {
          var x10 = $as_Ltutorial_webapp_TutorialApp$Number(p7);
          var p11 = x10.n$2;
          if ((p11 === 2)) {
            return "(1 + 2)"
          }
        }
      }
    }
  };
  if (rc20) {
    var e1 = x5.exp1$2;
    var e2 = x5.exp2$2;
    return (((("(+ " + this.print__Ltutorial_webapp_TutorialApp$Exp__T(e1)) + " ") + this.print__Ltutorial_webapp_TutorialApp$Exp__T(e2)) + ")")
  };
  if ($is_Ltutorial_webapp_TutorialApp$Product(e)) {
    var x13 = $as_Ltutorial_webapp_TutorialApp$Product(e);
    var e1$2 = x13.exp1$2;
    var e2$2 = x13.exp2$2;
    return (((("(* " + this.print__Ltutorial_webapp_TutorialApp$Exp__T(e1$2)) + " ") + this.print__Ltutorial_webapp_TutorialApp$Exp__T(e2$2)) + ")")
  };
  if ($is_Ltutorial_webapp_TutorialApp$Fun(e)) {
    var x14 = $as_Ltutorial_webapp_TutorialApp$Fun(e);
    var e$2 = x14.e$2;
    return (("(fn [] " + this.print__Ltutorial_webapp_TutorialApp$Exp__T(e$2)) + ")")
  };
  throw new $c_s_MatchError().init___O(e)
});

// type check for Number class
function $is_Ltutorial_webapp_TutorialApp$Number(obj) {
  return (!(!((obj && obj.$classData) && obj.$classData.ancestors.Ltutorial_webapp_TutorialApp$Number)))
}
andy-hanson commented 8 years ago

@roganov: 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.

Artazor commented 8 years ago

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:

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.

@danquirk, what is your opinion?

weswigham commented 8 years ago

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

DanielRosenwasser commented 8 years ago

@Artazor with @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.

simonbuchan commented 8 years ago

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:

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.

basarat commented 8 years ago

FWIW @Artazor's proposal https://github.com/Microsoft/TypeScript/issues/186#issuecomment-173719173 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 :rose:

weswigham commented 8 years ago

@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 notifications@github.com wrote:

FWIW @Artazor https://github.com/Artazor's proposal #186 (comment) https://github.com/Microsoft/TypeScript/issues/186#issuecomment-173719173 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: :rose:]

— You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub https://github.com/Microsoft/TypeScript/issues/186#issuecomment-197070489

ahejlsberg commented 8 years ago

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

shelby3 commented 8 years ago

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

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

@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 wrote:

@roganov wrote:

@andy-hanson wrote:

@ivogabe wrote:

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

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