Hacker News new | ask | show | jobs
by lomnakkus 3747 days ago
> TypeScript has has union types i.e. "number | string" which are similar to algebraic data types.

I understand that you said "similar", but there's actually a big difference that should be mentioned explicitly, namely that algebraic data type (ADT) sums always have "constructors" which you can use to disambiguate with. That means that you can meaningfully do the equivalent of "int | int" whereas for union types that would just be "int". (I'm sure you already know this, I'm just pointing this out for those who may not know or appreciate the subtleties.) Example:

    data Maybe a = Nothing | Just a
In this example the "constructors" are Nothing and Just.

Of course, you can emulate first-order ADT sums using union types by just introducing artifical container classes and doing a union on those. While this works for simple cases, I believe (but cannot prove) that it's impossible to emulate GADTs using union types -- my intuition is that the presence of constructors to match on is an essential part of being able to "narrow" the types sufficiently to actually act upon what they "contain" (for each case).

However, and notwithstanding all of that... the use of union types in TypeScript is absolutely the best way to align with JavaScript since there's so much JS that just takes/returns values of type "whatever" (string | number | ...).

Btw, also agreed on the productivity boost. In the short term, it may not appear that you're getting faster, but once your application starts to grow beyond "trivial" you really start to notice the fact that you can refactor without fear.

3 comments

Flow has pretty good support for tagged unions like this which act like ADTs:

http://flowtype.org/blog/2015/07/03/Disjoint-Unions.html

This is great. Seems less cumbersome than TypeScript's user defined type guards for distinguishing cases of a union. The sentinel value concept is something you could at least steal when creating user defined type guards to make them simpler, at least.
TypeScript union types and Flow tagged unions are nice to have, but to make them really useful you also have to provide a good way to pattern match on them. And there they both are still way behind Haskell.
That's interesting! I haven't paid much attention to Flow since trying out the initial release and finding it a bit lacking. (But then, I'm used to GHC/Haskell, so everything lacking!)

I truly feel that "disjoint unions" is one of those things you don't appreciate the true value of until you've used them for quite a while. The real eye-opener for me was implementing a state machine and finding that I could explicitly state exactly which bits of the state machine would propagate to $NEXT_STATE at every step... and have the compiler double-check for me that I got it right.

EDIT: I accidentally a accidentally.

It's gotten a lot better since the initial release. Obviously no match for GHC but it's not trying to be, either. :)
Understood :).
This is really nice with a very good much to common JS patterns. Among other things it types the usage of string literals making possible to typecheck and refactor code that contains if (foo === 'bar').
Thanks for elaborating on the distinction. User defined type guards can get you some of the way there to distinguishing "int | int", but you don't get it for free-- the reality of being a superset of JavaScript. https://gist.github.com/RikkiGibson/74fa3dbfdb1b2d7ab86d
Oh, wow, that's pretty evil code! :)
ADTs can be very easily replaced with visitors.

  type Maybe<T> = <V>(v:{just(x:T):V, nothing():V}) => V
  
  let just = <T>(x:T):Maybe<T> => <V>(v:{just(x:T):V}) => v.just(x)
  let nothing = <T>():Maybe<T> => <V>(v:{nothing():V}) => v.nothing()
  
  let values:Maybe<number>[] = [just(6), nothing()]
  
  values.forEach(maybe => {
    alert(maybe({
      just: x => 'got: ' + x,
      nothing: () => 'nothing'
    }))
  })
Easy, but absolutely horrid on a large scale. (I mean, just try to enumerate the amount of redundancy you have in that short snippet you posted!)