Language constructs
The core TypeScript surface this compiler lowers to native code: control flow, operators, declarations, functions and closures, async/Promise, and classes. Most of it matches JS/TS exactly, but the recurring divergences are worth knowing up front — strings are NUL-terminated (an embedded null truncates), a nullable scalar's null is often an in-band presence bit rather than a real undefined, `-compat` governs whether `&&`/`||` are value-preserving, and a top-level `const`/`let` is only readable from a named function when its type is knowable before main() runs (an arrow closure captures anything regardless). The whole surface is statically typed and structurally shaped — there is no dynamic property add/delete and no runtime eval.
if / else if / else
ExactConditional branching with the usual else-if chain and optional braceless single-statement bodies.
while loop
ExactPre-condition loop; the body runs zero or more times.
do…while loop
ExactPost-condition loop; the body always runs at least once before the condition is checked. break/continue work inside it.
let i = 0
do {
console.log(i)
i++
} while (i < 3)for (init; cond; update)
ExactThe C-style three-clause for loop.
for…of over arrays, strings, Map, Set, a bare any, and a class implementing next(): T | null
Works, with caveatsIterates arrays, a string (its code points), Map (its [key, value] entries, live: an entry added during the loop is visited, a deleted one is not), Set (elements), generators, a bare any holding a string, a dynamic or boxed array (by its runtime length), or any iterable through its [Symbol.iterator]() (an any loop variable; anything else throws Node's 'is not iterable' TypeError), and both the structural next(): T | null shape and the spec's real [Symbol.iterator]() protocol. The loop variable may be a destructuring pattern, including nested. A scalar next() returns a presence-flagged aggregate so a yielded 0/false doesn't end iteration.
- Over a bare
any, each element is read through a numeric-string key — a per-tag element walk is the perf follow-up.
for…in over object keys
ExactIterates the field names of an object as strings.
const point = { x: 10, y: 20, z: 30 }
for (const key in point) {
console.log(key)
}switch / case / default / break
ExactMulti-way branch with numeric, string, and boolean discriminants.
break / continue in loops, including labeled (outer: for (...) { break outer; })
ExactLoop break/continue, including a labeled target on an enclosing loop. A break/continue inside a nested closure can only target a loop declared within that closure's own body — a clean compile-time rejection otherwise, matching JS's no-cross-function-boundary label rule.
return
ExactTyped return; an implicit void return is handled.
throw new Error(msg)
ExactThrows via setjmp/longjmp. A built-in error constructor is also a first-class value (a boxed funcref with exact identity, typeof "function").
try / catch / finally
Works, with caveatsException handling with a single catch variable or a destructured object pattern. finally runs on early return/break/continue (labeled or not), a return in finally overrides, and nested finallys run innermost-first — inside async functions and generators too, including a finally that itself awaits.
function readWithCleanup(): number {
try {
return 42
} finally {
console.log("close resource")
}
}- A catch pattern can only destructure the caught value's fixed
{kind, message, name}shape (every thrown value, including a thrown non-Error primitive, is force-shaped into that one runtime layout), not an arbitrary custom object's own fields, and there's no array-pattern form.
Optional catch binding (try {} catch {}, no bound param)
ExactThe catch parameter clause is optional (ES2019).
Automatic semicolon insertion
ExactSemicolons are optional where JavaScript inserts them: a statement ends at a line break, before `}` or at the end of the file. A line break is refused where the grammar refuses one — before a postfix `++`/`--`, after `return`/`throw`/`break`/`continue`/`yield`, before `=>`, and between `async` and its function — so `a` then `++b` on the next line is two statements. `async` and `abstract` stay usable as names.
TypeError on a property access off undefined/null (s.length, r.name, p.m(), s[i], p.x = v)
Works, with caveatsReading a property, calling a method, indexing, or writing a property on a value that is undefined or null at run time throws Node's TypeError — Cannot read properties of undefined (reading 'length') / Cannot set properties of null (setting 'x') — catchable and instanceof TypeError; uncaught, the program reports it and exits 1. Covers string, object, class and Map bases, a T | undefined scalar receiver, x!.p, compound assignment and ++. Optional chaining never throws. An out-of-range element read is undefined, so rows[5].name throws too; an absent array (T[] | null, an omitted xs?: T[]) throws on xs[0] / xs.length / xs.push(), and TypeError: xs is not iterable on for…of and spread. An empty array is present: truthy and !== null.
interface User { name: string; manager?: User }
const boss: User = { name: "Ada" }
try {
console.log(boss.manager.name)
} catch (err) {
console.log(err instanceof TypeError) // true
console.log((err as Error).message) // Cannot read properties of undefined (reading 'name')
}
console.log(boss.manager?.name) // undefined- In
p.x = f()the base is checked before the right-hand side is evaluated, sof()does not run ahead of theTypeError(JS evaluates it first).
debugger statement
ExactParsed and erased to a no-op — an AOT-compiled native binary has no attached inspector to break into, matching plain JS with no debugger attached. debugger is a reserved word, never a real identifier.
Numeric literals (42, 3.14, 0xFF, 0b101, 0o77)
ExactDecimal, float, hex, binary, and octal numeric literals.
Numeric separators (1_000_000)
ExactUnderscore digit separators, stripped at lex time so every downstream consumer sees a clean literal. A leading/trailing/doubled separator, or one inside a legacy-octal literal, is a clean lexer error.
String literals (single/double quote)
Works, with caveatsSingle- and double-quoted strings; the full ES escape grammar is decoded (\b \f \v, \xHH, \uHHHH, \u{H…}, Annex B legacy octal, line continuation, and the NonEscapeCharacter rule). Templates share the same decoding.
- An embedded NUL (
\x00,\0) truncates the string, since strings are stored as NUL-terminated C strings. - No strict-mode
SyntaxErrorfor a legacy octal escape (accepted unconditionally).
Boolean literals (true / false)
ExactBoolean literals.
null literal
ExactThe null literal; a T | null union type is supported.
undefined literal
ExactThe undefined literal.
Template literals `Hello ${name}`
ExactBacktick template literals with arbitrary interpolation depth.
const msg = `Hello, ${name}!`Arithmetic operators + - * / %
ExactThe five basic arithmetic operators.
** (exponentiation)
ExactRight-associative exponentiation (ES2016), binding tighter than * / %, with **=. number operands are doubles so 2 ** -1 is 0.5 and 2 ** 0.5 is a real root; explicit integer-typed operands use exact i64 exponentiation-by-squaring and stay i64. -2 ** 2 is a SyntaxError, matching JS.
Comparison operators == === != !== < > <= >=
ExactThe comparison operators; string comparison via strcmp. NaN compares per JS: NaN !== NaN is true while ===/</>/<=/>= against NaN are all false.
Logical operators && || !
ExactGenuinely short-circuiting logical operators. Result type is bool under -compat=strict (default); -compat=js makes &&/|| value-preserving (x || "default" yields the string; scalars of different kinds yield their union, 0 || "x" is number | string).
Bitwise operators & | ^ ~ << >> >>>
ExactThe bitwise and shift operators, including the unsigned right shift >>>.
Assignment operators += -= *= /= %= &= |= ^= <<= >>= >>>=
ExactThe full set of compound assignment operators.
Logical assignment operators &&= ||= ??=
ExactGenuinely short-circuiting logical assignment (ES2021). Works against scalar variables, array elements, object fields, static class fields, and a computed dynamic-object key (dynObj[key] ??= v), including a nullable-scalar field routed through the presence-flagged path so obj.n ??= v fires exactly when the field is absent.
Increment / decrement ++ -- (prefix & postfix)
fullPrefix and postfix increment/decrement on an identifier, member (obj.x++, this.x++, C.staticField++, makeObj().x++), or index (arr[i]++, makeArr()[i]++) target. A member/index target desugars to += 1 / -= 1; prefix yields the new value, postfix the old. A consumed postfix hoists any side-effecting component so each runs exactly once, matching JS.
Ternary cond ? a : b
Works, with caveatsThe conditional (ternary) operator. A cond ? scalar : null mix is a T | null value, and scalar branches of different kinds (c ? 1 : "a", c ? 1 : true) yield their union (number | string), narrowable with typeof.
function label(n: number) { return n % 2 === 0 ? n : "odd" }
console.log(label(2), label(3)) // 2 odd
console.log(typeof label(3)) // string- A pointer branch mixed with a scalar one that is not a scalar union (
c ? 1 : someObject), and an array-typed ternary, are compile-time rejections — narrow first, or assign each branch separately.
Comma / sequence operator (a, b, c)
Works, with caveatsEvaluates each operand left to right and yields the last (its type is the sequence's type). Also usable in a for header.
- A sequence whose *first* operand is a lone identifier (
(a, b)) is instead parsed as an arrow-function parameter list — a known ambiguity; write a non-identifier first operand.
Nullish coalescing ??
ExactWorks on a pointer T | null (string/object/…) and a non-pointer scalar T | null (number | null, boolean | null) via a real presence bit — so let x: number | null = null; x ?? 42 yields 42 while a present 0 yields 0. Operands of different kinds (n ?? "none", s ?? 0) yield their union, number | string, narrowable with typeof. A pointer left operand keeps its own type, so the result is usable in place — (row ?? dflt).name — and stays T | undefined when the right operand can itself be absent; an element of a (number | undefined)[] coalesced with a number is a plain number.
Optional chaining ?.
ExactAll three forms — property a?.b, element a?.[k] and call f?.(args) / a?.m(args) — short-circuit to a real undefined on a null or undefined base, and the chain short-circuits as a whole: in a?.b.c or a?.b.m(x) a nullish a skips every later link and leaves call arguments and element keys unevaluated. Parentheses end the chain, as in JavaScript. The result is typed T | undefined.
typeof operator
Works, with caveatsCompile-time constant resolved from inferred type. Heap-backed built-in values (a Promise, Map/Set, a generator, Date, null) are "object"; classes, implemented built-in constructors, and implemented global functions are "function"; Math/JSON/console are "object"; an undeclared name or runtime-absent static is "undefined".
typeof <namespace>.<method>answers"function"for the common built-in namespaces (Promise/Math/JSON,console, and the static methods ofObject/Number/String/Array/Date/Symbol/Reflect); a non-method static (Number.MAX_VALUE) answers through normal inference, and a namespace outside this allow-list falls back to inference.typeof value.methodis"function"for a class instance method or a common built-in string/array method (a class getter reads as its return type); a string/array method name outside the curated set falls back to inference.
void operator
ExactEvaluates its operand for its side effects and yields undefined. A function may name a parameter or variable undefined, which then reads that binding.
console.log(void 0) // undefined
console.log(void track()) // undefined, track() still runsin operator (key in obj)
ExactReduces to the same compile-time FieldIndex lookup Object.hasOwn() uses — object shapes are fully structural/static, so a string-literal key against an object is a compile-time-constant true/false, not a runtime scan. Parsed as a contextual keyword at instanceof precedence.
Spread in array literals [...a, ...b]
ExactArray-literal spread.
const / let / var declarations
Works, with caveatsvar is function-scoped and re-declarable; let/const are block-scoped and const rebinding is rejected. A top-level const/let/var is a module global — readable and writable from any named function whenever its type is determinable before main() runs. Block redeclaration, TDZ, and use-before-assignment are compile errors.
- Definite-assignment analysis is sound-not-complete: a typed
var/letassigned only in a maybe-skipped loop or across correlated conditions still falls through to theundefined/zero default rather than raisingused before being assigned. - A top-level
const/letstill isn't readable from a namedfunctiononly in the invariant's genuine edges — abigint, an un-annotated call *through another top-level binding* (const f = () => …; const x = f()), anew Set([…])typed from its initializer array, or an init depending on a runtime-local (a{ …rest }spread of a destructuring target); an arrow/closure still captures any of them.
Array destructuring const [a, b] = arr
Works, with caveatsArray destructuring with holes, defaults (fire when past the source length), and a rest element that makes a real independent copy. Usable as a for-of loop variable and a destructured parameter; nested patterns work in a declaration/for-of/parameter.
- *Assignment* to already-declared targets (
[a, b] = expr, not a fresh declaration) reaches declaration-form parity for nesting: nested array/object patterns, a trailing...rest, and a per-element= defaultall work. A compound operator ([a, b] += …) and a member target ([obj.x] = e) stay rejected.
Object destructuring const { x, y } = obj
Works, with caveatsObject destructuring with renaming, string/numeric-literal keys, nested patterns, and a rest element that binds a fresh residual object. Usable as a for-of loop variable and destructured parameter. Assignment to already-declared targets requires parens.
- Default values (
{ x = 1 }) are supported only for a field typed as a nullable *reference* (T | nullwhere T is string/array/object/class) — the only field shape with a real "not provided" signal; a default on a nullable scalar or any non-nullable field is a clean compile-time rejection. - *Assignment* to already-declared targets supports nested patterns (
({ p, q: { r } } = obj),({ arr: [x, y] } = obj)) and object-property defaults ({ x = default },{ k: x = default }) on a nullable-reference field, at declaration-form parity. { ...rest }over anany/union/generic source and in the *assignment* form (({ a, ...rest } = e)) stay clean rejections.- A computed key (
{ [k]: v }) is supported only for a constant string/number literal key ({ ["a"]: v },{ [0]: v }); a runtime-valued key on a fixed object is a clean rejection.
Tagged template literals
supportedDesugars to a plain call — interpolated values reach the tag as real typed values, not implicitly stringified. Any callable works as a tag, including an arrow/closure with a ...rest parameter. The strings parameter is a TemplateStringsArray with its raw text in strings.raw, each call site's template object is the same object on every call, and a generic tag takes explicit type arguments (tag<T>`…`).
Function declarations (top-level)
Works, with caveatsNamed top-level functions with typed params and return. A leading this parameter (function f(this: T, …)) is parsed and erased, exactly as real TS does. The arguments object holds the values a call passed (.length counts them), with indexing and for…of.
- Cannot reference a sibling top-level binding whose value is a connection handle (
Worker/WebSocket/EventSource,BroadcastChannel/MessageChannel,XMLHttpRequest), aPromise, or a generic class instance with **inferred** rather than explicit type arguments (new Box(5)) — such a binding stays amain()local and fails withundefined variable. Scalars, strings, arrays,TypedArrays, objects,Map/Set, class instances (including explicitnew Box<number>()), the value/event handles, and the streams +EventEmitterandhttp.createServerhandles are promoted to module globals; arrow functions/closures capture everything regardless. - A genuinely circular pair of mutually-recursive unannotated functions can't converge on a return type and keeps the scalar-default fallback.
argumentsin a function with an explicit...restor a destructured parameter is a compile error; a call throughanymakesarguments.lengthcount every declared parameter.
Arrow functions / lambdas
ExactFull closures; captures via a heap-allocated env struct.
function makeCounter(): () => number {
let count = 0
return (): number => {
count = count + 1
return count
}
}Function expressions
Works, with caveatsAnonymous and named function expressions, including as callback arguments. A named function expression binds its own name inside its own body for recursion without leaking — even when that name shadows a top-level function of the same name (the self-reference is the expression; outer calls reach the top-level function). async function(...) {...} expressions are supported. Reuses arrow closure machinery.
const add = function (x: number, y: number): number {
return x + y;
};- A generic function used by value is out of scope (no single monomorphized symbol to point at).
Default parameter values
Works, with caveatsDefault parameter values.
- A default may reference an earlier scalar/string parameter —
function f(a: number, b: number = a * 2), chaining included (c = b + 1) — for a free function, an instance/static method, and a constructor (parameter properties included,constructor(public x = 1, public y = x)); an earlier param of any supported kind is referenceable — scalar/string, array (b = a.length,a[i]), and nullable-scalar (b = (a ?? 0) + 1). A rest-parameter constructor is unsupported. - Defaults are also filled when the function is a first-class value — a closure bound to a variable or object field, an object shorthand method, an IIFE — including cross-parameter defaults (
(x, y = x) => …) and defaults that reference a variable captured from the closure's defining scope (each returned closure keeps its own capture). This covers scalar/string/object/array captured-scope defaults, async closures, and.bind(the presence mask threads through the bind trampoline). A captured-default on a nullable-scalar or destructured parameter is still a clean compile error, and a default that references a captured array *variable* (rather than a module-global array) hits the separate array-capture limitation.
Optional parameters (param?)
Works, with caveatsOptional parameters. An omitted argument reads as a real `T | undefined` inside the body (as in TS) — a present 0/"" is distinguishable from an omitted argument; narrow, `?? d`, or `!` to consume it.
- Works through a first-class function value too — an arrow or function expression bound to a variable or object field — not only a named function; an omitted optional argument is absent there as well, not a typed zero.
- An un-narrowed optional scalar is still lenient in arithmetic — the payload is unwrapped before the operator, so
a + bon an absentbreads 0 rather than erroring (the same residual as any un-narrowed nullable local).
Rest parameters (...args: number[])
ExactWorks on named function declarations, class methods, arrow functions/closures, and function type annotations ((...xs: number[]) => R and the same as a type alias) alike.
Destructured function parameters
Works, with caveatsObject- and array-destructured parameters. A destructured callback parameter needs no annotation — its type is supplied by contextual typing from the higher-order method's element type, so the common `Object.entries(o).map(([k, v]) => …)` / Map-entries / pair-array idiom works. A heterogeneous tuple pattern binds each position by its own type. Each element/field may have its own default; nested patterns work; supported on named functions, class methods/constructors, and arrow functions.
- No combination with
...or a whole-parameter default value; a pattern param with no annotation and no contextual type (a plainfunction f([a, b])) is still rejected, matching TS's implicit-any error. - A
= defaulton a nested position is a clean rejection; per-element defaults follow the same rules as the statement-level destructuring rows.
Nested function declarations
Works, with caveatsA helper function declared inside another function's body — hoisted (letrec-style: a capturing declaration is usable before its declaration statement), forward-referenceable, self-recursive, and visible to its own further-nested descendants. Captures enclosing locals/parameters by reference — arrays and loop variables included (a C-style or iterator `let` loop variable is captured per-iteration, JS semantics; `var` shares) — and escapes by name as a real closure value with an inferred type; a nested generator escapes as a first-class constructor closure.
function outer(x: number): number {
function double(y: number): number {
return y * 2;
}
return double(x) + 1;
}- Generic (
<T>) nested declarations and same-scope duplicate names are clean compile errors. - A hoisted call that reads a captured variable declared *between* the call and the declaration is a clean compile error (Node crashes with a runtime TDZ error there).
- Two mutually-recursive *capturing* nested siblings (
fcallsg,gcallsf, both closing over an enclosing local) are a clean compile error; self-recursion works, including unannotated.
Function.prototype.call / .apply / .bind
Works, with caveatsf.call/f.apply lower to a direct call; f.apply(thisArg, arr) spreads into a rest slot; f.bind builds a new partially-applied closure. All type-checked against f's parameters exactly as a normal call.
const add = (a: number, b: number): number => a + b;
console.log(add.call(null, 3, 4));
console.log(add.apply(null, [5, 6]));thisArgis the receiver only of a function with athis: Tparameter (declared, or from its contextual type); for any other function it is evaluated then ignored, so borrowing a class method against another receiver is unsupported.- call/apply/bind on a Node module's function isn't supported (it isn't a function value); the ECMAScript builtins (
parseInt,Math.max,encodeURIComponent, …) are function values. .applytakes either a literal array or a runtime array spread into a **rest** parameter; a runtime array into a fixed-arity function is the usual spread-into-fixed-arity rejection.
Function overload signatures
Works, with caveatsOverload signature heads (a `function` head terminated by `;` with no body) are parsed and then erased — call sites type-check against the implementation's parameter list, matching this compiler's TS-modifier stance.
- Overload heads are **erased**, not used for per-signature overload resolution: a call is checked against the implementation signature only, so give it parameter/return types (often a union) wide enough to cover every overload.
Generator functions
Works, with caveatsReal suspend/resume via this compiler's ucontext-based fiber primitive. The full iterator protocol: .throw(e) injects at the suspension point, .return(v) completes running enclosing finally blocks, and yield* delegates to an inner generator.
function* basic(): Generator<number, number> {
yield 1;
yield 2;
return 3;
}
const g = basic();
console.log(g.next().value, g.next().done);- A free
function*may be top-level **or nested** (a nested one captures enclosing state by reference; an *array* capture stays a clean rejection); a generator *expression* is supported only as a top-levelconst/let/var G = function* ...binding — an argument/nested/IIFE use is a clean rejection.static/abstractgenerator methods stay clean rejections. - The return-type annotation is optional (inferred from the body's yields); a destructured, defaulted or optional parameter is still a compile error (rest and array parameters work). An array element type is supported — yielded/sent arrays round-trip through for...of, .next(), yield*, and for await.
- Calling
.next()again after completion returns{value: <T's zero value>, done: true}, notundefined— deliberately kept bare: tsc types the result'svalueasany, so the typedThere is already stricter than TS.
async functions
ExactEvery async fn returns a real task-shaped Promise<T>: a fn containing an await runs on a ucontext fiber; a no-await one settles inline. Named declarations, async arrows, and async function expressions are all supported. No libcurl linked unless fetch is used.
async function add(a: number, b: number): Promise<number> {
return a + b
}
const sum = await add(10, 32)await expressions
ExactA Promise is a reusable value — await p twice returns the same result. Every await yields a microtask tick, of an already-settled promise and of a non-promise value, FIFO-ordered with .then/queueMicrotask, matching JS. Registers a resume reaction and parks — at the entry program's top level too: a module with a top-level await runs as a coroutine, so its continuation is ordered among the promise's other reactions exactly as in Node, and an await nothing can settle ends the process with Node's warning and exit code 13.
Promise.all / .race / .allSettled
ExactReal concurrency over Array<Promise<Response>> (fetch) and over Array<Promise<T>> from may-suspend async fns. Each member is a reusable value. .race/.any return the shortest-delay member via an event-driven waiter list. A rejecting member rejects .all/.race and reports rejected in .allSettled. A rejected .allSettled element's .reason is the original rejected value with full fidelity — a primitive keeps its typeof and JSON, a rejected Error serializes as {} and reads .message/.name/String and instanceof (TDD-00169). An error-subclass instance reason reads .message/.name, renders Name: message and serializes its own enumerable fields as Node does. A list may mix promises and plain values ([p, 4]).
Promise.any
ExactOver task promises it skips rejected members and resolves to the first fulfilled one; when every member rejects it throws a real AggregateError carrying each reason on .errors. Over raw fetches, an HTTP 404/500 is a fulfilled fetch, per WHATWG.
Promise.resolve / Promise.reject
ExactBoth build a settled task promise, so await/.then/.catch/.finally and the combinators all work. Promise.resolve(v) fulfils with v (an already-promise arg flattens through); Promise.reject(e) is typed Promise<never>, so a rejected value assigns to any binding. No fiber/libcurl linked.
new Promise((resolve, reject) => …)
ExactThe executor constructor with thenable adoption (resolve(anotherPromise) settles the outer promise with the inner's value, reaction-based; a class instance, object or interface value with a then method is adopted through a job that calls its then, as await, Promise.resolve and the combinators also do), synchronous resolve/reject, first-settle-wins, and deferred resolution from a setTimeout callback. The executor may be an arrow, function expression, closure variable, or bare top-level-function reference.
Promise.prototype.then / .catch / .finally + queueMicrotask
Exact.then/.catch/.finally directly on a raw fetch Promise<Response> never block the synchronous script — the wait is deferred to a queued microtask. The callback runs as a microtask when the promise settles and its result chains into the returned promise. A missing callback slot is a pass-through. queueMicrotask is a FIFO drained after the sync script and before timers.
Async generators (async function*, async *m())
ExactV8/spec step timing: .next() starts the body synchronously up to the first await/yield; an await inside the body parks the step. Top-level or nested async function* and a class async *m() method. .next() returns a real Promise<{value,done}>. Full protocol: .throw(e)/.return(v) and yield* over an inner async generator or a Symbol.asyncIterator iterable.
for await...of (async iteration)
ExactConsumes an async-generator instance, a user class or object literal implementing the async-iteration protocol, or any sync iterable the sync for...of takes (awaiting each element). Awaits each .next() promise before reading .done/.value; the loop is itself a suspension point. A destructuring loop variable unpacks each element.
Enums (numeric)
ExactNumeric enums with auto-increment and explicit values.
enum Direction {
North,
East,
South,
West,
}
console.log(Direction.East) // 1Enums (string)
ExactString enums; members are interned string constants. The enum name (and its [] array form) as a type annotation resolves to the enum's ptr backing type, so a typed variable/param/field/array, comparison, and switch all work.
Namespaces (namespace X {}, function merging)
Works, with caveatsMerges with a same-name top-level function X(...) — TS's callable-object duality idiom (assert(x) + assert.sameValue(...)); members desugar to flat top-level declarations.
- V1 scope: top-level only, members must be
export function/export const/let(single binding); no nesting, nodeclare namespace, no cross-moduleexport namespace; the same namespace member declared in two files is a link-time duplicate-symbol error, not file-private.
Interfaces (structural)
ExactStructural interfaces backed by heap-allocated objects.
Type aliases
ExactType alias declarations.
Object literals { key: value }
Works, with caveatsA key may be an identifier, or a string/numeric literal.
- Constant-key bracket access reads and writes the matching field on a fixed object or class instance (
o["a"],o[0],o["a"] = v) — the same fieldo.areaches; a non-constant key on a fixed object stays a clean rejection (use a{ [k: string]: T }dynamic object for runtime keys). A numeric-literal key still can't be declared in aninterface/type annotation. - No duplicate-
__proto__-key detection (Annex B legacySyntaxErrorrule).
Getters / setters on classes and object literals
Works, with caveatsget/set are contextual (not reserved). No new storage: an accessor is registered as an ordinary method under an internal dispatch key. obj.x / obj.x = v / compound assignment all route through the accessor, including from inside the class's own methods and through optional chaining. On object literals too: the literal is lowered to a synthetic anonymous class so the accessor body gets a real this reading/writing the object's own fields.
- An accessor-bearing object literal can't be assigned to a differently-shaped structural object type (its accessors are methods, not fields) — a clean rejection; used directly it works.
- console.log of an accessor object prints only its data fields, not x: [Getter].
- Static accessors, and logical assignment (
&&=/||=/??=) on an accessor, remain out of scope for V1.
new Error(msg)
ExactThe base Error constructor.
Built-in Error subtypes
Works, with caveatsA small fixed kind enum (a hidden runtime tag on the shared Error object shape, not a real class hierarchy), plus a real .name field. instanceof Error matches any kind; instanceof TypeError matches only that one. new DOMException(message?, name?) takes its .name from the 2nd argument. The ES2022 error-options bag works too: new Error(msg, { cause }) stores any value as err.cause (default undefined), preserved through throw/catch.
.stack/Error.captureStackTracedon't exist: console.log shows an error as Node does minus the stack's frames.class X extends Errorworks at any depth — construction,super(msg),.message/.name, fields of its own,instanceofat every level.DOMExceptioncarries no legacy numeric.code.- The error-options second argument must be a
{ cause: <expr> }object literal — a variable/computed options bag is a clean rejection;AggregateErrortakes no options (its.causereadsundefined).
new Array<T>(n)
ExactConstructs an array of length n.
new Map<K,V>()
ExactConstructs a Map.
new Set<T>()
ExactConstructs a Set.
class (fields, constructor, methods, this, new)
Works, with caveatsInstances reuse the same heap-object/GEP machinery interfaces use; methods compile to plain static calls (this as an implicit first arg). Instance-field initializers work, and a class with fields needs no explicit constructor. Static field initializers run in declaration order. Qualified constructors/bases parse.
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const p = new Point(3, 4);- A **derived** class adding fields when its base has a rest-parameter constructor requires an explicit
super(...). An instance-field initializer lowered into the constructor can currently see the constructor's own parameters (which real JS's separate initializer scope forbids), and an unannotated initializer of an unrecognized expression shape falls back toi64. - A computed/dynamic member name is a clean rejection; the recognized well-known-symbol keys are
[Symbol.asyncIterator]and[Symbol.iterator]. - A class expression used as a runtime value or a named self-reference (
class D {...}whose body referencesD) is a clean rejection;extends <expression>is out of scope. - Class early-error gaps (deferred): a
super()in a method parameter default, field-definition ASI on the same line, and the#constructorprivate-name ban all compile instead of raising the spec'sSyntaxError. - The **
readonlyfield modifier** is enforced as a compile-time immutability check — a write is allowed only in the declaring class's constructor and rejected elsewhere (method, outsideobj.x = v, nested closure, subclass constructor). Deep immutability (readonly xs: number[]thenxs.push(...)) stays unenforced, matching TS.
Constructor parameter properties
fullA visibility/readonly modifier on a constructor parameter (public/private/protected/readonly) both declares the field and assigns it from the parameter — the TypeScript shorthand for the declare-plus-assign boilerplate.
class Point {
constructor(public x: number, public readonly y: number) {}
}
const p = new Point(3, 4);
console.log(p.x, p.y);- Desugars at parse time to a declared field plus a
this.x = xconstructor prologue;private/protectedget the same enforcement as ordinary declared fields, andreadonlyis enforced (a write after construction is rejected). - The modifiers are valid only on a constructor parameter — elsewhere they are a clean parse error.
class static members / visibility / abstract / implements
Works, with caveatsstatic members are inherited through extends (a non-redeclared static field shares its base's storage). private/protected are compile-time-only (zero runtime check, matching TS erasure), checked with tsc's own diagnostics (including super.privateMethod() and private or protected constructors); o["p"] reaches a private member, as in TypeScript. abstract reuses override-detection; implements is checked by the type checker (tsc's TS2420).
- A plain object passed where another plain object type is expected is copied into that type's layout, so writes through the copy do not reach the original (a class instance keeps its identity).
Real JS/TS #x runtime-private field syntax
Works, with caveatsFields, methods, static fields/methods, and get/set accessors — exact-class-only access (the same rule private enforces), plus real reflection-hiding (Object.keys/.values/.entries/JSON.stringify/spread never see a #-named field, unlike private's type-level-only erasure).
- No class-field-initializer syntax (
#x = 1;— a pre-existing gap for every field, not#-specific). - No early-error check for a
#m/static #mname collision in the same class, or for the#constructor-is-always-banned rule.
class inheritance (extends/super)
ExactSingle inheritance, base-first field layout, super(args)/super.method(args), and an implicit pass-through constructor when a derived class adds no fields. Dispatch is static except for a method provably overridden somewhere, which goes through a per-tree vtable (decided at compile time via whole-program analysis). extends also accepts the imported EventEmitter, with or without an event-map type argument.
class X extends Promise
partialThe instance is the promise itself: await, instanceof Promise and instanceof the class, passing it where a Promise is expected, console.log as Node shows it (Timed [Promise] { 'x', label: 'timed' }), and Promise's then/catch/finally and statics (resolve, reject, all, race, allSettled, any) making instances of the class through its constructor, as Node's species does. The same holds through any.
- Only a non-generic class extends Promise:
class P<T> extends Promise<T>meets the generic-class extends limitation.
class X extends RegExp
ExactThe instance is the RegExp itself: test and exec, source, flags and lastIndex (read and written) on it and through super, the string methods that take a RegExp (replace, split, match and the rest), new RegExp(r), instanceof RegExp and instanceof the class both ways, passing it where a RegExp is expected, String(r), the same through any, and console.log as Node shows it (Slug /[^a-z0-9]+/g { label: 'slug' }).
class X extends Date
ExactThe instance is the Date itself: new D(…) and super(…) take Date's arguments, Date's getters and setters act on it, an override (toString) wins, and instanceof Date and the class both ways, String(d), +d, JSON.stringify, passing it where a Date is expected, the same through any, and console.log as Node shows it (Day 2024-02-28T00:00:00.000Z).
class X extends Array / a typed array
Works, with caveatsThe instance is the array itself: elements and length, every Array (or typed array) method on it and through super, an override reached even through a plain number[], species results (map, filter, slice, subarray) and from/of making instances of the class, Array.isArray, instanceof both ways, destructuring, spread, Object.keys/values/entries, for…in, passing it where an array is expected, the same through any, and console.log as Node shows it (Stack(2) [ 1, 2 ], Bytes(3) [Uint8Array] [ 1, 2, 3 ]). A static [Symbol.species] override is a compile error, and a species result that changes the element type (a generic map<U> override returning super.map, or Array's map through any) is a plain array.
class X extends Map / extends Set
ExactThe instance is the collection itself: Map's or Set's methods and size on it and through super, iteration and spread as the host's, instanceof Map and instanceof the class both ways, passing it where a Map or Set is expected, the same through any, and console.log as Node shows it (Visits(2) [Map] { 'Athens' => 2, x: 1 }). Extending Array or a typed array is not supported yet.
instanceof (against user-defined classes)
Works, with caveatsEvery instance carries a hidden runtime type tag. A class-typed variable whose static class is T or an ancestor of T folds to a compile-time constant; one whose static class is an ancestor of the right-hand class needs a real runtime tag read. instanceof against Array/Map/Set/Date/RegExp is a compile-time constant for a statically typed value; a value held in any is tested at run time.
x instanceof Objectis a compile-time constant — true for any non-primitive (class instance, object, array, function, Date, Map/Set, RegExp, …), false for a primitive. Only an undecidable static type (any/union/nullable) is a clean rejection.
Experimental decorators — class, property, parameter & method @decorator
Works, with caveatsExperimental class (constructor), property (target, key), parameter (target, key, index), and method (target, key, descriptor) decorators run at the class's source position — member decorators first, then class decorators (bottom-up). A method decorator receives a live descriptor whose value is the method as a callable; mutating descriptor.value or returning a new descriptor re-routes that method's calls through a per-method runtime slot. With -emit-decorator-metadata, decorated members and a decorated class's constructor get design:* metadata readable via Reflect.getMetadata.
- A class decorator that returns a replacement constructor is refused at runtime (a loud throw) — the static-class model has no constructor-replacement routing yet; observe-only class decorators (registration) run faithfully. A generic class decorator is a compile-time rejection.
- Accessor (get/set), static, and generator method decorators, and method decorators on a generic class, are rejected; so is a method whose parameter/return types can't be marshalled (array/nullable/rest params).
- Factory-call decorators (@dec(...) — the @Controller('/api')/@Get('/users') shape) work across placements, including a returned closure that captures the factory's arguments (dynamic functions now carry their captures). The standard dialect covers every member placement — class, method, instance-field, getter/setter, and accessor auto-field decorators — plus context.addInitializer (per-instance). A decorated
accessor xdesugars to a backing field + get/set and follows the TC39 { get, set, init } protocol. A decorator that *replaces* a method/getter/setter can't do typed this.field access (the replacement runs with a boxed this) — observe-only and init decorators are unaffected. - The standard (TC39) (value, context) dialect (-decorators=standard, default is experimental) runs class and method decorators; its field/accessor/getter-setter decorators and context.addInitializer are a rejected sub-stage, and TC39 has no parameter decorators. design:* metadata types are name-carrying descriptors, not the real constructors.