-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Suggestion: Sum Types and Tagged Unions without Reflection #9241
Comments
I really like the type safety of this proposal. Much safer and better than switching on strings. |
Just as an update, I have been reflecting on this problem some more. It turns out that there are some problems with this technique that give me some pause. The problem is that this can introduce a lot of 'boilerplate' For example, lets say we used the above as the Node/AST for TypeScript. In order to traverse these trees, we would need to write code. Ditto for serialization and deserialization. This is already happening in the TypeScript compiler with: ts.forEachChild. The function is 371 lines long. In the Haskell,OCaml, and Scala communities, the discussion about 'Boilerplate' has been going on for quite some time and continues to this day. This may be a really complicated problem! |
Few remarks:
|
@drudru Oh, and although I'm neutral, expect some resistance from others. |
Hi - one more update. Some interesting things have happened since I opened this issue.
Given the above, I think compatibility will be important. Also, I think the larger community will gain experience and some new solutions will be proposed. I think it makes sense at this point to close this issue. Thanks for reading. |
I'll also mention that a limited form of pattern matching also exists with destructuring. It's not exactly conditional like true pattern matching, but it's a step in that direction. (And tagged unions in 2.0 RC do technically check for completeness with the unreachable code path analysis, building on the 1.8 type narrowing. Things like this would result in a compiler warning:) // Switch statement
function getValue(ref: One | Two | Three): number {
switch (ref.type) {
case "one": return 1
case "two": return ref.number
case "three": return 3
default: return 4 // unreachable
}
}
// If-else
function doSomething(ref: One | Two | Three) {
if (ref.type === "three") {
blah()
} else {
if (ref.type !== "two") return
baz()
if (ref.type === "one") {
doSomethingElse() // unreachable
}
}
}
interface One { type: "one" }
interface Two { type: "two"; value: number }
interface Three { type: "three" } |
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:
Proposed implementation:
Or an enum like type without associated values.
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.
... 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.
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.
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.
The text was updated successfully, but these errors were encountered: