Having worked with OCaml and F#, I find that TypeScript requires fewer changes to non-type code in response to changes to the types.
For instance, given `type T = { a: string; b: string; c: string }`, if I want to make `b` and `c` nullable but always provided together:
// OCaml
type TOpt = { a: string; bc: (string * string) option }
// TypeScript
type TOpt = T | { a: string; b: undefined; c: undefined }
With the TypeScript approach, code which worked with T still works with TOpt (only requiring an `if(x.b)` guard). With the OCaml approach, any code that accessed b or c needs to be rewritten.
The same applies to cases where an object can be in several different modes, but some properties are present in all modes. For example, an AST node can be a literal, identifier, binary operation, etc. but it always has a source location and an inferred type. In OCaml this has to be represented by either separating a "common properties" type from a "kind of node" union type:
type node = { loc: location ; inferred_type: exprtype option ; kind: nodekind }
and nodekind = Lit of string | Id of string | Unary of op * node
Or by repeating the common properties in all union type constructors:
type node = Lit of location * exprtype option * string
| Id of location * exprtype option * string
| Unary of location * exprtype option * op * node
Both are tedious (although F# makes the second one slightly less tedious by allowing one to define computed properties on union types). By contrast, TypeScript allows you to have only one type:
type NodeCommon = { loc: location; inferred_type: exprtype|undefined }
type Lit = NodeCommon & { kind: "lit"; value: string }
type Id = NodeCommon & { kind: "id"; value: string }
type Unary = NodeCommon & { kind: "unary"; op: op; node: Node }
type Node = Lit | Id | Unary
This is surprising. Which other type systems have you worked with?