type narrowing
Narrow an unknown or a KitError to a specific code with hasErrorCode and metaOf, and write exhaustive code-to-behavior maps the compiler checks for you.
instanceof narrows the kit, not the code
try {
doThing()
} catch (err) {
if (err instanceof AppError) {
// err.code is `any` here, not narrowed to the registry's codes. TypeScript can't
// resolve the constructor's generic code parameter from an `instanceof` check alone.
// err.meta is Record<string, unknown>.
}
}
instanceof AppError tells you the error came from this kit. It does not narrow err.code to
a specific literal. err.code stays any, and err.meta stays Record<string, unknown>,
effectively untyped either way. Narrowing to one code needs a second check.
hasErrorCode: narrow to one code and its metadata shape
kit.hasErrorCode<C>(err: unknown, code: C): err is Error & { readonly meta: ErrorKit.Meta<R, C> }
catch (err) {
if (hasAppErrorCode(err, 'RATE_LIMITED')) {
// err.meta is now { retryAfter: number }, narrowed, no cast
setHeader('Retry-After', String(err.meta.retryAfter))
}
}
Two things make this the right tool over a manual err instanceof AppError && err.code === 'RATE_LIMITED':
- It's the only thing that actually narrows.
err.code === 'RATE_LIMITED'looks like it should narrowerr.codeto that literal, but it doesn't:err.code's declared type is alreadyanyafterinstanceof AppErroralone (see above), and comparing ananyagainst a literal doesn't narrow it. Botherr.codeanderr.metastay untyped after the manual check;hasErrorCode's type predicate is what actually narrows both. - It survives duplicate package instances. It checks
'code' in err && err.code === code, notinstanceof. In a monorepo or a dependency tree where two versions of this package ended up installed, an error constructed by one copy failsinstanceofagainst the other copy's class, a real, common failure mode forinstanceof-based checks across package boundaries.hasErrorCodedoesn't have that problem, at the cost of not verifyingerris *this specific kit's* error rather than a lookalike with a matchingcodestring. In practice, prefixed code names (see Error codes and status design) make an accidental collision between unrelated kits vanishingly unlikely.
metaOf: read a meta you've already confirmed the shape of
kit.metaOf<C>(err: KitError<R>, code: C): ErrorKit.Meta<R, C>
metaOf does no runtime check. It exists purely to attach a type to a read you've already
established is safe some other way (typically, you're inside a branch where err.code === code
is already known, but hasErrorCode's inline narrowing wasn't available, e.g. after a
switch):
switch (err.code) {
case 'RATE_LIMITED': {
const meta = kit.metaOf(err, 'RATE_LIMITED') // { retryAfter: number }
break
}
}
Reach for hasErrorCode first; metaOf is for the narrower set of cases where you already have
the narrowing some other way and just need the type.
Exhaustive code-to-behavior maps
ErrorKit.Code<R> is a plain string union, which means satisfies Record<Code, X> gives you a
compile error the moment a registry gains a code your map doesn't handle:
const STATUS_MESSAGE = {
NOT_FOUND: 'Not found.',
RATE_LIMITED: 'Too many requests, try again shortly.',
STORAGE_FAILED: 'Something went wrong on our end.',
} as const satisfies Record<ErrorKit.Code<typeof REGISTRY>, string>
Add a new code to the registry and forget to update STATUS_MESSAGE, and this line fails to
compile. The same exhaustiveness guarantee Bare<R>/Args<R, C> give the constructor, applied
to your own downstream code. This is the pattern to prefer over a switch with a default
branch, which silently swallows an unhandled code instead of failing to build.
If you do use a switch for control flow rather than a lookup table, get the same guarantee
with a never check in the default branch:
function messageFor(code: ErrorKit.Code<typeof REGISTRY>): string {
switch (code) {
case 'NOT_FOUND':
return 'Not found.'
case 'RATE_LIMITED':
return 'Too many requests, try again shortly.'
case 'STORAGE_FAILED':
return 'Something went wrong on our end.'
default: {
const _exhaustive: never = code
return _exhaustive
}
}
}