Last active
September 21, 2025 00:09
-
-
Save VictorQueiroz/231ff0eaf2c1a06115d896f59a851cdd to your computer and use it in GitHub Desktop.
Helper that can be used to build strict Typebox types.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| ArrayOptions, | |
| DateOptions, | |
| IntegerOptions, | |
| ObjectOptions, | |
| SchemaOptions, | |
| Static, | |
| StringOptions, | |
| TBoolean, | |
| TInteger, | |
| TNull, | |
| TNumber, | |
| TObject, | |
| TSchema, | |
| TString, | |
| TUnion, | |
| Type, | |
| Union | |
| } from "@sinclair/typebox"; | |
| import JSBI from "jsbi"; | |
| import assert from "node:assert"; | |
| function getIntegerRange(bitLength: number, unsigned: boolean) { | |
| const max = BigInt(2) ** BigInt(bitLength) - BigInt(1); | |
| const min = unsigned ? BigInt(0) : (-2n) ** BigInt(bitLength - 1); | |
| return { min, max }; | |
| } | |
| const integer = { | |
| int32: (options: SchemaOptions = {}) => | |
| VariableBitsInteger(32, false, options), | |
| uint32: (options: SchemaOptions = {}) => | |
| VariableBitsInteger(32, true, options), | |
| int8: (options: SchemaOptions = {}) => | |
| VariableBitsInteger(8, false, options), | |
| uint8: (options: SchemaOptions = {}) => | |
| VariableBitsInteger(8, true, options), | |
| int16: (options: SchemaOptions = {}) => | |
| VariableBitsInteger(16, false, options), | |
| uint16: (options: SchemaOptions = {}) => | |
| VariableBitsInteger(16, true, options) | |
| }; | |
| const dataType = { | |
| ...integer, | |
| float32: (options: SchemaOptions = {}): TNumber => | |
| Type.Number( | |
| schemaOptions<SchemaOptions>({ | |
| description: `A 32-bit floating point number.`, | |
| examples: [3.4028235e38], | |
| default: 0.0, | |
| minimum: -3.4028235e38, | |
| maximum: 3.4028235e38, | |
| multipleOf: 1e-7, | |
| ...options | |
| }) | |
| ), | |
| str: (options: StringOptions = {}): TString => | |
| Type.String({ | |
| contentEncoding: "8bit", | |
| contentMediaType: "text/plain", | |
| examples: [], | |
| ...options | |
| }), | |
| date: (options: DateOptions = {}) => | |
| Type.Date( | |
| schemaOptions<DateOptions>({ | |
| description: `A date type in ISO 8601 format.`, | |
| examples: [ | |
| "2025-09-19T14:34:16.830Z", | |
| "1970-01-01T00:00:00.000Z" | |
| ], | |
| default: "1970-01-01T00:00:00.000Z", | |
| ...options | |
| }) | |
| ), | |
| null: (options: SchemaOptions = {}) => | |
| Type.Null( | |
| schemaOptions({ examples: [null], default: null, ...options }) | |
| ), | |
| boolean: (options: SchemaOptions = {}): TBoolean => | |
| Type.Boolean( | |
| schemaOptions({ examples: [true, false], ...options }) | |
| ) | |
| }; | |
| function schemaId(id: string) { | |
| assert.strict.ok( | |
| id.length > 0, | |
| "Schema must have a non-empty $id." | |
| ); | |
| assert.match( | |
| id, | |
| /^[A-Za-z_]+[A-Za-z0-9_]+$/, | |
| [ | |
| `${id} is not a valid schema $id.`, | |
| `$id must start with a letter or underscore and contain only alphanumeric characters and underscores.` | |
| ].join(" ") | |
| ); | |
| return id; | |
| } | |
| function schemaOptions< | |
| T extends | |
| | DateOptions | |
| | SchemaOptions | |
| | ObjectOptions | |
| | IntegerOptions | |
| | StringOptions | |
| | ArrayOptions | |
| >(options: T): T { | |
| options = { examples: [], readOnly: true, ...options }; | |
| if (!Array.isArray(options.examples)) { | |
| console.warn( | |
| "Schema options `examples` property must be an array. Found: %o", | |
| options.examples | |
| ); | |
| } | |
| const id = options["$id"] ?? null; | |
| if (id !== null) { | |
| options["$id"] = schemaId(id); | |
| } | |
| return options; | |
| } | |
| function union<T extends TSchema[]>( | |
| elements: T, | |
| options: SchemaOptions = {} | |
| ): Union<T> { | |
| return Type.Union<T>( | |
| elements, | |
| schemaOptions<SchemaOptions>({ | |
| description: `An union of the following types: ${elements | |
| .map(e => e.$id) | |
| .join(", ")}.`, | |
| examples: elements.map(e => e.examples).flat(), | |
| ...options | |
| }) | |
| ); | |
| } | |
| function nullable<T extends TSchema>( | |
| schema: T, | |
| options: SchemaOptions = {} | |
| ): TUnion<[T, TNull]> { | |
| return union<[T, TNull]>( | |
| [schema, dataType.null()], | |
| schemaOptions<SchemaOptions>({ | |
| description: `An union describing a type that can also be null.`, | |
| examples: [null], | |
| ...options | |
| }) | |
| ); | |
| } | |
| const composedTypes = { | |
| nullable, | |
| uuid: (options: StringOptions = {}) => | |
| types.str( | |
| schemaOptions<StringOptions>({ | |
| pattern: `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, | |
| maxLength: 36, | |
| minLength: 36, | |
| description: `A string following the UUID format as per RFC 4122.`, | |
| examples: ["3fa85f64-5717-4562-b3fc-2c963f66afa6"], | |
| ...options | |
| }) | |
| ) | |
| }; | |
| interface IObjectOptions<T extends TSchema> extends ObjectOptions { | |
| examples?: Static<T>[]; | |
| } | |
| const types = { | |
| ...composedTypes, | |
| ...dataType, | |
| union, | |
| literal: Type.Literal, | |
| unknown: Type.Unknown, | |
| array: <T extends TSchema>(items: T, options: ArrayOptions = {}) => | |
| Type.Array<T>( | |
| items, | |
| schemaOptions<ArrayOptions>({ | |
| description: `An array of ${items.$id} items.`, | |
| minItems: 0, | |
| minContains: 0, | |
| examples: items.examples, | |
| ...options | |
| }) | |
| ), | |
| ref: <T extends TSchema>( | |
| schema: T, | |
| options: SchemaOptions = {} | |
| ) => { | |
| assert.strict.ok( | |
| schema.$id, | |
| "Schema must have an $id to be referenced." | |
| ); | |
| const schemaRef = Type.Ref( | |
| schema.$id, | |
| schemaOptions<SchemaOptions>(options) | |
| ); | |
| return Type.Unsafe<Static<T>>(schemaRef); | |
| }, | |
| optional: Type.Optional, | |
| obj: <TProperties extends Record<string, TSchema>>( | |
| properties: TProperties, | |
| objectOptions: IObjectOptions<TObject<TProperties>> = {} | |
| ): TObject<TProperties> => { | |
| const options = schemaOptions<ObjectOptions>({ | |
| minProperties: Object.keys(properties).length, | |
| maxProperties: Object.keys(properties).length, | |
| additionalProperties: false, | |
| ...objectOptions | |
| }); | |
| return Type.Object( | |
| Object.fromEntries( | |
| Object.entries(properties).map(([key, value]) => [ | |
| key, | |
| { | |
| description: | |
| value.description ?? | |
| `The ${key} property of the ${options.$id} object.`, | |
| readOnly: options.readOnly, | |
| ...value | |
| } | |
| ]) | |
| ) as TProperties, | |
| options | |
| ); | |
| } | |
| }; | |
| function VariableBitsInteger( | |
| bitLength: number, | |
| unsigned: boolean = false, | |
| options: IntegerOptions = {} | |
| ): TInteger { | |
| const { min, max } = getIntegerRange(bitLength, unsigned); | |
| return Type.Integer( | |
| schemaOptions<IntegerOptions>({ | |
| $id: `${unsigned ? "Unsigned" : "Signed"}${bitLength}BitInteger`, | |
| description: `A ${bitLength}-bit ${unsigned ? "unsigned" : "signed"} integer.`, | |
| examples: [max.toString()], | |
| default: min.toString(), | |
| minimum: JSBI.toNumber(JSBI.BigInt(min.toString())), | |
| maximum: JSBI.toNumber(JSBI.BigInt(max.toString())), | |
| ...options | |
| }) | |
| ); | |
| } | |
| export default types; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment