diff --git a/@coderspirit/nominal-inputs/package.json b/@coderspirit/nominal-inputs/package.json index 1025aa4..fcac919 100644 --- a/@coderspirit/nominal-inputs/package.json +++ b/@coderspirit/nominal-inputs/package.json @@ -37,7 +37,7 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.15.4", "@biomejs/biome": "1.8.3", - "@types/node": "^22.1.0", + "@types/node": "^22.2.0", "get-tsconfig": "^4.7.6", "publint": "^0.2.9", "rollup": "^4.20.0", diff --git a/@coderspirit/nominal-symbols/package.json b/@coderspirit/nominal-symbols/package.json index b7d8a17..5bb0aa3 100644 --- a/@coderspirit/nominal-symbols/package.json +++ b/@coderspirit/nominal-symbols/package.json @@ -37,7 +37,7 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.15.4", "@biomejs/biome": "1.8.3", - "@types/node": "^22.1.0", + "@types/node": "^22.2.0", "get-tsconfig": "^4.7.6", "publint": "^0.2.9", "rollup": "^4.20.0", diff --git a/@coderspirit/nominal-typebox/.node-version b/@coderspirit/nominal-typebox/.node-version new file mode 120000 index 0000000..bb25ae9 --- /dev/null +++ b/@coderspirit/nominal-typebox/.node-version @@ -0,0 +1 @@ +../../.node-version \ No newline at end of file diff --git a/@coderspirit/nominal-typebox/README.md b/@coderspirit/nominal-typebox/README.md index c08c135..773832a 100644 --- a/@coderspirit/nominal-typebox/README.md +++ b/@coderspirit/nominal-typebox/README.md @@ -31,7 +31,7 @@ yarn add @coderspirit/nominal-typebox ## Usage instructions -### Typebox' Type.String -> brandedString +### TypeBox' Type.String -> brandedString ```typescript import type { FastBrand } from '@coderspirit/nominal' @@ -61,7 +61,42 @@ const username: Username = requestObject.username // OK const corruptedUserame: Username = 'untagged string' // type error ``` -### Typebox' Type.Number -> brandedNumber +### TypeBox' Type.RegExp -> brandedRegExp + +```typescript + +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 + +// 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 ```typescript @@ -93,12 +128,14 @@ const corruptedLat: Latitude = 10 // type error const corruptedLon: Longitude = 10 // type error ``` -### Typebox' Type.Integer -> brandedInteger +### 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 +--- + +### 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: @@ -115,7 +152,7 @@ const arraySchema = brandedArray( ) ``` -### Typebox' Type.Object -> brandedObject +### 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: @@ -134,7 +171,7 @@ const objectSchema = brandedObject( ) ``` -### Typebox' Type.Union -> brandedUnion +### 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: @@ -149,6 +186,8 @@ const unionSchema = brandedUnion( ) ``` +--- + ### Fallback alternative In case this library does not provide a specific schema factory for your type, diff --git a/@coderspirit/nominal-typebox/package.json b/@coderspirit/nominal-typebox/package.json index 2949714..1bd4714 100644 --- a/@coderspirit/nominal-typebox/package.json +++ b/@coderspirit/nominal-typebox/package.json @@ -1,6 +1,6 @@ { "name": "@coderspirit/nominal-typebox", - "version": "1.0.0", + "version": "1.1.0", "description": "Integration of @coderspirit/nominal with @sinclair/typebox", "main": "./dist/main.cjs", "module": "./dist/main.mjs", @@ -48,8 +48,8 @@ "@arethetypeswrong/cli": "^0.15.4", "@biomejs/biome": "1.8.3", "@coderspirit/nominal-inputs": "workspace:^", - "@sinclair/typebox": "^0.33.3", - "@types/node": "^22.1.0", + "@sinclair/typebox": "^0.33.4", + "@types/node": "^22.2.0", "@vitest/coverage-v8": "^2.0.5", "get-tsconfig": "^4.7.6", "publint": "^0.2.9", diff --git a/@coderspirit/nominal-typebox/src/main.mts b/@coderspirit/nominal-typebox/src/main.mts index a4b435e..57025df 100644 --- a/@coderspirit/nominal-typebox/src/main.mts +++ b/@coderspirit/nominal-typebox/src/main.mts @@ -4,6 +4,7 @@ export type { BrandedSchema } from './schema.mts' // Basic types export type { BrandedIntegerSchema, BrandedNumberSchema } from './number.mts' export type { BrandedStringSchema } from './string.mts' +export type { BrandedRegExpSchema } from './regexp.mts' // Complex types export type { BrandedArraySchema } from './array.mts' @@ -16,6 +17,7 @@ export { brandedSchema } from './schema.mts' // Basic schemas export { brandedInteger, brandedNumber } from './number.mts' export { brandedString } from './string.mts' +export { brandedRegExp } from './regexp.mts' // Complex schemas export { brandedArray } from './array.mts' diff --git a/@coderspirit/nominal-typebox/src/regexp.mts b/@coderspirit/nominal-typebox/src/regexp.mts new file mode 100644 index 0000000..fe83936 --- /dev/null +++ b/@coderspirit/nominal-typebox/src/regexp.mts @@ -0,0 +1,31 @@ +import type { FastBrand } from '@coderspirit/nominal' +import type { Kind, RegExpOptions, TSchema } from '@sinclair/typebox' +import { RegExp as TBRegExp } from '@sinclair/typebox' + +export interface BrandedRegExpSchema extends TSchema { + // Copied from TRegExp + [Kind]: 'RegExp' + type: 'RegExp' + source: string + flags: string + + // Our sauce + static: FastBrand +} + +function brandedRegExp( + regex: RegExp, + options?: RegExpOptions, +): BrandedRegExpSchema +function brandedRegExp( + regex: string, + options?: RegExpOptions, +): BrandedRegExpSchema +function brandedRegExp( + pattern: string | RegExp, + options?: RegExpOptions, +): BrandedRegExpSchema { + return TBRegExp(pattern as string, options) as BrandedRegExpSchema +} + +export { brandedRegExp } diff --git a/@coderspirit/nominal-typebox/src/tests/regexp.test.mts b/@coderspirit/nominal-typebox/src/tests/regexp.test.mts new file mode 100644 index 0000000..f0d1ca7 --- /dev/null +++ b/@coderspirit/nominal-typebox/src/tests/regexp.test.mts @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict' + +import type { FastBrand } from '@coderspirit/nominal' +import { TypeCompiler } from '@sinclair/typebox/compiler' +import { describe, expect, it } from 'vitest' + +import { brandedRegExp } from '../main.mjs' + +describe('brandedRegExp', () => { + it('lets typebox to annotate a regexp with a brand', () => { + const regexpSchema = brandedRegExp<'UUID'>( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + ) + const regexpValidator = TypeCompiler.Compile(regexpSchema) + + const valueToCheck = '09e77e4a-175d-46a1-b6c6-ff960c30193b' + if (!regexpValidator.Check(valueToCheck)) { + throw new assert.AssertionError({ message: 'validation should pass' }) + } + + const regexpSink: FastBrand = valueToCheck + expect(regexpSink).toBe('09e77e4a-175d-46a1-b6c6-ff960c30193b') + + // We perform the following useless assignments to show the contrast between + // tagged values and untagged values. + + // @ts-expect-error + const corruptedRegexpSink: FastBrand = + '09e77e4a-175d-46a1-b6c6-ff960c30193b' + expect(corruptedRegexpSink).toBe('09e77e4a-175d-46a1-b6c6-ff960c30193b') + }) +}) diff --git a/@coderspirit/nominal/package.json b/@coderspirit/nominal/package.json index 55e5d4b..bc72422 100644 --- a/@coderspirit/nominal/package.json +++ b/@coderspirit/nominal/package.json @@ -39,7 +39,7 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.15.4", "@biomejs/biome": "1.8.3", - "@types/node": "^22.1.0", + "@types/node": "^22.2.0", "@vitest/coverage-v8": "^2.0.5", "get-tsconfig": "^4.7.6", "publint": "^0.2.9", diff --git a/@coderspirit/nominal/rollup.config.mjs b/@coderspirit/nominal/rollup.config.mjs index 8d22eb4..9a2f161 100644 --- a/@coderspirit/nominal/rollup.config.mjs +++ b/@coderspirit/nominal/rollup.config.mjs @@ -11,6 +11,7 @@ const tsconfig = getTsconfig(projectDir) const target = tsconfig?.config.compilerOptions?.target ?? 'es2020' const input = 'src/main.mts' +const external = ['@coderspirit/nominal-symbols'] export default defineConfig([ { @@ -26,7 +27,7 @@ export default defineConfig([ minify: true, }), ], - external: ['@coderspirit/nominal-symbols'], + external, }, { input, @@ -35,6 +36,6 @@ export default defineConfig([ { format: 'esm', file: 'dist/main.d.mts' }, ], plugins: [dts()], - external: ['@coderspirit/nominal-symbols'], + external, }, ]) diff --git a/@coderspirit/safe-env/package.json b/@coderspirit/safe-env/package.json index 887afb9..8439c0c 100644 --- a/@coderspirit/safe-env/package.json +++ b/@coderspirit/safe-env/package.json @@ -40,7 +40,7 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.15.4", "@biomejs/biome": "1.8.3", - "@types/node": "^22.1.0", + "@types/node": "^22.2.0", "@vitest/coverage-v8": "^2.0.5", "get-tsconfig": "^4.7.6", "publint": "^0.2.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 186553f..53eb3d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,7 +49,7 @@ importers: version: 1.8.3 '@vitest/coverage-v8': specifier: ^2.0.5 - version: 2.0.5(vitest@2.0.5(@types/node@22.1.0)) + version: 2.0.5(vitest@2.0.5(@types/node@22.2.0)) get-tsconfig: specifier: ^4.7.6 version: 4.7.6 @@ -76,7 +76,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.1.0) + version: 2.0.5(@types/node@22.2.0) '@coderspirit/nominal': dependencies: @@ -91,11 +91,11 @@ importers: specifier: 1.8.3 version: 1.8.3 '@types/node': - specifier: ^22.1.0 - version: 22.1.0 + specifier: ^22.2.0 + version: 22.2.0 '@vitest/coverage-v8': specifier: ^2.0.5 - version: 2.0.5(vitest@2.0.5(@types/node@22.1.0)) + version: 2.0.5(vitest@2.0.5(@types/node@22.2.0)) get-tsconfig: specifier: ^4.7.6 version: 4.7.6 @@ -122,7 +122,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.1.0) + version: 2.0.5(@types/node@22.2.0) '@coderspirit/nominal-inputs': dependencies: @@ -137,8 +137,8 @@ importers: specifier: 1.8.3 version: 1.8.3 '@types/node': - specifier: ^22.1.0 - version: 22.1.0 + specifier: ^22.2.0 + version: 22.2.0 get-tsconfig: specifier: ^4.7.6 version: 4.7.6 @@ -165,7 +165,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.1.0) + version: 2.0.5(@types/node@22.2.0) '@coderspirit/nominal-symbols': devDependencies: @@ -176,8 +176,8 @@ importers: specifier: 1.8.3 version: 1.8.3 '@types/node': - specifier: ^22.1.0 - version: 22.1.0 + specifier: ^22.2.0 + version: 22.2.0 get-tsconfig: specifier: ^4.7.6 version: 4.7.6 @@ -204,7 +204,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.1.0) + version: 2.0.5(@types/node@22.2.0) '@coderspirit/nominal-typebox': dependencies: @@ -222,14 +222,14 @@ importers: specifier: workspace:^ version: link:../nominal-inputs '@sinclair/typebox': - specifier: ^0.33.3 - version: 0.33.3 + specifier: ^0.33.4 + version: 0.33.4 '@types/node': - specifier: ^22.1.0 - version: 22.1.0 + specifier: ^22.2.0 + version: 22.2.0 '@vitest/coverage-v8': specifier: ^2.0.5 - version: 2.0.5(vitest@2.0.5(@types/node@22.1.0)) + version: 2.0.5(vitest@2.0.5(@types/node@22.2.0)) get-tsconfig: specifier: ^4.7.6 version: 4.7.6 @@ -256,7 +256,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.1.0) + version: 2.0.5(@types/node@22.2.0) '@coderspirit/safe-env': dependencies: @@ -274,11 +274,11 @@ importers: specifier: 1.8.3 version: 1.8.3 '@types/node': - specifier: ^22.1.0 - version: 22.1.0 + specifier: ^22.2.0 + version: 22.2.0 '@vitest/coverage-v8': specifier: ^2.0.5 - version: 2.0.5(vitest@2.0.5(@types/node@22.1.0)) + version: 2.0.5(vitest@2.0.5(@types/node@22.2.0)) get-tsconfig: specifier: ^4.7.6 version: 4.7.6 @@ -305,7 +305,7 @@ importers: version: 5.5.4 vitest: specifier: ^2.0.5 - version: 2.0.5(@types/node@22.1.0) + version: 2.0.5(@types/node@22.2.0) packages: @@ -667,8 +667,8 @@ packages: cpu: [x64] os: [win32] - '@sinclair/typebox@0.33.3': - resolution: {integrity: sha512-2MputLKNw0OxPGWF8KWkLt8B/csTZUkK2tCtiZwJT3NtVOfYPVi6ZEchZAHEHre/qJ4skcS9fL7TuHG36Dk3WQ==} + '@sinclair/typebox@0.33.4': + resolution: {integrity: sha512-IUMFWdOtTqJ5F/rCjy2f3InRS9i4EWaCAMRsXVpUUQVi8hVByz/ifOi41Qt0Law8lz01/6zTz67vT70sB1VZbw==} '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} @@ -677,8 +677,8 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/node@22.1.0': - resolution: {integrity: sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==} + '@types/node@22.2.0': + resolution: {integrity: sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==} '@vitest/coverage-v8@2.0.5': resolution: {integrity: sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==} @@ -1629,17 +1629,17 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.20.0': optional: true - '@sinclair/typebox@0.33.3': {} + '@sinclair/typebox@0.33.4': {} '@sindresorhus/is@4.6.0': {} '@types/estree@1.0.5': {} - '@types/node@22.1.0': + '@types/node@22.2.0': dependencies: undici-types: 6.13.0 - '@vitest/coverage-v8@2.0.5(vitest@2.0.5(@types/node@22.1.0))': + '@vitest/coverage-v8@2.0.5(vitest@2.0.5(@types/node@22.2.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -1653,7 +1653,7 @@ snapshots: std-env: 3.7.0 test-exclude: 7.0.1 tinyrainbow: 1.2.0 - vitest: 2.0.5(@types/node@22.1.0) + vitest: 2.0.5(@types/node@22.2.0) transitivePeerDependencies: - supports-color @@ -2257,13 +2257,13 @@ snapshots: validate-npm-package-name@5.0.1: {} - vite-node@2.0.5(@types/node@22.1.0): + vite-node@2.0.5(@types/node@22.2.0): dependencies: cac: 6.7.14 debug: 4.3.6 pathe: 1.1.2 tinyrainbow: 1.2.0 - vite: 5.4.0(@types/node@22.1.0) + vite: 5.4.0(@types/node@22.2.0) transitivePeerDependencies: - '@types/node' - less @@ -2275,16 +2275,16 @@ snapshots: - supports-color - terser - vite@5.4.0(@types/node@22.1.0): + vite@5.4.0(@types/node@22.2.0): dependencies: esbuild: 0.21.5 postcss: 8.4.41 rollup: 4.20.0 optionalDependencies: - '@types/node': 22.1.0 + '@types/node': 22.2.0 fsevents: 2.3.3 - vitest@2.0.5(@types/node@22.1.0): + vitest@2.0.5(@types/node@22.2.0): dependencies: '@ampproject/remapping': 2.3.0 '@vitest/expect': 2.0.5 @@ -2302,11 +2302,11 @@ snapshots: tinybench: 2.9.0 tinypool: 1.0.0 tinyrainbow: 1.2.0 - vite: 5.4.0(@types/node@22.1.0) - vite-node: 2.0.5(@types/node@22.1.0) + vite: 5.4.0(@types/node@22.2.0) + vite-node: 2.0.5(@types/node@22.2.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.1.0 + '@types/node': 22.2.0 transitivePeerDependencies: - less - lightningcss