How to compose shapes#
Goal: accept a value that matches one of several shapes, or all of them, or a fixed set of literal values.
Exactly one of a set of shapes: One#
TS
const { Shape, One } = require('shape') // Number and String are the JS globals
const id = Shape({ id: One(Number, String) })
id({ id: 42 }) // OK
id({ id: 'x42' }) // OK
id({ id: true }) // throws: does not satisfy one of: Number, String
Go
s := shape.MustShape(map[string]any{"id": shape.One(shape.Number, shape.String)})
At least one: Some#
Some passes if the value matches any branch.
Shape({ v: Some(Number, String) })
All of them: All#
All requires every branch to match. It threads the value through each branch,
so defaults from earlier branches are visible to later ones.
Shape({ v: All(Number, Min(1), Max(9)) }) // number, and 1..9
One of a set of literal values: Exact#
Exact compares by equality against the listed literals.
TS
const { Shape, Exact } = require('shape')
const role = Shape({ role: Exact('admin', 'user', 'guest') })
role({ role: 'user' }) // OK
role({ role: 'root' }) // throws: must be exactly one of: admin, user, guest
Go
s := shape.MustShape(map[string]any{
"role": shape.Exact("admin", "user", "guest"),
})
A tagged union: Discriminated#
When the alternatives are objects told apart by one property, Discriminated
picks the branch by that property's value and reports only that branch's
errors—not a list of every alternative.
TS
const { Shape, Discriminated } = require('shape')
const pet = Shape(Discriminated('kind', {
dog: { bark: Boolean },
fish: { fins: Number },
}))
pet({ kind: 'dog', bark: true }) // OK — the "kind" property is added to each branch
pet({ kind: 'fish', fins: 'x' }) // throws: property "fins" ... not of type number
pet({ kind: 'cat' }) // throws: has unknown "kind" "cat", expected one of: dog, fish
pet({ bark: true }) // throws: is not an object with a "kind" property
Go
pet := shape.MustShape(shape.Discriminated("kind", map[string]any{
"dog": map[string]any{"bark": shape.Boolean},
"fish": map[string]any{"fins": shape.Number},
}))
Notes#
Onestops at the first matching branch and uses that branch's output.SomeandAllevaluate every branch (no short-circuit), so default injection from any branch is never skipped.- No branch changes the value it was given: each matches and produces from a
copy.
Sometakes the last matching branch's result (Some(Open({a:1}), Open({b:2}))on{}gives{b:2}),Allthreads the result from branch to branch ({a:1,b:2}), and a failing composition leaves the input as it was. Exactalso matches when the value is absent but the node's default equals one of the listed literals.- These builders are not chainable methods—call them as top-level builders.
- An absent optional composition (
Optional(One(String, Number))) is simply absent; it is not put to its branches.