Control flow
Statements
if/else if/else,while,do … while, C-stylefor(comma lists allowed:for (let i = 0, j = n; i < j; i++, j--)),for (const x of xs),for (const k in o),for await (const x of xs)in async code,breakandcontinue(optionally labeled:outer: for (…)…continue outer;),return, blocks, and the ternary?:. A body without braces (if (c) return x;) is a one-statement block.- Conditions take values of any type and test them as JavaScript does:
0,NaN,""andnullare falsy (truthiness).
for...of
for...ofiterates arrays, maps ([key, value]pairs), strings (their characters, as in JS), classes with anentries()method, and iterables: values with a[Symbol.iterator]()method returning anIterator<T>(see below).- The loop variable is each element itself (objects are references): you can call its methods
(including modifying ones), assign its fields, and store it elsewhere, which shares the
element. Index the array to replace an element: the loop variable keeps the element it was
given until the iteration ends, as in JS (
for (const v of vs) { vs[0] = new V(); v.n }reads the oldvs[0]). Such a loop takes a reference to each element it visits; a loop whose body does not replace elements takes none. Changing the iterated array itself in the body (push,pop,splice,length = …, assigning the variable, or passing it to a call that may change it) is a compile error (cannot modify `xs` while a `for...of` loop iterates over `xs`): loop over indexes instead. - Iterating a temporary (a call result,
await …, a literal) consumes it: each element is handed to the loop variable without a count.
for...in
for (const k in o) visits the keys of an object, as in JavaScript: the same keys, in the same
order, as Object.keys(o), and it is that loop
(for (const k of Object.keys(o)), with the built-in Object even where a local one hides
it). k is a string.
ois an object type, a struct or class instance or aRecord. A record's keys come in insertion order; an object's fields in declaration order, a base class's first. An optional field (label?: string) is visited only while it is present. Methods are not keys.- The keys are listed when the loop starts: a key added during the loop is not visited. A key
of a
Record<string, V>deleted during the loop is not visited either, as in JavaScript, whenois a name or a property of one (r,this.scores). let kmakes the variable assignable in the body; the loop variable must be declared in the head (for (k in o)with an outerkis an error, as forfor...of).- For a
Map, iteratem.keys()withfor...of;for...inover a number or string is an error (TypeScript's TS2407).
interface Point {
x: number;
y: number;
label?: string;
}
const p: Point = { x: 1, y: 2 };
for (const k in p) {
console.log(k); // x, then y
}Iterables
The prelude declares TypeScript's iteration protocol; a finished result has no value:
type IteratorResult<T> = { value: T; done: false } | { done: true };
interface Iterator<T, E = never> {
next(): IteratorResult<T> throws E;
return(): IteratorResult<T> { // early exit; the default does nothing else
return { done: true };
}
}
interface Iterable<T, E = never> {
[Symbol.iterator](): Iterator<T, E>;
}for (const x of src)callssrc[Symbol.iterator]()once, thennext()until a result isdone, binding eachvalue(owned by the loop variable).srcmay be any type with that method, a class declaringimplements Iterable<T>or not, or anIterable<T>value.- A finished result has no
value(TypeScript's hasvalue: undefined);if (r.done)narrowsrlike any discriminated union. Readingr.valuewithout narrowing givesT | null:nullwhenris done (TS:undefined), sog().next().valueworks. Destructured intoconst { value, done } = r, a test ofdone(if (done) break;,if (!done),done === false) narrowsvaluetoTas well, as in TypeScript (whenTitself cannot benull). TypeScript's{ value: undefined, done: true }is accepted for a finished result, without thevalue.console.logof a finished result prints{ done: true }where Node prints{ value: undefined, done: true }. return()returns anIteratorResult<T>, as in TS ({ done: true }; the loop ignores it). An iterator class that releases something there writesreturn(): IteratorResult<T>and returns{ done: true }, which also works in Node (where areturn()returning nothing makesbreakthrow aTypeError). TypeScript'sit.return(undefined)(itsreturn(value)takes the generator's return value) callsreturn(); another argument is an error, since a finished result carries no value.- Typed errors:
Eis whatnext()throws (never, the default, means nothing). The loop throws it, so a function iterating anIterable<T, IoError>throwsIoError; a genericfunction sum<E>(xs: Iterable<i64, E>): i64 throws Ethrows what its argument does.
Early exit
Leaving the loop before done (break, return, a thrown error, or a
labeled break / continue of an outer loop) calls the iterator's return() exactly once,
as in JS, so an iterator holding a resource can release it. Running to the end, continue,
and an error thrown by next() itself do not call it.
- An iterator is not itself iterable: iterate the iterable that creates it (as in TS, where
for...ofneeds[Symbol.iterator]()). - A generator (
function*) is the short way to write an iterable:for (const x of gen(a))over a direct call needs no iterator object at all, and a class whose[Symbol.iterator]is a generator method (*[Symbol.iterator]()) is iterable without an iterator class (so is a type whoseextendblock has one: it becomes anIterable<T>). Leaving the loop early closes the generator (itsfinallyblocks run). AsyncIterator<T, E>(next(): Promise<IteratorResult<T>, E>) andAsyncIterable<T, E>([Symbol.asyncIterator]()) are their async counterparts, iterated withfor await.
Builtin iterables
Arrays, strings, Maps and Sets (velt/collections) are
Iterable<T> (Iterable<[K, V]> for a map, Iterable<string> for a string), so they convert
to an Iterable<T> value and satisfy an Iterable<T> bound: sum(xs: Iterable<number>)
takes [1, 2, 3], a set, m.values() or (with Iterable<string>) a string. An array
literal written where an Iterable<T> is expected holds Ts. for...of over them directly
keeps its own loops; only code written against Iterable<T> goes through the protocol.
Converting an array to an Iterable<T> value copies no elements (the value refers to the
array; at most its 24-byte header is boxed), and each loop over it creates one iterator.
x[Symbol.iterator]()on them returns anIterator<T>(TS:ArrayIterator<T>and so on; the prelude classesArrayIterator<T>andStringIteratorare its implementations). An array's iterator is a live view, as in JS: it reads the length at each step, so it visits elements pushed meanwhile, and once it reporteddoneit stays done. A string's yields characters (code points). A map's and a set's iterate the entries as of the call (JS's map and set iterators are live views).for...ofover a map, or over itskeys(),values()orentries(), is live as in JS: entries the body adds are visited, deleted ones are not. That holds for a map in a variable, a field, a getter, an array element or a call result (for (const [k, v] of ms[0])). The map is read once, when the loop starts: assigning another map to its source in the body does not change what the loop visits.IterableIterator<T, E>,IteratorObject<T, E>andAsyncIterableIterator<T, E>are TS's iterators that are also iterable ([Symbol.iterator]()returns the iterator itself, declared asIterator<T, E>, since Velt has no covariant returns). Generators implement them and may be declared to return them (function* f(): IterableIterator<number>), and such a value converts to anIterable<T, E>/AsyncIterable<T, E>. It does not convert to anIterator<T, E>yet (interface values don't convert to the interfaces they extend); callit[Symbol.iterator]()for one.- TS's two-argument
IteratorResult<T, TReturn>isIteratorResult<T>whenTReturnmeans "nothing" (void,undefined,unknown,any); anotherTReturnis an error, since a finished result carries no value. The second argument ofIterableIterator,IteratorObjectandAsyncIterableIteratorfollowsGenerator's rules: it is dropped when it means "nothing", and is the error typeEotherwise.
class Countdown implements Iterator<i64> {
n: i64;
constructor(n: i64) {
this.n = n;
}
next(): IteratorResult<i64> {
if (this.n == 0) {
return { done: true };
}
this.n -= 1;
return { value: this.n + 1, done: false };
}
return(): IteratorResult<i64> {
console.log(`stopped at ${this.n}`);
return { done: true };
}
}
class From implements Iterable<i64> {
start: i64;
constructor(start: i64) {
this.start = start;
}
[Symbol.iterator](): Iterator<i64> {
return new Countdown(this.start);
}
}
for (const n of new From(3)) {
console.log(n); // 3 2 1
}
for (const n of new From(5)) {
if (n == 4) {
break; // prints "stopped at 3"
}
}
function total(xs: Iterable<number>): number {
let sum = 0.0;
for (const x of xs) {
sum += x;
}
return sum;
}
const prices = new Map<string, number>([["tea", 2.5], ["cake", 4.0]]);
console.log(total([1, 2, 3]), total(prices.values())); // 6 6.5
const it = ["a", "b"][Symbol.iterator]();
console.log(it.next()); // { value: 'a', done: false }Consuming an iterable
Everything that takes the values of an array takes whatever for...of takes: a string's
characters, a Map's entries (and those of a class with entries()), a Set's elements, a
generator, any iterable. It uses the same loop as for...of (so a direct generator call needs
no generator object, and stopping early closes the iterator):
- Spread:
[...gen()],[0, ...it, 9], and spread arguments of a rest parameter (sum(...gen()),Math.max(...values())). The values are taken in order, where the spread stands. Array.from(src)andArray.from(src, (value, i) => ...)over anythingfor...oftakes; the callback runs as each value arrives.
Array destructuring
const [a, b] = gen() takes only the values the pattern needs, then
closes the iterator (return(), a generator's finally) if it has more, as JS does;
...rest takes the remaining values, and a default applies when the iterable ended early
([x = 0]). Also in for...of heads: for (const [a, b] of rows()) over iterables of
iterables, and nested: const [[a, b], [c]] = [gen(), gen()], for (const [k, [x, y]] of map). A nested pattern takes its values after the outer pattern has taken all of its own
(JS goes element by element; only an outer generator with effects can tell).
new Map(iterable)of[key, value]pairs andnew Set(iterable)(velt/collections).- An object literal with a
*[Symbol.iterator]()method is an iterable (Iterable object literals).
function* countTo(n: number): Generator<number> {
for (let i = 1; i <= n; i++) {
yield i;
}
}
function* squares(n: number): Generator<[number, number]> {
for (const v of countTo(n)) {
yield [v, v * v];
}
}
function sum(...xs: number[]): number {
let total = 0;
for (const x of xs) {
total += x;
}
return total;
}
console.log([0, ...countTo(3)]); // [ 0, 1, 2, 3 ]
console.log(sum(...countTo(4))); // 10
console.log(Array.from(countTo(3), (v, i) => v * 10 + i)); // [ 10, 21, 32 ]
const [first, second] = countTo(100); // takes two values, then closes it
console.log(first, second); // 1 2
const bySide = new Map(squares(3));
console.log(bySide.get(3)); // 9
console.log([...bySide.keys()], [..."héllo"]); // [ 1, 2, 3 ] [ 'h', 'é', 'l', 'l', 'o' ]
const [head, ...tail] = "abc";
console.log(head, tail, Math.max(...bySide.values())); // a [ 'b', 'c' ] 9Arrays keep their own (faster) spread and destructuring: a spread array is copied with one
allocation, and destructuring an array reads its elements by index. A string or a Map is
consumed as the array of its characters or entries, as for...of iterates it.
for await
interface AsyncIterator<T, E = never> {
next(): Promise<IteratorResult<T>, E>;
async return(): Promise<IteratorResult<T>> { // early exit; the default does nothing else
return { done: true };
}
}
interface AsyncIterable<T, E = never> {
[Symbol.asyncIterator](): AsyncIterator<T, E>;
}for await (const x of src)is allowed in async functions, async generators and a script's top-level statements (whose generatedmainis thenasync, Scripts); elsewhere it is an error that names the fix. It callssrc[Symbol.asyncIterator]()once, then awaitsnext()until a result isdone.srcmay be any type with that method (a class, anAsyncIterable<T>value, an async generator).- Typed errors: the loop rethrows what
next()rejects with (E), likefor...of. - Early exit: leaving the loop before
done(break,return, a thrown error, a labeledbreak/continueof an outer loop) awaits the iterator'sreturn()exactly once, as in JS; for an async generator that runs itsfinallyblocks, which mayawaitthemselves. - Over a sync source (an array, an iterable, a generator)
for awaitworks like JS too: each value that is a promise is awaited (for await (const v of [load(a), load(b)])), other values are used as they are. An array of promises is consumed: its promises move into the loop, so a variable holding the array cannot be used after it. for await (const x of agen(a))over a direct async generator call keeps the generator's state inside the enclosing async function's: no allocation (see Cost).
class Ticks implements AsyncIterable<i64> {
n: i64;
constructor(n: i64) {
this.n = n;
}
[Symbol.asyncIterator](): AsyncIterator<i64> {
return new TickIter(this.n);
}
}
class TickIter implements AsyncIterator<i64> {
left: i64;
constructor(left: i64) {
this.left = left;
}
async next(): Promise<IteratorResult<i64>> {
await sleep(1);
if (this.left == 0) {
return { done: true };
}
this.left -= 1;
return { value: this.left, done: false };
}
}
for await (const t of new Ticks(3)) {
console.log(t); // 2 1 0
}switch
switch has JavaScript semantics: the discriminant is evaluated once; the first case whose
value equals it is entered (default when none does, wherever it is written); bodies fall
through until break, return, throw or continue. continue cannot target a switch; a
labeled switch can be left with break label from nested loops. Each case body is its own
block scope.
- Case values: see Case values below.
Narrowing
switch (x.kind) on a discriminated union narrows x in each case (a case
reached by fallthrough sees the union of the members that can get there); switch (typeof x)
narrows like typeof tests; switch (x) on a union narrows by literal member and by
case null. Case values that are not literals narrow nothing.
Coverage
As in TypeScript, a switch need not have a case for every member. The members
no case takes skip it: default and, without default, the code after the switch see the
value narrowed to them. A switch with a case for every member of a discriminant, a
typeof, a union of literals or an enum needs no default and no code after it. With no
member left in default, the value is never, so const _x: never = s; checks exhaustiveness
the TypeScript way.
- Performance: cases on tags, enums and integers dispatch through one jump table; string cases compare in order.
enum Level { Debug, Info, Warn }
function describe(n: i64): string {
let s = "";
switch (n) {
case 1:
s += "one "; // falls through
case 2:
s += "two";
break;
default:
s = "other";
}
return s;
}
function tag(l: Level): string {
switch (l) {
case Level.Debug:
return "D";
case Level.Info:
return "I";
case Level.Warn:
return "W";
}
}
type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number } | { kind: "dot" };
function size(s: Shape): number {
switch (s.kind) {
case "circle":
return s.r * 2;
case "square":
return s.side;
}
return 0; // s is { kind: "dot" } here
}
function label(x: string | null): string {
switch (x) {
case null:
return "none";
default:
return x; // x is a string here
}
}
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j == 1) {
continue outer;
}
console.log(i, j, describe(i), tag(Level.Info), label(null), size({ kind: "dot" }));
}
}Case values
Case values are literals (numbers, also negative ones; strings; bools), null, enum members, or
any expression that === can compare with the discriminant (and compared like it: a
string | null case value on a string discriminant, or the reverse; a string constant on a
union of string literals; a string on a discriminant s.kind or on typeof x). A value may
appear in several cases: the first one wins.
A constant of a literal type acts like the literal: it narrows and counts toward covering the
members. As in TypeScript, that is a const (local or module-level) initialized with a literal
and without a type annotation (const A = "a"), or a local whose type is a literal type
(const y: "y" = "y"):
const A = "a";
function code(k: "a" | "b"): number {
switch (k) {
case A:
return 1;
case "b":
return 2;
}
}A literal that is not a possible value (case "square": when s.kind is "circle" | "rect") is
an error, as in TypeScript. For typeof cases:
- A
typeofcase that no member of a union has is an error (TypeScript accepts it). - On a type that fixes the
typeofof its values (a number, string, boolean, function, class, array, ...), also a union local that flow narrowing reduced to one member (let u: number | string = 5), such a case is accepted, as in TypeScript, and never matches. - On
any,unknownandobject, cases test the run-timetypeof. - On a type parameter, a case its type doesn't fix is an error.