This is my proposal for TypeScript-Ultra-Strict: a stricter TypeScript for a world where AI writes most of the code.
This repo is a thought experiment, not a production tool.The compiler works and every example passes, but there is no semantic type-checking, no exhaustiveness checking, no published packages, and no stability promise. It exists to make the proposal concrete enough to argue with. Do not build anything you care about on it yet.
Status: working MVP. packages/cli
implements tsus check
and tsus build
, packages/runtime
implements Option
/Result
/match
and the boundary layer, packages/plugin
provides webpack/Vite/esbuild/Bun adapters, and all 29 projects under examples/
pass (npm install && npm run examples
). LEARNINGS.md
documents what broke before the toolchain existed, how that shaped the design, and the known limitations.
TypeScript's early design decisions assumed humans would write and maintain it, so a lot of features exist to make a human's life easier. We valued speed over quality. With AI writing most of our code, we can flip that tradeoff and force the language to be declarative and verifiable, like Rust.
- Removes
null
andundefined
. In their place,Option<T>
andResult<T, E>
become first-class types. This is the centerpiece: the removals below only work because these replacements exist. - Removes classes. Data is plain objects; behavior lives in functions.
- Removes
var
. - Introduces an SDK for macros, used for pattern matching and variable destructuring over
Option
,Result
, and tagged unions. (TC39 already has a pattern-matching proposal in the pipeline; this accelerates where the language is heading.)
Every existing DOM and npm API returns undefined
, throws, and hands you classes. Ultra-Strict code never sees those directly. At the boundary, a checked bindings layer converts T | undefined
and T | null
into Option<T>
, and throwing calls into Result<T, E>
. Inside the boundary, the strict rules hold everywhere.
Banning classes, nulls, and var
can be approximated with tsconfig and lint rules today. Two things need a real compiler:
- The macro SDK. This is the honest cost of the proposal: TypeScript's types are fully erasable, which is why esbuild and swc can strip them in a single fast pass. Macros break that property. The bet is that with AI writing the code, we should spend build-time budget on expressiveness and checkability rather than on keeping type-stripping trivial.
- The boundary layer, which needs type information to generate the
Option
/Result
wrappers.
Back in 2008, Google shipped Chrome and V8, and the industry built its edge compute on top of that runtime. Cloudflare Workers runs V8 isolates directly. Node, Deno, and Bun's ecosystem all trace back to it. JavaScript and ESM are the substrate that's already deployed everywhere.
TypeScript is the typed layer the ecosystem already adopted. Rather than asking everyone to move to a new language, Ultra-Strict is a subset-plus-macros of a language AI models already know deeply, targeting a runtime that's already everywhere.
Any file with a .tsus
extension (TypeScript Ultra Strict) is processed under the strict rules and compiled to plain JavaScript. .ts
and .tsus
files coexist in one project, so adoption is per-file: an AI agent can write new modules in .tsus
while the legacy .ts
code keeps working untouched. Importing a .tsus
module from .ts
just works; importing .ts
(or any npm package) into .tsus
goes through the boundary layer, which rewrites the types so nullable returns arrive as Option<T>
and throwing functions arrive as Result<T, E>
.
What a .tsus
file looks like:
import { readFile } from "node:fs/promises"; // boundary-wrapped: returns Result
type User = { name: string; email: Option<string> };
const loadUser = async (path: string): Promise<Result<User, IoError>> => {
const raw = await readFile(path, "utf8");
return raw.map(parseUser);
};
// macro from the SDK
const greeting = match(user.email) {
Some(email) => `Reach me at ${email}`,
None => "No email on file",
};
packages/plugin
(@tsus/plugin) is one transform core with a thin adapter per host, all sharing the CLI's preprocessor and checker:
@tsus/plugin/webpack
: a . In Next.js, one rule innext.config.mjs
makes.tsus
importable from any page or component (examples/27-nextjs
, verified bynext build
emitting the rendered string into static HTML).@tsus/plugin/vite
: a Vite plugin, which also covers Astro, SvelteKit, and Nuxt through theirvite.plugins
config (examples/28-astro
).@tsus/plugin/esbuild
: an esbuild plugin. Bun's plugin API is esbuild-shaped, so the same object registers withBun.plugin
; with a one-linebunfig.toml
preload,bun run
executes.tsus
imports natively with no build step (examples/29-bun
).
The adapters deliberately leave import specifiers untouched: the host bundler resolves .tsus
to .tsus
chains through its own pipeline and each hop hits the transform again. The tsus
CLI stays the standalone path (and the CI path, via tsus check --json
).
Tooling:
tsus build
compiles to JS,tsus check
type-checks without emitting. Both run as an esbuild/swc plugin so existing bundler setups pick up.tsus
files without config changes.- Editor support ships as a TypeScript language service plugin, so VS Code and every TS-aware editor get diagnostics, go-to-definition, and macro expansion previews for free.
- Diagnostics are machine-first: every error has a stable code, a JSON output mode, and a suggested fix, so an agent can consume
tsus check --json
in a loop without parsing prose. Human-readable output is a rendering of the same data. - No new package manager and no new registry. npm packages work through the boundary layer, and published
.tsus
packages ship compiled JS plus.d.ts
files, so consumers don't need to know the source language existed.
examples/
is the test suite: 29 small projects, each with an example.json
declaring what must happen. npm run examples
builds and executes all of them and fails on any drift.
Core language and runtime
| Example | Shows |
|---|---|
01-hello |
|
Smallest possible .tsus file compiling to plain ESM |
|
02-option-basics |
|
Option<T> with map /andThen /unwrapOr |
|
03-result-basics |
|
Result<T, E> with map /mapErr |
|
04-match-option , 05-match-result |
|
The match macro over Some/None and Ok/Err |
|
06-tagged-union |
|
User-defined tagged unions with a _ wildcard arm |
Rejected on purpose (these pass by failing the checker with the right code)
| Example | Code |
|---|---|
07-banned-null |
|
| TSUS002 | |
08-banned-class |
|
| TSUS001 | |
09-banned-var |
|
| TSUS004 | |
10-banned-undefined |
|
| TSUS003 | |
11-banned-throw |
|
| TSUS005 |
Interop and stress
| Example | Shows |
|---|---|
12-tsus-imports-tsus , 20-deep-chain |
|
.tsus import chains, four levels deep |
|
13-tsus-imports-ts , 14-ts-imports-tsus |
|
Mixed projects in both directions; .ts keeps its classes and nulls |
|
15-npm-dependency |
|
zod feeding Result |
|
16-boundary-fs , 26-json-boundary |
|
wrapAsync /wrapSync turning throws into Result |
|
17-nested-match , 18-generics , 19-async-result |
|
Nested matches, generic signatures, await inside match arms |
|
21-circular-imports |
|
Function-level .tsus cycles (fine, because ESM) |
|
22-mixed-project |
|
| ts + tsus + zod + boundary in one project | |
24-hostile-syntax |
|
Decoy match blocks in strings, comments, and template literals |
|
25-shadowed-match |
|
A user function named match staying a plain call |
Runtimes and frameworks
| Example | Shows |
|---|---|
23-cloudflare-worker |
|
Cloudflare { fetch } module shape, tested against real Request /Response |
|
27-nextjs |
|
Next.js 14 via the webpack ; next build renders .tsus output into static HTML |
|
28-astro |
|
| Astro 4 via the Vite plugin, verified in built HTML | |
29-bun |
|
Bun runs .tsus imports natively via a bunfig.toml preload, no build step |