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.
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.
Need to be able to define a union type that can include multiple types.
So... I'm imagining something like this:
Note: The
( )
are optional but useful for readability. These would work too:Function Parameter Notation
Used for a function parameter:
empty
Union TypeIt's useful to combine
null
andundefined
into a union type to allow a variable to hold either of those "empty" values:So
empty
would just be a good union-type example to just build in... a type that already includes bothundef
andnul
. It's trivial to define:And easy to use:
This
empty
union type would be especially useful for "optional" annotations, as another union type, like: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:
Of course, if no parsing is involved, order is irrelevant (but still a good idea to follow the above rules):
Union Collapsing
A special case of union types is ones which should be collapsed, like
number | finite
, as that should just collapse tonumber
.finite | int
should collapse tofinite
.empty | nul
should collapse toempty
.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:
Illustrating its operation:
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:
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 ofundef | nul
(which might collapse toempty
), sincea
now potentially holds either value during the program.Another example:
Again, if you config it, the
x || y
will produce an error about mixed operand types. But the resulting type that will be implied toz
will be a union type ofint | string
.And:
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
.