Skip to content

Latest commit

 

History

History
231 lines (172 loc) · 7.12 KB

README.md

File metadata and controls

231 lines (172 loc) · 7.12 KB

@coderspirit/nominal-typebox

NPM version TypeScript License npm downloads Known Vulnerabilities Security Score

Nominal-Typebox brings nominal typing capabilities to Typebox schema definitions by leveraging Nominal.

Install instructions

Node

# 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

Usage instructions

TypeBox' Type.String -> brandedString

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 error

TypeBox' Type.RegExp -> brandedRegExp

import 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 error

TypeBox' Type.Number -> brandedNumber

import 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 error

TypeBox' Type.Integer -> brandedInteger

The same applies as for the two previous examples, you can use brandedInteger instead of Typebox' Type.Integer.


TypeBox' Type.Array -> brandedArray

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 }
)

TypeBox' Type.Object -> brandedObject

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 }
)

TypeBox' Type.Union -> brandedUnion

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')]
)

Fallback alternative

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.