Error Messages in Type-Level DSLs
Surface helpful compile errors to DSL users.
Error Messages in Type-Level DSLs is a free TypeScript Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the TypeScript Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Helpful Errors in Type-Level DSLs
The hardest part of a type-level DSL is making failures readable. Raw never or "not assignable" errors confuse users. We surface descriptive errors using branded error types and crafted never messages.
The Problem With never
When validation fails by resolving to never, the compiler says "Argument of type X is not assignable to never", which does not tell the user why. We can do better.
Branded Error Types
Instead of plain never, return a uniquely shaped error type carrying a human-readable message string in its type.
type TypeError<Msg extends string> = {
readonly __error: Msg;
};
type E = TypeError<"Column 'foo' does not exist">;Returning Errors From Validation
A validator returns either the valid value type or a branded error describing the problem. The "has a dot" check is a template literal pattern (backticks in real code); we denote it HasDot.
// HasDot<S> is the backtick pattern: any text, ".", any text.
type Validate<S extends string> =
S extends HasDot
? S
: TypeError<"Path must contain a dot, e.g. user.name">;Forcing the Error to Surface
Constrain the parameter so that passing anything not assignable to the error brand prints the message. The error type appears directly in the compiler output.
declare function path<S extends string>(
p: Validate<S> extends TypeError<infer M> ? TypeError<M> : S
): void;
path("oops");
// Error message includes: __error: "Path must contain a dot..."Discriminating Multiple Errors
Different failures return different messages, so users get specific guidance instead of one generic rejection. HasDot is again the backtick template literal pattern for "contains a dot".
type Check<S extends string> =
S extends "" ? TypeError<"Path cannot be empty">
: S extends HasDot ? S
: TypeError<"Missing dot separator">;never With a Twist
Another technique pairs a value position with a literal message so hovering reveals it. You intersect the offending type with a labeled object.
type Invalid<M extends string> = { error: M } & never;
// using never keeps it unassignable while the label hints the causeErrors in Fluent DSLs
In a chained DSL, make an invalid next step return an error-branded type instead of a valid stage, so the editor surfaces the message right where the mistake is.
interface Stage {
// calling done() before where() yields a labeled error
done(): TypeError<"Call .where() before .done()">;
}Keeping Messages Short
Long message types bloat compiler output and slow tooling. Favor concise, actionable phrases. Include the offending token when cheap, but avoid huge interpolations.
Testing Your Errors
Write type-level tests that assert the error brand appears for known-bad inputs, so refactors do not silently degrade the developer experience.
type Expect<T extends true> = T;
type _t = Expect<Validate<"oops"> extends TypeError<any> ? true : false>;Why This Matters
A DSL is only as good as its errors. Branded error types turn cryptic never failures into self-explaining messages, dramatically improving the experience of using your type-level API.
Quick Check
Confirm your understanding of type-level error messages.
Recap
To make type-level DSLs usable, replace bare never with branded error types that embed readable messages. Validators return either the valid type or a specific error brand; constraining parameters surfaces the message in compiler output. Keep messages short and test them.
Frequently asked questions
Is the “Error Messages in Type-Level DSLs” lesson free?
Yes — the full text of “Error Messages in Type-Level DSLs” is free to read here on the web, and the TypeScript Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the TypeScript Academy course, upgrade to CoddyKit PRO.
What will I learn in “Error Messages in Type-Level DSLs”?
Surface helpful compile errors to DSL users. You practise TypeScript Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start TypeScript Academy?
No prior experience is required. TypeScript Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Error Messages in Type-Level DSLs” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this TypeScript Academy lesson?
Yes. Every TypeScript Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- What Is a Type-Level DSL
- Designing a Fluent Query DSL
- Compile-Time Input Validation
- Error Messages in Type-Level DSLs