tc39 / proposal-record-tuple

ECMAScript proposal for the Record and Tuple value types. | Stage 2: it will change!
https://tc39.es/proposal-record-tuple/
2.5k stars 62 forks source link

how to determine whether an unknown variable is a specific Record / Tuple #388

Open eczn opened 7 months ago

eczn commented 7 months ago
let p = #{ x: 11, y: 22 }

function handlePoint(x: unknown) {
  if (x instanceof /* ??? */) {  // how to determine whether an unknown variable is a specific Record / Tuple
    console.log(`x is a point:`, x)
  } else {
    throw new Error(`x is not a point`)
  }
}

try to name #{ x, y }:

record Point #{ x, y }
const p = Point #{ x: 1, y: 2 }
x instanceof Point

or:

x instanceof #{x, y} 
demurgos commented 6 months ago

Records are anonymous primitive compound value, use a structural check (test fields and types) instead of a nominal check (instanceof). They are closer to string values than class instances.

Your example would be:

function handlePoint(x) {
  if (isPoint(x)) {
    console.log(`x is a point:`, x)
  } else {
    throw new Error(`x is not a point`)
  }
}

function isPoint(x) {
  return typeof x === "record" && typeof x.x === "number" && typeof x.y === "number"
}

There are many runtime or compile time libraries to help with type checking, this is out of scope for this proposal in my opinion. The Pattern Matching proposal may be a better place to discuss such feature.

mhofman commented 6 months ago

Having an "is same structure" predicate could be interesting, but the exact semantics would be tricky.

For example, taking #{foo: #[ 1, false ] } as an example.

First, should such a predicate recurse into values that are R/T? Aka, should it check the structure of .foo? Then if it does, what should it do regarding non R/T values? Should it compare them by value, by type, or not at all? If not at all, any tuple of length 2 for foo would pass. If by type, it's need to be a tuple of [number, boolean]. If by value, should there be a difference if the value is forgeable (string, number, bool, null?, registered symbol) or unforgeable (unique symbol, objects if they're allowed).

Comparing structure deeply by type of the leaf values might be a good compromise. Then you could implement the isPoint above simply as const isPoint = x => isSameStructure(#{ x: 0, y: 0 })

demurgos commented 6 months ago

Comparing structure deeply by type of the leaf values might be a good compromise.

This is still type checking, but the type is defined with a "template" value. This feels like a needless extra indirection when you could have const isPoint = x => isType(x, RecordType({x: NumberType, y: NumberType})). A downside of this indirection is that it forces dummy values (0 in your example) and prevents finer type constraints (literals, unions, etc.)

The main issue is what you described: the semantics are not clear cut. Answering the semantics means defining a type system for JavaScript (beyond typeof or instanceof).

TC39 is very far from being ready to standardize on a type system that can be checked IMO. Type Annotations is related when going in this direction. Reserving type syntax for custom use means that a standard type system would have to coexist with it, making it a bit harder to get.