Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SSPUR for TypeScript programmers

You already think in types, unions and arrow functions, so most of SSPUR will look familiar. The differences are where SSPUR is stricter: types are checked soundly with no any, values are immutable, there is no null, and side effects and errors are part of a function’s type.

What maps to what

TypeScriptSSPUR
numberInt (64-bit, overflow traps) or F64; no implicit conversion between them
string, booleanStr, Bool
T[], readonly T[]List[T], always immutable
T | null, T | undefinedOpt[T]: some(x) or none
interface, object typea record type, type User = {id: Str, name: Str}
discriminated uniona sum type, type Shape = Circle{r: F64} | Rect{w: F64, h: F64}
switch (s.kind)match s, which must be exhaustive
(x) => x * 2x => x * 2, or _ * 2 inside a call
{...u, age: u.age + 1}u with age := u.age + 1
=== on objects==, which compares structurally
throw / try / catchraise / catch, with the error type in the signature as fail[E]
interface with methods, implementstrait and impl
<T extends Named>[T: Named]
Promise.allpar(a, b); there is no async, and no colored functions
npm installsspur add with a lock pinned by hash

Unions and switch

type Shape = { kind: "circle"; r: number } | { kind: "rect"; w: number; h: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return 3.14 * s.r * s.r;
    case "rect": return s.w * s.h;
  }
}
type Shape = Circle{r: F64} | Rect{w: F64, h: F64}

fn area(s: Shape) -> F64
= match s
  | Circle{r} => 3.14 * r * r
  | Rect{w, h} => w * h

No kind tag is needed: a variant is its own tag. If you add a Triangle variant, every match that doesn’t handle it stops compiling.

Errors and missing values

function contact(users: User[], id: string): string {
  const u = users.find(u => u.id === id);
  if (!u) return `no user ${id}`;
  return u.email ?? "no email";
}
type User = {id: Str, name: Str, email: Opt[Str]}
type LookupErr = NotFound{id: Str}

fn find_user(users: List[User], id: Str) -> User ! fail[LookupErr]
= users.find(_.id == id).ok_or(NotFound{id})

fn contact(users: List[User], id: Str) -> Str
= catch find_user(users, id).email.or("no email")
  | NotFound{id} => "no user {id}"

find_user declares ! fail[LookupErr], so every caller can see it may fail and how. contact catches every variant, so it is infallible and its signature has no !. .or("no email") plays the part of ??.

Interfaces and generics

trait Named
  fn label(x: Self) -> Str

impl Named for User
  fn label(u: User) -> Str = "{u.name} <{u.id}>"

fn labels[T: Named](xs: List[T]) -> Str
= xs.map(label).join(", ")

Traits are like interfaces, except that an impl is written apart from the type, so you can implement your trait for a type you didn’t write. Bounds are checked where the generic function is defined, not only where it is called.

$ sspur test from-typescript.ssp
2 passed, 0 failed

Things that will surprise you

  • Indentation is the block structure. No braces, no semicolons.
  • and, or, not, not &&, ||, !.
  • if c then a else b is an expression, like the ternary.
  • "{x}" interpolates, so a literal { in a string is \{.
  • x.len is a field-style call; there is no length property.
  • There are no classes and no this. Methods are functions whose first parameter is the receiver, called as x.f(a).

The handbook covers all of this in order. The agent benchmarks compare SSPUR with TypeScript on real agent tasks.