Functions and closures
Declarations
function scale(xs: f64[], k: f64 = 2.0): f64[] {
return xs.map((x) => x * k);
}-
function name(p: T, q: U = default): R { … }, optionally with athrows Eclause after the return type (Errors). Parameter types are required, except that a parameter with a default the type plainly shows has that type, as in TypeScript (status = 302is anumber,live = trueaboolean; the same literals,new C(…)and arrays of them that type class fields without annotations). A missing return type is inferred from the body (Return types); default values are below. -
An optional parameter
q?: Tisq: T | undefined = undefined, as in TypeScript. Left out, it isundefined: it printsundefined,typeof qis"undefined",q === undefinedis true andq === nullfalse. A call may also passundefinedfor it (or a value that may beundefined).q?: T | nullisT | null | undefined: left out it isundefined, and a call may passnull, which staysnull. Passingnullfor aq?: Tis an error (TS2322), as in TypeScript: leave the argument out or passundefined.tsfunction show(z?: string, n?: number | null): string { return `${z} ${typeof z} ${n} ${n === null}`; } console.log(show()); // undefined undefined undefined false console.log(show("a", null)); // a string null trueq === nullon such a parameter is alwaysfalse: tsc accepts it, and Velt warns, since an omitted argument isundefined. Writeq == nullto test for either nothing. -
A parameter of type
voidappears in function types and arrows ((value: void) => void), not in afunctiondeclaration (function f(x: void)is an error). Trailingvoidparameters may be left out, as in TypeScript: theresolveof aPromise<void>is called asresolve(), andput(x: T)on aBox<void>asput(). As in TypeScript, a call's own type arguments don't make a parameter optional:h<void>()forfunction h<T>(x: T)is an arity error (TS2554). -
A rest parameter collects the remaining arguments into an array.
-
A spread into fixed parameters passes the elements of a tuple or an array literal.
-
A function or method may have overloads in TypeScript's form.
-
Functions may be nested inside blocks.
Default values
Default values work on functions, methods, constructors and an interface method's default
body. A method an interface only declares takes none, as in TypeScript (TS2371, "a parameter
initializer is only allowed in a function or constructor implementation"): it declares q?: T,
and an implementation gives its own default with q: T | undefined = 1.
A default may use earlier parameters, this and closures over the function's surroundings
(q: number = p * 2, by: number = this.step, cb: () => number = () => local), as in
JavaScript: the function applies such a default itself when the call starts (also to a passed
undefined), and so every later default that is not a constant, which keeps JavaScript's
order. Its parameter is a T | undefined to callers (q?: number, as TypeScript shows it) and
a T in the body. Since the parameter is a T | undefined, a method with such a default
doesn't match an interface method or a base class method whose parameter is a T yet (the
error says so); declare that one q?: T too.
The default of a parameter of a dynamic type (x: any = "d", x: unknown = 0) also applies to
a passed undefined, as in JavaScript. The function applies it itself when the call starts, so
it is the function's own default whichever way it is called:
- through an interface whose method takes
x?: any; - through a base class whose method the class overrides;
- through a function value (
const g = d; g(undefined), andg(): a left-out argument of a dynamic type isundefinedthrough a function value too); - as a callback (
xs.map((v: any = 0) => …)).
So does the default of a parameter whose type allows undefined (x: number | undefined = 5,
or status = 302 where an interface declares status?: number), which is a number in the
body. Any other default also applies to a passed undefined, tested where it is passed:
f(undefined), and f(v) with a v that may be undefined
(Null and undefined).
Each with an error saying why: a default that uses earlier parameters, this or closures on a
parameter without a written type (an arrow's (p: number, q = p * 2)) or whose type includes
null, also as a later non-constant default that would have to run after one the function
applies (r = f() after q: number = p * 2); one using a name a destructured parameter binds;
and one in a generator, whose defaults are evaluated where it is called. For those, take
q?: number and write const r = q ?? p * 2; in the body.
Rest parameters
A rest parameter ...xs: T[] (the last one) collects the remaining arguments into an
array, and a call may spread arrays into it: sum(1, ...more, 4) passes [1, ...more, 4]. The
standard library's variadic functions (Math.max, Math.min, Math.hypot,
String.fromCharCode, an array's push) accept a spread anywhere.
Its type may be any array type: readonly T[], or an alias of an array type (...xs: Nums with
type Nums = number[]); another type is an error, as in TypeScript (TS2370).
An arrow function, a function expression or an object-literal method may have one too, typed
the same ways: a module-level constant const total = (...xs: Nums) => … is the function
function total(...xs: Nums) (its parameters must be typed), and any other is a closure whose
function type ends in the rest parameter (see
Rest parameters in function types).
type Nums = number[];
const total = (...xs: Nums): number => xs.reduce((a, b) => a + b, 0);
const join = function (sep: string, ...parts: readonly string[]) {
return parts.join(sep);
};
console.log(total(1, 2, 3), join("-", "a", "b")); // 6 a-bA rest parameter typed by a type parameter (...args: A with A extends unknown[], which
TypeScript infers as the tuple of the argument types); a rest parameter on a method of an
object literal implementing an interface, or on an interface method.
Spread into fixed parameters
A spread into fixed parameters passes the elements of a value whose length is known when
compiling, as in TypeScript: a variable or field of a tuple type (f(...t) with
t: [number, string] is f(t[0], t[1])) or an array literal (f(...[1, 2])). Too few or too
many elements are the usual arity error, and a missing optional parameter takes its default. A
spread of an array type (T[]) into fixed parameters is an error, as in TypeScript (JS would
bind undefined to the parameters its elements do not fill).
function label(name: string, n: number, suffix?: string): string {
return `${name}=${n}${suffix ?? ""}`;
}
const p: [string, number] = ["x", 4];
console.log(label(...p), label(...p, "!")); // x=4 x=4!A tuple returned by a call or a getter (f(0, ...g.pair), f(...pair())) is read once into a
hidden temporary before the call, as JS reads it once, after the function or receiver:
getf()(...pair())andmk().m(...pair())callgetfandmkfirst;- a receiver or a leading argument read through a getter,
h.g.m(...pair())orf(h.g, ...pair()), is read first too; - and so is a function a getter returns,
h.fnget(...pair()).
After another argument with effects (f(next(), ...pair())), which JS evaluates first, store it
in a variable first (const t = pair(); f(next(), ...t);).
Nested functions
Functions may be declared inside blocks, and are hoisted to the start of their block as in JS (a call before the declaration works).
One that uses the enclosing function's locals (or another such nested function) is a closure,
with the captures of an arrow function: it sees every later change of what it
uses, may assign it, and may be passed on or returned as a value. Such closures may call
themselves and each other (mount / patch / unmount of a renderer), async ones too. Nested
functions calling each other this way share one environment holding what they use, so
returning or storing them makes no reference cycle: the environment is freed with the last of
them.
Each one's parameter types and result type must be known before the bodies are checked, so one
that returns a value needs its result type written (…calls itself: not supported yet
with write its result type); without return value; it is void (Promise<void> when
async).
A generic nested function cannot use the enclosing function's locals. A nested function may
name the type parameters of the enclosing function or class (function failed(): Res<T>) even
when it uses no locals.
function tally(words: string[]): string {
const seen = new Map<string, number>();
let total = 0;
for (const w of words) add(w); // before the declaration
return `${total} ${seen.get("a")}`;
function add(w: string): void {
seen.set(w, (seen.get(w) ?? 0) + 1);
total += 1;
}
}
console.log(tally(["a", "b", "a"])); // 3 2
function parity(base: number): Array<(n: number) => number> {
function even(n: number): number {
return n === 0 ? base : odd(n - 1); // calls each other
}
function odd(n: number): number {
return n === 0 ? -base : even(n - 1);
}
return [even, odd];
}
const [even, odd] = parity(7);
console.log(even(4), odd(4)); // 7 -7A generator calling itself this way (pass the locals it uses as parameters, or declare it at module level), and a use before a local it uses is declared, which JS allows when the call runs later (declare the local first).
Overloads
As in TypeScript, signatures without a body, written right before the implementation, give a function or method several forms:
function describe(x: string): string;
function describe(x: number): number;
function describe(x: string | number): string | number {
return typeof x === "string" ? `s:${x}` : x * 2;
}
const s: string = describe("a"); // the first signature: a `string`
const n: number = describe(21); // the second: a `number`
console.log(s, n + 1); // s:a 43-
A call takes the first signature, in order, that accepts its arguments (after inferring its type arguments, which must satisfy their bounds) and has that signature's result type. When none does, the error lists why each one didn't (TypeScript's TS2769). The implementation's own signature is not one of the forms:
describe(v)withv: string | numberis an error, as in TypeScript. -
Signatures may be generic and have optional and rest parameters. The implementation takes every signature's arguments: a signature it can't take is an error at the signature (TS2394). A signature with fewer parameters, or an optional one left out of a call, leaves the implementation's parameters out, which then take their defaults. A signature's parameters have no defaults of their own (TS2371). A type parameter of the implementation that a signature leaves open is
voidthere (TypeScript'sunknown). -
A signature's function parameter may take fixed parameters where the implementation's takes a rest parameter (
setup: (ctx: Ctx, p1: T1) => Rforsetup: (ctx: Ctx, ...args: unknown[]) => R), which TypeScript relates loosely. The signature passes the implementation a function that converts each rest argument to the callback's parameter type when the implementation calls it:- an argument the implementation leaves out is
undefined, as in Node, where the parameter's type admits it (p1: number | undefined, or a type parameter instantiated with such a type); a callback parameter with a default (p1: number = 5, which TypeScript typesp1?: number) takes its default; - where it doesn't (
p1: number), the program stops with aTypeErrornaming the callback and the argument (the callback `fn` of `setup` was called without its argument 2 (Node would pass `undefined`): the value is not a number (it is undefined)): Velt can't passundefinedthere. AT | nullparameter admitsnull, notundefined, so it stops too; - an argument of another type stops the same way
(
… was called with a wrong argument 1: the value is not a string (it is a number)), where Node would pass it on.
That function is a new one for each call of the signature, so the implementation may only call such a parameter: comparing it (
===, aMaporSetkey,includes), keeping it or passing it on is a compile error (it would see that function, not the caller's). Declare the signature's callback with the implementation's parameter types to keep the caller's function. - an argument the implementation leaves out is
-
A signature may promise a narrower result than the implementation's:
Tout ofT | null, one member of a union, a literal type out of its base ("U"out ofstring), a class out of a base class or an interface, or any of these in aPromiseof anasyncimplementation. The result is then tested, and another value panics (overload `f` returned a `…` where its signature promises a `…`), where TypeScript would trust the implementation. Where the types already agree nothing is tested. -
Every signature is followed by the next one or by the implementation (TS2391), not by a function of another name (TS2389); all of them are
exported or none (TS2383), and a method's are allstaticor none (TS2387, TS2388). Signatures may beasync, as the implementation is. Methods overload the same way,staticones too, and a subclass's override of the implementation is what the signatures call;overrideon the signatures overrides the base class's. -
A signature ends at its
;or, as TypeScript's automatic semicolon insertion has it, at a line break (function f(x: number): numberon a line of its own), as does adeclare function. -
import { f }, a re-export and an export list (export { f }) bring the signatures along. Where a function type is expected (const h: (s: string) => string = describe, a callback argument), an overloaded function is the first signature that fits it; elsewhere (const g = f) it is its implementation. -
An interface may declare a method more than once (
directive(name: string, def: Def): App; directive(name: string): Def | undefined;). As in TypeScript, an implementation, in an object literal or a class, is one method that fits every signature: its result isanyor a type every signature's result takes (a resultApp | Def | undefinedis an error there, TS2416). An object literal's method takes each parameter as every signature writes it or wider (definition?: any), optional where a signature leaves it out or makes it optional; a class's takesanywhere the signatures differ or one leaves the parameter out (directive(name: string, definition?: any): any), and their shared type elsewhere, and returnsanywhere the signatures' results differ, since a class method implements an interface method with exactly its types. A call takes the first signature that accepts it, and its result is tested as above. Generic signatures, rest parameters andvoidresults there are not supported yet, nor is an implementation with a rest parameter ((...args: any[])).tsinterface Def { n: number } interface App { directive(name: string, def: Def): App; directive(name: string): Def | undefined; } const defs = new Map<string, Def>(); const app: App = { directive(name: string, definition?: any): any { if (definition === undefined) return defs.get(name); defs.set(name, definition); return app; }, }; app.directive("x", { n: 2 }); console.log(app.directive("x")?.n); // 2 -
velt docshows the signatures, not the implementation. -
Not supported yet: overloaded constructors, overloads of a function declared inside a function, and several declarations of one name that each have a body (overloads in Velt's own form, chosen by the most specific parameter types).
Return types
As in TypeScript, a function or method without a return type returns the type of its return
expressions (exported ones too):
- one type when they agree, or the one the others convert to:
return 1andreturn 0.5givef64, a class and its base class give the base; - otherwise their union (
return "positive"andreturn ngivestring | i64), made nullable by areturn null; voidwhen noreturnhas a value,neverwhen every returned value never completes (return fail()), andneverfor an arrow or function expression withoutreturnwhose end is never reached (() => { throw e; }), as in TypeScript (one whose result a type argument is inferred from,xs.map((x) => { throw e; }), givesvoid); such a function converts to any function type with its parameters (() => void,() => number);Promise<T>for anasyncfunction,Tfrom its returns.
A function declared to return never must not reach its end, as in TypeScript (TS2534): every
path throws, loops forever (while (true), for (;;) or do … while (true) without a break
out of it) or calls another never function. function f(x: number): never { if (x > 0) { throw new Error("p"); } }
is an error (function `f` returns `never`, so it cannot have a reachable end point), and so
is a return; in it (TS2322); return fail(); with fail(): never is fine.
Integers inferred this way are JavaScript numbers, as in TypeScript: (await half())/2 and
a narrowed T | null result divide like numbers, and a method returning integers that a
subclass overrides returns number (f64), so an override may return 2.5.
function describe(n: i64) {
if (n > 0) {
return "positive";
}
return n; // describe returns string | i64
}
async function double(n: i64) {
return n * 2; // Promise<i64>
}
console.log(describe(3), describe(-1)); // positive -1An unannotated method that overrides a base class method or implements an interface method
returns that method's type, and its returns are checked against it. Arrow functions follow
the same rules when no function type is expected; where one is, its result type applies.
Generic arrow functions (const id = <T>(x: T) => x) follow them too.
A return; next to return value; is not supported yet (TypeScript returns undefined there):
write return undefined; and give the function a T | undefined type.
A function may use itself (or other functions whose return types are being inferred) anywhere
outside its return expressions, as in TypeScript: count below returns i64. Only when its
return expressions depend on the function itself, directly, through a local (const m = f(x); return m;) or through other functions whose return expressions use it in turn (isEven
returning isOdd(n - 1), which returns isEven(n - 1)), it needs an annotation, as
TypeScript's "implicitly has return type 'any'" does:
class TreeNode {
kids: TreeNode[] = [];
}
function count(n: TreeNode) {
let total = 1;
for (const c of n.kids) {
total += count(c); // fine: not in a `return` expression
}
return total;
}
console.log(count(new TreeNode())); // 1function fib(n: i64) {
// error: function `fib` needs a return type annotation
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}Write the type: function fib(n: i64): i64. A function without a return value is void
before its body is checked, so it may call itself freely (a recursive walk(child);). A body
that uses itself outside its returns is checked twice: once to find the return type, then
against it.
Generic functions
function f<T, U extends Bound>(…) is monomorphized: every instantiation is compiled
separately, so there is no boxing and bounds resolve to direct calls. Type arguments are
inferred or given explicitly (f<f64>(2)). Bounds are interfaces
(Generics) or, as in TypeScript, other types (see below). A const type
parameter (function tag<const K>(k: K), TypeScript 5.0) is accepted and treated as an ordinary
type parameter.
Type arguments are inferred from the arguments first and, as in TypeScript, from the expected
type of the call (an annotated variable, a return statement, a typed parameter) second. The
expected type types the arguments of type parameters it fixes, before an untyped number
literal falls back to i64; where an argument's own type disagrees, the argument decides,
unless the result would then not convert to the expected type: a type parameter the arguments
fixed to a type that converts to the expected one takes the expected one, and the arguments
convert to it (const ns: Named[] = wrap(new C()) calls wrap<Named>;
const ps: (i64 | null)[] = pair(1, 2) calls pair<i64 | null>):
import { Set } from "velt/collections";
function id<T>(x: T): T {
return x;
}
const y: i32 = id(1); // T = i32
const z: f64 = id(2); // T = f64
const s: Set<u8> = new Set([1, 2]); // T = u8
const c: Map<string, u16> = new Map([["a", 1]]);
console.log(y, z, s.size, c.get("a"));An arrow function argument is checked against its parameter type with the type parameters the expected type fixed, so its parameters need no annotations there either:
function id<T>(x: T): T {
return x;
}
const inc: (x: i32) => i32 = id((x) => x + 1); // x: i32
const lengths: ((s: string) => number)[] = id([(s) => s.length]);
console.log(inc(1), lengths[0]("abc")); // 2 3A bound that is not an interface is a type the type argument must be assignable to, as in
TypeScript: T extends Record<string, V>, T extends readonly X[], T extends string | number,
T extends "a" | "b", an alias of one (type Primitive = string | number | boolean | null), a
function type. The type argument is related to it as tsc relates types (structurally for object
types); one that is not assignable is an error (the type `boolean` does not satisfy the constraint `string | number` , TypeScript's TS2345 and TS2344), reported at an explicit type
argument when there is one (id<boolean>(x), new G<boolean>(x)), else at the argument. A
literal argument for a type
parameter whose bound has literal types keeps its literal type (same("a") of
same<T extends "a" | "b"> is same<"a">), also where the parameter's type contains the type
parameter (t?: T, t: T | null, t: T[], o: { k: T }). An overloaded call takes the first
signature whose bounds its arguments satisfy (Overloads), so the bounds choose the
form as they do in TypeScript:
type Primitive = string | number | boolean | null;
function wrap<T extends Primitive>(v: T): { value: T };
function wrap<T extends object>(v: T): { inner: T; tag: string };
function wrap<T>(v: T): { value: T } | { inner: T; tag: string } {
return String(v) === "[object Object]" ? boxed(v) : plain(v);
}
function plain<T>(v: T): { value: T } {
return { value: v };
}
function boxed<T>(v: T): { inner: T; tag: string } {
return { inner: v, tag: "object" };
}
function same<T extends "a" | "b">(t: T): T {
return t;
}
const n = wrap(5); // the first signature: `{ value: number }`
const o = wrap({ st: "idle" }); // `{ st: string }` is no `Primitive`: the second
const a: "a" = same("a");
console.log(n.value, o.inner.st, o.tag, a); // 5 idle object aIn the body, a value of a type parameter bounded by an array or primitive type reads the
bound's fields and elements (xs.length and xs[0] on a T extends number[]), through the
value as an any, converted to the bound's type. A value of a type parameter bounded by a
class has the class's members (x.size on a T extends Map<K, V>, e.message on an
E extends Error; Generics). Not yet: its methods
(s.toUpperCase() on a T extends string), Object.keys(o) on a T extends Record<string, V> (take the bound's
type as the parameter's where the body needs them), and more than one such bound for a
parameter (T extends A & B with both no interfaces).
A type parameter may have a default (<T = string>), as in TypeScript: a type parameter
nothing infers takes it, an inferred one ignores it, and explicit type arguments may leave out
the parameters that have defaults (f<number>() of function f<A, B = A[]>() is
f<number, number[]>(); the left-out ones are not inferred then). A default may use the type
parameters declared before it (and a method's, its class's), and a parameter without a default
may not follow one with a default:
function list<T = string>(): T[] {
return [];
}
function pair<A, B = A[]>(a: A, b: B): [A, B] {
return [a, b];
}
const names = list(); // string[]
names.push("ada");
const ns = list<number>(); // number[]
ns.push(1);
const p = pair<number>(1, [2, 3]); // pair<number, number[]>
console.log(names, ns, p); // [ 'ada' ] [ 1 ] [ 1, [ 2, 3 ] ]Because each instantiation is compiled, a generic function may call itself (directly or
through other generic functions) with the same type arguments, but not with growing ones:
f<T> calling f<T[]> would need f<T[][]>, f<T[][][]> and so on without end. The
compiler reports this at the growing call, like TypeScript's "type instantiation is
excessively deep":
function nest<T>(x: T, n: i64): i64 {
// error: instantiating `nest<T[]>` from `nest<T>` grows without end
return n == 0 ? 0 : nest<T[]>([x], n - 1);
}Recurse with a fixed type instead: a non-generic helper, or a JsonValue for data whose
shape is only known at run time.
One growing call is allowed: in the then branch of if (g(x)), where g is a type predicate
whose type has only primitive members (v is string | number | null) and x has the type of
the parameter that grows, when the grown argument is an object type (a struct, class, object
type, array, tuple, map or function type). An instantiation whose argument is such an object type
does not compile that branch (g(x) still runs; if it returns true, which its type rules
out, the program stops with a TypeError), so the recursion ends there. An argument that may
hold a primitive (a union with a primitive member, any, unknown) keeps the branch. What
the branch throws is part of the function's error type only for the instantiations that have
it (throw new Fail(x) there makes depth<number> throw Fail<number>, and
depth<{ value: number }> nothing). The recursion is still rejected when only a value
bounds it (if (n > 0) return depth({ value: x }, n - 1)): the type predicate is what tells
the instantiations apart.
type Primitive = string | number | boolean | null | undefined;
function isPrimitive(v: unknown): v is Primitive {
return v === null || (typeof v !== "object" && typeof v !== "function");
}
function depth<T>(x: T): number {
// `depth<{ value: T }>` has no `then` branch here: its argument is an object.
if (isPrimitive(x)) return depth({ value: x }) + 1;
return 0;
}
console.log(depth(3), depth([1])); // 1 0Type tests
typeIs<A, B>() tells whether two types are the same, decided where the generic is used: a
constant for each instantiation, and an if on it runs only the branch it takes, so generic
code can choose a representation per type at no cost (the other branch is not compiled for that
instantiation). Inside if (typeIs<A, B>()), asType<B>(x) converts a value x: A to B, the
same type there; anywhere else it is an error, and neither function can be used as a value.
Both are Velt's (TypeScript has no counterpart).
What counts as the guard
Only the then branch of an if whose condition is the call
itself, and closures written inside it. Not !typeIs… with its else, typeIs… && c, a
ternary, code after an early if (!typeIs…) return;, a test saved in a const, or a chain
(typeIs<A, B> with asType<C>).
- The call:
asType<B>(x), withBwritten and nothing else: the type ofxis the type converted from.
class Slot<V> {
word: u64 = 0;
boxed: V[] = [];
put(v: V) {
if (typeIs<V, u64>()) {
this.word = asType<u64>(v); // numbers inline: one store for `Slot<u64>`
} else {
this.boxed = [v];
}
}
}
const s = new Slot<u64>();
s.put(7);
console.log(s.word, s.boxed.length); // 7 0Generators
function* range(from: i64, to: i64): Generator<i64> {
for (let i = from; i < to; i++) {
yield i;
}
}
function* evens(limit: i64): Generator<i64> {
for (const i of range(0, limit)) {
if (i % 2 == 0) {
yield i;
}
}
}
for (const i of evens(7)) {
console.log(i); // 0 2 4 6
}
const g = range(1, 3); // nothing has run yet
console.log(g.next(), g.next().value, g.next().done); // { value: 1, done: false } 2 truefunction* name(…): Generator<T>is a generator, and so is a method written*name(),static *name()or*[Symbol.iterator](). Calling one creates aGenerator<T, E>(prelude) without running the body. Eachnext()runs the body up to its nextyield vand returns{ value: v, done: false }; when the body ends it returns{ done: true }, and keeps doing so (next().valueis thennull;undefinedin aGenerator<any>orGenerator<unknown>, as in JS). AGenerator<T, E>is anIterator<T, E>and anIterable<T, E>(its[Symbol.iterator]()returns itself), sofor...oftakes it (Iterables).- The return type is required:
Generator<T>,Iterator<T>,Iterable<T>,IterableIterator<T>orIteratorObject<T>, whereTis the type of the yielded values; a call has that type, with the generator's error type asE.return;ends the generator; there is noTReturn, soreturn valueis an error. A bareyieldis allowed in aGenerator<void>only. - TypeScript's spellings
Generator<T, void>and the same withundefined,unknownoranyasTReturnmeanGenerator<T>, so TS signatures compile as they are; so doIterator,Iterableand their async twins. Any other second type argument is the error typeEand must be one (a class extendingError, a union of them, an interface or a type parameter):Generator<number, string>is an error naming the fix, since there it can only be TS's return type.
next(value)
The third type argument of Generator<T, X, N> is TypeScript's TNext,
the type of a yield expression's value and of next(value)'s argument
(Generator<number, void, string>: const s = yield 1 is a string). The first next()
starts the body, so its value is ignored, as in JS. A next() without a value resumes a
yield with undefined when N holds it (unknown, any, T | undefined); for another
N, a yield whose value is used then panics (TS gives it undefined whatever N says).
Without N (Generator<T>, also void, undefined or never as N), next(value) and
using the value of a yield are errors, so a yield is a statement of its own (also as a
branch of c ? yield a : yield b). The other protocol types (Iterator<T, X, N>, …) and
async generators ignore N.
-
throw(e)resumes the body withethrown at the pausedyield: atry/catcharound it catches it (eis ananythere, asthrow(e: any)takes it) and the body goes on to its nextyieldor its end, which is whatthrow()returns; otherwise the generator is closed (itsfinallyblocks run, as forreturn()) andthrow()throwse. A generator that has not started, or has finished, throwseat once (and is done), as in JS. Only a program that callsthrow()gets the code that handles it; there thecatchof atryaround ayieldalso catches anany.tsfunction* worker(): Generator<string, void, number> { try { const n = yield "ready"; yield `got ${n}`; } catch (e) { yield `stopped: ${(e as Error).message}`; } } const w = worker(); console.log(w.next().value, w.next(7).value); // ready got 7 const v = worker(); v.next(); console.log(v.throw(new Error("cancelled")).value); // stopped: cancelled -
yield* srcyields every value ofsrc: another generator, an array, or any iterablefor...oftakes. In a generator that takesnext(value)(or in a program that callsthrow()),yield*of a generator of the sameT(andN, or none) passesnext(value),throw(e)and closing on to it; an error it does not catch is thrown at theyield*.
Errors
E is what the body throws, inferred like a function's throws (or written:
Generator<T, E>, also through a type alias such as type Gen = Generator<i64, A>, or
throws E after the return type). next() throws it, and so does
for...of over the generator; creating the generator never throws. A generator that threw is
done. A generator method implementing an interface whose result names an error type writes
it: class Lines implements Iterable<string, IoError> declares *[Symbol.iterator](): Iterator<string, IoError>. Each class keeps its own E: calling a generator method never
throws, so implementations of one interface method don't share an error type.
Closing
return() (which for...of calls when it is left early), the end of a using
block (using g = gen();), and dropping the generator all close a generator suspended at a
yield: its finally blocks run and its using values are disposed, as if the yield were
a return. A generator that never started or already finished has nothing to clean up.
Because closing runs finally blocks where the generator can neither pause, report an
error, nor go on with its body, a finally block in a generator cannot yield, throw, or
break / continue to a loop outside it (TS allows all three).
- A generator's parameter defaults are evaluated when it is called, before the body runs, as
in JS; a default of a parameter of a dynamic type (
x: any = "g") also applies to a passedundefined, whether the generator is called directly or through a function value (const gv = gen; gv(undefined), also for a generic generator and a static generator of a generic class). - A generator keeps its arguments (they are owned, as for an async function) and its locals
between
yields. A generator method seesthisas it is when the body runs, not when the method was called, as in JS. Callingnext()orreturn()on a generator from inside its own body panics (generator is already running; JS throws aTypeError). A call keeps the generator alive while it runs: a body that drops the last reference to its own generator (this.gen = other()) finishes its step, and the generator is closed and freed after it. - A generator cannot be copied or leave its thread:
clone()of one, passing one to a spawned task (or sending it on a channel), putting one inshared(...)(also inside aMutexor an object) and capturing one in an async closure that may run on another thread (see Captures) are errors, like for a promise. An async closure that stays on its task shares a generator it captured, as in JS (a stream'spullreading a generator:async () => (await chunks.next()).value). Behind an interface value (Iterator<T>) the compiler cannot see it, and handing it to another thread stops the program instead. Pass the arguments and create the generator where it is used. - Not supported: a return value (TS's
TReturn),return(value),awaitin a (sync) generator (write an async generator), andnext(value)/throw()on an async generator.return()returns{ done: true }, as in TS (where it may carry a value);return(undefined)isreturn(), and so isg.return?.(undefined)(a method is never nullish, so an optional call of one is a plain call, as in JS). Arrow functions cannot be generators (as in TS).
Cost
for (const x of gen(a)) with a direct call (or over a class whose
[Symbol.iterator] is a generator method) keeps the generator's state in the loop: no
allocation, no IteratorResult objects, and the body is resumed by a direct call that the
optimizer can inline; such a loop runs as fast as the equivalent hand-written loop. A
generator used as a value is one heap object; each next() then returns a small
IteratorResult value. Spreading a direct call ([...gen(a)]) and destructuring one
(const [x, y] = gen(a)) use the same loop: only the resulting array is allocated.
Generator function expressions
const evens = function* (limit: i64): Generator<i64> {
for (let i = 0; i < limit; i += 2) {
yield i;
}
};
const step = 10;
const tens = function* (n: i64): Generator<i64> {
for (let i = 0; i < n; i++) {
yield i * step;
}
};
console.log([...evens(5)], [...tens(3)]); // [ 0, 2, 4 ] [ 0, 10, 20 ]function* (…): Generator<T> { … }andasync function* (…): AsyncGenerator<T> { … }are generator function values, optionally named (function* walk(…)); their type is(…) => Generator<T, E>with the body's error type asE(a call never throws). Otherfunctionexpressions are function expressions.- At module level,
const g = function* (…) { … };is the generator functiong. - Elsewhere it is a closure that uses the variables around it as JS does: its generators see
the objects it captured (
xs.push(3)after creating a generator shows up in it), a variable assigned after the expression (by the function or by a generator) is one variable that all of them see, and the body may assign it (count++). The name of a named expression is not in scope in its body (TS allows recursion through it): declare afunction*to recurse. Type parameters and rest parameters are errors there too; declare afunction*. - The return type is required, as for any generator, unless the expression is written where
a function type is expected (
return function* () { … }in a function returning() => Generator<string>, orconst g: Gen = function* () { … }): then it is that type's result, as for an arrow.
Async generators
async function* pages(n: i64): AsyncGenerator<string> {
for (let i = 1; i <= n; i++) {
await sleep(1); // e.g. fetch the page
yield `page ${i}`;
}
}
for await (const p of pages(3)) {
console.log(p); // page 1, page 2, page 3
}async function* name(…): AsyncGenerator<T>is an async generator, and so is a method writtenasync *name(),static async *name()orasync *[Symbol.asyncIterator](). Its body may bothawaitandyield. Calling one creates anAsyncGenerator<T, E>(prelude) without running the body; eachnext()returns aPromise<IteratorResult<T>, E>that runs the body to its nextyield, awaiting what it awaits on the way. It is anAsyncIterator<T, E>and anAsyncIterable<T, E>, sofor awaittakes it.- The return type is required:
AsyncGenerator<T>,AsyncIterator<T>,AsyncIterable<T>orAsyncIterableIterator<T>(a sync result type on anasync function*, or an async one on afunction*, is an error naming the fix). yield* srcdelegates to an async iterable (another async generator) and, as in JS, to a sync one (a generator, an array).yield pwherepis a promise (Promise<T, E2>) awaits it before yielding its value, as in JS:yield fetchPage(i)yields the page. If it rejects, the error is thrown at theyield, andE2joins the generator's error type.- Errors:
Eis what the body throws, awaited calls included, inferred or written (AsyncGenerator<T, E>).next()rejects with it, andfor awaitrethrows it, whether it is thrown before or after the body's firstawait. A generator that threw is done.
Closing
return() (which for await awaits when it is left early) and await using g = agen() close a generator suspended at a yield like a sync one, and here its finally
blocks may await (so may await using disposals in the body); return() resolves once
they are done. A using block's end and dropping the generator close it without awaiting:
if that cleanup would await, it is cancelled instead (its values are dropped, its finally
blocks do not run), like a cancelled async function. A finally
block cannot yield, throw or break out, as in a sync generator.
-
Overlapping calls are queued, as in JS: a
next()orreturn()made while an earlier call is still running waits for its turn, and each promise settles with its own step, in call order. A call nobody awaits (aPromise.raceloser, a call abandoned bytimeout) still takes its turn; its value is dropped and the next call gets the next one. -
A stored generator is closed when its last reference goes, like any object (Memory): when the variable's last use is a
next()call, that is when the call's promise settles, since the call holds the generator until then. JS never closes an abandoned generator (itsfinallyblocks never run); Velt has no garbage collector to wait for, so it closes it as soon as nothing can resume it:tsasync function* ticks(): AsyncGenerator<i64> { try { yield 1; yield 2; } finally { console.log("ticks closed"); } } const g = ticks(); const p = g.next(); // the last use of `g`: `p` holds it now // `p` settled at once (the body reached `yield 1` without awaiting), which released the // last reference: "ticks closed" has been printed. console.log("waiting"); const r = await p; if (!r.done) { console.log(r.value); // 1 }To keep it open, use it again later (
await g.return()closes it explicitly). -
An async generator belongs to the task that created it, like a started promise: it cannot be copied or passed to another task (see above), also not as an
AsyncIterable<T>orIterable<T>value made at the call (spawn(sum(gen()))). An interface value made earlier and stored is not looked into: passing one backed by a generator tospawnpanics (a generator cannot be copied).
Cost
for await (const x of agen(a)) with a direct call (or over a class whose
[Symbol.asyncIterator] is an async generator method) keeps the generator's state inside the
enclosing async function's state, like await f() does: no allocation for the generator or
per item, and each step is a direct call of the generator's poll function. A stored
generator is one heap object; await g.next() on an AsyncGenerator<T> variable is a direct
call too, with the result as a small IteratorResult value. Through an AsyncIterator<T> or
AsyncIterable<T> interface value, each next() allocates its promise.
Parameters
function sum(...xs: number[]): number {
return xs.reduce((a, b) => a + b, 0);
}
const more = [2, 3];
console.log(sum(), sum(1, ...more, 4), Math.max(...more)); // 0 10 3A parameter may be destructured, in functions, methods and arrows alike: [a, b]: T or
{ a, b }: T, with defaults ({ x = 0 }, [a, b = 5]) and holes ([, c]) inside the pattern
and a default for the whole parameter ({ host, port }: Opts = defaults). An arrow passed where
a function type is expected takes the pattern's type from it (pairs.map(([k, v]) => …)).
The body starts by destructuring the argument, exactly as let [a, b] = arg; would there, so a
destructured parameter costs what destructuring by hand does, and its bindings are let
variables (a closure may capture and assign them; each call has its own). As for let, an array
pattern may bind fewer elements than a tuple has, but not more (TS2493). A destructured
parameter can't be optional ({ a }?: P, TS2463) or have a type that includes null
({ a }: P | null): null can't be destructured, and destructuring any value that may be
null is an error, in a let too (TS2339, or TS2488 for an array pattern). The defaults inside
a pattern run when the body starts, in order; since a later parameter's default is evaluated at
the call, before them, a default after a destructured parameter with defaults must be a constant
for now (y = 10, s = "z", xs = [1, 2]).
function label([name, n]: [string, number], { sep = "=" }: { sep?: string }): string {
return name + sep + n;
}
const counts = new Map<string, number>([["a", 1], ["b", 2]]);
console.log(label(["x", 4], {}), label(["y", 5], { sep: ":" })); // x=4 y:5
console.log([...counts.entries()].map(([k, v]) => k + v).join(",")); // a1,b2Parameters behave like let locals: reassigning one never affects the caller. Objects are
shared with the callee (xs.push(1) or p.x = 2 inside the function is visible to the
caller); numbers, bools and strings are copies. Which parameters a function
modifies is inferred (Memory model).
Function expressions
const twice = function <T>(x: T): T[] {
return [x, x];
};
const later = async function (n: number): Promise<number> {
return n + 1;
};
const id = "xx-yx".replace(/[xy]/g, function (c) {
return c === "x" ? "a" : "b";
});
console.log(id, twice(1), await later(1)); // aa-ba [ 1, 1 ] 2-
function (params) { … }as a value, optionally named (function square(n) { … }),asyncor generic, is the arrow function with the same parameters, result and body: parameter types may be left out where a function type is expected, as for an arrow. -
Its
thisis the object it is called on in TS. Without athisparameter,thisin the body is TS's error'this' implicitly has type 'any' because it does not have a type annotation, as under--strict, also in a generator (function* () { … }). A function expression that is the value of an object-literal property ({ v: 1, get: function () { return this.v; } }) is a method of the literal:thisis the object, as in TS (thisin object-literal methods). -
With a
this: anyparameter and a body that usesthisorarguments, the value is aFunctionwhosethisis bound at the call, as in JS. A method call passes its object:o.m(x),o[k](x)ando.m?.(x)on ananyor a proxy (p.push(x)where thegettrap answers the function),arr[0](x)on anany[], ando.m(x)on a typedowhosemis aFunction.f.call(o, …),f.apply(o, args)(nullorundefinedfor no arguments) andf.bind(o, …)passo, and a plain callf(x)passesundefined.argumentsholds the call's arguments (anany[]). A parameter of typeany(or an optional one) isundefinedwhen its argument is left out; one of another type converts its argument, checked, so leaving it out is aTypeError(Node would passundefined), and so does an argument of another type. A rest parameter's elements convert the same way.tsconst methods: Record<string, Function> = {}; methods.total = function (this: any, extra: number) { return this.n + extra + arguments.length; }; const o = { n: 1, total: methods.total }; console.log(methods.total.call({ n: 1 }, 10), o.total(20)); // 12 22Not supported yet: another
thistype, an async such function, such a function where a function type is expected (it is held as aFunctionorany), and a destructured parameter; declare the function and call it withcallorapply(below), take the object as an ordinary parameter, or write a class method. -
A
this: Tparameter (first in a function's or method's parameter list) declares the type ofthisand is no parameter.this: voidsays the function doesn't usethis. An arrow cannot have one (TS2730).velt fmtdoes not keep it yet, so it leaves a file with one as written and reports it. Seethisparameters for function declarations. -
The name of a named expression inside its own body (recursion) is not supported yet: declare the function with
function name(…) { … }.
Arrow functions and function types
-
Arrow functions
(x: T) => exprand(x) => { … }are closures. Parameter types are inferred where a function type is expected. There is nothisrebinding:thisinside an arrow is the enclosing method'sthis. -
Arrow parameters take defaults and may be optional, like a function's: with
const fmt = (n: number, digits = 2) => n.toFixed(digits);,fmt(3.14159)is"3.14". A call through theconstfills in left-out arguments; an unannotated parameter takes its default's type. -
Generic arrow functions are written as in
.tsfiles. -
As in TS, a function may take fewer parameters than the function type it is passed as:
xs.map((x) => x * 2)wheremappasses(x, i), andxs.map(double)with a one-parameterdouble, also a method value or a generic callback type (xs.reduce(plus, 0)andxs.reduce(acc.add, 0)with a two-parameterplusandadd, wherereducepasses(acc, x, i)). A function may also take more, when the extra ones are optional or have defaults (setTimeout(tick, 10)withfunction tick(n?: number), or{ patch }for a fieldpatch: (a: VNode, b: VNode) => voidwithfunction patch(a: VNode, b: VNode, ns = false)): they are left out and take their defaults. A parameter with a default also takes theundefinedan optional parameter of the type may pass (before: Node | null = nullforbefore?: Node | null, at the same number of parameters too): the default applies there. A function with a rest parameter takes fixed ones ((...args: unknown[]) => Ras a(p1: T1) => R, also returned from a call): the arguments past its fixed parameters become the rest array. -
As in TS, a function whose parameter types are wider, or whose result type is narrower, than the function type's is accepted where the function type is expected: see Wider parameters and narrower results.
-
As in TS, a function that returns a value is accepted where a
void-returning function type is expected; the value is evaluated and dropped. This holds for an arrow without a return type (its expression body, or areturn value;in it) and for a named function or a function value passed or assigned there. An arrow or function annotated: voidstill may not return a value, as in TS.tsfunction each(xs: string[], f: (s: string) => void) { for (const x of xs) { f(x); } } const seen: string[] = []; each(["a", "b"], (s) => seen.push(s)); each(["c\n"], (s) => process.stdout.write(s)); // c console.log(seen); // [ 'a', 'b' ] -
A function returning
Tis also accepted where the function type returns a union withTas a member ((x: number) => Respfor(x: number) => Resp | Promise<Resp>), as in TS; its result converts to the union. An error type parameter that nothing fixes (<E>(h: (x: number) => Resp | Promise<Resp, E>)called with a sync function) isnever. -
A value whose type is a union of function types with the same parameters can be called: the call runs whichever function the value holds, and its result is the union of their results (a
voidmember makes itT | null;awaiton that givesvoid). Members may take fewer parameters when theirs begin the longest member's:f(1)on((a: number) => string) | (() => string)passes1to the first and nothing to the second (the arguments are evaluated either way, in order). An arrow passed as such a union is typed by the member it fits, the first in order where several do.tstype Format = ((n: number) => string) | ((n: number) => number); function show(f: Format, n: number): string { const v = f(n); return typeof v === "string" ? `text ${v}` : `number ${v}`; } console.log(show((n) => `#${n}`, 3), show((n) => n * 2, 3)); // text #3 number 6 -
Methods as values:
obj.methodread without calling it is a function value bound toobj. -
Function types
(x: T) => Uaccept closures and named functions alike. One that may throw says so:(x: T) => U throws E(Errors). Calling a named function through a value behaves like calling it directly: an object it keeps or modifies is the caller's object. A promise passed through a function value is shared with the function, as any object is. -
A function type may end in a rest parameter.
-
Construct signatures (
new (x: number) => Point) type a class used as a value, and generic function types (<T>(x: T) => T) a value each call instantiates (generic function values). -
A function type's parameter may be named by a destructuring pattern (
([a, b]: [number, number]) => number), which, like a name, is only for the reader. -
A function type's parameter may be optional (
(app: App, options?: Options) => void): its type isOptions | undefined, as for a function's optional parameter, and a required parameter cannot follow it (TypeScript's TS1016). A call through such a value may leave it out (install(app)), which passesundefined; so may a call of a parameter typedT | undefined, which TypeScript requires to be passed.
Wider parameters and narrower results
As in TS, a function whose parameter types are wider than the function type's is accepted
where the function type is expected: p.then((v: any) => …) for a Promise<number>,
xs.forEach((x: unknown) => …), const f: (x: number) => void = (v: number | string) => …, a
function value or named function taking an any, object, union or interface. Each argument
converts to the function's parameter type when it is called, as any argument does (an any
boxes the number). An argument that doesn't convert ((x: string) => … where
(x: number) => void is expected) is an error, as it is in TS.
So is a function whose result type is narrower than the function type's: a () => void
where a () => any is expected (its result is undefined), a () => string for a
() => unknown, an async function's () => Promise<R> for a () => R | Promise<R>. Its result
converts to the expected result type when it returns, through the same wrapper.
An arrow written there takes the expected parameter types and converts each one where it starts, so it is one function, as fast as one written with those types.
A function value (a variable, a named function, a method value) is wrapped instead: each such
use makes a new function value that calls it, so g === f is false after
const g: (x: number) => void = f, a Set holds both, and an emitter whose off(f) looks for
the function on(f) stored doesn't find it (each call passes its own wrapper). To register and
remove the same handler, declare it with the parameter types the registry expects
(const h = (x: number) => …), or convert it once and pass that value to both.
const show = (v: unknown) => console.log(typeof v, v);
[1, 2].forEach(show); // number 1, number 2
const both: (x: number) => void = (v: number | string) => console.log(v);
both(3); // 3Generic arrow functions
Generic arrow functions are written as in .ts files, <T>(x: T): T => x (the .tsx spelling
<T,> works too, and JSX is allowed alongside). As the value of a const with typed parameters
it is a generic function. Without a return type it returns the type of its body
or its returns, by the rules of Return types (const id = <T>(x: T) => x
returns T; an async one returns Promise<T>).
At module level it is an ordinary generic function; in a function body it is a generic function
nested there, so each call instantiates it, unless it uses the local variables around it or is
used as a value: then it is a generic function value (one using
this in a method is not supported yet). A generic function used as a value is instantiated
where a function type is expected (const f: (x: i64) => i64 = id;), and is a generic function
value elsewhere. A generic arrow elsewhere (an argument, a let) is a generic function value
where a generic function type is expected, and is instantiated where a function type is
(apply(<T>(x: T): T => x) with apply(f: (x: i64) => i64) gives T the type i64); without
either it needs typed parameters:
const firstOr = <T>(xs: T[], fallback: T): T => (xs.length > 0 ? xs[0].clone() : fallback);
console.log(firstOr([3, 4], 0), firstOr([], "none"));
const pair = <A, B>(a: A, b: B) => `${a}:${b}`; // returns string
console.log(pair(1, true), pair("x", 2.5)); // 1:true x:2.5
const id = <T>(x: T) => x;
const n: i64 = id(41) + 1;
console.log(n); // 42Methods as values
obj.method read without calling it, and obj.method.bind(obj), are function values bound to
obj: xs.forEach(log.write), const add = counter.add.bind(counter). The object is evaluated
once, where the method is read, so a later assignment to the variable does not change which
object it calls, as with bind in JS. The method dispatches like a call (an override in the
object's class runs), and converts its arguments as a named function does
([1, 2].forEach(p.log) with log(x: number)).
Read from this or a const or using local, it is the arrow (x) => obj.method(x) itself:
it captures the object as that arrow would, and costs no allocation where such an arrow costs
none.
Two deliberate differences from JavaScript:
- An unbound read: there
thisis lost (calling the value with a method that usesthisthrows aTypeError); Velt binds it to the object it was read from, so the two differ only where JavaScript would throw. - Identity: each read is a new function value, as each
bindis in JavaScript, where an unbound read returns the one function of the prototype (o.get === o.getistrue). So===and!==on a method read are errors: store it in aconstfirst and use that, e.g. to register and remove a handler (const tick = w.tick; e.on(tick); e.off(tick)).
bind takes exactly the object the method is read from (a.m.bind(b) is an error: write
(x) => b.m(x)). A generic method, or one with a rest parameter, has no single function type:
wrap it in an arrow.
class Log {
lines: string[] = [];
write(s: string): void {
this.lines.push(s);
}
}
const log = new Log();
const words: string[] = ["a", "b"];
words.forEach(log.write.bind(log));
const write = log.write;
write("c");
console.log(log.lines.join(",")); // a,b,cRest parameters in function types
A function type may end in a rest parameter: (event: string, ...args: T[]) => void. A call
through the value packs the remaining arguments into the array, as a call to a function with a
rest parameter does (emit("tick") passes [], emit("tick", ...xs) spreads), and a function
or arrow with the same rest parameter is such a value.
An arrow passed where one is expected may:
- leave the rest arguments out (
(event) => …); - take them as its own rest parameter in the same place (
(event, ...args) => …); - name them one by one (
(event, a) => …): a missing one isundefined, which the parameter's type (or the rest element type) must admit (a: any,a?: number; a default is not supported yet there).
An arrow's own rest parameter may also take fixed arguments ((...xs: number[]) => … for
(a: number, b: number) => number), as an array, or as a tuple when their types differ.
A rest parameter typed by a tuple is that tuple's parameters: (...args: [string, number]) => R
is (a: string, b: number) => R, also when the tuple is a type argument
(type Factory<A extends unknown[], T> = (...args: A) => T).
type Listener = (event: string, ...args: number[]) => void;
function fire(f: Listener) {
f("none");
f("two", 1, 2);
}
fire((event, ...args) => console.log(event, args.length)); // none 0, then two 2A named function or a function value passed where a function type with the other kind of parameters is expected is adapted as TypeScript allows:
- A function with a rest parameter gets every argument the caller passes
(
const h: (a: number, b: number) => number = sumgivessumboth). - A function with fixed parameters, passed where the function type ends in a rest parameter,
gets the rest arguments one by one (
call(Math.max)); a parameter whose argument is missing gets its default, orundefined, which its type (or the rest element type) must then admit ((a: any) => …,b?: number); one of another type is an error. - Through
anya function with a rest parameter gets the arguments past its fixed ones in its array, each converted as a fixed parameter's argument is.
In type-level code, F extends (...args: any[]) => any holds for every function type, and
Parameters<F> of (...xs: T[]) => R is T[], as is A in
F extends (...a: infer A) => R ? A : never.
A default on a parameter of an arrow that names the rest arguments one by one; an arrow's own
rest parameter after more fixed parameters than the function type has ((p, q, ...xs) => … for
(p: string, ...xs: number[]) => R); calling a union of function types where one has a rest
parameter; Parameters of a function type with fixed parameters before its rest parameter,
which would be a variadic tuple type ([e: string, ...xs: number[]]); and a generic function
whose rest parameter is typed by its type parameter
(wrap<A extends unknown[], R>(f: (...args: A) => R)).
Construct signatures
Construct signatures (new (buffer: ArrayBuffer) => ArrayBufferView) type a class used as a
value. A class named where a value of that type is expected (a variable's or field's type, an
argument, an array or tuple element, a returned value) is one when its constructor takes the
signature's parameters and its instance converts to the signature's instance type, as
TypeScript checks it.
Through a value C of that type:
new C(args)constructs an instance (what the constructor throws, the call throws);v instanceof Ctests for an instance of the classCholds (and narrowsvto the signature's instance type when that is a class);C.nameis the class's name andtypeof Cis"function"(also held asunknown).
C may be a variable, a module constant, a member of one (new reg.Default(x)) or a class's
static field (new Registry.DEFAULT(x)), and an optional parameter of the signature may be left
out (new (x?: number) => Point).
class Point {
constructor(public x: number) {}
}
class Point3 extends Point {}
type PointCtor = new (x: number) => Point;
const kinds: PointCtor[] = [Point, Point3];
for (const K of kinds) {
const p = new K(1);
console.log(K.name, p instanceof K, p instanceof Point3); // "Point true false", then "Point3 true true"
}
console.log(kinds.indexOf(Point3), kinds[0] === Point); // 1 trueA class has one value per signature type, as JavaScript has one class object: C === Point,
kinds.includes(Point), Object.is and Map and Set keys see the same object, also where a
generic function takes the class (register<T>(C: new (x: number) => T) called as
register(Point); as in TypeScript, the call's other arguments take part in inferring T,
which is the common supertype: mk(Point3, new Point(1)) for
mk<T>(C: new (x: number) => T, v: T) is T = Point, in either argument order).
Two cases differ from JavaScript, which has one object per class:
- one class at two signature types is two objects (
new () => Pointandnew (x?: number) => Point, orBoxatnew (v: number) => Box<number>and atnew (v: string) => Box<string>), so comparing values of two signature types directly is an error, and compared asunknownor asMapandSetkeys they are not equal; - a class read at a signature type over a generic function's type parameters in its body
(
const k: new (v: T) => Box<T> = Box;) is one object per call of that function.
A class declared in a function gets a new value each time its declaration runs (each call, each loop iteration), as JavaScript makes a new class object there; a function declared there that reads the class as a value reads that value too (it becomes a closure, as one using the enclosing function's variables does).
These are errors for now: reading a class declared in a function as a value from a method or
field initializer of a class, or from a parameter default; a class used as a value
without such a type (const C = Point;); generic construct signatures
(new <T>(x: T) => Box<T>); printing or JSON.stringify of one; converting one to another
construct signature type (new (x: number) => Point3 to new (x: number) => Point; name the
class instead); and new of an expression that is not a name (new (pick())(x)).
An abstract construct signature, abstract new () => Base (TypeScript 4.2), also takes
abstract classes: .name and instanceof work through such
a value, new through it is an error
(cannot create an instance through a value of type `abstract new () => Base`: it may hold an abstract class (TypeScript's TS2511)),
and an abstract class where a plain new () => Base is expected is an error too.
abstract class Base {}
class One extends Base {}
function nameOf(c: abstract new () => Base): string {
return c.name;
}
console.log(nameOf(Base), nameOf(One)); // Base OneGeneric function types
A const of a generic function type (<T>(x: T) => T, a value each call instantiates) is
a generic function, as a generic arrow constant is, so each call instantiates it and nothing is
boxed. The type may be written at the constant, named by a type alias (also a generic one at its
arguments, const pair: Pair<string> = …, or one imported from another module), or by an
interface with one generic call signature
(interface Mapper { <A, B>(xs: A[], f: (a: A) => B): B[] }).
Its value is:
- an arrow function, whose parameters take their types from the function type where it writes none (it may take fewer parameters than the type, as in TypeScript);
- or a name, which each call reads and calls at the instantiated type: also a name asserted to
the type,
const createToken = Symbol as unknown as <T>(description: string) => Token<T>, whose function converts to the instantiated type directly, a branded result such assymbol & { __t?: T }taking the value as it is.
Like a local generic arrow, a local constant of such a type cannot use the local variables around it.
type Id = <T>(x: T) => T;
const id: Id = (x) => x;
type Pair<K> = <V>(key: K, value: V) => [K, V];
const pair: Pair<string> = (k, v) => [k, v];
type Token<T> = symbol & { readonly __type?: T };
const createToken = Symbol as unknown as <T>(description: string) => Token<T>;
console.log(id(3), id("s")); // 3 s
console.log(pair("a", true)); // [ 'a', true ]
const t = createToken<number>("port");
console.log(typeof t, t.description); // symbol portA generic function type anywhere else (a parameter, a field, a let, a constant with another
initializer) is a generic function value. Generic construct
signatures (new <T>(x: T) => Box<T>) are not supported yet.
Generic function values
A value of a generic function type (<T>(x: T) => T[]) stays generic: each call through it
instantiates it, as in TypeScript. It may be a parameter (also optional, f?: Id or
Id | undefined), a field (also static), a variable, an element or a result, written as a
function type with type parameters (also with optional and rest parameters,
<K extends string>(name: K, ...args: unknown[]) => void), as a type alias of one, an
interface with one generic call signature, or a generic method of an object type
({ run<T>(fn: () => T): T }). Such a value is:
- a generic arrow (
<T>(x: T): T[] => [x]), which may use the local variables around it; - an arrow without type parameters where the type is expected (
(x) => [x]): its parameters take the type's types, its type parameters included; - a generic method of an object literal (
{ run<T>(fn: () => T): T { … } }); - a generic function (
apply(id)), also where no function type is expected (const f = id,[id]: the value of its own generic type), each call of the value calling it at the instantiated types; a module-level one is one value per type, soid === id; - a generic method of an object (
apply(k.id)), bound to it as other method values are; - another value of the same type, or of the same type written with other names
(
<U>(y: U) => U).
type Wrap = <T>(x: T) => T[];
function twice(n: number): Wrap {
let calls = 0;
return <T>(x: T): T[] => {
calls++;
const out: T[] = [];
for (let i = 0; i < n * calls; i++) out.push(x);
return out;
};
}
function show(f: Wrap): void {
console.log(f(1), f("a"));
}
show(twice(1)); // [ 1 ] [ 'a', 'a' ]
show((x) => [x]); // [ 1 ] [ 'a' ]A value is itself however it is copied, stored or converted between spellings of its type:
===, includes, Map and Set keys compare generic function values by identity, as
JavaScript does. A module-level generic function used as such a value is one value per type
(id === id, also through const asA: A = id, and for a generic function declared in a
function body). A const holding a named generic function (const g: G = id, also through a
chain of constants, local or module-level) is that function wherever a function of fixed types
is expected (const p: N = g, an argument, a return, an element, as N, N | null), so those
values compare equal to each other and to id itself, as in JavaScript; a generic arrow
constant used only there is lifted to a function, with the same effect.
Some values are made anew at each use, where JavaScript keeps one function: a generic value
converted to another function type (a parameter of type G as (x: number) => number, or as
a generic type taking more optional parameters), a function asserted to a generic type, and a
generic function used at a type that mentions the caller's type parameters (P<U> in
getP<U>). A program that makes such a value and compares function values of its type (the
same parameter and result types, or the same generic function type, also as a member of a
union, an option or a struct key, or after a conversion to another function type; with ===,
includes, indexOf, a Set or Map key) is an error, rather than giving another answer
than JavaScript, as is one that compares unknown or any values while one holds such a
value; holding it as unknown alone is fine. A generic function used as a value at more than
one generic type (or at one and also at a fixed type) has one value per type: comparing those
of one type is fine, and comparing unknown or any values while one is held is the error.
The program's build reports these, as it needs every instantiation: velt check, which only
type-checks, does not.
Where a function type of fixed types is expected (const p: (x: number) => number = f,
[1, 2].map(f)), a generic function value is instantiated at it, as TypeScript does. A
function of fixed types is not a generic one ((x: number) => number is not a
<T>(x: T) => T, as TypeScript reports), also not through unknown (plus as unknown as Id
is an error), unless it takes the type's parameters at every type argument:
Symbol as unknown as <T>(d: string) => Token<T> is the generic arrow calling Symbol (the
type written out, or an alias of it declared in the module without type parameters), which is
evaluated once (f as unknown as … keeps the function f held then; a function that doesn't
take them at every type argument, Symbol as <T>(d: string) => Token<T> with Token<T> an
object type, is an error). Held as unknown, a generic function value's typeof is
"function", it compares with ===, and u as Id gives it back; it can't be held as any or
Function yet (an error), since a call through those would need its type arguments, and
calling one through an any reached otherwise ((u as any)(1)) stops the program with a
TypeError. An overloaded generic function is its implementation as a value (as for any
overloaded function): where a generic function type is expected it is called through its
signatures.
Velt compiles the value's code once per list of type arguments the program calls such values
with, and a call picks its entry from a table, so nothing is boxed (no any). Without a
throws clause the type throws what the values stored into it throw (a generic arrow throwing
NotFound makes calls through the type throw it), found as for
dynamic calls. Generic function values, like other
function values, can't be printed.
A function of fixed types adapted to a generic type (TypeScript accepts
(event: string) => void as a <K extends string>(event: K) => void), overloaded generic call
signatures (an interface with several), rest parameters typed by a type parameter or a
conditional tuple type (...args: A), generic construct signatures, and this in a generic
method of an object literal that implements an interface.
this parameters
A function declaration whose body uses this may declare its type with a this parameter,
function f(this: T, a: A), as in TypeScript. The caller passes this:
f.call(obj, a) and f.apply(obj, [a]) run the body with this = obj, which must be a T
(TypeScript's TS2345 otherwise). A plain call f(a) passes undefined, which only this: any
and this: unknown accept; for another T it is TypeScript's TS2684 ("The 'this' context of
type 'void' is not assignable to method's 'this' of type 'T'"), also for T | undefined.
interface Counter {
n: number;
}
function bump(this: Counter, by: number): number {
this.n += by;
return this.n;
}
const c: Counter = { n: 1 };
console.log(bump.call(c, 2), bump.apply(c, [3])); // 3 6
function kind(this: unknown): string {
return typeof this;
}
console.log(kind(), kind.call("s")); // undefined stringA function with a this parameter that its body doesn't use is called like any other function.
Not supported yet: using a function whose body uses its this parameter as a value (const g = f, { m: f }, xs.map(f)), a function expression whose body uses one, one with overloads, a
generator with one, and this in a function type ((this: T, a: A) => R is (a: A) => R for
now).
Functions with properties
A function can carry properties, as in JavaScript: an interface or object type with one call
signature and members (interface Runner { (): number; stop(): void }, or
(() => number) & { stop(): void }) types such a value. It is called like a function, its
members are read and written like an object's fields, typeof is "function", and it is
accepted where its plain function type or Function is expected. An interface extending a function type
(interface Subscriber extends EffectFn { deps: string[] }) is one too. It is built in one of
two ways:
Object.assign(fn, { … }): typed by the expected type, or without one as TypeScript types it (F & { … }).- A function cast to the type, followed by the writes of its members:
const r = (() => …) as Runner; r.stop = …;. The writes right after the declaration give the members their values (declarations of other functions may come between them, since they run nothing); a required member with no write there is an error (Node would readundefinedbefore the write).
Both work in generic code too, including with a type whose call signature takes a parameter
known only once the type arguments are (((props: Partial<T>) => R) & { name?: string }).
interface Runner {
(): number;
stop(): void;
}
function makeRunner(): Runner {
let count = 0;
let stopped = false;
const runner = (() => {
if (!stopped) count += 1;
return count;
}) as Runner;
runner.stop = () => {
stopped = true;
};
return runner;
}
const r = makeRunner();
r();
r.stop();
console.log(r(), typeof r); // 1 function
const job = Object.assign((n: number) => n * 2, { id: 7 });
console.log(job(21), job.id, Object.keys(job)); // 42 7 [ 'id' ]As in JavaScript, JSON.stringify leaves a function with properties out of an object and
writes null for it in an array; JSON.stringify(f) itself (Node's undefined) is not
supported yet. Object.assign(fn, { … }) onto a variable makes a new object, where JavaScript
adds the properties to fn itself: using fn again after the call (other than in a closure) is
an error; use the call's result.
The value is an object with a fixed set of fields, the function among them: properties the type
does not declare can't be added, and a function whose type has no members has no properties of
its own (const f = () => 1; f.label = "one" is an error).
Held as any, any function takes new properties, as in JavaScript: every reference to the
function (copies of the any, a closure that captured it) sees them, and reads, method calls,
in, Object.keys and delete find them. A property never written reads as undefined.
function component(setup: (n: number) => number, name?: string): any {
const factory: any = function (props: any) {
return { type: factory, props: props || {} };
};
factory.__setup = setup;
factory.__name = name;
return factory;
}
const C = component((n) => n * 2, "C");
console.log(C.__name, C.__setup(21), typeof C); // C 42 function
console.log(C({}).type === C, Object.keys(C)); // true [ '__setup', '__name' ]A declared function is one function, as in JavaScript: every any holding it sees the same
properties and compares === (also as a Map key), in other tasks too, as a module-level let
is one variable (a task that reads or writes its properties takes the module lock, and so does
a with, read or write whose callback may: Async). Any other
function (a function expression, an arrow, a closure) sent to another task (spawn, a channel,
shared) while it is still used here gets a copy of its properties there, as an object gets a
copy of its members (Async), so a write in one task is not seen in
the other. Converted back to a callable interface
(f as Factory with interface Factory { (p: number): number; __name?: string }), the
function's properties fill the interface's members.
Through any, a function's own name and length, and call, apply and bind (a
TypeError); several call signatures next to members (overloads); and
Object.defineProperty onto a function.
call and apply
f.call(thisArg, a, b) and f.apply(thisArg, [a, b]) call a function value as JavaScript's
Function.prototype does: f(a, b), with thisArg evaluated first (after f). apply's
argument spreads into the call as f(...args) does: an array literal or a tuple fills the
parameters one by one, and an array of unknown length only a rest parameter
(String.fromCharCode.apply(null, codes), Math.max.apply(null, xs)), as TypeScript's
strictBindCallApply checks it.
this matters only to a function that reads it. A function declaration with a this
parameter runs on the thisArg. Other function declarations, arrow functions and the
static functions of built-in namespaces read none, so any thisArg works for them
(Math.max.apply(Math, xs)), except Promise's, which take Promise. A method read as a value
(o.m, C.m) keeps the object or class it was read from: o.m.call(o, x) works, and null or
undefined works when the method doesn't use this. A variable holding a method read as a value
(const f = o.m) takes no other this; a const read from an object (const f = (o as T).m,
cast to a function type where it is called: (f as () => R).call(o)) takes that variable o
while nothing assigns it (a const, or a let or parameter never assigned); other function values, such as parameters, take null or
undefined, as f(x) calls them. Any other this is not supported yet (an error): Velt would
run the function on the object it keeps, where Node uses the this given or throws.
f.bind(thisArg) without further arguments follows the same rules and makes a new function
that forwards its arguments to f, which keeps the object it was read from
((res as { dispose: () => void }).dispose.bind(res)); as in JavaScript it is not === to f
or to another bind of it. bind with arguments to fix is not supported yet (write an arrow
function).
function add(a: number, b: number): number {
return a + b;
}
const codes = [104, 105];
const pair: [number, number] = [3, 4];
console.log(String.fromCharCode.apply(null, codes)); // hi
console.log(add.call(null, 1, 2), add.apply(null, pair)); // 3 7
const sum = add.bind(null);
console.log(sum(5, 6)); // 11Captures
A closure passed directly as a call argument, or called immediately, is non-escaping and
captures by reference: xs.forEach((x) => { total += x; }) updates total, as in JS. A
closure that is stored or returned is escaping and captures by value.
function apply(f: (x: i64) => i64, v: i64): i64 {
return f(v);
}
function makeCounter(): () => i64 {
let n = 0;
return () => {
n += 1; // the closure owns `n`
return n;
};
}
function scale(xs: f64[], k: f64 = 2.0): f64[] {
return xs.map((x) => x * k);
}
let total = 0;
[1, 2, 3].forEach((x) => {
total += x; // non-escaping: captures `total` by reference
});
const next = makeCounter();
next();
console.log(total, next(), apply((x) => x * 10, 5), scale([1.5])); // 6 2 50 [ 3 ]Borrowed and owned function parameters
Parameters of function type are borrowed unless the callee keeps them. One that the body
stores, returns, captures in an escaping closure (spawn(async () => cb())), copies or passes
to an async function is owned (inferred like other parameters), and a closure literal
passed to an owned parameter is escaping, so new Holder(() => msg) keeps a closure that stays
valid.
Closures and overridden or interface methods have fixed borrowed parameters; they may still
keep a function they receive, as JS callback registries do
(onCleanup((fn) => (cleanupFn = fn)), subscribe(handler) { this.handlers.push(handler); }).
A program that does so passes such callees only closures that own their environment:
- every closure literal passed to a function value or an interface method escapes (also one that forwards a borrowed function, below);
- a parameter of a named function that is kept, or passed to a function value, an interface
method, an overridden method or a parameter of a generic type, becomes owned, also when a
closure inside the function does it (
untrack(() => topic.subscribe(f))). A generic type may stand for a function type, so a closure literal passed to a parameter of a generic type always escapes, and a method keeping a value of a generic type,put(t: T) { this.items.push(t); }, counts as keeping a function; - a closure may likewise keep a function it captured from a local variable
(
untrack(() => registry.add(onInvalidate))).
The executor of new Promise((resolve, reject) => …) is the exception: its resolve and
reject are handles the compiler creates on the heap (never a caller's closure), so it may
keep them (Async). A spawned or stored closure can't
capture a borrowed function parameter either (same error).
Closures passed to function values
A closure literal passed directly to a function value or an interface method (whose parameters
are fixed, so it may keep them, for example as a generic T) is escaping: it owns a heap
environment.
Unless it captures a borrowed function parameter of the enclosing function, as a middleware
forwarding its next does (inner.handle(req, (r) => next(r))): then it stays non-escaping,
like the parameter it forwards, and is safe as long as the callee only calls it.
A parameter declared with a generic type (keep(x: T), with T a function type) may be kept,
and so may anything passed to a callee that returns a promise the caller does not await right
away (the callee may run after the caller returned): a forwarding closure passed there escapes
like any other. It captures the function it forwards by value, and that parameter of the
enclosing function becomes owned. In a closure, or an overridden or interface method, where that
parameter cannot become owned, it is kept as described
above.
What remains an error (cannot keep a copy of `g`, a borrowed function parameter) is
keeping a function that reached the closure through a destructured or for...of binding of a
borrowed value, or through this.
Closures that are only called
A closure held in a const that is only ever called
(const add = (n: number) => { this.total += n; }; add(1); add(2);) is non-escaping too: it
captures by reference, sees every later change of what it captures, and lives in the frame, so
capturing this does not make the class reference-counted. The compiler proves it:
- the closure's variable is used only as
f(...)(never copied, stored, returned, passed on or captured by another closure); - the enclosing function and the closure are not
asyncor generators; - it captures no
usingvalue and nothing holding a promise; - no closure created inside it captures a variable the enclosing function assigns;
- no call runs while a reference into a captured object is held (the call's own arguments do
not use what it captures, and it is not inside a
for...ofover such an object, amatchon one, or next to an argument borrowing one).
Otherwise it is escaping, as below; the results are the same, only the cost differs.
Borrowing never makes a program an error: where a closure borrowing this (or
const me = this) would conflict with a caller's borrow, such as a for...of over c.items
around a c.clear() that replaces items, the closure and me share instead. The fallback is
for the whole program: one such conflict makes every held closure and every const me = this
in it share, as if none of them borrowed.
Escaping closures
A closure stored in a let, a field or an array, held in a const that is used other than by
calling it (or one that does not meet the conditions above), or returned, is escaping and
captures by value: objects are shared with it (the closure and the enclosing code see the same
object), numbers and strings are copied. A closure passed to push is stored, so it is
escaping too.
A captured object the enclosing code does not use again moves into the closure, so it is released (and disposed) when the closure is, even if other captures are still used afterwards.
A variable that the closure or the enclosing code assigns while the other still uses it
(let count = 0; const incs = [() => { count++; }]; incs[0](); console.log(count)) lives in a
shared, reference-counted cell, so both see every change, as in JS; a closure that is the only
remaining user (a makeCounter returning () => ++n) keeps a plain copy.
A closure created inside another closure that assigns a variable the outer closure captured
(let last = ""; const f = (v: string) => { const g = () => { last = v; }; g(); }) assigns the
variable itself, as in JS: it lives in a cell that the function declaring it and every closure
on the way share. A variable holding a promise cannot live in a cell, so a closure created
inside another closure may not assign one the outer closure captured
(…is assigned by a closure created inside another closure, and cannot be shared with it);
assign it in the function that declares it.
A for (let …) loop's step runs on a fresh binding per iteration, as in JS, so the same error
applies to a variable the loop's head declares; a variable declared in the loop body is one per
iteration and may be assigned. So is a variable a for (let … of …) or for await loop
declares (over an array, a Map, a Set or a generator), as in JS: each iteration gets its own
cell, so the closures the iterations create do not share one
(for (let x of ["a", "b"]) fs.push(() => { x += "!"; return x; }) gives a! and b!). The
same holds for the variables a destructuring let declares, each time it runs.
A closure called on the spot or a callback ([1, 2].forEach(() => { x += "!"; })) that assigns
such a variable changes that variable too, never the array element it was bound from: the array
keeps its contents, as in JS. Such a variable cannot live in a cell when it holds a promise or a
spawned closure also captures it; a closure assigning it then gets cannot assign to `x`, which borrows an array element: copy it into a let declared in the loop body and assign
that.
Async closures
An async closure that stays on the task that created it captures like any other escaping
closure, as in JavaScript: it may change what it captured, the enclosing code may assign the
variables it captured, and every call sees the same objects and variables. Each call shares the
captured objects with the closure (a count increment, no copy), and a variable that the closure
assigns, or that the enclosing code assigns after creating it, lives in a cell. Every call runs
as a started promise on the caller's task, so it interleaves with the rest of the task only at
awaits, never in parallel (Async).
An async closure that may run on another thread copies what it captured for each call (one copy
per call: two reads of a captured object in a call are the same object, === also as an
any). It may not modify a captured variable or object, itself or through a closure it creates
(this async closure modifies captured `n`, so it must stay on the task that created it,
with where it leaves its task); nor may the enclosing code assign one it captured
(cannot assign to `k` after a stored closure captured it), nor a closure created in
another closure
(…is assigned by a closure created inside another closure, and cannot be shared with it).
The compiler proves which closures stay. One may leave:
- when it is spawned (
spawn(async () => …),spawn(f()), an argument of a spawned call), is an HTTP handler, goes intoshared(...)or aMutex, is sent on a channel or settles a promise; - when a parameter it is passed to (a generic one included), a variable holding it, a closure capturing it or a task returning it does;
- when it is stored in an object, array or map whose type reaches one of those places;
- when it is passed directly to a function value or an interface or overridden method, which may keep it.
A timer callback (setTimeout, setInterval) runs on the task that created the timer, so it
stays (it shares its captures, as in Node) unless it leaves another way.
class Ctx {
count: number = 0;
}
// The closure outlives `methods`, changes `ctx` and keeps `calls` in a cell.
function methods(ctx: Ctx): (by: number) => Promise<number> {
let calls: number = 0;
return async (by: number): Promise<number> => {
calls += 1;
ctx.count += by;
await sleep(1);
return ctx.count * 100 + calls;
};
}
const ctx = new Ctx();
const inc = methods(ctx);
console.log(await Promise.all([inc(1), inc(2)]), ctx.count); // [ 302, 302 ] 3A closure using the constant it initializes
A closure created in a const's initializer may use that constant, as in JavaScript: an
object literal whose methods return the object, a callback that stops its own timer, a
function that refers to itself.
interface Counter {
n: number;
inc(): Counter;
}
function makeCounter(): Counter {
const c: Counter = {
n: 0,
inc() {
c.n += 1;
return c; // the object being initialized
},
};
return c;
}
function show(): void {
const fact = (n: number): number => (n <= 1 ? 1 : n * fact(n - 1));
console.log(makeCounter().inc().inc().n, fact(5)); // 2 120
}
show();- An arrow constant that names itself (
factabove), with typed parameters (or a function type annotation giving them,const fib: (n: number) => number = (n) => …), is the functionfunction fact(n: number): number { … }: at module level a module function, in a body a nested function (which may use the enclosing function's variables, see Declarations). Nothing refers to itself, so nothing leaks. An arrow usingthisstays a closure (a function would have athisof its own). - Any other closure (an object literal method, an arrow or function expression) that uses the
constant it is part of captures the variable, which the constant's value fills in once the
initializer has run, and reads it when it runs. Without an annotation, the constant's type
is the initializer's type with the closure checked first; when that type depends on the
constant itself (a method returning it without a written result type), write the type, as
TypeScript requires there too (TS7022, TS7023):
type annotation needed for `c`: a closure in its initializer uses it. - A closure reading the constant while its initializer is still running (it is called from
the initializer) stops the program with JavaScript's error,
ReferenceError: Cannot access 'c' before initialization. A read of the value checks this where it happens, so a read on a path that does not run checks nothing (ready ? x : 1); a use of the object in place (a field read, a method call) checks at the start of its statement (in an expression-bodied arrow, when the arrow is called). A closure created inside such a closure shares the constant too. In Velt this is a panic, whichcatchdoes not intercept. - A closure written before the declaration of a
constorletof its block may use it too, as in JavaScript, where the variable is in scope in its whole block:const effectFn = () => { runJob(); }; const runJob = () => { … };, orqueueMicrotask(() => stop());aboveconst stop = () => { … };. A closure there names the later variable even where an outer function or constant has the same name. The variable is treated as above from the first statement using it to its declaration: closures created in between share it, and one reading it before the declaration ran stops with the sameReferenceError. When the statements in between only create values (closures, literals, objects of those), no closure can run before the declaration, and the program has no such check. Without an annotation, the type is the initializer's, checked as if the closures were not there yet. Not supported yet, an error even where an outer name is the same: a destructuring declaration (const { a } = o;) or aletwithout a value (let x: T;) used so (…which is not supported yet for a destructuring declaration); declare the variable by itself with its value. A class declared in a function body cannot use the function's variables at all, before or after it. - An object holding a closure that captures the object is a reference cycle, which reference
counting does not free: each such object stays allocated until the program exits (one per
constant initialized, for instance one per component created). Break the cycle when the
object is done with: give it a
[Symbol.dispose]()that clears the closure fields (or replaces them with closures that capture nothing) and declare it withusing, or call that method yourself. A recursive arrow constant (above) holds no cycle. - Not supported yet: a closure that runs on another task (a timer callback, a spawned
closure) using the constant it initializes (
const t = setInterval(() => clearInterval(t), 5)reports…is used by a closure in its own initializer, which cannot share it). - A module-level constant read by a closure in its own initializer
(
const INERT: Recorder = { status() { return INERT; } }) is kept in the module state: the closures read the one object once it is initialized, and a read while the initializer still runs stops the program withUncaught ReferenceError: Cannot access 'INERT' before initialization(exit code 1). Without an annotation, its type is the initializer's, checked with those reads first (const Level = component<P>(() => … Level(…) …)); when that type depends on the constant itself, write it, as above.