Unions, literals, and enums
The three “choice” kinds in Sapphire — union, literal, enum — let you express “one of these shapes” or “one of these values”. They all live under the a.type() namespace because they’re builders, not stand-alone primitives. This page covers their construction, their inferred types, and how each adapter materializes them.
Union
a.type().union([f1, f2, ...]) accepts any value that successfully parses against at least one of the inner fields. The output type is the union of inner output types.
const id = a.type().union([a.string(), a.number()])
type Id = Infer<typeof id>
// Id = string | number
Adapter mapping:
| Adapter | Output |
|---|---|
| Mongo | Schema.Types.Mixed (no runtime narrowing — caveat is documented). |
| JSON Schema | oneOf: [...]. |
| Drizzle | jsonb column (Postgres) / fallback per dialect. |
Literal
a.type().literal(value) matches exactly one string, number, or boolean value. The output type is narrowed to that literal.
const kind = a.type().literal('admin')
type Kind = Infer<typeof kind>
// Kind = 'admin'
Enum
a.type().enum(values) accepts either a readonly array of literals (use as const) or a TypeScript enum object.
const role = a.type().enum(['admin', 'editor', 'viewer'] as const)
type Role = Infer<typeof role>
// Role = 'admin' | 'editor' | 'viewer'
With a TS enum:
enum Status {
Active = 1,
Inactive = 2,
}
const status = a.type().enum(Status)
// _output is Status (a numeric union); reverse mappings ('1','2' → name)
// generated by TS are filtered before being baked into the IR.
TS numeric enums compile to { Active: 1, Inactive: 2, '1': 'Active', '2': 'Inactive' }. Sapphire’s extractEnumValues drops keys that match /^\d+$/ so only the forward direction makes it into the IR — string values are NOT accepted as members of a numeric enum.
Pitfalls
[!WARNING]
literal('admin')andenum(['admin'] as const)have the same_output. For a single value the distinction is cosmetic — pick by intent. Useliteralwhen the value is a tag (discriminant in a discriminated union, MCP tool’stypefield); useenumwhen membership is the point and you expect to grow the list.
[!WARNING] Always pass
as consttoenum(...). Without it,['admin','editor']widens tostring[]and you’ll getRole = stringinstead of the narrowed union.as constpreserves the literal types end-to-end.
Related
- Fields and modifiers — the
a.type()builder reference. - Inferring types.
- Recipes → Share types with the frontend.