Language Specification
This document is the technical specification for SuperJS (SJS). It defines the type system semantics, syntax forms, compilation pipeline, and diagnostic codes. It is authoritative over the language reference for matters of precision.
1. Design Philosophy
SJS is designed around four convictions:
Sound type system. Every type error caught at compile time is a guarantee. SJS does not include escape hatches that silently undermine soundness (no any, no ! assertion). The dynamic type is the only opt-out, and it is explicit and runtime-checked.
Go-inspired simplicity. The type system has a fixed, small surface area. There are exactly 10 types. Object types are structural and satisfied implicitly. There are no mapped types, conditional types, or infer — features that make TypeScript's type system Turing-complete but also opaque.
Dart 2.12-style null safety. Non-nullable by default. T? is the only way to express nullability. The compiler tracks null flow through ?., ??, and narrowing. There is no non-null assertion operator.
Rust-inspired sum types. Variant types are a first-class construct, not a convention over discriminated union objects. match is an expression with compiler-enforced exhaustiveness.
2. The 10 Types
SJS has exactly 10 built-in types. The set is closed — new built-in types cannot be added by user code.
| SJS Type | Description | Runtime representation |
|---|---|---|
number | IEEE 754 double-precision float | JS number |
string | UTF-16 string | JS string |
boolean | true or false | JS boolean |
bigint | Arbitrary-precision integer | JS bigint |
symbol | Unique opaque symbol | JS symbol |
void | Absence of a return value | JS undefined |
null | Explicit null | JS null |
never | Unreachable / bottom type | — (no value reaches this) |
dynamic | Runtime-checked escape hatch | JS value, checked at use sites |
object T | Heap-allocated typed object | JS object |
any does not exist in SJS. Using any in an .sjs file is a parse error.
3. Null Safety Semantics
3.1 Non-nullable by default
Every type T is non-nullable unless explicitly declared T?. This includes all 10 built-in types and all user-defined classes and object types.
const x: string = null // SJS-E001
const y: string = undefined // SJS-E001
const z: string? = null // OK3.2 Nullable types
T? desugars to T | null | undefined in the type algebra, but SJS surfaces it only as T?. The distinction matters: T | null is not valid SJS syntax — write T? instead.
3.3 Null-safe operators
?. (optional chaining) and ?? (nullish coalescing) are both type-checked. The operand on the left of ?. must be T?; the result is U? where U is the property type.
const len: number? = user?.length // user must be string?
const name: string = user ?? "Anon" // result is string (non-nullable)3.4 Narrowing
The compiler tracks null flow through if/else and typeof guards. After a null check, the type is narrowed to the non-nullable variant.
const user: string? = findUser(id)
if (user !== null) {
console.log(user.toUpperCase()) // user: string here
}3.5 No non-null assertion
! is not a postfix type operator in SJS. There is no way to tell the compiler "trust me, this is not null" without an actual runtime check. This is intentional — ! is a common source of null pointer exceptions in TypeScript codebases.
4. Sum Type Syntax and Runtime Representation
4.1 Declaration syntax
type Result<T, E> = Ok(T) | Err(E)
type Shape = Circle({ radius: number }) | Rect({ w: number, h: number })
type Option<T> = Some(T) | NoneEach variant is either:
- A unit variant:
None(no payload) - A tuple variant:
Ok(T)(single positional payload) - A record variant:
Circle({ radius: number })(named payload fields)
4.2 Constructor functions
Each variant name is a constructor function at runtime:
const r: Result<number, string> = Ok(42)
const e: Result<number, string> = Err("bad input")
const s: Shape = Circle({ radius: 5 })
const n: Option<string> = None4.3 Runtime representation
The compiler emits discriminated union objects. The _tag field holds the variant name as a string literal. The payload is placed in _0 (tuple variants) or spread into the object (record variants):
| SJS expression | Emitted JS object |
|---|---|
Ok(42) | { _tag: "Ok", _0: 42 } |
Err("bad") | { _tag: "Err", _0: "bad" } |
Circle({ radius: 5 }) | { _tag: "Circle", radius: 5 } |
None | { _tag: "None" } |
SJS code never references _tag or _0 directly. These are internal to the compilation target. Use match to destructure.
5. Match Expression Semantics and Exhaustiveness
5.1 Syntax
match is an expression, not a statement. It always produces a value.
const result = match expr {
Variant1(x) => expression1,
Variant2({ a, b }) => expression2,
default => expressionDefault,
}5.2 Compilation target
Match expressions compile to IIFE switch statements on ._tag:
// SJS:
const msg = match r { Ok(val) => `Got ${val}`, Err(e) => `Failed: ${e}` }
// Compiled JS:
const msg = (() => {
switch (r._tag) {
case "Ok": { const val = r._0; return `Got ${val}`; }
case "Err": { const e = r._0; return `Failed: ${e}`; }
}
})()5.3 Exhaustiveness
When the matched expression has a sum type, the compiler verifies that every variant is covered. If any variant is missing and there is no default branch, SJS-E007 is emitted at compile time.
type Color = Red | Green | Blue
const label = match color {
Red => "red",
Green => "green",
// SJS-E007: match is not exhaustive — missing variant: Blue
}Adding default suppresses the check:
const label = match color {
Red => "red",
default => "other",
}5.4 Destructuring in arms
Tuple payload: Ok(val) binds val to r._0.
Record payload: Circle({ radius }) destructures the record fields from the variant object.
Unit variants: None matches when _tag === "None" with no binding.
6. Structural Object Types
6.1 Definition
Object types use the brace form of type (no =):
type Printable {
toString(): string
}6.2 Implicit satisfaction
A value of type C satisfies object type I if and only if C exposes every member declared in I with a compatible type. No implements declaration is required or supported.
class Celsius {
constructor(public value: number) {}
toString(): string { return `${this.value}°C` }
}
function print(p: Printable): void {
console.log(p.toString())
}
print(new Celsius(100)) // OK — Celsius satisfies Printable structurally6.3 Object type extension
Object types may extend one or more other object types. The extending type inherits all member requirements.
type Serializable extends Printable {
serialize(): string
}6.4 No intersection types
A & B is not valid SJS syntax. Use object type extension to compose contracts:
// Wrong (banned):
type Named = HasName & HasAge
// Correct:
type Named extends HasName, HasAge {}7. Generics
7.1 Syntax
Generic type parameters use angle brackets on functions, classes, and object types:
function identity<T>(x: T): T { return x }
class Stack<T> {
private items: T[] = []
push(item: T): void { this.items.push(item) }
pop(): T? { return this.items.pop() ?? null }
}
type Container<T> {
get(): T?
set(value: T): void
}7.2 Constraints
Use : TypeName to constrain a type parameter:
function max<T: Comparable>(a: T, b: T): T {
return a.compareTo(b) > 0 ? a : b
}The constraint is checked structurally — T must satisfy the Comparable object type.
7.3 Monomorphization
SJS generics are monomorphized at compile time, not type-erased. Each instantiation of a generic at a distinct type produces a distinct specialization. This means:
- Generic code has no runtime type-erasure cost.
- Type parameters are not available at runtime (no
T.name, noinstanceof T). - The compiled output is larger than a type-erased equivalent for many distinct instantiations.
7.4 Banned generic features
The following TypeScript generic features are not in SJS:
- Conditional types:
T extends U ? A : B inferkeyword- Mapped types:
{ [K in keyof T]: ... } - Template literal types:
`prefix_${T}`
8. The dynamic Type
8.1 Purpose
dynamic is the opt-out from the static type system. It exists for interoperability with untyped external data: JSON responses, third-party libraries without type definitions, and runtime-constructed objects.
8.2 Semantics
- A
dynamicvalue may hold any JavaScript value at runtime. - Accessing a property or calling a method on
dynamicsucceeds at compile time but is checked at runtime. dynamicdoes not propagate silently. Assigning adynamicto a statically typed variable requires a runtime narrowing check.- In strict mode, positions that would implicitly receive
dynamicemitSJS-W001.
function parseJSON(raw: string): dynamic {
return JSON.parse(raw)
}
const data: dynamic = parseJSON('{"count": 3}')
const count = data.count // dynamic — runtime-checked
// To use as a typed value, narrow explicitly:
if (typeof count === "number") {
const n: number = count // OK
}8.3 Difference from any
any in TypeScript is unsound — it silently opts out of type checking for all downstream expressions. dynamic in SJS is explicitly runtime-checked: the compiler inserts guards at use sites and the type does not widen surrounding expressions.
9. Compilation Pipeline
SJS compiles .sjs → .js through a hand-written pipeline — no Babel and no
TypeScript at runtime — in five ordered phases:
Phase 1: Lex
The source file is read as UTF-8 and tokenized by a hand-written lexer (numbers in all bases, templates with nested interpolation, regex-vs-division disambiguation, BiDi-control rejection).
Phase 2: Parse
A recursive-descent parser with a Pratt expression layer produces an AST that is a superset of the JavaScript AST, including SJS-specific nodes (sum type declarations, match expressions, type annotations). The parser recovers from errors so a single mistake does not abort the whole file.
Phase 3: Type check
A bidirectional type checker (synth/check) runs over the AST and emits diagnostics:
SJS-E001/SJS-E003— null/undefined assigned to a non-nullable type; access on a possibly-null valueSJS-E002— type mismatch on assignment or returnSJS-E007— non-exhaustive match on a sum typeSJS-W001— implicitdynamic(strict mode only)
Type errors do not block emission by default. Set compilerOptions.noEmitOnError to halt compilation when any error is present.
Phase 4: Lower to SJS-IR
The typed AST is lowered to SJS-IR (an ESTree-subset JavaScript AST). All type syntax is erased and SJS constructs are desugared:
- Sum type constructors → tagged objects (
{ _tag, _0 }) - Match expressions → an invoked arrow (IIFE) that switches on
_tagand binds payloads - JSX →
React.createElement(or the configured runtime); class parameter properties →this.x = x
Phase 5: Codegen & emit
A precedence-aware printer renders the IR to JavaScript at the configured ES target, generating a Source Map v3 alongside. The compiler writes one .js (and .js.map, when source maps are enabled) per input file, preserving directory structure under outDir.
10. Diagnostic Code Reference
All SJS diagnostics have stable, permanent codes. Codes are never reused after retirement.
Error codes (SJS-E)
| Code | Name | Description |
|---|---|---|
SJS-E001 | Null safety violation | A value of type null, undefined, or T? was assigned to a non-nullable binding. |
SJS-E002 | Type mismatch | The type of an expression is not compatible with the declared type at an assignment, return site, or call argument position. |
SJS-E007 | Non-exhaustive match | A match expression on a sum type is missing one or more variants and has no default arm. |
SJS-E001 example:
const name: string = null
// error[SJS-E001]: cannot assign null to non-nullable type 'string'
// --> app.sjs:1:22
// hint: use 'string?' to allow null, or assign a non-null valueSJS-E002 example:
function double(n: number): number {
return "oops"
}
// error[SJS-E002]: expected return type 'number', found 'string'
// --> app.sjs:2:10SJS-E007 example:
type Color = Red | Green | Blue
const label = match color {
Red => "red",
Green => "green",
}
// error[SJS-E007]: match is not exhaustive — missing variant: Blue
// --> app.sjs:2:15
// hint: add an arm for 'Blue', or add a 'default' armWarning codes (SJS-W)
| Code | Name | Activated by | Description |
|---|---|---|---|
SJS-W001 | Implicit dynamic | strict mode | A variable or parameter has no type annotation and would implicitly receive type dynamic. |
SJS-W001 example (with compilerOptions.strict: true):
function add(a, b) { return a + b }
// warning[SJS-W001]: parameter 'a' has implicit type 'dynamic'
// warning[SJS-W001]: parameter 'b' has implicit type 'dynamic'Diagnostic output format
Default (human-readable):
error[SJS-E001]: cannot assign null to non-nullable type 'string'
--> src/app.sjs:3:22
JSON mode (--json flag, one object per line):
{"code":"SJS-E001","severity":"error","message":"cannot assign null to non-nullable type 'string'","file":"src/app.sjs","line":3,"column":22}11. Permanently Banned Features
The following features are not part of SJS and will not be added. They are excluded by design, not omission.
| Feature | Reason for exclusion |
|---|---|
any | Unsound. Use dynamic — it is explicit and runtime-checked. |
T extends U ? A : B (conditional types) | Makes the type system Turing-complete; produces inscrutable error messages. Use sum types and match instead. |
{ [K in keyof T]: ... } (mapped types) | Produces types that are correct by construction but hard to read and diagnose. Use explicit object types. |
| Template literal types | Expressive but adds significant type checker complexity for marginal practical benefit. |
infer | Tied to conditional types; removed along with them. |
namespace | Superseded by ES modules. |
TypeScript enum | Enums have confusing runtime semantics. Use sum types — they are explicit, exhaustively matchable, and compile cleanly. |
A & B (intersection types) | Intersection of two object types is rarely what the author intends and is unsound in several positions. Use object type extension. |
! (non-null assertion) | Allows bypassing null safety without a runtime check. Use narrowing — it is both safe and readable. |
12. File Format
- Extension:
.sjs - Encoding: UTF-8
- Line endings: LF preferred
- Comments: Standard JavaScript
//and/* */ - Shebang: Supported (
#!/usr/bin/env superjs) - JSX: Enabled in all
.sjsfiles — no opt-in required