The typed language implementation is in progress and considered unstable. This is a usage guide that explains how to use it and what features currently work.
The Flow type syntax is supported (use -typed).
The TypeScript type syntax is partially supported (use -parse-ts to convert TS to Flow before compilation).
void(undefined)null(null)boolean(true,false)string("abc")number(1,3.5,Infinity,NaN)bigint(1n)
c_ptr(raw C pointer)
anyis a supertype of all types, casts implicitly to other types with a runtime type check.mixedis a supertype of all types, but does not cast implicitly.
emptyhas no values. It is the subtype of all other types.
- String literal types:
"left","right" - Number literal types:
1,-3 - Boolean literal types:
true,false
"abc" is typed as "abc". Literal types flows into their general (widened) types, and into another literal only when the value is identical. Operators treat a literal as the widened type. This can be used for tagged unions and string enums.
A fresh literal is widened to its general type ("abc" -> string) when it becomes the inferred type of a mutable binding, for example:
let/var:let x = 'a'infersstring.- Array literals:
['a','b']infersArray<string>. - Object fields, indexer keys, and indexer values.
The literal is kept (no widening) for const, or any annotated context (e.g. Array<"a">).
A literal arm is dropped from a union that already contains a type the literal flows into: its general type, any, or mixed. So string | "abc" is just string, while "a" | "b" stays a union.
A | B indicates a union between two types A and B. Values that are either A or B are valid values of A | B. Unions may be cyclic, but cycles must include another type. For example: type A = A is invalid, but type A = [A, B] | null is valid.
The typed language implements Array as a generic: Array<T> or T[] both work, and mean the same thing.
These soundly typed arrays are incompatible with untyped JS arrays and are more restrictive. They do not allow holes or empty spaces: every element of an array a in the range [0, a.length) must be of type T. Indexed access to arrays is bounds-checked and throws on out-of-bounds access.
Tuples are shown with brackets: [A, B, C].
When instantiating a tuple, it is typically necessary to specify the type to avoid confusion with arrays:
var x: [number, string] = [1, "abc"];
Tuples are plain objects with properties "0", "1", ..., not arrays, so
untyped code that expects an iterable (e.g. new Map(Object.entries(o)))
does not accept them.
All object types in the typed language are exact objects.
var obj: {a: number, b: string} = {a: 1, b: "xyz"};
They must have exactly the set of properties listed in their type (no extra properties allowed). Properties must be in the same order as the type requires. This allows the compiler to make access extremely efficient.
Object properties (and indexers, below) may carry a variance sigil:
var obj: {+ro: number, -wo: string} = {ro: 1, wo: "x"};
+ or readonly makes a property read-only (covariant); - or writeonly makes it write-only (contravariant). The default (no sigil) is invariant: readable and writable, and subtyping requires the property type to match exactly.
An object type may declare a single indexer (index signature) instead of named properties, describing a dictionary with a uniform value type:
var dict: {[string]: number} = {};
var n: number = dict["x"]; // read
dict["y"] = 3; // write
var m: number = dict.x; // dot access works too
dict.y = 3;
An object type has at most one indexer, and an indexer cannot be combined with named properties. The key type is checked against the index expression.
Reads yield T but are checked by a checked cast at runtime (so a read of a missing key throws if undefined is not in T). Writes require the value type T.
Non-computed access (dict.x) routes through the indexer using a string key, so it requires a string-keyed indexer.
Spreading an object that has an indexer produces an object with an indexer of the same key type. If the result also has named properties, they fold into the indexer and its value type becomes the union of all the value types:
var d: {[string]: number} = {};
var a = {...d}; // {[string]: number}
var b = {...d, x: "s"}; // {[string]: number | string}
A computed property ({[k]: v}) in an object literal also produces an indexer object: the key type is the union of all computed key types (plus string if named fields are present, plus any spread indexer key type), and the value type is the union of all value types. Mixing computed and named properties results in an indexer. If the literal is constrained to an indexer type, its key and value types are used.
var k: string;
var x = {[k]: 1}; // {[string]: number}
var n: number;
var y = {[n]: 1}; // {[number]: number}
Function types can be quite complex:
type F = (this: string, x: number, y?: boolean) => void;
The type can contain the type of this inside the function, as well as optional arguments.
If a function has NO type annotations, it is considered to be an "untyped" function, and calls to it will not be typechecked. This allows for convenient calling to untyped JS from typed JS without extra effort or modification.
Classes are declared as in JS:
class C {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
Classes in the typed language are nominally typed. This means that two classes that happen to have the same field names cannot be used interchangeably.
Class methods can be decorated as final, meaning they cannot be overridden by a subclass:
class A {
@Hermes.final
foo(): number { return 1; }
}
class B extends A {
foo(): number { return 2; } // ERROR
}
Methods and getter-only accessors are read-only; setter-only accessors are write-only.
C.prototype is the object holding the class's methods. It is read-only, and only methods that live on it are reachable: @Hermes.final, private and accessor members are stored in variables instead and cannot be read through it.
class A { foo(): number { return 1; } }
const f = A.prototype.foo; // OK
A.prototype.foo = f; // ERROR
A method read through .prototype is selected statically, not dispatched on the receiver.
Static members are stored in variables and resolved at compile time, including through this (the class) and super (the superclass) inside static members. To keep that consistent with JS:
- A subclass cannot redeclare an inherited static field or method.
- A static field can only be written through the name of the class that declares it, not through
this,superor a subclass name, because JS would create a separate property on the receiver. - A class declaration's name cannot be reassigned, so it always refers to exactly that class.
- A private static can only be accessed through the name of the class that declares it, because JS would throw for a subclass receiver.
- Static members cannot reference the class type parameters.
- Inside a generic class, a member access on the class name (
G.x,G.#x) refers to the current specialization, which has its own static storage. new this()in a static method constructs the declaring class.
class A {
static x: number = 1;
static set(): void { A.x = 2; } // OK
static bad(): void { this.x = 2; } // ERROR
}
class B extends A {
static x: number = 3; // ERROR
}
B.x = 4; // ERROR
The ? indicates that a type may also be either null or void:
type A = ?number;
Types can be aliased using the type keyword:
type A = number;
Classes and type aliases can both be generic.
class Wrapper<T> { x: T }
type NumberTuple<T> = [T, number];
A type parameter may have a bound (<T extends Bound>). Each type argument must flow into the bound, otherwise it's an error. Bounds are checked for each specialization of a generic. A bound may reference any of the type parameters, including its own (<T extends C<T>>). Example:
function g<T extends Base, U extends T>(x: T, y: U): void {}
The compiler will emit errors for any type errors which can be detected ahead of time.
Certain operations will result in checked casts which must happen at runtime.
function foo(a: any) {
var b: number = a; // Checked cast inserted, will throw if 'a' is not a number.
}
If the declarator has an annotation, it wins.
Otherwise the initializer is visited and its type becomes the declaration's type. Globals and catch variables are always any.
Reassignments to a let/var variable must obey the type inferred/declared at the variable's declaration.
An array literal becomes a tuple or an array based on the constraint:
- Tuple constraint: each element is checked against the matching slot.
- Array constraint: each element is checked against
T, with checked casts inserted as needed. - No constraint specified: element types are unioned and the literal becomes
Array<union>. - Empty literal, no constraint: Warns and assumes 'any'. Annotate the declaration for a proper type.
Tuples are never inferred without context — [1,"x"] alone is Array<number|string>, not a tuple.
When a generic call omits type arguments, each type parameter is attempted to be inferred. Arguments are processed in two passes: first those whose parameter type is a bare type variable, then those with complex types like (T) => U from left to right. Inference succeeds only if every placeholder is resolved.
Type arguments may also be supplied partially, in which case only the unspecified trailing type parameters are inferred:
function makePair<T, U>(a: T, b: U): [T, U] { return [a, b]; }
makePair<number>(1, 'a'); // T = number (explicit), U = string (inferred)
Matching is structural: same-kind containers (Array, Tuple, ExactObject, function types) recurse into their components, a union constraint tries each arm against a non-union concrete, etc.
Limitations:
- First-write wins:
[T, T]against[number, string]fixesT = number, and the second slot then fails the regular flow check. - Single forward pass: fixpoint iteration.
Typed language doesn't have full proper type refinement support yet but it supports a partial implementation that allows for certain use cases by implicitly checking null or void at usage time.
function foo(a: number | void): number {
var b: number = a; // Typechecks at compile time, but will throw if 'a' is not a number.
return b;
}
JS decorator syntax (@Hermes.foo) does not apply to function declarations, so the typed language uses a Hermes.decorate(...) call to attach decorations:
function charAt(this: string, pos: number): string { return this[pos]; }
Hermes.decorate(charAt, Hermes.builtin);
- The call must appear as a statement immediately following the
FunctionDeclarationit decorates. - The first argument must be an identifier matching the preceding function's name.
- The second argument must be the decorator.
- Exactly two arguments are required.
The typed language is a subset of JavaScript. Most common ES2015+ constructs work, with the notable exceptions listed under "Not yet supported" below.
Function parameters can be typed, optional, defaulted, or destructured.
// Typed parameter.
function f(x: number): void {}
// Optional parameter (must come last). Inside the body, type is `T | void`.
function g(x: number, y?: string): void {}
// Default parameter.
function h(x: number = 1): number { return x; }
// Object destructuring parameter, with optional default.
function i({x, y}: {x: number, y: string} = {x: 0, y: ""}): void {}
// Array (tuple) destructuring parameter, including nested patterns.
function j([a, b]: [number, string]): void {}
function k({a: {b}}: {a: {b: number}}): void {}
Destructuring is supported in variable declarations and parameters, including nested patterns and default values. Type annotations inside destructuring patterns are not allowed; annotate the whole pattern instead.
const [x, y]: [number, string] = [1, "abc"];
const {a, b}: {a: number, b: number} = obj;
Destructuring names in object patterns can have defaults, but typed tuple and Array<T> patterns don't support defaults yet.
An unannotated let/const loop variable is typed from the loop: the element type for for-of over a typed array, string for for-in, and any otherwise. Loop targets are checked against that type. An annotated destructuring pattern must match it exactly (no implicit casts), since its leaves are read without a runtime check.
Spread of an array into an array literal is supported.
const xs: number[] = [1, 2, ...other];
Only a tuple can be spread into a call, because only a tuple has a width that is known at compile time. The elements are read out individually and passed as ordinary arguments, so the call is a direct call, and arity and element types are checked as if the elements had been written out by hand.
function f(a: number, b: string): void {}
const t: [number, string] = [1, "abc"];
f(...t); // same as f(t[0], t[1])
f(...t, ...([]: []));
This works in calls, method calls, new, super(), fn.call() and
$SHBuiltin.call(). In the last two, the this argument is consumed
positionally and cannot itself be a spread.
Array.push() is the exception: it is special-cased rather than treated as a
real function, so it takes an array spread and rejects a tuple one.
An array cannot be spread into a call: its length is not known at compile time, so neither is the call's arity. An untyped callee accepts an array spread, but not alongside a tuple spread, because tuples are not iterable.
An element whose type needs a checked cast to reach the parameter type is rejected, because there is no argument expression to attach the cast to.
Class methods can be overloaded using the @Hermes.overload decoration. Overloaded methods must be final.
class C {
@Hermes.final
@Hermes.overload
foo(x: number): string { return 'a'; }
@Hermes.final
@Hermes.overload
foo(x: string): void {}
}
Overloads are resolved at compile time, and are not stored on the home object.
Both public and private methods may be overloaded, and an overload set may mix non-generic and generic overloads.
Restrictions:
- No overloading constructors/accessors
- Overloaded methods must be called (
var x = obj.overloadedMethodis an error)
Strict equality (=== and !==) is type-checked. An expression a === b (or a !== b) is legal only when one operand's type flows into the other's without a checked cast.
Loose equality (== and !=) is generally disallowed. To compare two values of the same type, use === / !==. It exists for the x == null / x == undefined idiom used to test nullability. Specifically, a == b is allowed only when either operand has type any, or one operand has type null or void and null or void flows into the other type.
- Arrow functions (without type parameters).
for-ofloops over arrays.try/catch/throw(the catch binding is typedany).- Template literals.
- Classes with static members, private fields and methods (
#name), inheritance,supercalls, andsuper.method()access. - Getters and setters on classes (must be marked
@Hermes.final). - Rest parameters in function declarations and function types
(
function f(...args)).
- Spreading an array into a call (
f(...args)whereargsis an array). - Rest elements in tuple destructuring (
[a, ...rest]on a tuple). - Spread elements in tuple literals.
asyncmethods and generator methods.- Computed property names in class definitions.
- Optional call expressions (
f?.()). - Type parameters on function expressions and arrow functions.