Skip to content

Commit 913b01c

Browse files
committed
feat(nominal-typebox): new typebox integration
Signed-off-by: Andres Correa Casablanca <[email protected]>
1 parent 74cc3c9 commit 913b01c

24 files changed

+1030
-57
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"extends": ["@coderspirit/dev-configs/biome"],
3+
"overrides": [
4+
{
5+
"include": ["*.ts", "*.mts", "*.cts", "*.tsx"],
6+
"linter": {
7+
"rules": {
8+
"complexity": { "useLiteralKeys": "off" }
9+
}
10+
}
11+
},
12+
{
13+
"include": ["*.test.ts", "*.test.mts"],
14+
"linter": {
15+
"rules": {
16+
"style": { "noParameterProperties": "off" }
17+
}
18+
}
19+
},
20+
{
21+
"include": ["rollup.config.mjs", "vitest.config.mts"],
22+
"linter": {
23+
"rules": {
24+
"style": { "noDefaultExport": "off" }
25+
}
26+
}
27+
},
28+
{
29+
"include": ["src/main.mts"],
30+
"linter": {
31+
"rules": {
32+
"performance": {
33+
"noBarrelFile": "off"
34+
}
35+
}
36+
}
37+
}
38+
]
39+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"name": "@coderspirit/nominal-typebox",
3+
"version": "1.0.0",
4+
"description": "Integration of @coderspirit/nominal with @sinclair/typebox",
5+
"main": "./dist/main.cjs",
6+
"module": "./dist/main.mjs",
7+
"types": "./dist/main.d.cts",
8+
"files": ["dist"],
9+
"exports": {
10+
"import": {
11+
"types": "./dist/main.d.mts",
12+
"default": "./dist/main.mjs"
13+
},
14+
"require": {
15+
"types": "./dist/main.d.cts",
16+
"default": "./dist/main.cjs"
17+
}
18+
},
19+
"keywords": [
20+
"nominal",
21+
"taint",
22+
"typecheck",
23+
"types",
24+
"typescript",
25+
"validate",
26+
"validation"
27+
],
28+
"author": "Andres Correa Casablanca <[email protected]>",
29+
"private": false,
30+
"license": "MIT",
31+
"scripts": {
32+
"build": "rm -rf dist/* && rollup --config rollup.config.mjs",
33+
"format": "pnpm biome check --write --files-ignore-unknown=true .",
34+
"format-staged": "biome-check-staged",
35+
"lint": "pnpm lint:biome",
36+
"lint:biome": "pnpm biome check --files-ignore-unknown=true .",
37+
"lint:publint": "publint",
38+
"prepublishOnly": "turbo all",
39+
"publish:safe": "safe-publish",
40+
"test": "vitest -c vitest.config.mts run",
41+
"test:cov": "vitest -c vitest.config.mts run --coverage",
42+
"typecheck": "tsc --incremental true --tsBuildInfoFile .tsbuildinfo --noEmit -p ./tsconfig.json"
43+
},
44+
"dependencies": {
45+
"@coderspirit/nominal": "workspace:^"
46+
},
47+
"devDependencies": {
48+
"@arethetypeswrong/cli": "^0.15.4",
49+
"@biomejs/biome": "1.8.3",
50+
"@coderspirit/nominal-inputs": "workspace:^",
51+
"@sinclair/typebox": "^0.33.3",
52+
"@types/node": "^22.1.0",
53+
"@vitest/coverage-v8": "^2.0.5",
54+
"get-tsconfig": "^4.7.6",
55+
"publint": "^0.2.9",
56+
"rollup": "^4.20.0",
57+
"rollup-plugin-dts": "^6.1.1",
58+
"rollup-plugin-esbuild": "^6.1.1",
59+
"tslib": "^2.6.3",
60+
"turbo": "^2.0.12",
61+
"typescript": "^5.5.4",
62+
"vitest": "^2.0.5"
63+
},
64+
"peerDependencies": {
65+
"@sinclair/typebox": "^0.33.2"
66+
},
67+
"engines": {
68+
"node": ">=18.0.0"
69+
},
70+
"repository": {
71+
"type": "git",
72+
"url": "git+https://github.com/Coder-Spirit/nominal.git"
73+
},
74+
"bugs": {
75+
"url": "https://github.com/Coder-Spirit/nominal/issues"
76+
},
77+
"homepage": "https://github.com/Coder-Spirit/nominal#readme",
78+
"publishConfig": {
79+
"provenance": true
80+
}
81+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { dirname } from 'node:path'
2+
import { fileURLToPath } from 'node:url'
3+
4+
import { getTsconfig } from 'get-tsconfig'
5+
import { defineConfig } from 'rollup'
6+
import dts from 'rollup-plugin-dts'
7+
import esbuild from 'rollup-plugin-esbuild'
8+
9+
const projectDir = dirname(fileURLToPath(import.meta.url))
10+
const tsconfig = getTsconfig(projectDir)
11+
const target = tsconfig?.config.compilerOptions?.target ?? 'es2020'
12+
13+
const input = 'src/main.mts'
14+
const external = ['@coderspirit/nominal', '@sinclair/typebox']
15+
16+
export default defineConfig([
17+
{
18+
input,
19+
output: [
20+
{ format: 'cjs', file: 'dist/main.cjs', sourcemap: true },
21+
{ format: 'esm', file: 'dist/main.mjs', sourcemap: true },
22+
],
23+
plugins: [
24+
esbuild({
25+
target: ['node20', 'node22', target],
26+
loaders: { '.mts': 'ts' },
27+
minify: true,
28+
}),
29+
],
30+
external,
31+
},
32+
{
33+
input,
34+
output: [
35+
{ format: 'cjs', file: 'dist/main.d.cts' },
36+
{ format: 'esm', file: 'dist/main.d.mts' },
37+
],
38+
plugins: [dts()],
39+
external,
40+
},
41+
])
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { FastBrand } from '@coderspirit/nominal'
2+
import type { ArrayOptions, Kind, TArray, TSchema } from '@sinclair/typebox'
3+
import { Array as TBArray } from '@sinclair/typebox'
4+
5+
export interface BrandedArraySchema<
6+
B extends string,
7+
S extends TSchema,
8+
A extends TArray<S>,
9+
> extends TSchema,
10+
ArrayOptions {
11+
// We cannot rely on `&`, `extends` or generics here, because that would
12+
// impose too much work on the type inference engine.
13+
14+
// Copied from TArray
15+
[Kind]: 'Array'
16+
type: 'array'
17+
items: S
18+
19+
// Our special sauce
20+
static: FastBrand<A['static'], B>
21+
}
22+
23+
export const brandedArray = <const B extends string, const S extends TSchema>(
24+
_b: B,
25+
schema: S,
26+
options?: ArrayOptions,
27+
): BrandedArraySchema<B, S, TArray<S>> => {
28+
// We have to separate declaration and return statement because otherwise
29+
// TS goes crazy, thinking that there is some kind of type circularity.
30+
return TBArray(schema, options) as BrandedArraySchema<B, S, TArray<S>>
31+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Catch-all types
2+
export type { BrandedSchema } from './schema.mts'
3+
4+
// Basic types
5+
export type { BrandedIntegerSchema, BrandedNumberSchema } from './number.mts'
6+
export type { BrandedStringSchema } from './string.mts'
7+
8+
// Complex types
9+
export type { BrandedArraySchema } from './array.mts'
10+
export type { BrandedObjectSchema } from './object.mts'
11+
export type { BrandedUnionSchema } from './union.mts'
12+
13+
// Catch-all schemas
14+
export { brandedSchema } from './schema.mts'
15+
16+
// Basic schemas
17+
export { brandedInteger, brandedNumber } from './number.mts'
18+
export { brandedString } from './string.mts'
19+
20+
// Complex schemas
21+
export { brandedArray } from './array.mts'
22+
export { brandedObject } from './object.mts'
23+
export { brandedUnion } from './union.mts'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { FastBrand, FastBrandAndProperties } from '@coderspirit/nominal'
2+
import type { IntegerOptions, NumberOptions, TSchema } from '@sinclair/typebox'
3+
import { Kind } from '@sinclair/typebox'
4+
import { Integer as TBInteger, Number as TBNumber } from '@sinclair/typebox'
5+
6+
export interface BrandedIntegerSchema<B extends string>
7+
extends TSchema,
8+
IntegerOptions {
9+
// Copied from TInteger
10+
[Kind]: 'Integer'
11+
type: 'integer'
12+
13+
// Our sauce
14+
static: FastBrandAndProperties<number, B, { Fractional: false }>
15+
}
16+
17+
export interface BrandedNumberSchema<B extends string>
18+
extends TSchema,
19+
NumberOptions {
20+
// Copied from TNumber
21+
[Kind]: 'Number'
22+
type: 'number'
23+
24+
// Our sauce
25+
static: FastBrand<number, B>
26+
}
27+
28+
export const brandedInteger = <const B extends string>(
29+
options?: IntegerOptions,
30+
): BrandedIntegerSchema<B> => {
31+
return TBInteger(options) as BrandedIntegerSchema<B>
32+
}
33+
34+
export const brandedNumber = <const B extends string>(
35+
options?: NumberOptions,
36+
): BrandedNumberSchema<B> => {
37+
return TBNumber(options) as BrandedNumberSchema<B>
38+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { FastBrand } from '@coderspirit/nominal'
2+
import type {
3+
Kind,
4+
ObjectOptions,
5+
TObject,
6+
TProperties,
7+
TSchema,
8+
} from '@sinclair/typebox'
9+
import { Object as TBObject } from '@sinclair/typebox'
10+
11+
export interface BrandedObjectSchema<
12+
B extends string,
13+
T extends TProperties,
14+
O extends TObject<T>,
15+
> extends TSchema,
16+
ObjectOptions {
17+
// We cannot rely on `&`, `extends` or generics here, because that would
18+
// impose too much work on the type inference engine.
19+
20+
// Copied from TObject
21+
[Kind]: 'Object'
22+
type: 'object'
23+
properties: T
24+
required?: string[]
25+
26+
// Our special sauce
27+
static: FastBrand<O['static'], B>
28+
}
29+
30+
export const brandedObject = <
31+
const B extends string,
32+
const T extends TProperties,
33+
>(
34+
_b: B,
35+
properties: T,
36+
options?: ObjectOptions,
37+
): BrandedObjectSchema<B, T, TObject<T>> => {
38+
return TBObject(properties, options) as BrandedObjectSchema<B, T, TObject<T>>
39+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { FastBrand } from '@coderspirit/nominal'
2+
import type {
3+
Hint,
4+
Kind,
5+
OptionalKind,
6+
ReadonlyKind,
7+
TSchema,
8+
} from '@sinclair/typebox'
9+
10+
export type BrandedSchema<B extends string, T extends TSchema> = Pick<
11+
T,
12+
| typeof Kind
13+
| typeof ReadonlyKind
14+
| typeof OptionalKind
15+
| typeof Hint
16+
| '$schema'
17+
| '$id'
18+
| 'title'
19+
| 'description'
20+
| 'default'
21+
| 'examples'
22+
| 'readOnly'
23+
| 'writeOnly'
24+
| 'params'
25+
// We leave out `static` on purpose, but we also loose some other properties
26+
// that might exist in TSchema subtypes, we can't do anything about it without
27+
// overcomplicating the code.
28+
> & {
29+
static: FastBrand<T['static'], B>
30+
}
31+
32+
export const brandedSchema = <const B extends string, S extends TSchema>(
33+
schema: S,
34+
): BrandedSchema<B, S> => {
35+
return schema as BrandedSchema<B, S>
36+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { FastBrand } from '@coderspirit/nominal'
2+
import type { Kind, StringOptions, TSchema } from '@sinclair/typebox'
3+
import { String as TBString } from '@sinclair/typebox'
4+
5+
export interface BrandedStringSchema<B extends string>
6+
extends TSchema,
7+
StringOptions {
8+
// Copied from TString
9+
[Kind]: 'String'
10+
type: 'string'
11+
12+
// Our sauce
13+
static: FastBrand<string, B>
14+
}
15+
16+
export const brandedString = <const B extends string>(
17+
options?: StringOptions,
18+
): BrandedStringSchema<B> => {
19+
return TBString(options) as BrandedStringSchema<B>
20+
}

0 commit comments

Comments
 (0)