You write Submilli programs in TypeScript. Submilli makes deliberate choices
about which TypeScript features it supports and how some of them behave.
Without TypeScript’s need to accommodate existing JavaScript code, it can
enforce stricter checks. It excludes any, and as checks that a value
matches its type. This page describes the shape of a program and those
choices. The globals a program has are on
Built-ins. The submilli: modules are on
Standard library.
A program
Section titled “A program”A program is one .ts file with a function named main, which takes no
parameters and declares its return type. Top-level statements run first,
then main. What main returns is the program’s result.
import * as fs from "submilli:fs";
interface Ticket { id: string; hours: number;}
function main(): { count: number; hours: number } { const tickets = JSON.parse(fs.readText("/tickets.json") ?? "[]") as Ticket[]; console.log(`read ${tickets.length} tickets`); return { count: tickets.length, hours: tickets.reduce((sum, t) => sum + t.hours, 0) };}main returns |
The result is |
|---|---|
string |
The string |
number, boolean |
Its text, such as 5 or true |
| An object or array | JSON, with object keys sorted |
void |
Nothing |
console.log writes to a separate log, not to the result. submilli run <file> runs a program. submilli check <file> only compiles it.
Modules are imported by name: submilli:<name> for the standard library,
@<org>/<name> for installed Packages, and @mcp/<server> for MCP servers
a Blueprint declares. Namespace (import * as fs), default
(import fs), and named (import { sha256 }) imports all work. Built-ins
need no import. Importing a module grants nothing. Every gated call is
checked against the Blueprint.
Stricter by design
Section titled “Stricter by design”unknown instead of any
Section titled “unknown instead of any”JSON.parse and response.json() return unknown, and Submilli has no
any, so data from outside can’t bypass type checking and fail later in the
program. Narrow an unknown value with typeof, Array.isArray,
instanceof, in, or === null, or cast it with as:
const tickets = JSON.parse(text) as Ticket[];as checks the value
Section titled “as checks the value”A cast checks the value when it runs: every required field present with its
type, every array element matching, extra fields allowed. Data of the wrong
shape throws TypeError at the cast, where it arrived. A cast doesn’t
convert, so the string "3" stays a string.
One absent value: null
Section titled “One absent value: null”Submilli has no undefined, so a missing value is always null, typed
T | null. An omitted optional property, a missing Map key, and a missing
record key all read as null.
| Write | Means |
|---|---|
if (x !== null) |
x is T inside the branch |
x?.field, x?.method() |
null when x is null |
x ?? fallback |
fallback when x is null |
x! |
x as T. Throws TypeError when it is null |
Calls return their result
Section titled “Calls return their result”Every call into the standard library or a Package returns its result
directly, with no async, await, or Promise, so a program reads top to
bottom:
const page = jina.read(url);fs.writeText("notes.md", summarize(page));Record<string, V> for runtime keys
Section titled “Record<string, V> for runtime keys”An object whose keys are known only at runtime is a Record<string, V>, and
reading a missing key gives null:
const counts: Record<string, number> = {};for (const word of words) { counts[word] = (counts[word] ?? 0) + 1;}Other differences
Section titled “Other differences”| Feature | In Submilli |
|---|---|
== and != |
Strict, like === and !==, so 1 == "1" is a compile error |
| Return types | Declared on functions and methods; arrow functions infer them |
items[i] |
Out-of-range reads and writes throw RangeError. push appends |
| Runtime APIs | The standard library and Submilli Packages, in place of Node.js APIs, browser globals, and npm |
| Dates and times | Temporal, in place of Date |
Symbol, Proxy, prototype reflection |
Not available |
Errors
Section titled “Errors”A compile error shows the line, marks the position, and says how to fix it. For a misspelled field it names the closest one and prints the type’s declaration:
error: field `amout` does not exist on `Credit` --> credit.ts:15:29 |14 | const credit = applyCredit("cus_northwind", 1500);15 | return `credited ${credit.amout} cents`; | ^^^^^16 | } |help: did you mean `amount`? |help: /** A credit added to a customer's account. */interface Credit { /** Amount in cents. */ amount: number; /** The customer who received the credit. */ customerId: string;}A runtime error names the error and shows the call stack and the line that
threw. A call the Blueprint doesn’t allow throws PermissionDeniedError
(Permissions).
Looking declarations up
Section titled “Looking declarations up”| Command | Prints |
|---|---|
submilli search <query> |
Modules and Packages whose name, description, or exports match, such as submilli search writeText |
submilli docs <module> |
A module’s declarations, such as submilli docs submilli:http. With --blueprint <file>, only what that Blueprint’s programs can import |
submilli builtins |
The built-in types and namespaces |
submilli builtins <name> |
A built-in’s declarations, or one member, such as submilli builtins Map.get |
$ submilli builtins Map.getinterface Map<K, V> { /** * Returns the value associated with `key`, or `null` if the key is not present. * @param key The key to look up. */ get(key: K): V | null;}