Skip to main content
Version: 0.3

Best practices and what not to do

Most typed TypeScript compiles to fast code as written. The patterns below are the exceptions: each one compiles, or fails with a clear diagnostic, but costs more than it looks. Warnings are counted on every build; --warnings lists them, and --deny-warnings turns them into errors for CI. The performance model explains the mechanisms behind each row.

Types and values​

patternwhat happensdo this instead
any, as any, (x as any)[k]Dynamic path, warning XS2007. A field read costs about 9 ns against 0.4 ns typed (10 million reads, Apple M4, 12 Nov 2026). An object created as any is a Record, larger than a struct.Declare an interface, a class or Record<string, T>. See any.
undefinedRead as null, warning XS2101. A comparison of a never-null value with null is always the same: XS2102.Write null; test with x == null.
a == b where JavaScript would convert (int and string, say)Error XS8148.Convert explicitly: Number(s) === n or String(n) === s.
if (n) with an intRead as n !== 0, warning XS2105. A number as a condition is accepted only with --tsc-checked.Compare explicitly: n !== 0, n > 0.
int mixed with numberThe result is number, warning XS2103. The loop can leave integer registers.Keep counters and indices int; convert once, where you mean it.
A class object where a data shape ({ k?: T }) goesA field-by-field copy: writes through the parameter do not reach the caller; === and instanceof differ.Use the class type in the signature.
A class passed where an interface with methods goes, without implementsAn adapter object per conversion; === with the original is false.Declare class C implements I: no adapter.
Record<string, number> where Record<string, unknown> goes; T[] where U[] goesA copy, made only when the callee provably only reads; otherwise error XS8018 asks for the type.Declare the wider type at the source, or make the callee generic.
structuredClone(x)A deep copy by run-time class; the copy stays an instance of the class (Node returns a plain object). A function or a cycle throws DataCloneError.Copy explicitly when the structure is known.

Functions and generics​

patternwhat happensdo this instead
A generic arrow value const f = <T>(x: T[]): T => ... returning objectsT is read as unknown; the result is copied through JSON on every call, warning XS2110. The cost grows with the value.Write function f<T>(...) or a generic method: compiled per type, no cost.
Generic functions over mapped tuples ({ [K in keyof T]: ... })One box per item and a checked conversion per read. Not yet measured.Use homogeneous Promise.all over one type where possible.
A closure that reassigns a captured variableThe variable moves to a heap cell: an allocation, and counting on every access.Capture a const; return the new value instead of assigning it.
Patching a prototype (Object.getPrototypeOf(x).m = ...)Every reachable class with that method calls it through a slot: an indirect call.Use a subclass or a wrapper function.
x = x.with(v) (immutable update returning new C(...))When x holds the only reference, the fields are rewritten in place: no allocation.Keep this style; avoid keeping a second reference to the old object in hot loops.

Async, I/O and memory​

patternwhat happensdo this instead
Storing a promise, or .then chainsA coroutine and a promise: about 73 ns per kept promise against 0.7–0.9 ns for await f() (2 million calls, Apple M4, 12 Nov 2026; Node: about 20 ns for both).await the call directly when you do not need the promise.
A bare f(); of an async function, as fire-and-forgetIt is a direct call: f runs to completion before the next line, unlike Node.Keep the promise (const p = f();) when it must run in the background.
readFileSync of a large fileThe whole file in memory: 977.8 MB peak RSS for a 1 GB log against 1.3 MB streaming (M4, 5 Nov 2026, bench/results/2026-11-05-Apple-M4-streams.csv).Read it as a stream, or by parts with readStdinParts from xisto:io.
An allocation loop with await, yield, closures, unions, casts or unknown insideNo region: every object is counted and freed one by one. A try anywhere in the program rules out dual regions; a try in a function rules out stack allocation there.Keep allocation-heavy loops plain; move the await or the closure out. Check with XISTO_REGION_WHY=1.
parallelMap / parallelFrom / parallelFill / parallelReduce with a callback that prints, calls a function value, uses timers or writes outside itselfThe proof fails: a plain sequential loop and a warning saying why.Callbacks may only read shared numbers, booleans and numeric arrays.

Arrays and builds​

patternwhat happensdo this instead
nums.sort() on numbersError XS7089: JavaScript would compare as strings. sort() on strings works as in JavaScript.nums.sort((a, b) => a - b).
xs.push(a, b)On a variable: one push per argument. On an expression that cannot be read twice (a call result): XS7083.Push to a variable.
xs.unshift(a, b)Error XS7117: unshift takes one argument.One unshift per value, last first.
Measuring or serving with --dev-O0: 4–10× slower than release (fib 0.11 vs 0.02 s, nbody 3.78 vs 0.39 s; M4, 7 Nov 2026, bench/results/2026-11-07-Apple-M4.csv). Frames over 16 KB can overflow a server's 64 KB request stack (SIGBUS).--dev for the edit loop only; release (the default) for anything you measure or deploy.
Leaks you do not seeReference counting cannot leak silently, except through cycles of types the compiler did not mark.Run with XISTO_RC_STATS=1: the line must say 0 still alive at exit. xisto test requires it.

How to find what is slow​

Measure first. Build with --profile to find the function, use XISTO_RC_STATS=1 to count allocations, read GET /__metrics on a server, and ask the compiler why an optimization did not apply with the XISTO_*_WHY knobs. Measuring a program has the workflow.

See also​