Polymorphic Constructors

A class with more than one way to be built: write one constructor per way, and TJS picks the right one from the arguments.

JavaScript allows one constructor per class. A class that can be built "from two numbers or from an object" usually starts with a hand-written type test at the top of that one constructor, or grows static factories such as Point.fromObject(). In TJS each way of building the object is its own constructor, with its own typed parameters.

Two things to know before reading the example. First, x: 0.0 is not a TypeScript annotation and not a default: it is an example value, and it means "x is a number" (0.0 is a float, and an integer is a valid float too). The : 0.0 after distanceTo(…) says the same about the return value. TJS Syntax has the full rules. Second, TJS classes are called without new: Point(0, 0) constructs a Point.

class Point {
  constructor(x: 0.0, y: 0.0) {
    this.x = x
    this.y = y
  }

  constructor(coords: { x: 0.0, y: 0.0 }) {
    this.x = coords.x
    this.y = coords.y
  }

  distanceTo(other: { x: 0.0, y: 0.0 }): 0.0 {
    const dx = this.x - other.x
    const dy = this.y - other.y
    return Math.sqrt(dx * dx + dy * dy)
  }
}

const a = Point(0, 0)
const b = Point({ x: 3, y: 4 })
console.log('distance:', a.distanceTo(b))
console.log('both Points?', a instanceof Point, b instanceof Point)

const bad = Point('3', 4)
console.log('Point("3", 4) is an error?', bad instanceof Error)
console.log('passed on?', a.distanceTo(bad) === bad)

It prints a distance of 5, then true true, true and true. Both calls produce a real Point, the method works on both, and arguments that fit neither constructor produce an error value instead of a half-built object. Handing that error to distanceTo returns the same error rather than running the method.

In a .tjs file, writing new Point(3, 4) is a compile error, because Point(3, 4) already constructs. (new is still required for built-ins such as new Set().) Better Classes covers callable classes in general; this page is about having more than one constructor.

How it works

A second example: units at the boundary

A constructor is a natural place to normalise input. Here a temperature is stored in Celsius however it arrives, and a value that fits neither constructor is refused:

class Temperature {
  constructor(celsius: 0.0) {
    this.celsius = celsius
  }

  constructor(reading: { value: 0.0, unit: 'C' | 'F' }) {
    this.celsius =
      reading.unit === 'F' ? ((reading.value - 32) * 5) / 9 : reading.value
  }

  toString() {
    return `${this.celsius.toFixed(1)}°C`
  }
}

const a = Temperature(21.5)
const b = Temperature({ value: 98.6, unit: 'F' })
console.log('from a number: ', a.toString())
console.log('from a reading:', b.toString())
console.log('same class?', a instanceof Temperature, b instanceof Temperature)

const kelvin = Temperature({ value: 300, unit: 'K' })
console.log('unit K is an error?', kelvin instanceof Error)
const warm = Temperature('warm')
console.log('"warm" is an error?', warm instanceof Error)

It prints 21.5°C, 37.0°C, true true, true and true.

'C' | 'F' is a literal union: the unit must be one of those two strings, so 'K' is refused. A bare unit: 'C' would be an example, meaning only "a string", and 'K' would then be accepted and silently treated as Celsius.

The two refusals come from different steps. 'warm' is a string, and no constructor takes a string, so the dispatcher finds no match. { value: 300, unit: 'K' } is an object, so the dispatcher picks the second constructor, and that constructor's own check refuses the unit.

Gotchas

Constructor dispatch is simpler than polymorphic function dispatch. Polymorphic functions are several same-name function declarations, which TJS dispatches in a similar way but checks more strictly. The differences matter when you design the signatures.