AnyOf
Describes a union of types, i.e. a range of possible types:
import { anyOf } from 'dynamodb-toolbox/schema/anyOf'
const pokeTypeSchema = anyOf(
string().const('fire'),
string().const('grass'),
string().const('water')
)
type PokeType = FormattedValue<typeof pokeTypeSchema>
// => 'fire' | 'grass' | 'water'
In this example, an enum
would have done the trick. However, anyOf
becomes particularly powerful when used in conjunction with a map
and the enum
or const
directives of a primitive attribute, to implement polymorphism:
const captureSchema = anyOf(
map({
status: string().const('caught'),
// 👇 captureState.trainerId exists if status is "caught"...
trainerId: string()
}),
// ...but not otherwise! 🙌
map({ status: string().const('wild') })
)
// Discriminate on string enum attributes for faster parsing! 🙌
const fasterSchema = captureSchema.discriminate('status')
type Capture = FormattedValue<typeof captureSchema>
// =>
// | { status: "caught"; trainerId: string }
// | { status: "wild" }
In the absence of discriminating attribute, the parsing an anyOf
schema value returns the parsed output of the first sub-schema it validates against.
This means the order of the sub-schemas matters: you should always start with the strictest schemas.
For the moment, anyOf
properties can only be set by using methods.
AnyOf elements can have any type. However, they must respect some constraints:
- They cannot be
optional
or always required - They cannot be
hidden
orkey
(tagging theanyOf
itself askey
is enough) - They cannot have
default
orlinks
// ❌ Raises a type AND a run-time error
const union = anyOf(number(), string().optional())
const union = anyOf(number(), string().hidden())
const union = anyOf(number(), string().key())
const union = anyOf(number(), string().default('foo'))
Properties
.required()
string | undefined
Tags schema values as required (within items
or maps
). Possible values are:
'atLeastOnce' (default)
: Required (starting value)'always'
: Always required (including updates)'never'
: Optional
const pokeTypeSchema = anyOf(
string().const('fire'),
string().const('grass'),
string().const('water')
).required()
// shorthand for `.required('never')`
const pokeTypeSchema = anyOf(...).optional()
.hidden()
boolean | undefined
Omits schema values during formatting:
const pokeTypeSchema = anyOf(
string().const('fire'),
string().const('grass'),
string().const('water')
).hidden()
.key()
boolean | undefined
Tags schema values as a primary key attribute or linked to a primary key attribute:
// Note: The method also sets the `required` property to 'always'
// (it is often the case in practice, you can still use `.optional()` if needed)
const pokeTypeSchema = anyOf(
string().const('fire'),
string().const('grass'),
string().const('water')
).key()
.savedAs(...)
string
Renames schema values during the transformation step (within items
or maps
):
const pokeTypeSchema = anyOf(
string().const('fire'),
string().const('grass'),
string().const('water')
).savedAs('pkt')
.discriminate(...)
string
Leverages a specific attribute as a discriminator to efficiently match between different schema options. Optimizes performance during Parsing
and Formatting
:
const catSchema = map({
kind: string().enum('cat'),
... // Other cat attributes
})
const dogSchema = map({
kind: string().enum('dog'),
... // Other dog attributes
})
const petSchema = anyOf(catSchema, dogSchema)
.discriminate('kind')
You can retrieve a matching schema using the match
method:
const matchingSchema = petSchema.match('dog') // => dogSchema
To be used as a discriminator, an attribute must meet all of the following conditions:
- ✅ It must be present within a
map
attribute, either directly or as part of anotheranyOf
schema. - ✅ It must be present in every schema option.
- ✅ It must be of type
string
and use theenum
property. - ✅ If renamed, the same
savedAs
value must be used across all options. - ❌ It must not be
optional
ortransformed
.
The following examples raises both type and runtime errors:
// ❌ 'age' is not a string with enum values
const petSchema = anyOf(
map({ age: number().enum(1, 2, 3) })
).discriminate('age')
// ❌ 'kind' is marked as optional
const petSchema = anyOf(
map({ kind: string().enum('cat').optional() })
).discriminate('kind')
// ❌ 'savedAs' is inconsistent across options
const petSchema = anyOf(
map({ kind: string().enum('cat').savedAs('kind') }),
map({ kind: string().enum('dog').savedAs('__kind__') })
).discriminate('kind')
.default(...)
ValueOrGetter<ATTRIBUTES>
Specifies default values. See Defaults and Links for more details:
- Put/Update
- Key
const now = () => new Date().toISOString()
const hasUpdateSchema = anyOf(
map({ hasUpdate: boolean().const(false) }),
map({ hasUpdate: boolean().const(true), date: string() })
)
.default(() => ({ hasUpdate: false }))
.updateDefault(() => ({ hasUpdate: true, date: now() }))
// 👇 Similar to
const timestampsSchema = anyOf('...')
.putDefault(() => ({ hasUpdate: false }))
.updateDefault(() => ({ hasUpdate: true, date: now() }))
const idsSchema = anyOf(
map({
hasSubId: boolean().const(false),
id: string()
}),
map({
hasSubId: boolean().const(true),
id: string(),
subId: string()
})
)
.key()
.default({ hasSubId: false, id: '123' })
// 👇 Similar to
const idsSchema = anyOf(...)
.key()
.keyDefault({ hasSubId: false, id: '123' })
.link<Schema>(...)
Link<SCHEMA, ELEMENTS>
Similar to .default(...)
but allows deriving the default value from other attributes. See Defaults and Links for more details:
const pokemonSchema = item({
name: string().optional(),
level: number()
}).and(prevSchema => ({
metadata: anyOf(string(), number()).link<
typeof prevSchema
>(
// 🙌 Correctly typed!
({ name, level }) => name ?? level
)
}))
.validate(...)
Validator<ELEMENTS>
Adds custom validation. See Custom Validation for more details:
const nonEmptyListSchema = anyOf(
list(string()),
list(number())
).validate(input => input.length > 0)
// 👇 Similar to
const nonEmptyListSchema = anyOf(
list(string()),
list(number())
).putValidate(input => input.length > 0)