Tagged Errors
One-line error classes with discriminated unions
The simplest building block — a concise way to define Error classes with discriminated unions.
The problem
Standard try/catch in TypeScript provides no type information about the error:
try {
await getUser("123")
} catch (e) {
// `e` is `unknown`. No type narrowing. No way to know what went wrong.
}Custom error classes solve this, but require significant boilerplate:
class NotFoundError extends Error {
readonly name = "NotFoundError"
readonly id: string
constructor(id: string) {
super(`Not found: ${id}`)
this.id = id
}
}Every error class needs the same boilerplate: extend Error, set name, declare fields, write a constructor, call super. Multiply this by every error in your app.
The solution
Define.Error generates these classes for you in one line:
import { Define } from "within-ts"
class NotFoundError extends Define.Error("NotFound")<{
readonly id: string
}>() {}
class ValidationError extends Define.Error("Validation")<{
readonly field: string
readonly message: string
}>() {}That's it. Each error class:
- Extends
Error(so it has.message,.stack, etc.) - Has a
_tagproperty with a literal string type ("NotFound","Validation") - Takes its fields as a single object in the constructor
- Fields are assigned directly onto the instance
Usage:
const err = new NotFoundError({ id: "123" })
err._tag // "NotFound" (literal type, not just string)
err.id // "123"
err instanceof Error // true
err instanceof NotFoundError // trueWhy _tag?
The _tag field enables discriminated unions. Use a switch to narrow the type:
function handle(err: NotFoundError | ValidationError) {
switch (err._tag) {
case "NotFound":
// TypeScript knows `err` is NotFoundError here
console.log(`Missing: ${err.id}`)
break
case "Validation":
// TypeScript knows `err` is ValidationError here
console.log(`Bad field: ${err.field}`)
break
}
}This is called a discriminated union — TypeScript narrows the type based on _tag. No instanceof chains, no type guards. Just a switch on a string.