type BinaryNode<T> = { tag: 'leaf', value: T }
| { tag: 'branch', left: BinaryNode<T>, right: BinaryNode<T> };
Then things like the following will typecheck correctly:
function traverse<T>(node: BinaryNode<T>): void {
switch (node.tag) {
case 'leaf':
console.log(node.value);
break;
case 'branch':
traverse(node.left);
traverse(node.right);
break;
}
}
In this example, once the compiler recognizes the tag is 'leaf', it knows there is a node property and will report an error if you try to access the left or right fields, which do not exist. The reverse is true for 'branch'.
Not as nice as Haskell for sure, but the presence of union types is something I've found very handy.
You can also declare tuple types:
type MinMaxAvg = [number, number, number];
and do pattern deconstruction (though not pattern matching) in function arguments and assignments:
function test([min, max, avg]: MinMaxAvg): void {
// ...
}