Description
I’m very happy to see #186 close with pull request: #9163. This is a big step forward.
I read both of those and read Wes Wigham's recent work. I openned this as a new issue to avoid cluttering the closed issue.
Tagged Union without Reflection
I still think there is room to add some tagged-unions in the style of an FP language to TypeScript. The benefits are widely understood and documented. The key attribute of this suggestion is the removal of visibility into the tag of the union. This is how every other statically-typed FP language implements them since they also erase types at run-time.
Hopefully this can become part of the language at some point in the future. Considering the recent code landing in 2.0, the urgency is greatly reduced, but still worth discussing.
A quick list of benefits:
- The structure should be hidden for safety
- The syntax and semantics are much more 1 to 1 with other languages
- A new type gets created so we can support recursive definitions
- It would provide a clean syntax for class destructuring in the future (with pattern matching after that)
- The definitions are defined in one place and are not allowed to be extended. Any conditionals would be exhaustive and easy to see.
Proposed implementation:
- Use the 'class system', with nominal typing by default
- Add a new keyword to define these new types
- It should work now with 'switch' statements
data Shape =
| Square(size:number)
| Rectangle(width:number, height:number)
| Circle(radius: number)
| Shapes(shapes:Shape[])
;
Or an enum like type without associated values.
data StatusRegister =
| Sign
| Zero
| F5
| HalfCarry
| F3
| ParityOverflow
| Subtract
| Carry
;
If ‘data’ is controversial as a keyword, then ‘datatype’ could be used.
I chose 'data' since the keyword 'type' is a type-synonym declaration in both Haskell and TypeScript.
Since Haskell uses 'data' to define new data types, I thought it best to keep that similarity.
Once these declarations are done, they are closed. So you see the entirety of the definition in a single place.
In terms of implementation, the Shape declaration would create a new type or class called Shape. Square, Circle, and Rectangle would be the subclasses. This is a common strategy for supporting the feature in OO languages. It was most notably used in Scala.
The Shape type above could be equivalent to.
interface Shape {};
class Square implements Shape {
constructor(private size:number);
}
class Rectangle implements Shape {
constructor(private width:number, private height:number);
}
class Circle implements Shape {
constructor(private radius:number);
}
class Shapes implements Shape {
constructor(private shapes:Shapes[]);
}
... or they could be defined as functions that construct that type.
The type guards would switch purely on the value vs. the ‘kind’.
No run-time reflection should be used.
function area(s: Shape) {
switch (s) {
case Square: return s.size * s.size;
case Rectangle: return s.width * s.height;
case Circle: return Math.PI * s.radius * s.radius;
case Shapes: return union_rect(s.shapes);
}
}
// Handling serialization and deserialization say from some Protobuf/Thrift type system
// Notice ordering may not correspond
function fromJSON(json:any):Shape {
If (json.hasOwnProperty(‘kind’) === false) throw “Error”;
switch (json.kind) {
case 1: return Square(json.size);
case 2: return Circle(json.radius);
case 3: return Rectangle(json.width, json.height);
case 4: return Shapes(json.shapes.map(el => fromJSON(el));
}
}
function SRfromJSON(json:any):StatusRegister {
If (json.hasOwnProperty(‘obType’) === false) throw “Error”;
switch (json.obType) {
case 1: return ParityOverflow;
case 2: return Carry;
case 3: return Zero;
case 4: return HalfCarry;
default: throw "Invalid json";
}
}
Although destructuring is part of ES6, and it can work on a class instance, it uses structural typing.
If two objects have the same shape, the developer may want the guarantee of nominal matching for destructuring. This proposal would make that simple.
// Contrived example of same shape destructing with
// a contrived 'match' expression
data Vector2D =
| Vec2D(x: number, y:number)
| PosVec2D(x: number, y:number) // Position Vector
;
function vecop_jj(v: Vector2D) {
let res = match (v) {
| PosVec2D: throw “Error - cannot mix position vectors with this operation”;
| Vec2D(x, y): x - y;
}
}
That is what I have so far. It might be interesting to consider pretty-printing or serialization, but I think the overall object system may determine the preferred implementation.
As a side note, I was thinking very hard about #186 2 weeks ago. I wanted to have a proposal for something this Monday. However, I was trying to unify all the possible cases (enums, string types, classes, and a new variant type). I couldn't come up with a clean solution.
When the recent code landed from Anders on Monday, it was great news. Wwe going to have something we can use very soon in 2.0. It also simplified the problem this suggestion was originally addressing.