Skip to content

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.

TypeDescription
numberFloating-point number (IEEE 754)
stringUTF-16 string
booleantrue or false
bigintArbitrary-precision integer
symbolUnique symbol
voidNo return value
nullExplicit null
neverUnreachable code path
dynamicRuntime-checked escape hatch — use instead of any
object THeap-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 string

To 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?.length

There 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) | None

Constructors 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))  // 5

Inheritance 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-checked

Assigning 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 FeatureUse Instead
anydynamic
T extends U ? A : B (conditional types)Sum types + match
{ [K in keyof T]: ... } (mapped types)Explicit object types
Template literal types
infer
namespaceES modules
TypeScript enumSum types
A & B (intersection types)Object type extension (extends)
! non-null assertionNarrowing (if (x !== null))

These are not missing features — they are deliberate omissions. SJS prioritizes a sound, predictable type system over maximum expressiveness.


Diagnostic Codes

CodeSeverityMeaning
SJS-E001ErrorNull/undefined assigned to non-nullable type
SJS-E002ErrorType mismatch on assignment or return
SJS-E007ErrorNon-exhaustive match on sum type
SJS-W001WarningImplicit 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