Nominal-Typebox brings nominal typing
capabilities to Typebox schema
definitions by leveraging Nominal.
# With NPM
npm install @sinclair/typebox
npm install @coderspirit/nominal-typebox
# Or with PNPM
pnpm add @sinclair/typebox
pnpm add @coderspirit/nominal-typebox
# Or with Yarn:
yarn add @sinclair/typebox
yarn add @coderspirit/nominal-typebox
import type { FastBrand } from '@coderspirit/nominal'
import { brandedString } from '@coderspirit/nominal-typebox'
import { Object as TBObject } from '@sinclair/typebox'
import { TypeCompiler } from '@sinclair/typebox/compiler'
type Username = FastBrand<string, 'Username'>
// Use `brandedString` instead of Typebox' `Type.String`
const requestSchema = TBObject({
// We can pass the same options Type.String has
username: brandedString<'Username'>()
})
const requestValidator = TypeCompiler.Compile(requestSchema)
const requestObject = getRequestFromSomewhere() // unknown
if (!requestValidator.Check(requestObject)) {
throw new Error('Invalid request!')
}
// At this point, the type checker knows that requestObject.username is
// "branded" as 'Username'
const username: Username = requestObject.username // OK
const corruptedUserame: Username = 'untagged string' // type errorimport type { FastBrand } from '@coderspirit/nominal'
import { brandedRegExp } from '@coderspirit/nominal-typebox'
import { Object as TBObject } from '@sinclair/typebox'
import { TypeCompiler } from '@sinclair/typebox/compiler'
type UserId = FastBrand<string, 'UserId'>
// Use `brandedString` instead of Typebox' `Type.String`
const requestSchema = TBObject({
// We can pass the same options Type.String has
userId: brandedRegExp<'UserId'>(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
)
})
const requestValidator = TypeCompiler.Compile(requestSchema)
const requestObject = getRequestFromSomewhere() // unknown
if (!requestValidator.Check(requestObject)) {
throw new Error('Invalid request!')
}
// At this point, the type checker knows that requestObject.username is
// "branded" as 'Username'
const userId: UserId = requestObject.userId // OK
const corruptedUserId: UserId = 'untagged (and probably wrong) id' // type errorimport type { FastBrand } from '@coderspirit/nominal'
import { brandedNumber } from '@coderspirit/nominal-typebox'
import { Object as TBObject } from '@sinclair/typebox'
import { TypeCompiler } from '@sinclair/typebox/compiler'
type Latitude = FastBrand<number, 'Latitude'>
type Longitude = FastBrand<number, 'Longitude'>
const requestSchema = TBObject({
// We can pass the same options Type.Number has
latitude: brandedNumber<'Latitude'>(),
longitude: brandedNumber<'Longitude'>(),
})
const requestValidator = TypeCompiler.Compile(requestSchema)
const requestObject = getRequestFromSomewhere() // unknown
if (!requestValidator.Check(requestObject)) {
throw new Error('Invalid request!')
}
const latitude: Latitude = requestObject.latitude // OK
const longitude: Longitude = requestObject.longitude // OK
const corruptedLat: Latitude = 10 // type error
const corruptedLon: Longitude = 10 // type errorThe same applies as for the two previous examples, you can use brandedInteger
instead of Typebox' Type.Integer.
brandedArray has the same signature as Typebox' Type.Array, except that we
have to pass a "brand" string argument as its first parameter:
import { brandedArray } from '@coderspirit/nominal-typebox'
import { String as TBString } from '@sinclair/typebox'
const arraySchema = brandedArray(
'MyArray',
// Type.Array arguments:
TBString(),
{ minItems: 2 }
)brandedObject has the same signature as Typebox' Type.Object, except that we
have to pass a "brand" string argument as its first parameter:
import { brandedObject } from '@coderspirit/nominal-typebox'
import { String as TBString } from '@sinclair/typebox'
const objectSchema = brandedObject(
'MyObject',
{
a: TBstring(),
b: TBString()
},
{ additionalProperties: true }
)brandedUnion has the same signature as Typebox' Type.Union, except that we
have to pass a "brand" string argument as its first parameter:
import { brandedUnion } from '@coderspirit/nominal-typebox'
import { Literal } from '@sinclair/typebox'
const unionSchema = brandedUnion(
'State',
[Literal('on'), Literal('off')]
)In case this library does not provide a specific schema factory for your type,
you can rely on brandedSchema. Notice that if you are using it for complex
schemas, it can loose some branding information from inner/nested properties.
import type { FastBrand } from '@coderspirit/nominal'
import {
brandedInteger,
brandedSchema,
brandedString,
} from '@coderspirit/nominal-typebox'
import { Record as TBRecord } from '@sinclair/typebox'
const personNameSchema = brandedString<'PersonName'>()
const personAgeSchema = brandedInteger<'PersonAge'>()
const recordSchema = brandedSchema('PeopleAges', TBRecord(
personNameSchema,
personAgeSchema,
))
const recordValidator = TypeCompiler.Compile(recordSchema)
const requestRecord = getRequestFromSomewhere() // unknown
if (!requestValidator.Check(requestRecord)) {
throw new Error('Invalid request!')
}
// OK
const recordSink: FastBrand<Record<string, number>, 'PeopleAges'> =
requestRecord
// @ts-expect-error Type Error!
const corruptedRecordSink: FastBrand<
Record<string, number>, 'PeopleAges'
> = { Alice: 20, Bob: 30 }
// IMPORTANT!: Notice that `brandedSchema` is unable to preserve the
// brands of keys & values in the record. This limitation
// is due to the fact that `brandedSchema` is too generic.