Getting started#
By the end of this tutorial you will have built a real configuration validator, starting from a single default and growing it into a nested schema with required fields, arrays, and custom rules. Everything here works the same way in TypeScript/JavaScript, in Go, and in Rust.
1. Install#
TypeScript / JavaScript
npm install shape
const { Shape } = require('shape')
// import { Shape } from 'shape' // ESM and browsers work too
Go
go get github.com/rjrodger/shape/go
import "github.com/rjrodger/shape/go"
2. Your first shape is just your data#
A Shape specification looks like the data you expect. A literal value means "optional, with this default, and this type".
TS
const shape = Shape({
port: 8080,
host: 'localhost',
})
shape({ port: 9090 })
// → { port: 9090, host: 'localhost' }
shape({})
// → { port: 8080, host: 'localhost' }
Go
s := shape.MustShape(map[string]any{
"port": 8080,
"host": "localhost",
})
out, _ := s.Validate(map[string]any{"port": 9090})
// out == map[string]any{"port": 9090, "host": "localhost"}
Missing fields are filled in from the defaults. This is the most common case for options objects—everything optional, the default defines the type.
3. Nesting just works#
Objects fill out to any depth (unlike Object.assign or spread).
TS
const shape = Shape({
server: {
port: 8080,
host: 'localhost',
},
})
shape({})
// → { server: { port: 8080, host: 'localhost' } }
4. Make a field required#
Use a type marker instead of a literal. In TS these are the standard wrapper
objects (String, Number, Boolean, …). In Go they are the exported
sentinel tokens (shape.String, shape.Number, shape.Boolean, …).
TS
const shape = Shape({
timeout: 10000, // optional, default 10000
message: String, // required string
debug: Boolean, // required boolean
})
shape({ message: 'hi', debug: true }) // OK
shape({ debug: true })
// throws: Validation failed for property "message" because the property is missing.
Go
s := shape.MustShape(map[string]any{
"timeout": 10000,
"message": shape.String,
"debug": shape.Boolean,
})
s.Validate(map[string]any{"message": "hi", "debug": true}) // OK
s.Validate(map[string]any{"debug": true})
// error: Validation failed for property "message" because the property is missing.
Required fields have no default—you only declare the type.
5. Validate an array#
Give one example element and every element must match it.
TS
const shape = Shape({
tags: [String], // array of strings
})
shape({ tags: ['a', 'b'] }) // OK
shape({ tags: [1] })
// throws: Validation failed for index "tags.0" with number "1" because the number is not of type string.
A single-element array means "zero or more of this shape". Multiple elements make a fixed-length tuple—see Validate arrays and tuples.
6. Add a constraint#
Builders wrap a shape to add rules. They can be combined.
TS
const { Shape, Min, Max } = require('shape')
const shape = Shape({
name: Min(1, String), // non-empty string
age: Min(0, Max(120, Number)), // 0..120
})
Go
s := shape.MustShape(map[string]any{
"name": shape.Min(1, shape.String),
"age": shape.Min(0, shape.Max(120, shape.Number)),
})
7. Handle failures#
By default TS throws a ShapeError; Go returns an error.
TS
try {
shape({ name: 'Ann', age: 999 })
} catch (err) {
console.log(err.message) // Value "999" for property "age" must be a maximum of 120 (was 999).
}
Go
out, err := s.Validate(map[string]any{"name": "Ann", "age": 999})
if err != nil {
fmt.Println(err) // Value "999" for property "age" must be a maximum of 120 (was 999).
}
The thrown message lists every error found, one per line. To get the errors as data instead of an exception, see Handle and collect errors.
Where to next#
- Everything is optional-by-default; the mental model is Schema by example.
- Pick a task from the how-to guides.
- Look up any builder in the builder reference.