Glyph compiles to clean TypeScript and reads almost identically, so
you can adopt it one file at a time. The difference is what it won't
let through: no any and no casts you can write, a
match that must handle every case, and record types that
validate themselves at runtime. The footguns an agent ships that
tsc --strict waves past don't compile here.
npm install -g @glyphlang/glyph
module welcome import std/result { Result, Ok, Err } type Role = | Admin | Member | Guest type User = { email: string, role: Role,} 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): { tag: "Ok"; value: Role } | { tag: "Err"; value: string } { return this.is(value) ? { tag: "Ok", value: value } : { tag: "Err", value: "expected Role" }; }, 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): { tag: "Ok"; value: User } | { tag: "Err"; value: string } { return this.is(value) ? { tag: "Ok", value: value } : { tag: "Err", value: "expected User" }; }, 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"); }}
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. Fixed formatting, one element per line, trailing commas, no reflow, 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.