The prelude
The prelude is the part of the standard library that every module sees without an import. It
lives in std/prelude/*.vlt; some of it (arrays' push/pop, length, clone, spawn,
shared) is implemented by the compiler.
Node's web globals need no import either: fetch, Request, Response, Headers, Blob,
File and FormData (fetch), URL, URLSearchParams, encodeURIComponent, encodeURI, decodeURIComponent and decodeURI (velt/net/url), AbortController and
AbortSignal (velt/task), Set (velt/collections),
RegExp (velt/text; a regex literal counts as naming it), TextEncoder,
TextDecoder, btoa and atob (velt/encoding), StringBuilder
(velt/text), Symbol (Symbol), JSON (JSON), DOMException
(Errors), ArrayBuffer, DataView, Uint8Array and the other typed arrays
(typed arrays), and ReadableStream and its reader and controller
(streams), and Proxy, Reflect, ProxyHandler and PropertyDescriptor
(Proxy and Reflect). They are loaded when a module names one and
doesn't bind that name itself. structuredClone(x) is a deep copy (x.clone()); a value that
JS can't clone (a function, at any depth) or would copy as a plain object (an instance of a
class of your own: write x.clone()) is a compile error. The builtin process
(process.stdout.write(s), process.env, process.argv, process.exit(code),
process.memoryUsage(); velt/process) needs no import either.
Every export and public member in std/prelude has a doc comment, which velt doc --std shows
under its signature. Where TypeScript has the same function, the text is adapted from the JSDoc
of TypeScript's lib.*.d.ts (Apache-2.0, see NOTICE) and edited for Velt's differences; this
page lists those differences.
Strings
string is an immutable sequence of UTF-16 code units, as in JS
(Types): lengths and positions count code units (an emoji is
two), and a negative position counts from the end. Buffer.byteLength(s) is the size in UTF-8.
| Method | Notes |
|---|---|
length | code units |
charAt(i = 0), s[i] | the code unit at i as a string, or "" (JS: s[i] past the end is undefined) |
at(i): string | undefined | like charAt; a negative i counts from the end, undefined past the end |
slice(start = 0, end?), substring(start, end?) | |
indexOf(s, from = 0), lastIndexOf(s, from?), includes(s) | -1 when absent |
startsWith(s), endsWith(s) | |
split(sep): string[] | split("") gives the code units (a pair splits into two halves); for (const c of s) and [...s] give characters |
trim(), trimStart(), trimEnd() | |
toUpperCase(), toLowerCase() | |
replace(from, to), replaceAll(from, to) | from is plain text, or a RegExp (as in match, matchAll, search and split with a regex: velt/text) |
repeat(n), padStart(n, fill = " "), padEnd(n, fill = " ") | repeat panics on a negative n (JS's RangeError); the pads fill up to n code units |
charCodeAt(i = 0) | the code unit at i (one half of a pair for an emoji); -1 out of range (JS: NaN) |
codePointAt(i = 0): number | undefined | the character at i when i is at the high half of a pair, otherwise the code unit there; undefined out of range, as in JS |
localeCompare(t): i64 | -1, 0 or 1 in the CLDR root collation, like new Intl.Collator("und").compare(s, t) ("a" < "A" < "b", "e" < "é" < "f"; Node's own localeCompare uses the host's locale). Exact for strings made of U+0020..U+024F, U+0370..U+04FF, U+1E00..U+1EFF, U+2000..U+206F and U+20A0..U+20CF (Latin with Vietnamese, Greek, Cyrillic, general punctuation, currency signs), except a few characters that stand for three or more (¼, ½, ¾, ϗ); approximate for everything else. No locale or options arguments |
<, > and sort() order strings by code units, as JS. A position between the two halves of
a pair is allowed everywhere: "😀".slice(0, 1) is a lone surrogate, which output writes as
U+FFFD, and the searches can match half of a pair ("😀".indexOf(lo) is 1 when lo is the low
half).
Conversions: String.fromCharCode(...codes) (any number of code units, each taken modulo
2^16 as in JS, with NaN and the infinities 0; a lone surrogate gives a lone surrogate, a
surrogate pair its character), String.fromCodePoint(...codePoints) (one or two code units
each; a code point that is not an integer from 0 to 0x10FFFF panics with JS's RangeError,
Invalid code point 1114112),
parseInt(s, radix = 0) and parseFloat(s) (both return f64, NaN on failure),
Number(s), and String(x), which writes any printable value as `${x}` does
(String(1e21) is "1e+21", String(null) is "null", String([[1], [2, 3]]) is "1,2,3").
Numbers
-
NaN,Infinity,isNaN(x),isFinite(x). -
Number(x)converts a string, a number or a boolean (Number(true)is1), andBoolean(x)tests a value's truthiness, as in JS;Number.isInteger(x),Number.isNaN(x),Number.isFinite(x),Number.isSafeInteger(x),Number.parseInt(s, radix = 0),Number.parseFloat(s)and the constantsNumber.MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,EPSILON,MAX_VALUE,MIN_VALUE,NaN,POSITIVE_INFINITY,NEGATIVE_INFINITYare JS's, onf64(they live in the prelude classNumberConstructor, TypeScript's name for the type ofNumber). -
x.toFixed(digits = 0),x.toExponential(digits?)andx.toPrecision(precision?)onf64, with JS's output and rounding (the nearest, an exact tie away from zero):tsconsole.log((123.456).toExponential(2), (0).toExponential()); // 1.23e+2 0e+0 console.log((123.456).toPrecision(4), (0.000123).toPrecision(2)); // 123.5 0.00012A digit count is truncated as in JS (
toExponential(2.7)istoExponential(2)); one out of range (toFixedandtoExponential: 0 to 100,toPrecision: 1 to 100) panics like JS'sRangeError. -
x.toString(radix)on any number writes it in baseradix(2 to 36) with Node's digits, fractions and the sign included;NaNand ±Infinity print as themselves. The radix is truncated as in JS, and one out of range panics like JS'sRangeError. On an integer type (i64,u64, …) the digits are exact over the whole range:tsconsole.log((255).toString(16).padStart(6, "0"), (-0.5).toString(2), (3.75).toString(16)); // 0000ff -0.1 3.c -
Math:PI,E,sqrt floor ceil round trunc abs sign pow,max,minandhypot(any number of values, spreads included:Math.max(...xs)), andrandom()(uniform in[0, 1), not for secrets). On integer operands,Math.trunc(a / b)is integer division.imul(the 32-bit wrapping product, one multiply instruction) andclz32(leading zero bits) take the low 32 bits of their operands like JS.umulh(a, b)(not in JS) is the high 64 bits of the 128-bit product of twou64s, one multiply instruction as well. -
Bit casts (not in JS):
x.toBits()is the IEEE 754 bits of anf64as au64(sign, exponent, fraction), andf64.fromBits(b)thef64with the bits of au64(Rust'sto_bits/from_bits); each is one move between registers. They keep every bit pattern, NaN payloads and signalling NaNs included, sof64.fromBits(b).toBits() == bfor everyb. The bits of a NaN made by arithmetic (0 / 0) or of theNaNconstant are not specified: they depend on the processor (fff8000000000000on x86-64,7ff8000000000000on AArch64), as in Rust.f32has no bit casts yet.tsconst x: f64 = -1.5; console.log(x.toBits().toString(16)); // bff8000000000000 console.log(f64.fromBits(0x7ff0000000000000)); // Infinity -
Integers (not in JS):
x.leadingZeros(),x.trailingZeros()andx.countOnes(), counted over the type's own width ((0 as u8).leadingZeros()is 8,(-1 as i32).countOnes()is 32), one instruction each. They return ausize, in user code too, so the result indexes an array as it is:tsconst bits: u64 = 40; console.log(bits.leadingZeros(), bits.trailingZeros(), bits.countOnes()); // 58 3 2 -
Every number type implements
Comparable(Comparable).
Arrays
T[] is a growable array (Types).
Callback methods rethrow what their callback throws. A callback may change the array through
another reference to it (const ys = xs, or an object holding it), and the element it received
stays valid however the array changes. As in JS, the methods read the length once at the start,
so elements pushed meanwhile are not visited. Elements removed meanwhile:
forEach,filter,reduce,someandeveryskip them, as JS does;find,findIndex,findLastandfindLastIndexskip them too, where JS calls the callback withundefinedfor each missing index (aTcannot beundefined);mappanics with "the array shrank whilemapran", where JS returns an array with holes;filterandfinddo not return the element the callback was given when the callback removed it, where JS does.
| Method | Notes |
|---|---|
length, push(x), pop(): T | undefined | built in; undefined when empty |
at(i): T | undefined | a negative i counts from the end; undefined out of range |
forEach, map, filter, reduce(f, init) | callbacks get (x, i) (reduce: (acc, x, i)) and may take fewer |
find, findIndex, findLast, findLastIndex, some, every | likewise |
indexOf, lastIndexOf, includes | compare with == (objects by identity, like JS's ===), so indexOf never finds NaN; includes also finds the number NaN, as in JS (SameValueZero; tuples still compare part by part) |
slice(start = 0, end?), concat(other) | |
reverse(), fill(v, start?, end?), sort() | in place, returning nothing (JS returns the array: returning it would share it, which makes every array of its type reference counted). A member access chained on the call reads the changed array, as in JS: xs.sort().join(","), xs.reverse()[0], s.split(",").sort().map(f). Such a chain changes the array, so it can't be an argument of a call that also reads the array (console.log(xs.sort().join(), xs) is an error, like any call that changes an argument another one reads): sort on its own line first. On a temporary array, which nothing else holds, the result is that array, as in JS: const keys = Object.keys(r).sort(), return [...xs].sort(cmp). Any other use of the result on a variable, field or element (return xs.sort(), const ys = xs.sort(), f(xs.sort())) is an error: call the method on its own line and then use xs, or use toSorted() / toReversed() for a changed copy |
toSorted(cmp?), toReversed(), toSpliced(start, deleteCount?, ...items), with(i, v) | ES2023's copying forms: a new array, the receiver unchanged (the elements themselves are shared, as in JS); toSorted() without a comparator orders like sort(); with panics on an index out of range (JS's RangeError) |
splice(start, deleteCount?, ...items): T[] | removes and returns deleteCount elements (the rest when omitted) and inserts items there |
shift(): T | undefined, unshift(...items): i64 | take the first element (undefined when empty) / insert items at the front and return the new length, like JS; both O(length), as in V8 for large arrays: a queue that takes from the front belongs in a Deque (velt/collections) |
swap(i, j), rotateLeft(start, end), rotateRight(start, end) | not in JS: exchange two elements, or move the elements of [start, end) one place down (the first to the end) or up (the last to the front), in place; each element's bits move, nothing is copied or dropped. A RangeError panic for an index or range outside the array |
prefetch(i), reserve(n) | not in JS: a hint that element i is read soon, so the CPU starts loading it (it changes nothing; an index past the end does nothing); room for n more elements without growing again |
xss.swapIn(a, i, b, j) | not in JS, on T[][]: exchanges xss[a][i] and xss[b][j], also between two inner arrays, which [x, y] = [y, x] does by copying (and counting) both elements. For data kept in chunks |
xs.length = n, truncate(n) | drop the elements from n on, as in JS (xs.length = 0 empties the array; xs.length -= k works too). n is a number: one that is not a whole number from 0 to 2^32 - 1 (negative, fractional, NaN) panics with RangeError: Invalid array length, the error JS throws. Setting a larger length panics too: JS would add empty slots, which Velt has no value for (push the elements, or build the array with new Array<T>(n).fill(v)); truncate(n) leaves the array unchanged instead |
flat() | on T[][]: the inner elements, one level deep |
isEmpty(), entries(): [usize, T][] | the index is a JS number, like length |
join(sep = ","), toString() | as JS: strings, numbers and booleans as String(x), inner arrays joined with "," at any depth, null elements as empty text, class instances through their toString() (a Date too), other objects as [object Object], maps as [object Map] and sets as [object Set]; toString() is join(","), what ${xs} writes. Elements JS writes with a method Velt cannot call there (a struct's toString(), an Error, a RegExp) are a compile error: write xs.map((x) => x.toString()).join(sep) |
sort(), sort(cmp) | sort() on i64, i32, u64, usize, f64 and string elements, ascending with NaN last (unstable, pdqsort); other element types need a comparator; sort(cmp) is stable on any element type; the comparator returns a number (an i64 on arrays of declared integers), and an arrow comparator may return an integer of any type; a comparator that reaches the array through an alias (const ys = xs) sees it unchanged while it runs when the elements are numbers, booleans or plain structs (as in JS), and empty for strings, arrays and objects |
new Array<T>(n).fill(v), Array.from({ length: n }, (_, i) => f(i)) | n elements in one allocation; the callback's first argument is always undefined, so it is named _… or left out with the index (() => 0) |
Array.from(src), Array.from(src, (v, i) => f(v, i)) | the values of anything for...of takes (an array, a string's characters, a map's entries, a generator, an iterable), mapped as they arrive |
Byte arrays are plain u8[] with faster versions of indexOf, lastIndexOf, includes,
fill, slice and concat, plus set(src, offset), copyWithin(target, start, end) and
copy(target, targetStart, sourceStart, sourceEnd) (a range into another array, returning the
number of bytes copied) like Node's Buffer, and xorMask(mask: u32, start?, end?), which XORs the bytes with the four bytes
of mask (most significant first) repeating from start: WebSocket masking (RFC 6455 §5.3).
Text goes in and out of byte arrays without intermediate copies, as a server written in Velt
needs on its read and write buffers: write(s, offset = 0) writes the UTF-8 bytes of s and
returns how many it wrote (Node's buf.write: only whole characters that fit, the array does
not grow), utf8Slice(start?, end?) decodes a range (Node's buf.toString("utf8", start, end),
an invalid sequence as U+FFFD), and utf8Equals(s, start?, end?) /
utf8EqualsIgnoreCase(s, start?, end?) (Velt) tell whether a range holds the UTF-8 bytes of s,
the second with ASCII letters in any case (a method, a path, a header name), without making a
string. Their ranges are clamped as toString clamps them: a negative start is 0, an end
past the array is its end (Node's buf.utf8Slice throws for both instead). write and the comparisons are Velt
code in the prelude: they read the string's bytes where it keeps them, sixteen at a time with
SIMD vectors, and are compiled into their caller.
subarray(begin?, end?) of a u8[] (Node's buf.subarray) is a
Uint8Array over the array's bytes, sharing them: writes through either are
seen by both, and its buffer is the array. It is the array plus a range, so it is valid
whatever happens to the array or to the variable that held it: kept in a field, returned,
captured by a closure or held across an await, it keeps the array alive (a program that keeps
such a view counts its byte arrays' references; one that doesn't pays nothing), and it sees the
array after it grows. A Node Buffer cannot become shorter; a u8[] can (length = 0, pop,
splice), and then the view (and any typed array over its buffer) has the elements the array
still has in its range: its length counts those, xs[i] past them panics like any index, a
write past them does nothing (as one past a view's end in JS), and a method that needs the
missing elements (join, fill, …) panics. slice, subarray, with, toReversed and
toSorted take the elements that are left, as they take length (a subarray of a view of
which not even the start is left is empty, with a buffer of its own). Growing the array back
brings the elements back. The view's buffer follows the array's length (byteLength), though
its resizable is false and its maxByteLength is its byteLength at each read. Each
subarray of a u8[] has a buffer object of its own over the same array: they share the
bytes, but unlike Node, where b.subarray(0, 8).buffer === b.subarray(4, 12).buffer, they are
different objects (!==).
const buf: u8[] = Buffer.alloc(8);
buf.write("abcdefgh");
const head = buf.subarray(0, 4);
head[0] = 65;
console.log(buf.utf8Slice(0, 4), head.length); // Abcd 4
buf.length = 2;
console.log(head.length); // 2const buf: u8[] = Buffer.alloc(16);
const n = buf.write("GET /");
console.log(buf.utf8Equals("GET", 0, 3), buf.utf8Slice(0, n)); // true GET /Buffer.alloc(n) creates n zero bytes, and Buffer.allocUnsafe(n), as in Node, n bytes whose
values are whatever the memory held: for a buffer the program fills before it reads it (a
decoder's output), which zeroing would write twice. Buffer.byteLength(s, encoding = "utf8") is the
number of bytes s takes, as in Node: UTF-8 by default (O(1); a lone surrogate counts the 3
bytes of U+FFFD), two per code unit for utf16le/ucs2, one for latin1/binary/ascii,
the decoded size for base64/base64url and hex.
Map
Map<K, V> is an insertion-ordered hash map. Keys compare as in JavaScript (SameValueZero):
numbers, bool and strings by value, and objects (class instances, object types, arrays,
maps, records, interface and function values) by identity, as === compares them. An
object literal finds only itself: an equal object is another key, and changing an object after
using it as a key keeps its entry where it is. A copy ({ ...key }, key.clone()) is another
object, so it is another key.
A type declared with struct is a Velt value type: its keys compare and hash by content, field
by field, so an equal struct finds the entry (Map<Cell, number> counts each cell once). Each
field compares as a key does: numbers, strings and nested structs by value, objects (an array
field, a class instance) by identity. == still compares structs by identity.
struct Cell {
x: number;
y: number;
}
const visits = new Map<Cell, number>();
visits.set({ x: 1, y: 2 }, 1);
visits.set({ x: 1, y: 2 }, 2);
console.log(visits.size, visits.get({ x: 1, y: 2 })); // 1 2Float keys compare like JavaScript's: 0 and -0 are one key, and NaN is a key that finds
itself (m.get(NaN)). == keeps IEEE comparison (NaN == NaN is false).
const key: i64[] = [1];
const m = new Map<i64[], string>();
m.set(key, "one");
key.push(2);
console.log(m.get(key), m.has([1]), m.has([1, 2]), m.size); // one false false 1One difference from JavaScript's keys:
- Tuples compare part by part, as
===compares them:[1, "x"]finds an entry set with another[1, "x"](in JavaScript a tuple is an array, another key). A tuple key is hashed when it is inserted, so don't change one while it is in a map.
| Member | Notes |
|---|---|
new Map<K, V>(), new Map(entries: [K, V][]), new Map(iterable), size, clear() | new Map(entries) leaves entries as it is and shares their keys and values, like JS; a repeated key keeps its first position and its last value. Any iterable of [K, V] pairs (a generator, another map) works too |
set(k, v), get(k): V | undefined, has(k), delete(k): bool | get returns the stored value itself, as in JS, and undefined for a missing key. Where the value type includes null (Map<string, number | null>), get is a number | null | undefined: an entry holding null reads null, a missing key undefined. A value type that includes undefined (Map<string, number | undefined>) is not supported yet |
upsert(k, init, (v) => v + 1) | insert init or replace the value with the callback's result, in one lookup |
update(k, (v) => { … }): bool | modify the stored value in place; false when k is absent |
getOrInsert(k, () => v) | |
getUtf8(bytes: u8[]), hasUtf8(bytes), setUtf8(bytes, v) (Map<string, V> only, Velt) | get, has and set of the key whose UTF-8 bytes are bytes, without making the string: a server finds the keys it reads from the network as they came. The same entries as with the bytes decoded (utf8DecodeLossy: an invalid sequence reads as U+FFFD); setUtf8 makes the key string only for a new entry |
keys(), values(), entries(), forEach((v, k) => …) | in insertion order; the first three return arrays (JS: iterators). forEach is live, as in JS: it visits entries its callback adds and skips the ones it deletes |
[Symbol.iterator](): Iterator<[K, V]> | a map is an Iterable<[K, V]>; the iterator visits the entries as of the call (JS's is a live view) |
for (const [k, v] of map) | live, as in JS: so is for...of over map.keys(), map.values() and map.entries() when map is a variable, this, a field, a getter, an array element or a call result (ms[0], obj.m, get()!; a map expression with an optional chain, a spread or a function expression iterates the array of entries as of the loop's start). The loop walks the map object map named when it started, so assigning another map to map in the body does not change what it visits, as in JS |
A callback of forEach, upsert, update or getOrInsert may change the map through another
reference to it: the value the callback gets stays valid, forEach visits entries added
meanwhile, and upsert and getOrInsert store their result under the key even when the
callback deleted or added entries.
WeakMap, WeakSet and WeakRef
WeakMap<K, V>, WeakSet<T> and WeakRef<T> refer to objects without keeping them alive, as
in JavaScript: when nothing else refers to a key, its entries go (their values are released),
and a WeakRef to it returns undefined from then on. Keys, members and targets are objects: class
instances, object types and arrays, compared by identity (a primitive is an error, as in
TypeScript: type `string` does not satisfy the constraint `object` ; unions, tuples,
structs and function values are not supported as keys yet). An object key
(WeakMap<object, V>, as reactive libraries write their caches) is the object it holds
(object and Function), and so is an interface
value (WeakMap<Instance, V>): the class instance behind it, whichever interface or type it is
used through; deref() of a WeakRef<object> returns undefined once the target was freed, as
in JavaScript. Like
JavaScript's, the maps are not iterable and have no size.
class Node {
constructor(public label: string) {}
}
const sizes = new WeakMap<Node, number>();
const seen = new WeakSet<Node>();
const a = new Node("a");
sizes.set(a, 3);
seen.add(a);
console.log(sizes.get(a), sizes.has(new Node("a")), seen.has(a)); // 3 false true
const ref = new WeakRef(a);
console.log(ref.deref()?.label); // aA cache from an object to a wrapper that holds the object (rawToProxy.set(raw, proxy) in
reactive libraries) frees both once nothing else refers to either, as JavaScript's garbage
collector does (memory).
| Member | Notes |
|---|---|
new WeakMap<K, V>(); set(k, v), get(k): V | undefined, has(k), delete(k): bool | get returns the stored value itself (an object is shared), and undefined for a missing key. set returns the map, so calls chain |
new WeakSet<T>(); add(x), has(x), delete(x): bool | add returns the set, so calls chain |
new WeakRef(target); deref(): T | undefined | undefined once the target was freed |
Differences from JavaScript:
- Freed at once. Velt frees an object when its last reference goes (which can be the last
use of the variable holding it), so a
WeakRef'sderef()returnsundefinedright after, where Node keeps returning the object until a later garbage collection, and at least until the current job ends. A variable passed as a key, a member or a target (new WeakRef(k), alsok!of a nullable one, or through a function's parameter) stays alive to the end of its scope, so a freshWeakRefkeeps its target while the variable lives. - One task. A weak collection belongs to the task that made it (it keeps working when an
awaitresumes the task on another thread): it cannot be passed to a spawned task (in either branch ofspawn(c ? f(x) : g(y))), be its result or the error it throws, sent on a channel, captured by an async closure that may run on another thread, put inshared(...)or cloned (an error says so; in JavaScript,structuredClonerejects them too). Nor can aMutex'swithcallback use one from outside the lock (capture it, or call a closure that does) or return one (in its result, or captured by a function it returns): the lock guards the value only whilewithruns, and a key or target from the value would be released or read by another thread without it. Use the weak collection outsidewith, or make it inside and drop it there. The same holds for a value that may hold one without its type saying so: an interface value when some class converted to that interface in the program holds a weak collection, a base-class value when such a subclass is used as it, a type parameter where a generic function (lockWith<T, R>(m, f): R { return m.with(f); }) is called with such a type, and a function that captures one, passed to another task or stored into aMutex's value. This is decided for the whole program: once any class holding a weak collection is converted to an interface (even on one task only), every value of that interface counts as holding one wherever it crosses threads. A function value that is not written as a closure literal where it crosses (one a factory returned, or held in a field) does not show what it captured: if it captured a weak collection, the program panics when it is handed to another thread ("aWeakRefcannot go to another thread"), before the other thread can use it. A promise settled with such a function value (resolve(f)innew Promise<() => number>(...), or throughPromise.withResolvers) panics too, also when the same task awaits it (Node runs it). A class that extends a weak collection (class Cache extends WeakMap<K, V>) is one, and follows the same rules. - Module state.
const cache = new WeakMap<K, V>()at the top of a module is module state, made once, as in Node.
Record
Record<K, V> is a dictionary written with TypeScript object syntax: r[k], r.name,
r[k] = v, delete r[k] and object literals
(Reference). Object.keys(r),
Object.values(r) and Object.entries(r) return arrays in JavaScript's key order: array-index
keys such as "2" first, ascending, then the others in insertion order. Object.keys returns
a string[] and, as in TypeScript, also lists the fields of any object, struct or class
instance (except #private fields, as in JS). Of an any, object or Function it lists the
keys of the value held, at run time
(Reference). Object.fromEntries(pairs) makes a
Record<string, V> of [key, value] pairs.
class User {
name = "a";
private age = 3;
static readonly limit = 9;
}
console.log(Object.keys({ id: 1, tag: "x" }), Object.keys(new User()));
// [ 'id', 'tag' ] [ 'name', 'age' ]const env: Record<string, string> = { HOME: "/home/a" };
env["PATH"] = "/bin";
const limits: Record<"cpu" | "mem", i64> = { cpu: 2, mem: 512 };
limits.cpu += 1;
console.log(env.HOME ?? "/", Object.keys(env), limits);
// /home/a [ 'HOME', 'PATH' ] { cpu: 3, mem: 512 }A key of a Record<string, V> may be missing, so counting needs a starting value:
r[k] += 1 is an error there, and ?? supplies it. The logical assignments are defined for a
missing key, which reads as undefined: r[k] ??= v sets the key only when it is missing,
r[k] ||= v also when its value is falsy ("", 0, false, NaN), and r[k] &&= v only when
it is present and truthy. As in JavaScript, the record and the key are evaluated once, v only
when it is stored, and the expression's value is the value stored or kept.
const seen: Record<string, i64> = {};
for (const w of ["a", "b", "a"]) {
seen[w] = (seen[w] ?? 0) + 1;
}
console.log(seen); // { a: 2, b: 1 }
const kebab: Record<string, string> = {};
const prop = "backgroundColor";
const css = (kebab[prop] ||= prop.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase());
console.log(css, kebab); // background-color { backgroundColor: 'background-color' }Nullable values
On any T | null, T | undefined and T | null | undefined (what Map.get, find and pop
give): isNull() (either nothing), unwrap() (panics on either), unwrapOr(fallback), and
map(f) (undefined for either nothing on the last two). The value-extracting helpers return the payload itself (an object is shared, not
copied). A variable narrowed to its T (let x: string | null = "a"; x.unwrapOr("b")) keeps
the helpers T does not have; a method of T with the same name wins (xs.map(f) on a
narrowed number[] | null maps the elements).
Iteration
The iteration protocol behind for...of and for await (Control flow):
| Declaration | Notes |
|---|---|
type IteratorResult<T> = { value: T; done: false } | { done: true } | narrows on r.done; a done result has no value (unnarrowed, r.value is T | undefined, undefined when done) |
interface Iterator<T, E = never> | next(): IteratorResult<T> throws E; return(): IteratorResult<T> (default: returns { done: true }) runs when a loop leaves early |
interface Iterable<T, E = never> | [Symbol.iterator](): Iterator<T, E>; what for...of iterates |
class Generator<T, E = never, N = never> | what calling a generator (function*) creates: implements Iterator<T, E>, Iterable<T, E> ([Symbol.iterator]() returns itself); next(value) (with N, TypeScript's TNext, given) resumes the paused yield with value; throw(e: any) throws e at the paused yield; return() (which returns { done: true }) and [Symbol.dispose]() close it. Only generator calls create one (new Generator is an error) |
interface AsyncIterator<T, E = never> | next(): Promise<IteratorResult<T>, E>; async return(): Promise<IteratorResult<T>> (default: resolves to { done: true }) |
interface AsyncIterable<T, E = never> | [Symbol.asyncIterator](): AsyncIterator<T, E>; what for await iterates |
class AsyncGenerator<T, E = never> | what calling an async generator (async function*) creates: implements AsyncIterator<T, E>, AsyncIterable<T, E>; return() and [Symbol.asyncDispose]() close it (awaiting its cleanup), [Symbol.dispose]() closes it without awaiting. Overlapping next() / return() calls are queued and served in call order, like JS. Only async generator calls create one |
interface IterableIterator<T, E = never>, interface IteratorObject<T, E = never> | TS's iterators that are also iterable: extends Iterator<T, E>, Iterable<T, E> ([Symbol.iterator]() returns the iterator itself, as an Iterator<T, E>). Generator implements both; a value of either converts to an Iterable<T, E> |
interface AsyncIterableIterator<T, E = never> | extends AsyncIterator<T, E>, AsyncIterable<T, E>; AsyncGenerator implements it, and a value converts to an AsyncIterable<T, E> |
[Symbol.iterator](): Iterator<T> on T[], string (Iterator<string>), Map<K, V> (Iterator<[K, V]>) | makes them Iterable: they convert to Iterable<T> values and satisfy Iterable<T> bounds. An array's iterator is a live view (JS's: it reads the length at each step); a string's yields characters (code points); a map's iterates the entries as of the call (entries()) |
class ArrayIterator<T>, class StringIterator | the iterators of arrays and strings (TS's names): implements IterableIterator<T>, IteratorObject<T> |
An extend block defining [Symbol.iterator](): Iterator<T, E> (or
[Symbol.asyncIterator](): AsyncIterator<T, E>) makes its type an Iterable<T, E> (or
AsyncIterable<T, E>) the way the prelude does for arrays and strings, as compareTo makes it
Comparable.
JSON
JSON.stringify<T>(x), JSON.parse<T>(text, options?) (throws JsonError) and
JSON.parseValue(text, options?): JsonValue (velt/encoding/json); arrays and objects may
nest 128 levels deep unless options.maxDepth says otherwise.
Errors
class Error { message: string; name: string }, the base class of thrown errors (String(e)is"<name>: <message>"), and its subclassesTypeError,RangeError,SyntaxError,ReferenceError,EvalErrorandURIError(Errors).DOMException, a global as in Node: the error web APIs throw (btoaandatobthrow one namedInvalidCharacterError).new DOMException(message = "", name = "Error");e.nameis the name,e.codethe legacy code Node gives it (5forInvalidCharacterError,20forAbortError,25forDataCloneError, …, 0 for a name without one), and the codes are also static constants (DOMException.ABORT_ERR). It extendsError, soString(e)is"<name>: <message>";console.log(e)printsDOMException [<name>]: <message>, Node's first line (Node adds the stack);JSON.stringify(e)writes{"message":"…"}, as for the other errors (Node writes{}). Node's second form,new DOMException(message, { name, cause }), is not supported: pass the name as a string.
try {
btoa("€");
} catch (e) {
console.log(e instanceof DOMException, e.name, e.code, String(e));
// true InvalidCharacterError 5 InvalidCharacterError: Invalid character
}
const abort = new DOMException("This operation was aborted", "AbortError");
console.log(abort.code == DOMException.ABORT_ERR); // trueattempt(() => f()): a throwing call as a value,T | E.AggregateError, thrown byPromise.anywhen every promise rejects.assert(cond, msg?),assertEq(a, b, msg?)(compares withdeepEqual; the message shows both values): throw anAssertionError(anError, as Node'sassertdoes) when they fail.assertThrows(() => f(), msg?)returns the errorfthrows and throws anAssertionErrorif it returns normally.panic(msg)panics, for bugs: it ends the program and can't be caught. In a function that can't throw (a sort comparator, a promise executor,toString),assertis a compile error, as any throw is; check an invariant there withif (!cond) panic(msg)(Errors).deepEqual(a, b): bool: content comparison. Arrays, structs and object literals compare their contents recursively. AMaporRecordequals another with the same keys, each with a deeply equal value, in any order (a key is matched asgetmatches it, and an object key that isn't a key of the other map pairs with a deeply equal key there). Other class instances compare by identity;==compares every object by identity. This is Node'sutil.isDeepStrictEqualexcept for-0: floats compare as map keys do (SameValueZero), sodeepEqual([NaN], [NaN])istruelike Node, anddeepEqual([0], [-0])istrue(Node saysfalse). Values that hold themselves stop at the cycle: an object met again while it is being compared is equal if it meets the same partner again (two rings of one length), else different (rings of two lengths, a cycle on one side only), as Node answers. Numbers in a union of several number types compare by number, exactly, as===compares them: in anf64 | u8(what[bytes[0], bytes.length]holds),1.0equals1u8, and ani64above 2^53 equals nof64it only rounds to.
const a: Record<string, i64[]> = { x: [1], y: [2] };
const b: Record<string, i64[]> = { y: [2], x: [1] };
console.log(deepEqual(a, b), a == b); // true false
const bytes = new Uint8Array([7]);
console.log(deepEqual([bytes[0], bytes.length], [7, 1])); // trueDate
Date is JavaScript's: an instant in milliseconds since the epoch (NaN when invalid), with
months 0-11, local-time getters and setters (the OS time zone, DST-aware) and their UTC
variants. It is built on velt/time, whose DateTime (UTC-first, months
1-12) is the better choice for new code.
new Date(),new Date(ms),new Date(string),new Date(date),new Date(year, month, day?, hours?, minutes?, seconds?, ms?)(local time, rolling over).Date.now(),Date.parse(s)(ISO 8601 and HTTP dates; a date alone is UTC, a date-time without an offset local time, as in JS),Date.UTC(year, month, …).getTime(),valueOf(),getTimezoneOffset();getFullYear getMonth getDate getDay getHours getMinutes getSeconds getMillisecondsand thegetUTC…ones; the matchingset…andsetUTC…setters (with JS's optional extra fields), andsetTime.toISOString()(an invalid date panics),toJSON()(the ISO string, ornullwhen invalid;JSON.stringifywrites a date, or a subclass ofDate, through it, as Node does),toUTCString(),toString(),toDateString(),toTimeString(), andtoLocaleString(),toLocaleDateString(),toLocaleTimeString()(alwaysen-US).- Dates compare with
<(DateimplementsComparable);console.logprints one as its ISO string, as Node does.
const start = new Date(Date.UTC(2024, 0, 31, 12));
const end = new Date(start);
end.setUTCDate(end.getUTCDate() + 1);
console.log(start.toISOString(), end.getUTCMonth(), start < end); // 2024-01-31T12:00:00.000Z 1 trueAsync and concurrency
sleep(ms), yieldNow(), spawn(p, options?) (options: { worker }, see
velt/task), Promise.all, Promise.race, Promise.allSettled
(with PromiseSettledResult<T, E>), Promise.any, Promise.withResolvers (with
PromiseWithResolvers<T, E>), Promise.resolve, Promise.reject, the promise methods
then, catch and finally with queueMicrotask(f)
(Async), shared(x), Mutex<T>, RwLock<T>,
Atomic<T> (lock-free numbers and flags) and ThreadLocal<T> (velt/sync),
performance.now() and Date.now() (Async), and the timer
functions setTimeout, clearTimeout, setInterval, clearInterval, setImmediate and
clearImmediate with their Timer handle (velt/time).
const words = "the cat and the hat".split(" ");
const counts = new Map<string, i64>();
for (const w of words) {
counts.upsert(w, 1, (n) => n + 1);
}
const top = counts.entries();
top.sort((a, b) => b[1] - a[1]);
console.log(top[0], words.at(-1), (2.0 / 3.0).toFixed(3), Math.max(3.0, 7.5));
// [ 'the', 2 ] hat 0.667 7.5