Language Reference
SuperJS (SJS) is a strict, type-safe superset of JavaScript that follows the ECMAScript standard — every valid .js file is valid .sjs, with every feature ES5 through ES2025 type-checked. It has a sound static type system, null safety, sum types, match expressions, and JSX on by default. It compiles to clean JS today (native binaries and WASM are on the roadmap), and is not TypeScript with a different extension.
Basic Types
SJS has exactly 10 built-in types. There is no any.
| Type | Description |
|---|---|
number | Floating-point number (IEEE 754) |
string | UTF-16 string |
boolean | true or false |
bigint | Arbitrary-precision integer |
symbol | Unique symbol |
void | No return value |
null | Explicit null |
never | Unreachable code path |
dynamic | Runtime-checked escape hatch — use instead of any |
object T | Heap-allocated typed object |
const message: string = "Hello, World!"
const count: number = 42
const active: boolean = true
const big: bigint = 9007199254740993n
function greet(name: string): string {
return `Hello, ${name}!`
}Type Annotations
Type annotations are optional. When omitted, SJS infers the type from context. In strict mode, missing annotations on public API boundaries emit SJS-W001.
// annotated
const x: number = 10
// inferred — x is still typed as number
const y = 10
// function with full annotations
function add(a: number, b: number): number {
return a + b
}
// inferred return type
function addInferred(a: number, b: number) {
return a + b // inferred: number
}Null Safety
Non-nullable by default. A variable of type string cannot hold null or undefined. This is enforced at compile time.
const name: string = null // SJS-E001: null not assignable to stringTo allow null, use T? (nullable):
function findUser(id: number): string? {
if (id === 1) return "Alice"
return null // OK — return type is string?
}
const user: string? = findUser(42)Use ?? (nullish coalescing) and ?. (optional chaining) to work with nullable values. Both are type-checked against T?.
const display: string = user ?? "Unknown"
const length: number? = user?.lengthThere is no ! non-null assertion operator. Use narrowing instead:
if (user !== null) {
// user is narrowed to string here
console.log(user.toUpperCase())
}Sum Types
Sum types (tagged unions / variant types) are a first-class SJS feature. They use a syntax distinct from TypeScript discriminated unions.
type Result<T, E> = Ok(T) | Err(E)
type Shape = Circle({ radius: number }) | Rect({ w: number, h: number })
type Option<T> = Some(T) | NoneConstructors are callable as functions:
const success: Result<number, string> = Ok(42)
const failure: Result<number, string> = Err("something went wrong")At runtime, sum type values compile to { _tag: "Ok", _0: 42 } discriminated objects. SJS code never accesses _tag or _0 directly — use match expressions instead.
Match Expressions
match is an expression (it returns a value) used to destructure sum types. The compiler enforces exhaustiveness — if a variant is missing and there is no default branch, SJS-E007 is emitted.
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return Err("division by zero")
return Ok(a / b)
}
const r = divide(10, 2)
const msg = match r {
Ok(val) => `Result: ${val}`,
Err(e) => `Error: ${e}`,
}Destructuring works for variants with payload objects:
type Shape = Circle({ radius: number }) | Rect({ w: number, h: number })
const area = match shape {
Circle({ radius }) => Math.PI * radius * radius,
Rect({ w, h }) => w * h,
}Use default for partial matches:
const label = match status {
Ok(_) => "success",
default => "failure",
}Structural Object Types
SJS object types are satisfied implicitly — Go-style. A type satisfies an object type if it has all the required members. No implements keyword is needed or supported. Object types use the brace form of type (no =).
type Shape {
area(): number
perimeter(): number
}
class Circle {
constructor(public radius: number) {}
area(): number { return Math.PI * this.radius ** 2 }
perimeter(): number { return 2 * Math.PI * this.radius }
}
class Rect {
constructor(public w: number, public h: number) {}
area(): number { return this.w * this.h }
perimeter(): number { return 2 * (this.w + this.h) }
}
// Both Circle and Rect satisfy Shape — no declaration needed
function printShape(s: Shape): void {
console.log(`Area: ${s.area()}, Perimeter: ${s.perimeter()}`)
}
printShape(new Circle(5))
printShape(new Rect(4, 6))Object types can extend other object types:
type Printable {
toString(): string
}
type Serializable extends Printable {
serialize(): string
}Intersection types (A & B) are banned. Compose object types with extends instead.
Generics
Generics use angle-bracket syntax and are monomorphized at compile time.
function identity<T>(x: T): T {
return x
}
function max<T: Comparable>(a: T, b: T): T {
return a.compareTo(b) > 0 ? a : b
}Generic classes:
class Stack<T> {
private items: T[] = []
push(item: T): void {
this.items.push(item)
}
pop(): T? {
return this.items.pop() ?? null
}
peek(): T? {
return this.items[this.items.length - 1] ?? null
}
get size(): number {
return this.items.length
}
}
const s = new Stack<number>()
s.push(1)
s.push(2)
const top: number? = s.pop() // number?Generic object types:
type Container<T> {
get(): T?
set(value: T): void
}Banned generic features: conditional types (T extends U ? A : B), infer, mapped types ({ [K in keyof T]: ... }), and template literal types are not part of SJS.
Classes
SJS classes are standard JavaScript classes with type annotations. Constructor parameter shorthand is supported.
class Point {
constructor(
public x: number,
public y: number
) {}
distanceTo(other: Point): number {
return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2)
}
toString(): string {
return `Point(${this.x}, ${this.y})`
}
}
const p1 = new Point(0, 0)
const p2 = new Point(3, 4)
console.log(p1.distanceTo(p2)) // 5Inheritance uses standard extends:
class Animal {
constructor(public name: string) {}
speak(): string { return `${this.name} makes a sound` }
}
class Dog extends Animal {
speak(): string { return `${this.name} barks` }
}JSX
JSX is on by default in SJS — no pragma or config needed.
type ButtonProps {
label: string
onClick: () => void
disabled?: boolean
}
function Button({ label, onClick, disabled = false }: ButtonProps) {
return <button onClick={onClick} disabled={disabled}>{label}</button>
}
function App() {
return (
<div>
<h1>Hello</h1>
<Button label="Click me" onClick={() => console.log("clicked")} />
</div>
)
}The JSX transform targets the React 17+ automatic runtime by default. Configure the runtime in superjs.config.json.
Modules
SJS uses standard ES module syntax:
// Named exports
export function add(a: number, b: number): number {
return a + b
}
export class Calculator {
// ...
}
// Default export
export default class App {
// ...
}
// Type-only import (erased at compile time)
import type { UserRecord } from './types'
// Value imports
import { readFileSync } from 'fs'
import { add, Calculator } from './math'
import App from './app'Re-exports:
export { add } from './math'
export type { UserRecord } from './types'
export * from './utils'Imports resolve to the exporting module's real types — named, default,
namespace (import * as M), and export … from re-exports all carry types
across files. Relative specifiers resolve against the importing file; bare
specifiers resolve through superjs.config.json paths (how superjs add
wires in package types). An unresolved specifier leaves its bindings dynamic
rather than erroring.
The dynamic Type
dynamic is the runtime-checked escape hatch. Use it when interfacing with untyped external data (JSON responses, third-party libraries without types). It is not any — accesses on dynamic values are checked at runtime and do not silently propagate through the type system.
function parseConfig(raw: string): dynamic {
return JSON.parse(raw)
}
const config: dynamic = parseConfig('{"port": 3000}')
const port = config.port // runtime-checkedAssigning a dynamic value to a typed variable requires an explicit narrowing check. Unlike any, dynamic never silently widens the types of surrounding expressions.
What Is Banned (and Why)
These features from TypeScript are permanently excluded from SJS to keep the type system sound and simple:
| Banned Feature | Use Instead |
|---|---|
any | dynamic |
T extends U ? A : B (conditional types) | Sum types + match |
{ [K in keyof T]: ... } (mapped types) | Explicit object types |
| Template literal types | — |
infer | — |
namespace | ES modules |
TypeScript enum | Sum types |
A & B (intersection types) | Object type extension (extends) |
! non-null assertion | Narrowing (if (x !== null)) |
These are not missing features — they are deliberate omissions. SJS prioritizes a sound, predictable type system over maximum expressiveness.
Diagnostic Codes
| Code | Severity | Meaning |
|---|---|---|
SJS-E001 | Error | Null/undefined assigned to non-nullable type |
SJS-E002 | Error | Type mismatch on assignment or return |
SJS-E007 | Error | Non-exhaustive match on sum type |
SJS-W001 | Warning | Implicit dynamic — only in strict mode |
CLI Quick Reference
superjs build src/index.sjs # compile to JS
superjs build --watch # watch mode
superjs lint src/ # lint
superjs format src/ # format
superjs test # run tests