A type safe JS runtime.
- target JS (cleanup)
- target another PL that can export WASM or compile natively
define(type, definition)
Allow the definition of enums, structs, unions, or any other arbitrary type.
The type
parameter can be either a string or an array of strings, in case of multiple types aliases.
The definition
is either the returned value of enums
, and struct
, the union
reference itself, or an object that exposes at least 2 methods: check(value, asArray)
and cast(value)
.
define('special', {
check(value, asArray) {
// true when the type is in squared brackets
return asArray ?
value.map(v => this.check(v, false)) :
value instanceof Special;
},
cast(value) {
return this.check(value, false) ? value : new Special(value);
}
});
const {special: single} = new Special;
const {[special]: multi} = [new Special, new Special];
is({type: value})
It verifies that a specific value is an expected type, passing through the definition check(value, false)
when type is not in square brackets, and check(value, true)
when it is.
const value = 'test';
if (!is({string: value}))
throw new TypeError(`unexpected ${value}`);
const values = ['a', 'b', 'c'];
if (!is({[string]: values}))
throw new TypeError(`unexpected ${values}`);
as({type: value})
It performs a cast through the definition cast(value)
method, and it's responsibility of such method to understand what kind of value need to be casted, and throw in case there's no way to cast it.
// the following throws a TypeError
const {string: test} = 123;
// the following works as expected
const {string: test} = as({string: 123});
as({string: 123}) === "123"; // true
enums(name, ...)
enums are simple, static, values that could be just named, or have a simple value.
enums are (currently?) defined in the global context, and it's not possible to define different enums with the same name.
define('Color', enums(
'RED', // by default enums are Symbol
'GREEN',
{BLUE: 123} // but these could be simple values too
));
console.log(Color);
// {RED: Symbol(RED), GREEN: Symbol(GREEN), BLUE: 123}
const {Color: red} = Color.RED;
const {[Color]: colors} = [Color.RED, Color.BLUE];
Differently from other types, enums cannot really be casted.
fn({returnType: (...) => {}})
The syntax to define a safe function must provide all information needed to make it safe.
const sum = fn({int: ({int: arg0}, {int: arg1 = 0}) => {
return arg0 + arg1;
}});
sum(1); // 1
sum(1, 2); // 3
sum('a', 'b'); // throws a TypeError
Please note:
- optional arguments must be at the end of the signature.
({int: a}, {int: b = 1})
is OK, but({int: a = 1}, {int: b})
is not. - the return type must always be present. If nothing is returned, a
void
type is expected - rest arguments are probably supported but these should not be used
- for options/objects use the
{object: {props}}
if destructuring fields is needed - for overloads define unions
Differently from regular JS functions, jdes functions can be serialized as JSON, and these will be parsed back once parsed.
const json = JSON.serialize(sum);
const fn = JSON.parse(json);
fn(2, 3); // 5
struct(field, method, ...)
In jdes classes are mostly discouraged for at least two reasons:
- these cannot be used as type
- these cannot target other programming languages, as they all have slightly different classes
Accordingly, whenever you think you need a class, you need to create a struct.
define('Point2D', struct(
// mandatory fields {type: name}
{int: 'x'},
{int: 'y'}
));
// literals are casted automatically
const {Point2D: p2d} = {x: 1, y: 2};
const {[Point2D]: p2ds} = [p2d, {x: 3, y: 2}];
// also OK through explicit new Point2D
const myPoint = new Point2D({x: 1, y: 2});
If a mandatory field is not available as literal property, a TypeError will be thrown.
However, fields can also have optional entries that don't need to be present in the literal.
define('Point3D', struct(
// mandatory fields
// multiple type: [name, ...] allowed
{int: ['x', 'y']},
// optional fields
// {type: {name: defaultValue}}
{int: {z: 0}}
));
const {Point3D: p3d} = {x: 1, y: 2};
p3d.z; // 0
A struct can also have methods, which are just guarded functions.
define('Point3D', struct(
{int: ['x', 'y']}, // mandatory fields
{int: {z: 0}}, // default fields
// methods {returnType: {methodName({argType: name}, ...) {}}}
{[int]: {coords() {
return [this.x, this.y, this.z];
}}}
));
const {Point3D: p3d} = {x: 1, y: 2};
p3d.coords(); // [1, 2, 0]
union
The union utility makes overloads possible by defining multiple known types separated by an underscore.
define('int_float', union);
const {int_float: a} = 1;
const {int_float: b} = 1.2;
const {[int_float]: c} = [a, b];
As the _
underscore is used to split/check types, it is a good idea to never define a type within an underscore, in case it needs to be used as union.
map
The map utility helps defining Map instances with a well known type for both keys or values.
define('StrInt', map(str, int));
// maps definition work via shortcut
const {StrInt: si} = [];
si.set('one', 1); // OK
si.set(1, 'one'); // fails
Please note, when targeting compilable targets it is mandatory to define typed maps, and jdes maps should not be iterated right away:
// this breaks
for (const [key, value] of si);
// this works
for (const [key, value] of si.entries());
This is due to the fact for/of
loops in jdes are always transformed as regular array loops.
set
The set utility helps defining Set instances with a well known type for values.
define('Str', set(str));
// maps definition work via shortcut
const {Str: s} = [];
s.add('one'); // OK
s.add(1); // fails
Please note, when targeting compilable targets it is mandatory to define typed sets, and jdes sets should not be iterated right away:
// this breaks
for (const value of s);
// this works
for (const value of s.values());
This is due to the fact for/of
loops in jdes are always transformed as regular array loops.
unsafe()
jdes runtime guards properties access, type checks, arguments and much more, but all these runtime checks come with a cost.
Even if performance are still very reasonable, a safe execution takes 10X up to 1000X what would be an unsafe execution time.
Accordingly, it is highly recommended to mark jdes unsafe after importing it, when the code is meant to run in production.
import {unsafe} from 'jdes';
if (global.PRODUCTION)
unsafe();
The unsafe
call is not reversible: once jdes is unsafe it's unsafe.
If the code is transpiled though, and JS is used as target, there's no need to flag anything unsafe
, as the environment will be super clean and no guards whatsoever are used.
types
These are all the pre-defined generic types:
int
a generic integer, casted viaparseInt(value, 10)
float
a generic float, casted viaparseFloat(value)
boolean
-bool
eithertrue
orfalse
, casted viaBoolean(value)
number
-num
a generic number, casted viaNumber(value)
string
-str
a generic string, casted viaString(value)
object
-obj
a generic object (literal/instance), casted viaObject(value)
function
-fn
a generic function, casted viaFunction
when parsed via JSONvoid
usable to describe functions return type
There is no array
type for the simple reason that any type, except for the void
one, can be part of an array.
// not an array
const {int: i} = 0;
// as array of int
const {[int]: ii} = [0, 0];
These are all specialized types:
f32
a Float32Array compatible numberf64
-double
a Float64Array compatible numberi8
an Int8Array compatible numberi16
an Int16Array compatible numberi32
an Int32Array compatible numberu8
a Uint8Array compatible numberu16
a Uint16Array compatible numberu32
a Uint32Array compatible numberuc8
a Uint8ClampedArray compatible numberi64
a BigInt64Array compatible numberu64
a BigUint64Array compatible number
Each specialized type cast, as value, is performed by setting the value within the index 0 and retrieving it back, while as array, the cast is done via new SpecialConstructor(array)
if the array is not already an instanceof such constructor.
Please note that all specialized types are static when retrieved as array.
// single value
const {i32: i} = 0;
// as array - implicit cast
const {[i32]: ii} = [1, 2, 3];
If a predefined length is needed, it is always possible to create values explicitly.
const ii = new Int32Array(100);
Please note that not all these types are necessarily available, as some engine might not have all of them.
import {
define, // used to define types
as, is, // cast and check utils
enums, fn, struct, union, // specialized types
set, map, // typed Set and Map
unsafe // performance boost for production
} from 'jdes';
// values and arrays declaration
const {int: i} = 0; // is({int: i});
const {[int]: ii} = [1, 2]; // is({[int]: ii});
// explicit cast
const {string: s} = as({string: 123}); // s === "123"
// unions (multi type overloads)
define('int_float', union);
// functions {returnType: ({argType: name}, ...) => {}}
const squared = fn({int: ({int_float: num = 0}) => num * num});
squared(3); // 9
// enums
define('RGB', enums('RED', 'GREEN', 'BLUE'));
const {RGB: color} = RGB.GREEN;
// struct
define('Point3D', struct(
{int: ['x', 'y']}, // mandatory properties
{int: {z: 0}}, // default properties
// methods {returnType: {methodName({argType: name}, ...) {}}}
{[int]: {coords() {
return [this.x, this.y, this.z];
}}}
));
const {Point3D: p3d} = {x: 1, y: 2};
p3d.coords(); // [1, 2, 0]
is({Point3D: p3d}); // true
// set
define('Str', set(str));
const {Str: typedSet} = ['entry'];
// map
define('StrInt', map(str, int));
const {StrInt: typedMap} = [['first', 1]];