shape

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#