Documentation index
Russian translation
@modulify/validator exposes two related layers around machine-readable codes:
- exact literal codes in assertion descriptors and structured violations;
- an extensible global registry that can be used to derive a project-wide union of known codes.
This guide explains when each layer is useful, how they work together, and how to extend them safely.
The public entrypoints are:
ViolationCodeEntry- a compact contract record for a known code;ViolationCodeRegistry- an interface that collects known codes;ViolationCode- a union extracted fromkeyof ViolationCodeRegistry;ViolationArgs<C>,ViolationKindOf<C>, andViolationNameOf<C>- code-driven utility types.
The package ships built-in keys for its own built-in violations, for example:
'type.string''length.min''shape.unknown-key''runtime.rejection'
So the following works out of the box:
import type { ViolationCode } from '@modulify/validator'
const code: ViolationCode = 'type.string'Built-in assertions now preserve their exact code literals in introspection.
import {
describe,
hasLength,
isString,
} from '@modulify/validator'
const stringDescriptor = describe(isString)
const lengthDescriptor = describe(hasLength({ min: 3 }))In TypeScript this means:
stringDescriptor.codeis typed as'type.string';stringDescriptor.argsis typed as[];lengthDescriptor.codeis typed as'length.unsupported-type';lengthDescriptor.constraints[number].codeis typed as a concrete length-code union instead of plainstring.
This is useful when adapters inspect descriptors and want code-aware branching without hand-written casts.
Exact literals on individual values are good for local introspection.
The global registry solves a different problem: extracting one reusable union for the whole app.
import type { ViolationCode } from '@modulify/validator'
type AppViolationCode = ViolationCodeThat union can be reused in:
- message dictionaries;
- analytics payload contracts;
- API error envelopes;
- UI error-state mappers;
- shared helper utilities.
The registry is designed for module augmentation and now stores small code contracts.
import type { ViolationCodeEntry } from '@modulify/validator'
import '@modulify/validator'
declare module '@modulify/validator' {
interface ViolationCodeRegistry {
'user.email.taken': ViolationCodeEntry<'validator', 'user', readonly []>;
'profile.password.mismatch': ViolationCodeEntry<'validator', 'shape', readonly []>;
}
}After that:
import type { ViolationCode } from '@modulify/validator'
const codeA: ViolationCode = 'user.email.taken'
const codeB: ViolationCode = 'profile.password.mismatch'This lets you define project-specific codes once and reuse the extracted union everywhere else.
If you still have older augmentations that use never, they continue to contribute to ViolationCode, but they fall back to generic kind / name / args typing until you migrate them to ViolationCodeEntry.
Once a code is registered with a contract entry, it becomes a key into the related type information.
import type {
ViolationArgs,
ViolationKindOf,
ViolationNameOf,
ViolationSubject,
} from '@modulify/validator'
type PasswordArgs = ViolationArgs<'profile.password.mismatch'>
type PasswordKind = ViolationKindOf<'profile.password.mismatch'>
type PasswordName = ViolationNameOf<'profile.password.mismatch'>
type PasswordSubject = ViolationSubject<'profile.password.mismatch'>That means:
PasswordArgsbecomesreadonly [];PasswordKindbecomes'validator';PasswordNamebecomes'shape';PasswordSubjectgets the matchingkind,name,code, andargs.
Custom assertions can keep their own explicit code literals.
import { assert } from '@modulify/validator/assertions'
const isAvailableEmail = assert(
(value: unknown): value is string => typeof value === 'string' && value.includes('@'),
{
name: 'isAvailableEmail',
bail: true,
code: 'user.email.taken',
}
)Then describe(isAvailableEmail).code is typed as 'user.email.taken'.
This part does not depend on the global union extraction. The literal is preserved directly from the assertion definition.
The same idea applies to object-level refinement issues.
import type { ObjectShapeRefinementIssue } from '@modulify/validator'
import {
isEmail,
isString,
shape,
} from '@modulify/validator'
const signUpForm = shape({
email: [isString, isEmail],
password: isString,
confirmation: shape({
password: isString,
}),
}).refine(value => {
if (value.password === value.confirmation.password) {
return []
}
return [{
path: ['confirmation', 'password'],
code: 'profile.password.mismatch',
args: [],
}] satisfies ObjectShapeRefinementIssue<'profile.password.mismatch'>
})That keeps the refinement code aligned with the same registry-backed union used elsewhere.
Many consumers want a small helper layer that maps codes into rendering or transport concerns.
import type {
Violation,
ViolationCode,
} from '@modulify/validator'
const labels: Partial<Record<ViolationCode, string>> = {
'type.string': 'Expected a string',
'length.min': 'Value is too short',
'user.email.taken': 'Email is already taken',
}
function toLabel(violation: Violation) {
return labels[violation.violates.code as ViolationCode] ?? violation.violates.code
}You do not need to force every possible code into one giant exhaustive map. Partial<Record<ViolationCode, ...>> is often the most practical option.
These two behaviors are complementary:
- built-in and augmented codes appear in the reusable
ViolationCodeunion; - explicit custom literals are preserved directly where values are created, such as
assert(...)or typed refinement issues.
That distinction is important.
If you define a custom literal but do not augment ViolationCodeRegistry:
- local descriptor and violation values can still carry that exact literal;
- the global
ViolationCodeunion will not include it yet.
If you augment ViolationCodeRegistry with never instead of ViolationCodeEntry:
- the global
ViolationCodeunion will include the code; - the code still falls back to generic
kind,name, andargstyping.
So the usual recommendation is:
- define the custom code where the violation is produced;
- add it to
ViolationCodeRegistry; - reuse
ViolationCodein adapters and app-level helper types.