zigttp includes native TypeScript and TSX support through two features: a type stripper that removes type annotations at load time, and a compile-time evaluator for the comptime() function.
Strict ZigTS is the default profile. Named functions must carry explicit
parameter and return annotations, any is rejected, capability access
must use compiler-visible literal keys, dynamic computed property access
is rejected unless the key is a literal or const literal alias, and a
let binding is only allowed when the binding is actually reassigned.
The type stripper (packages/zigts/src/stripper.zig) removes TypeScript syntax before parsing, preserving line/column positions for error reporting by replacing stripped spans with spaces.
Type declarations (stripped entirely):
typealiases (including ADT unions)distinct typedeclarations (nominal/branded types)interfacedeclarationsexport type .../export distinct type .../import type ...
Type annotations (stripped in place):
- Variable annotations:
const x: T = ...or reassignedlet x: T = ... - Parameter annotations:
function f(x: T) { ... } - Return annotations:
function f(): T { ... }
Assertions (stripped):
asassertions:value as Tsatisfiesassertions:value satisfies T
Basic generics (stripped):
- Generic params on type/interface:
type Box<T> = ... - Generic params on functions:
function id<T>(x: T): T { ... } - Generic arrow functions in .ts files:
const id = <T>(x: T): T => x;
Generic type aliases (stripped and type-checked):
Generic type aliases like type Result<T> = { ok: boolean; value: T } are stripped at load time and resolved by the type checker. When the alias is used in an annotation (const x: Result<string>), the type checker instantiates the body by substituting the type parameters with the provided arguments, producing a concrete record type for structural checking.
type Result<T> = { ok: boolean; value: T; error: string };
type Pair<A, B> = { first: A; second: B };
const auth: Result<object> = jwtVerify(token, secret); // checked as { ok: boolean; value: object; error: string }
const pair: Pair<string, number> = { first: "a", second: 1 };Up to 8 type parameters per alias are supported.
Built-in Spec<...> for proof obligations:
zigttp:types exposes a built-in generic alias Spec<S> that lets the
author narrow which compiler-proven properties their handler must satisfy.
When no Spec<...> is present, every supported v1 spec is active by
default; when a Spec<...> is present, only the named specs are active.
It is structurally a phantom marker - stripped at runtime, read at
type-check time - and rides the same alias-resolution machinery as
Result<T>. Declare a named alias and intersect it on the handler's return
type:
import type { Spec } from "zigttp:types";
type Guardrails = Spec<
| "idempotent"
| "deterministic"
| "no_secret_leakage"
| "injection_safe"
>;
function handler(req: Request): Response & Guardrails {
return Response.json({ ok: true });
}The verifier walks the return-type intersection, follows the alias to
the Spec<...> body, and emits ZTS500 / ZTS501 / ZTS502 diagnostics if
any active spec is not discharged, contradicts an import, or names a
property outside the v1 set. The proof HUD, proof ledger, and
pi_specs_status agent tool all read from this annotation.
Helper capsules with Proof<T, S>:
Proof<T, S> is the helper-level companion to Spec<S>. It annotates a
helper's return type, resolving to T for type checking while carrying
S as a proof obligation the compiler discharges against the helper's
own body:
import type { Proof } from "zigttp:types";
function fullName(u: User): Proof<string, "pure" | "total"> {
return `${u.first} ${u.last}`;
}The v1 capsule properties are total (every path returns a value),
pure, read_only, and deterministic. A helper that declares a
capsule it cannot satisfy fails with ZTS500; an unknown name fails with
ZTS502. Proven and trivially-clean helpers compose into the caller's
proof; an effectful helper with no capsule that breaks a property the
handler's Spec<...> demands fails with ZTS606.
Capability capsules with Effects<T, S>:
Effects<T, S> is the capability dual of Proof<T, S>. Where a proof
property is a guarantee the compiler discharges, an Effects<...>
annotation declares a ceiling: the function's inferred effect row may
be no wider than S. It resolves to T for type checking and carries
S - a union of capability names - as the ceiling:
import type { Effects } from "zigttp:types";
function loadRegion(): Effects<string, "env"> {
return env("REGION");
}The capability vocabulary is the runtime capability set: env, clock,
random, crypto, stderr, runtime_callback, sqlite,
filesystem, network, policy_check, websocket. The check is inferred ⊆ declared: a function that reaches a capability outside its ceiling
fails with ZTS503; an unknown capability name fails with ZTS504; a
declared capability the function never reaches is the warning ZTS505.
The same annotation on the handler's return type is a budget that also bounds every helper the handler reaches. A capability the handler reaches directly outside its budget fails with ZTS506; one a reachable helper introduces fails with ZTS607, attributed to that helper:
function handler(req: Request): Effects<Response, "env" | "clock"> {
return Response.json({ region: loadRegion() });
}Effects<...> is opt-in: a function with no annotation gets no ceiling
and no check. Because the effect marker is distinct from the proof
marker, the two capsules compose - a helper can carry both:
function makeToken(u: User): Proof<Effects<string, "crypto">, "total"> {
return sign(u.id);
}Effects<...> declares an explicit contract; Proof<...> rides
inference. Both are checked only against facts inferred from real
function bodies - an annotation never substitutes for a proof.
Docs mode (--require-export-capsules):
zigts check --require-export-capsules is an opt-in, warning-only mode
that asks every exported helper to carry an explicit capsule:
ZTS507 when an exported helper has no Effects<...>, ZTS508 when it has
no Proof<...>. It is off by default, never touches non-exported
helpers, and never changes the exit code - it documents a package's
public API surface.
// Input
type User = { id: number; name: string };
let u: User = { id: 1, name: "a" };
function add(a: number, b: number): number { return a + b; }
const x = (foo as number) + 1;
// After stripping
let u = { id: 1, name: "a" };
function add(a , b ) { return a + b; }
const x = (foo ) + 1;These produce clear error messages at strip or parse time:
| Feature | Error Location | Suggested Alternative |
|---|---|---|
any type |
Stripper | Use specific types or union types |
enum / const enum |
Parser | Use object literals or discriminated unions |
namespace / module |
Parser | Use ES6 modules |
implements |
Parser | Use duck typing or runtime checks |
@decorator syntax |
Parser | Use function composition |
Access modifiers (public, private, protected) |
Parser | Use naming conventions |
TSX is supported: JSX tags remain intact while type annotations inside { ... } expressions are stripped normally. Angle-bracket type assertions (<T>expr) are disallowed in TSX to avoid JSX ambiguity.
The type checker (packages/zigts/src/type_checker.zig) validates type annotations at build time. It runs after stripping and parsing, before bytecode generation.
- Variable declaration types match initializer types
- Function argument types match declared parameter types
- Return values match declared return types
- Property access on known record types (including
readonlyenforcement) - Virtual module function signatures (argument count and types)
- Discriminated union narrowing in
matchexpressions andifconditions - Nominal type safety for
distinct typedeclarations - Template literal type pattern matching
- Type guard narrowing (
x is T) inifbranches andassertstatements
Object literals are structurally matched against declared interface and type alias types. A { message: string, count: number } literal passes as a ResponseData interface if the fields match, regardless of whether the type was declared as type or interface.
Interfaces whose members are all functions are treated as nominal (identity-based matching only). This prevents structural forgery of capability objects.
The type checker narrows nullable types through if-guards. Functions like env(), cacheGet(), and parseBearer() return optional values (T | undefined). Three guard patterns trigger narrowing:
const val = env("KEY");
if (val) {
// val is string here (narrowed from string | undefined)
sha256(val);
}
if (!val) return Response.text("missing");
// val is string here (early return pattern)
if (val !== undefined) {
// val is string here (explicit check)
}Discriminated unions narrow through if conditions on tag fields:
type Result = { kind: "ok", value: string } | { kind: "err", error: string };
if (r.kind === "err") {
return Response.json({ error: r.error }, { status: 400 });
}
// r is narrowed to { kind: "ok", value: string } from here
r.value.toUpperCase();match handles exhaustive branching. if handles control flow with early returns. Different tools for different jobs.
Type guard functions narrow in if branches. The assert statement installs permanent forward narrowing:
function isString(x: unknown): x is string {
return typeof x === "string";
}
if (isString(val)) {
val.toUpperCase(); // narrowed in then-branch
}
assert isString(val);
val.toUpperCase(); // narrowed from here forward
assert isString(name), Response.json({ error: "name required" }, { status: 400 });When assert fails with no error expression, the handler halts. With an explicit error expression, that value is returned.
distinct type creates nominal types that prevent accidental cross-assignment:
distinct type UserId = string;
distinct type SessionId = string;
const uid: UserId = UserId("usr_123"); // constructor wraps the base type
const sid: SessionId = SessionId("sess");
function lookup(id: UserId): UserId {
return id;
}
lookup(uid); // OK
lookup(sid); // ERROR: SessionId is not assignable to UserId
lookup("raw"); // ERROR: string is not assignable to UserId
uid.toUpperCase(); // operations unwrap to base typeThe readonly modifier prevents assignment to record fields:
type Config = { readonly port: number; host: string };
const cfg: Config = { port: 3000, host: "localhost" };
cfg.host = "other"; // OK
cfg.port = 8080; // ERROR: cannot assign to readonly propertyReadonly<T> marks all fields readonly.
Template literal types validate string patterns at build time:
type ApiRoute = `/api/${string}`;
const good: ApiRoute = "/api/users"; // OK
const bad: ApiRoute = "/other"; // ERRORconst bindings preserve their literal type (const x = 200 has type 200). Use let only for bindings that are reassigned; strict ZigTS rejects an avoidable let with ZTS604.
When a const binding has a base primitive annotation, the compiler validates assignability but keeps the narrower literal type:
const port: number = 3000; // type is 3000, validated against number
const bad: number = "oops"; // ERROR: string not assignable to numberFor union annotations, the declared type is preserved to support exhaustiveness checking in match expressions.
Generic type aliases (type Result<T> = { ok: boolean; value: T }) are instantiated when used in annotations. Result<string> resolves to { ok: boolean; value: string } for structural checking. Up to 8 type parameters per alias.
The comptime() function (packages/zigts/src/comptime.zig) evaluates expressions at compile time and replaces them with literal values. It integrates with the type stripper as a pre-parse transformation.
const x = comptime(1 + 2 * 3); // -> const x = 7;
const upper = comptime("hello".toUpperCase()); // -> const upper = "HELLO";
const etag = comptime(hash("content-v1")); // -> const etag = "a1b2c3d4";
const pi = comptime(Math.PI); // -> const pi = 3.141592653589793;
const cfg = comptime({ timeout: 30 }); // -> const cfg = ({timeout:30});
const region = comptime(Env.AWS_REGION); // -> const region = "us-east-1";Literals: number, string, boolean, null, undefined, NaN, Infinity
Operators: + - * / % **, | & ^ << >> >>>, == != === !== < <= > >=, && || ??, ? :, + - ! ~ (unary)
Arrays and objects: [1, 2, 3], { a: 1, b: "x" } (comptime values only)
Math constants: Math.PI, Math.E, Math.LN2, Math.LN10, Math.LOG2E, Math.LOG10E, Math.SQRT2, Math.SQRT1_2
Math functions: abs, floor, ceil, round, trunc, sqrt, cbrt, sin, cos, tan, asin, acos, atan, atan2, log, log2, log10, exp, pow, min, max, sign, clz32, imul, fround, hypot
String properties: length
String methods: toUpperCase(), toLowerCase(), trim(), trimStart(), trimEnd(), slice(), substring(), includes(), startsWith(), endsWith(), indexOf(), charAt(), split(), repeat(), replace(), replaceAll(), padStart(), padEnd()
Built-in functions: parseInt(), parseFloat(), JSON.parse(), hash() (FNV-1a, returns 8-char hex)
Environment variables: Env.VARNAME (configured via StripOptions.comptime_env)
Build metadata: __BUILD_TIME__, __GIT_COMMIT__, __VERSION__
Variables, arbitrary function calls, Date.now(), Math.random(), new, this, eval, assignments, loops, closures.
| Error | Description |
|---|---|
ComptimeUnsupportedOp |
Operation not supported in comptime context |
ComptimeUnknownIdentifier |
Variable/function not whitelisted |
ComptimeCallNotAllowed |
Function call not allowed (e.g., Math.random()) |
ComptimeSyntaxError |
Syntax error in comptime expression |
ComptimeDepthExceeded |
Expression nesting too deep (max 64) |
ComptimeExpressionTooLong |
Expression exceeds 8KB limit |
ComptimeTypeMismatch |
Type error (e.g., string op on number) |
ComptimeDivisionByZero |
Division by zero |
- Max expression length: 8KB
- Max AST depth: 64
packages/zigts/src/stripper.zig- Type stripper with comptime integrationpackages/zigts/src/comptime.zig- Compile-time expression evaluator (~2000 lines)StripOptionscontrols features:tsx_mode,enable_comptime,comptime_env
The stripper runs as a prepass for .ts and .tsx sources before zigts parsing. Runtime parser receives JS-only output. Enable comptime via StripOptions.enable_comptime.