Part 2 — Idiom changes
Syntax rewrites get files compiling; idioms determine whether migrated code stays safe and readable.
1. Error handling — throw → Result<T, E>
TypeScript often uses exceptions for expected failures. SuperJS codebases typically model those as sum types:
// TypeScript
function parseConfig(json: string): Config {
try {
return JSON.parse(json) as Config
} catch {
throw new Error("parse failed")
}
}// SuperJS
type Result<T, E> = Ok(T) | Err(E)
function parseConfig(json: string): Result<Config, string> {
const raw: dynamic = JSON.parse(json)
if (!isConfig(raw)) return Err("parse failed")
return Ok(raw as Config)
}
function isConfig(v: dynamic): boolean {
return v !== null && typeof v === "object"
}Callers use exhaustive match instead of try/catch for control flow.
2. Null handling — undefined checks → T? + narrowing
- Non-nullable by default:
stringcannot holdnull. - Nullable:
string?≡string | null. - Use
?.and??for optional chaining and defaults (same as modern JS). - Replace
value!with an explicitif (value === null)guard (SJS-E011).
function greet(name: string?): string {
if (name === null) return "stranger"
return "hello " + name
}3. Sum types — object unions → variants + match
// TypeScript
type Shape =
| { kind: "circle"; r: number }
| { kind: "rect"; w: number; h: number }
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.r * s.r
case "rect": return s.w * s.h
}
}// SuperJS
type Shape = Circle(number) | Rect(number, number)
function area(s: Shape): number {
return match s {
Circle(r) => Math.PI * r * r,
Rect(w, h) => w * h,
}
}The compiler emits SJS-E007 if a variant is not handled.
4. as casts — narrow dynamic, trust structure elsewhere
expr as Tis allowed for narrowingdynamicafter runtime checks.- Do not use
asto silence errors the way TS usesas any. - Structural object types infer field types without casts when shapes align.
5. const enum → unit sum type
const enum Dir { Up, Down }type Dir = Up | DownNo reverse numeric mapping at runtime — variants lower to tagged values.
6. Generics — no extends constraints on type parameters
SJS supports <T> type parameters with optional defaults, but not T extends U constraints (generics). Encode bounds structurally:
type HasLength { length: number; }
function count(xs: HasLength): number {
return xs.length
}If overloads differ by shape, use separate functions or a sum-type argument instead of T extends U constraints.
7. Module augmentation — not supported
TypeScript declare module "pkg" { ... } augmentation is not available. Options:
- Wrap the library in your own module and expose a typed facade.
- Use
@superjs/types-*when a wrapper exists (compat matrix). - Hold foreign values as
dynamicand validate at the boundary.
8. import type → regular import
Type-only imports merge into value imports. Types are erased at emit — no import type keyword required.
import { fastify } from "fastify"
import type { User } from "./user.sjs"Both forms parse; emitted JS contains only runtime imports.