Glyph compiles to TypeScript you can read, so you adopt it one file at a time and keep node, npm and everything you already deploy. What it adds is a compiler that answers questions about your program instead of leaving an agent to rediscover them with grep. Add a case to a union and it names every match that will fail, and every match that will keep compiling while silently swallowing it.
npm install -g @glyphlang/glyph
module welcome import std/result { Result, Ok, Err } pub type Role = | Admin | Member | Guest pub type User = { email: string, role: Role,} pub fn welcome(user: User) -> Result<string, string> { return match user.role { Admin => Ok("Welcome back, admin"), Member => Ok("Hello, ${user.email}"), Guest => Err("guests cannot sign in"), }}
import { schema as __glyph_schema } from "std/schema"; import { Result, Ok, Err } from "std/result"; export type Role = | { tag: "Admin" } | { tag: "Member" } | { tag: "Guest" }; export const Admin: Role = { tag: "Admin" };export const Member: Role = { tag: "Member" };export const Guest: Role = { tag: "Guest" };export const Role = { is(value: unknown): value is Role { if (typeof value !== "object" || value === null) { return false; } switch ((value as { tag?: unknown }).tag) { case "Admin": return true; case "Member": return true; case "Guest": return true; default: return false; } }, parse(value: unknown): Result<Role, Issue[]> { return this.is(value) ? Ok(value) : Err([{ path: [], message: "expected Role", code: "type" }]); }, schema: __glyph_schema<Role>("Role", (v): v is Role => Role.is(v)),}; export type User = { email: string; role: Role };export const User = { is(value: unknown): value is User { return typeof value === "object" && value !== null && typeof (value as Record<string, unknown>).email === "string" && Role.is((value as Record<string, unknown>).role); }, parse(value: unknown): Result<User, Issue[]> { return this.is(value) ? Ok(value) : Err([{ path: ["email"], message: "field `email` must be string", code: "type" }]); }, schema: __glyph_schema<User>("User", (v): v is User => User.is(v)),}; export function welcome(user: User): Result<string, string> { const __m0 = user.role; switch (__m0.tag) { case "Admin": { return Ok("Welcome back, admin"); } case "Member": { return Ok(`Hello, ${user.email}`); } case "Guest": { return Err("guests cannot sign in"); } default: throw new Error("non-exhaustive match"); }}
examples/apps/csvql is a CSV query engine in this
repository, eleven files, with a Value union at its
centre. Here is what happens when you add a case to it.
10 match sites across 4 files 8 will fail compilation 2 contain a catch-all and will silently absorb it FAILS value.glyph:18 value::render FAILS value.glyph:32 value::key FAILS bind.glyph:148 bind::literal_kind ABSORBS exec.glyph:130 exec::total ABSORBS render.glyph:128 render::literal_text ...then add the case for real, and run glyph check
8 errors, E0200 non-exhaustive match.
The same eight.
The two catch-all sites report nothing at all.
The prediction was exact, eight of eight. The interesting number is the
other one. exec::total and render::literal_text
keep compiling and route the new case wherever their catch-all points.
No build, no test and no type error will ever mention them. An agent
that fixes the eight failures and stops has a green tree and two live
bugs, and this is the part a language server cannot tell it.
tsc --strict
Every claim on this site has a pair of files behind it: a
.ts that type-checks clean under tsc --strict,
and the same program in Glyph that does not compile, with the error code
named. A build gate re-runs all of them and fails if either half stops
behaving, so a guarantee that quietly disappears takes the build with
it.
catches: 7/7 verified both ways (greppability: 1, verifiability: 6).
Glyph reads almost identically. You don't learn a new language, just a handful of deltas. Each one trades a footgun for a guarantee. Here are the ones in the sample above.
match
an exhaustive switch: the compiler makes you handle every case. There is no if/else; match is the only conditional, so a case can't be silently forgotten.
Result<T, E>
errors as return values, not thrown exceptions. You handle one with match, or propagate it with the ? operator.
fn f() -> T
a function; the -> is its return type. A type X = {...} is a record, and a type X = A | B is a tagged union.
mut x = e
reassignment is explicit and greppable. A plain let binding never changes under you; you write mut to change one.
"Agents" here means AI coding assistants (Claude, Copilot, Cursor). Glyph helps them most, but the guarantees are plain engineering wins whether or not you use one, and if you already run strict TypeScript with zod and eslint, here's what a language buys over that →
If you build with AI agents on a TypeScript codebase, you know the daily cost. Glyph is TypeScript with these footguns removed.
any and as unknown as T, and the compiler lets them.
grep for a symbol and find ten unrelated matches: overloads, decorators, merged namespaces.
Glyph removes each one, by construction.
In order of what matters most. The first two are the wedge, the problems you feel every day. The last two are the polish that makes it pleasant.
No any and no as you can write; match
must cover every case; and your record types generate strict runtime
validators, so a boundary value is checked, not assumed. An enforced
strict layer over tsc: the footguns an agent ships that
tsc --strict waves through don't compile here.
One name, one place. Every symbol has exactly one syntactic form at
its declaration, with no overloads, decorators, implicit this,
or namespace merging. grep "fn parseUser" finds the
definition. Always.
A one-line change is a one-line diff. One canonical layout: a list is inline while it fits, one element per line when it doesn't, never repacked, trailing commas, no barrel files. Agents stop rewriting whole files; reviewers read the change, not the churn.
Read the story →
Say what you mean: pattern matching over switch ladders,
Result over thrown exceptions, named records over
tuples, from a small orthogonal core rather than TypeScript's accreted layers.
Real questions, straight answers: what works today, and what's next. Read them as one guided walk-through.