getify/TypL

Union/Polymorphic Types

Open

#12 aberto em 13 de fev. de 2019

Ver no GitHub
 (1 comment) (0 reactions) (0 assignees)JavaScript (11 forks)github user discovery
enhancementhelp wanted

Métricas do repositório

Stars
 (385 stars)
Métricas de merge de PR
 (Métricas PR pendentes)

Description

Need to be able to define a union type that can include multiple types.

So... I'm imagining something like this:

var x = (union`int | string`)`42`;

Note: The ( ) are optional but useful for readability. These would work too:

union`int | string``42`;   // no space between
union`int | string` `42`;   // space between

Function Parameter Notation

Used for a function parameter:

function foo(x = union`int | string`) {
   // ..
}

empty Union Type

It's useful to combine null and undefined into a union type to allow a variable to hold either of those "empty" values:

var x = (union`undef | nul`)`null`;
x = undefined;  // OK
x = null;  // OK
x = 3;  // error!

So empty would just be a good union-type example to just build in... a type that already includes both undef and nul. It's trivial to define:

var empty = union`undef | null`;

And easy to use:

var x = empty``;
x;   // undefined
x = empty`null`;
x;   // null

This empty union type would be especially useful for "optional" annotations, as another union type, like:

function foo(x = union`int | empty`) {
   // ..
}

Union Order Matters

The order that types are listed in the union matters, for the purposes of parsing a literal and evaluating it as a type, since the processing is left to right and the first successful type evaluation "wins". In general, list types from left to right in order of most specific to most general/accepting. IOW: string, being the most liberally accepting, should be listed last, or otherwise no other types after it will ever even be evaluated for that literal.

Example:

(union`int | string`)`42`;    // 42
(union`string | int`)`42`;    // "42"

Of course, if no parsing is involved, order is irrelevant (but still a good idea to follow the above rules):

var x = 42;
(union`int | string`)`${x}`;    // 42
(union`string | int`)`${x}`;    // 42

Union Collapsing

A special case of union types is ones which should be collapsed, like number | finite, as that should just collapse to number. finite | int should collapse to finite. empty | nul should collapse to empty.

This would replace (or fit with) the "number sub-types" handling of #6.

Implementation

The way this works in runtime mode is that union() is a special tag function that produces another tag function which then runs against the subsequent template literal (for example, `hello` or `42`). The resulting tag function would allow any value that successfully passes either/any of the referenced tag functions, and error otherwise.

Rough-draft implementation:

function union([str]) {
  var typeFns = str
      .split(/\s+\|\s+/g)
      .map(s => {
        try { return Function(`return ${s.trim()};`)(); } catch (e) {}
      })
      .filter(Boolean);

  return function tag(...args) {
    var errs = [];
    for (let fn of typeFns) {
      try {
        return fn(...args);
      }
      catch (e) {
        errs.push(e.toString());
      }
    }
    
    throw new Error(`Invalid union-type value: ${errs.join(" ")}`)
  };
}

Illustrating its operation:

(union`int | string`)`hello`;  // "hello"
(union`int | string`)`42`;  // 42

(union`int | string`)`${true}`;  // error because bool is not in the union type

Polymorphic Types

The same union-types mechanism will be handle "polymorphic types" which are types that change throughout the program. There are a number of constructs that would implicitly "widen" a type to such a union type.

Example:

var a = undef``;
a = nul``;

The second assignment here will report an error (if you config it) about an unexpected assignment type, but ALSO now the type in a will be a union type of undef | nul (which might collapse to empty), since a now potentially holds either value during the program.

Another example:

var x = int`0`;
var y = string`foo`;
var z = x || y;

Again, if you config it, the x || y will produce an error about mixed operand types. But the resulting type that will be implied to z will be a union type of int | string.

And:

function foo(x = number) {
   if (x > 5) return int`${x}`;
   return "too small";
}

Error message (if config'd) about multiple return types, but also the return value in that function signature will be the union type of int | string.

Guia do colaborador