getify / TypL

The Type Linter for JS
https://TypL.dev
MIT License
374 stars 13 forks source link

Union/Polymorphic Types #12

Open getify opened 5 years ago

getify commented 5 years ago

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.

AlokTakshak commented 5 years ago

Hi Kyle I would like to help