Simple Example
This is an example using schemata-ts derivation on a simple schema.
Imports
This example uses the following imports:
import * as fc from 'fast-check'
import * as B from 'fp-ts/boolean'
import { pipe } from 'fp-ts/function'
import * as RA from 'fp-ts/ReadonlyArray'
import { strict as assert } from 'node:assert'
import * as S from 'schemata-ts'
import { deriveArbitrary } from 'schemata-ts/Arbitrary'
import { deriveEq } from 'schemata-ts/Eq'
import { type Float } from 'schemata-ts/float'
import { deriveGuard } from 'schemata-ts/Guard'
import { deriveJsonSchema } from 'schemata-ts/JsonSchema'
import { deriveMergeSemigroup } from 'schemata-ts/MergeSemigroup'
import * as TC from 'schemata-ts/Transcoder'
import { deriveTypeString } from 'schemata-ts/TypeString'
Schema
Use the
S
import to access all schemas and schema combinators.
This schema uses the Struct
combinator, String
schema, Number
schema, and Optional
combinator.
const Person = S.Struct({
name: S.String(),
age: S.Number,
favoriteColor: S.Optional(S.String()),
})
TypeScript Types
Use
S.InputOf
to extract the underlying input type, andS.OutputOf
to extract the underlying output type.
The Person
schema has nearly identical input/output types, with one exception. The favoriteColor
key may be present or not in the input type, but will always be present in the output type.
/**
* This type wull be:
*
* { name: string, age: Float, favoriteColor?: string | undefined }
*/
export type PersonInput = S.InputOf<typeof Person>
/**
* This type will be:
*
* { name: string, age: Float, favoriteColor: string | undefined }
*/
export type PersonOutput = S.OutputOf<typeof Person>
Validation
Use
deriveTranscoder
to interpret the schema as aTranscoder
.
Transcoders can decode
an unknown value of an expected input type and transform it to the expected output type. They can also encode
a value of the output type and transform it to the expected input type.
const transcoderPerson = TC.deriveTranscoder(Person)
// failed decoding
const decodeResult = transcoderPerson.decode({})
assert.deepStrictEqual(
decodeResult,
TC.failure(
TC.transcodeErrors(
TC.errorAtKey('age', TC.typeMismatch('Float', undefined)),
TC.errorAtKey('name', TC.typeMismatch('string', undefined)),
),
),
)
// successful decoding
const decodeResult2 = transcoderPerson.decode({
name: 'John',
age: 42,
})
assert.deepStrictEqual(
decodeResult2,
TC.success({ name: 'John', age: 42, favoriteColor: undefined }),
)
Type Guards
Use
deriveGuard
to interpret the schema as aGuard
Guards are used to determine if the type of an unknown value matches the output type, and also serve as TypeScript type guards.
const guardPerson = deriveGuard(Person)
const testPerson1: unknown = {
name: 'John',
age: NaN,
}
// failed type guard
const isPerson = guardPerson.is(testPerson1)
if (isPerson) {
// $ExpectType PersonOutput
testPerson1
}
assert.strictEqual(isPerson, false)
const testPerson2: unknown = {
name: 'John',
age: 42,
}
// successful type guard
const isPerson2 = guardPerson.is(testPerson2)
assert.strictEqual(isPerson2, true)
Json Schema
Use
deriveJsonSchema
to interpret the schema as aJsonSchema
.
Json Schemas are used to generate a JSON Schema Draft 7, 2019-09, or 2020-12 representation of the schema.
/** Derives Json-Schema version 2019-09 */
const jsonSchemaPerson = deriveJsonSchema(Person)
assert.deepStrictEqual(jsonSchemaPerson, {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
favoriteColor: { type: 'string' },
},
required: ['age', 'name'],
})
Arbitrary
Use
deriveArbitrary
to interpret the schema as anArbitrary
Arbitraries are used to generate random values of the expected output type.
/** Note: fast-check start imports needs to be supplied in the `arbitrary` method */
const arbitraryPerson = deriveArbitrary(Person).arbitrary(fc)
// generate 5 random people
const people = fc.sample(arbitraryPerson, 5)
// all five should be valid according to `guard`
assert.deepStrictEqual(
pipe(
people,
// check that all people are valid
RA.foldMap(B.MonoidAll)(guardPerson.is),
),
true,
)
Merge Semigroup
Use
deriveMergeSemigroup
to interpret the schema as aMergeSemigroup
Merge Semigroups are used to merge two values of the output type, and can be configured as first
, or last
semigroups; or to concat two primitives based on supplied semigroups and a fallback semigroup. MergeSemigroup will merge mergable data types such as arrays and records.
/** Derives a semigroup that configurably merges two people */
const mergeSemigroupPerson = deriveMergeSemigroup(Person)
const mergeSemigroupPersonFirst = mergeSemigroupPerson.semigroup('first')
assert.deepStrictEqual(
mergeSemigroupPersonFirst.concat(
{ name: 'John', age: 42 as Float, favoriteColor: 'brown' },
{ name: 'Tim', age: 12 as Float, favoriteColor: 'blue' },
),
{ name: 'John', age: 42 as Float, favoriteColor: 'brown' },
)
const mergeSemigroupPersonLast = mergeSemigroupPerson.semigroup('last')
assert.deepStrictEqual(
mergeSemigroupPersonLast.concat(
{ name: 'John', age: 42 as Float, favoriteColor: 'brown' },
{ name: 'Tim', age: 12 as Float, favoriteColor: 'blue' },
),
{ name: 'Tim', age: 12 as Float, favoriteColor: 'blue' },
)
Eq
Use
deriveEq
to interpret the schema as anEq
Eq
instances are used to determine if two values of the output type are equal. Which is in essence a schema-tailored equality check.
/** An `Eq` instance for person, essentially schema-tailored deep-equality */
const eqPerson = deriveEq(Person)
assert.deepStrictEqual(
eqPerson.equals(
{ name: 'John', age: 42 as Float, favoriteColor: undefined },
{ name: 'John', age: 42 as Float, favoriteColor: undefined },
),
true,
)
assert.deepStrictEqual(
eqPerson.equals(
{ name: 'John', age: 42 as Float, favoriteColor: undefined },
{ name: 'John', age: 42 as Float, favoriteColor: 'blue' },
),
false,
)
Type String
Use
deriveTypeString
to interpret the schema as aTypeString
TypeStrings are used to generate a user-descriptive string that describes the underlying input and output types.
const [inputString, outputString] = deriveTypeString(Person)
assert.deepStrictEqual(
inputString,
'{ age: Float, favoriteColor?: string?, name: string }',
)
assert.deepStrictEqual(
outputString,
'{ age: Float, favoriteColor: string?, name: string }',
)
Displaying Errors
Use
drawErrorTree
to display errors in a friendly way
There are many ways to format errors in schemata, but the easiest is drawErrorTree
from the Transcoder
module.
const testError = TC.transcodeErrors(
TC.errorAtKey('age', TC.typeMismatch('Float', undefined)),
TC.errorAtKey('name', TC.typeMismatch('string', undefined)),
)
assert.strictEqual(
TC.drawErrorTree(testError),
`Encountered 2 transcode errors:
┌ at key age:
└── Expected Float but got \`undefined\`
┌ at key name:
└── Expected string but got \`undefined\``,
)