Compile your first program
In this first tutorial we will install nothing new, write a tiny Bynk program,
compile it to TypeScript, and read what the compiler produced. By the end you
will have run bynkc end to end and seen the shape of its output.
We assume you have already installed bynkc. Check
that it is on your path:
bynkc --helpStarting a real project? The fastest way from nothing to a running service is
bynk new:bynk new hello && cd hello && bynk devscaffolds and serves a complete project. This tutorial deliberately takes the other path — a single bare file, compiled by hand — to show you whatbynkcproduces and how a Bynk source maps to TypeScript.
Write a program
Section titled “Write a program”Create a file called demo.bynk with this content:
commons demo { type Id = Int}That is a complete, valid Bynk program. commons demo { … } declares a module
named demo; inside it we declare one type, Id, an alias for the built-in
Int.
Compile it
Section titled “Compile it”Ask bynkc to compile the file to TypeScript:
bynkc compile demo.bynk --output demo.tsIf all is well the command prints nothing and exits successfully. Open
demo.ts and read it:
// Generated by bynkc — do not edit by hand.// commons demo
import { Ok, Err, Some, None, type Result, type Option, type ValidationError } from "./runtime.js";
export type Id = number & { readonly __brand: "Id" };
export const Id = { of(value: number): Result<Id, ValidationError> { if (!Number.isInteger(value)) { return Err({ field: "Id", message: "must be an integer", value }); } return Ok(value as Id); }, unsafe(value: number): Id { return value as Id; },};Notice that one line of Bynk produced more than a bare type alias. Id is a
branded type — number & { readonly __brand: "Id" } — so an Id is not
interchangeable with any other number. The compiler also generated two
constructors, Id.of and Id.unsafe. You will meet both properly in
Tutorial 4; for now, just note that Bynk types carry an
identity, even when they look like plain aliases.
Add a function
Section titled “Add a function”Let us make the program do something. Replace the contents of demo.bynk with:
commons demo { type Id = Int
fn classify(n: Int) -> String { if n < 10 { "small" } else if n < 100 { "medium" } else { "large" } }}fn classify(n: Int) -> String { … } declares a function taking an Int and
returning a String. Bynk is expression-oriented: the if/else if/else is
itself an expression, and its value is the function’s result — there is no
return keyword.
Compile again:
bynkc compile demo.bynk --output demo.tsThe new function appears at the bottom of demo.ts:
export function classify(n: number): string { return (n < 10 ? "small" : (n < 100 ? "medium" : "large"));}Int became number, String became string, and the if-expression became
a conditional expression with an explicit return. The TypeScript is meant to
be read: there is no hidden runtime magic in commons.
Type-check without emitting
Section titled “Type-check without emitting”While editing, you often just want to know whether the program is valid without
writing any output. That is what bynkc check is for:
bynkc check demo.bynkIt runs the same analysis as compile but stops before code generation. It
exits successfully on a valid program and reports diagnostics otherwise. Try
introducing a mistake — say, changing the function body to n + "oops" — and
run check again to see a Bynk diagnostic (+ requires Int operands, so
mixing in a String is rejected).
What you have done
Section titled “What you have done”You wrote a small commons module with a type and a function, compiled it to
readable TypeScript, and learned the difference between compile and check.
Next, we leave single files behind and build something that runs: a small HTTP service.
➡️ Tutorial 2: Build a small HTTP service
Want the bigger picture behind commons, branding, and compiling to
TypeScript? See How a Bynk program is shaped
and Why compile to TypeScript.