Coming from TypeScript
You already think in types. March’s type system will feel familiar — the main differences are syntax, how errors are handled, and what replaces classes.
Functions
TypeScript and March functions look similar. The key syntax difference: do...end instead of {}, no return keyword — the last expression is the result.
-- TS: const add = (x: number, y: number): number => x + y
fn add(x : Int, y : Int) : Int do x + y end
Named functions use fn, anonymous functions use fn param -> body:
let double = fn x -> x * 2
let add = fn (x, y) -> x + y
Private (module-internal) functions use pfn:
pfn validate(email : String) : Bool do
String.contains(email, "@")
end
Types look different, behave the same
TypeScript generics use <T>, March uses (a) in lowercase:
| TypeScript | March |
|---|---|
Array<string> |
List(String) |
Option<User> |
Option(User) |
Result<User, string> |
Result(User, String) |
[string, number] |
(String, Int) |
string \| null |
Option(String) |
{ name: string; age: number } |
{ name : String, age : Int } |
Type parameters on type declarations use lowercase names by convention:
type Pair(a, b) = { first : a, second : b }
type Tree(a) = Leaf | Node(Tree(a), a, Tree(a))
Sum types instead of discriminated unions
TypeScript discriminated unions:
type Shape =
| { kind: "circle"; r: number }
| { kind: "rect"; w: number; h: number }
March sum types are cleaner — no discriminant field needed:
type Shape = Circle(Float) | Rect(Float, Float) | Point
Pattern matching is exhaustive-checked:
fn area(s : Shape) : Float do
match s do
Circle(r) -> 3.14159 *. r *. r
Rect(w, h) -> w *. h
Point -> 0.0
end
end
No null / undefined — use Option
March has no nullable types. A value that might be absent is Option(a):
fn find_user(id : Int) : Option(User) do
-- returns Some(user) or None
end
match find_user(42) do
None -> "not found"
Some(user) -> user.name
end
Option.unwrap_or is the safe equivalent of ?? default:
Option.unwrap_or(find_user(42), default_user)
No try/catch — use Result
TypeScript exceptions are invisible in types. March errors are explicit:
// TS: can throw — nothing in the type tells you
async function fetchUser(id: number): Promise<User> { ... }
fn fetch_user(id : Int) : Result(User, String) do
-- Ok(user) or Err("not found")
end
let? chains fallible operations — it short-circuits on Err and returns early, like try/catch but tracked in the type system:
fn load(id : Int) : Result(Profile, String) do
let? user = fetch_user(id)
let? prefs = fetch_prefs(user.id)
Ok({ user: user, prefs: prefs })
end
This is the same as TypeScript’s async/await + try/catch pattern, but without the hidden control flow.
No classes — modules + record types
TypeScript:
class UserService {
private db: Database;
constructor(db: Database) { this.db = db; }
async getUser(id: number): Promise<User | null> { ... }
async createUser(data: CreateUserDto): Promise<User> { ... }
}
March keeps data and behavior separate. A module is the namespace; a record is the data:
mod UserService do
type Config = { db : Database }
fn get_user(cfg : Config, id : Int) : Result(User, String) do
Db.find(cfg.db, id)
end
fn create_user(cfg : Config, name : String, email : String) : Result(User, String) do
Db.insert(cfg.db, { name: name, email: email })
end
end
Interfaces via interface/impl
TypeScript structural interfaces:
interface Printable { toString(): string; }
function print<T extends Printable>(x: T) { console.log(x.toString()); }
March uses nominal interfaces with explicit impl blocks — more like Rust traits than TypeScript interfaces:
interface Show(a) do
fn show(x : a) : String
end
impl Show(Int) do
fn show(x) do int_to_string(x) end
end
fn print_it(x : a) : () needs Show(a) do
println(show(x))
end
let bindings, no const/let/var
All bindings are immutable by default. No const vs let distinction:
let x = 10 -- immutable, always
let y = x + 1
Record update syntax creates a new record instead of mutating:
let user = { name: "Alice", age: 30 }
let older = { user with age: 31 } -- user is unchanged
Concurrency & shared state
A Promise<T> maps to a Task(T): a value being computed concurrently. await
becomes Task.await, and Promise.all becomes Task.await_many.
// TypeScript
const [a, b] = await Promise.all([fetchUser(1), fetchUser(2)]);
-- March
let t1 = Task.async(fn () -> fetch_user(1))
let t2 = Task.async(fn () -> fetch_user(2))
let results = Task.await_many([t1, t2]) -- waits for both
For mutable, long-lived state (what you’d put in a class instance field), use an actor — a process that owns its state and receives messages:
| TypeScript | March |
|---|---|
Promise<T> |
Task(T) |
await p |
Task.await(t) |
Promise.all([...]) |
Task.await_many([...]) |
async function f() |
Task.async(fn () -> …) |
class with mutable fields |
actor with state { … } |
See Actors and Parallelism.
What’s the same
- Generics / type parameters (different syntax, same concept)
- Union types → sum types (pattern matching instead of narrowing)
- Modules / namespacing
- Type inference everywhere
List.map,List.filter,List.reduce— same idea, different name- Arrow functions:
fn x -> x + 1vsx => x + 1