Skip to content

Primary Inference Types Compile-time

This page covers InferType, InferSchemaType, and constraint brands. All examples use the bookstore domain. See Schemas for how schemas are registered.

json-tology derives TypeScript types from as const JSON Schema literals at compile time. No code generation. No separate type declarations. The types flow through instantiate(), is(), and the registry type map automatically.

See also Utility types, Range types.


InferType

Derives a TypeScript type from an as const JSON Schema literal.

Signature

/**
 * InferType — Signature
 *
 * `InferType<TSchema, TReferences = {}>` is the primary inference
 * utility: derive a TypeScript type from an `as const` JSON Schema
 * literal. Internally it threads `TSchema` through brand application
 * and restriction layers, ultimately delegating element-level
 * inference to `InferSchemaType<TSchema, TSchema, TReferences>`.
 */

import type { InferType } from '../../../src/types/index.js';
import type { BookSchema } from '../bookstore/index.js';

// Derive the TypeScript type of a registered schema literal:
type Book = InferType<typeof BookSchema>;

// Customary surface — same call shape across every consumer of the
// bookstore registry:
//   type Customer = InferType<typeof CustomerSchema>;
//   type Order    = InferType<typeof OrderSchema>;

const sampleIsbn = '9783522128001';
// Book fields — the inferred type surface is visible to editors and tooling.
const _bookFields: Array<keyof Book> = [
  'isbn',
  'title',
  'printStatus',
  'authors'
];

void _bookFields;

// At runtime we just hold a placeholder; the value of this example is
// in the type derivation above.
console.assert(sampleIsbn.length === 13);
Output
Press Execute to run this example against the real library.

When to use

Use InferType<T> everywhere you need the TypeScript type corresponding to a schema. It handles objects, arrays, enums, const, $ref, oneOf, allOf, if/then/else, and composition. Use InferSchemaType<T, Root, Refs> directly only when you need to specify a different root schema for $ref resolution within a sub-schema.

Examples

Example 1: Object schema with required and optional fields

/**
 * InferType — Example 1: Deriving TypeScript types from canonical schemas
 *
 * Demonstrates `InferType<typeof Schema>` on the canonical bookstore
 * schemas. The resulting types are derived purely at compile time —
 * no code generation, no separate `.d.ts`. Pass the same schema
 * literal that you registered into `bookstoreEntities` and you get
 * the TypeScript view of the wire shape for free.
 */

import type {
  Address, Book, Customer, Order
} from '../bookstore/index.js';

type AssertEqualType<TLeft, TRight>
  = [TLeft] extends [TRight] ? [TRight] extends [TLeft] ? true : false : false;

// Compile-time type test: the `T extends true` constraint rejects any call whose
// type argument resolves to `false`. `T` is consumed as the optional parameter
// type, so no runtime value or cast is needed.
function assert<T extends true>(_proof?: T): void {
  return;
}

// Customer — customerId, email, name required; addresses optional with default.
// Imported from bookstore/index.ts which resolves $ref fields via BookstoreRefs.

assert<AssertEqualType<Customer['customerId'] extends string ? true : false, true>>();
assert<AssertEqualType<Customer['email'] extends string ? true : false, true>>();
assert<AssertEqualType<Customer['name'] extends string ? true : false, true>>();

// Address — composed of named primitives via $ref.

assert<AssertEqualType<Address['street'] extends string ? true : false, true>>();
assert<AssertEqualType<Address['city'] extends string ? true : false, true>>();

// Book — printStatus is the closed enum.

assert<AssertEqualType<
  Book['printStatus'],
  'inPrint' | 'limitedRun' | 'outOfPrint'
>>();

// Order — orderLines is an array of OrderLine.

assert<AssertEqualType<Order['orderLines'] extends readonly unknown[] ? true : false, true>>();

// Log the inferred schema field names — demonstrating what InferType exposes at
// compile time. keyof gives us the property names the type system knows about.
const customerFields: Array<keyof Customer> = [
  'customerId',
  'email',
  'name',
  'addresses'
];
const bookFields: Array<keyof Book> = [
  'isbn',
  'title',
  'printStatus',
  'authors'
];
const addressFields: Array<keyof Address> = [
  'street',
  'city',
  'country',
  'postalCode'
];

console.log('InferType<CustomerSchema> fields:', customerFields.join(', '));
console.log('InferType<BookSchema> fields:', bookFields.join(', '));
console.log('InferType<AddressSchema> fields:', addressFields.join(', '));
Output
Press Execute to run this example against the real library.

The addresses array has default: [] in the schema - at the type level it remains optional because default is a runtime concept; at runtime instantiate() always fills it in.

This is the "default optional InferType" case: the property is optional in the InferType result but always present after instantiate(). A consumer that needs the field required downstream — for example, assigning the inferred type into a shape checked under exactOptionalPropertyTypes — has two options: declare the property in the schema's own required array too (a property can carry both default and required at once, and InferType then infers it as non-optional while instantiate() still fills it in when the caller omits it), or accept the optional type and narrow it at the call site.

/**
 * InferType — default-bearing properties stay optional; a downstream
 * consumer that needs the field required adds it to the schema's own
 * `required` array too.
 *
 * `default` is a runtime-fill concept: `instantiate()` always fills a
 * missing default in, but InferType keeps the property optional at the
 * type level, because nothing guarantees a default has run before the
 * type is read (a `materialize({ enablePartial: true })` result, or a
 * value constructed by hand, may not have it). A consumer that needs the
 * field required downstream — e.g. assigning into a shape checked under
 * `exactOptionalPropertyTypes` — declares the property `required` on the
 * schema too (a property can carry both `default` and `required` at
 * once), or accepts the optional type and narrows it at the call site.
 */

import type { InferType } from '../../../src/types/index.js';

const _AddressesOptionalSchema = {
  '$id': 'urn:example:AddressesOptional',
  'properties': {
    'addresses': {
      'default': [],
      'items': { 'type': 'string' },
      'type': 'array'
    }
  },
  'required': [],
  'type': 'object'
} as const;

const _AddressesRequiredSchema = {
  '$id': 'urn:example:AddressesRequired',
  'properties': {
    'addresses': {
      'default': [],
      'items': { 'type': 'string' },
      'type': 'array'
    }
  },
  'required': ['addresses'],
  'type': 'object'
} as const;

type AssertExtendsType<TLeft, TRight>
  = [TLeft] extends [TRight] ? true : false;

function assert<T extends true>(_proof?: T): void {
  return;
}

// default alone: addresses stays optional — addresses?: string[]
type OptionalShape = InferType<typeof _AddressesOptionalSchema>;

assert<AssertExtendsType<OptionalShape, { 'addresses'?: string[]; }>>();

// default AND required: addresses is required at the type level too —
// instantiate() still fills it in when the caller omits it.
type RequiredShape = InferType<typeof _AddressesRequiredSchema>;

assert<AssertExtendsType<RequiredShape, { 'addresses': string[]; }>>();

console.log('default alone -> optional at the type level (default optional InferType)');
console.log('default + required -> required at the type level too');
Output
Press Execute to run this example against the real library.

Example 2: Integer range, enum, and const

/**
 * InferType — Example: Integer range, enum, and const.
 *
 * Bounded `integer` schemas with both bounds in the 0–50 range produce
 * literal unions automatically. `enum` schemas resolve to the union of
 * their declared values.
 */

import type { InferType } from '../../../src/types/index.js';
import type {
  BookstoreRefs, ReviewSchema
} from '../bookstore/index.js';

// rating: minimum 1, maximum 5 — auto-generates a literal union.
type Rating = InferType<typeof ReviewSchema, BookstoreRefs>['rating'];
// 1 | 2 | 3 | 4 | 5

const _CurrencySchema = {
  '$id': 'https://bookstore.example/Currency',
  'enum': [
    'USD',
    'EUR',
    'GBP',
    'JPY'
  ],
  'type': 'string'
} as const;

type Currency = InferType<typeof _CurrencySchema>;
// 'USD' | 'EUR' | 'GBP' | 'JPY'

const rating: Rating = 4;
const currency: Currency = 'EUR';

// Rating is the literal union 1 | 2 | 3 | 4 | 5 — derived from the
// schema's minimum/maximum bounds. Currency is 'USD' | 'EUR' | 'GBP' | 'JPY'
// — derived from the schema's enum array.
console.log('rating (1..5 literal union):', rating);
console.log('currency (enum union):', currency);
Output
Press Execute to run this example against the real library.

Bounded integer schemas with both bounds in the 0-50 range automatically produce literal unions. See Constraint Brands for details on integer ranges and multipleOf ranges.

Example 3: Cross-schema $ref resolution

When a schema references another by absolute IRI, pass a reference map as the second type argument.

/**
 * InferType — Example: Cross-schema $ref resolution.
 *
 * When a schema references another by absolute IRI, the second type
 * argument carries a reference map so `$ref` resolves to the
 * downstream schema's type. Without the map, the resolved element
 * type would fall through to `unknown`.
 */

import type { InferType } from '../../../src/types/index.js';
import type {
  OrderLineSchema, OrderSchema
} from '../bookstore/index.js';

// Pass a reference map keyed by absolute IRI; values are the schemas
// to resolve those IRIs against.
type Order = InferType<
  typeof OrderSchema,
  { 'urn:bookstore:OrderLine': typeof OrderLineSchema; }
>;

// Sanity: the inferred items array has element type carrying at least
// the OrderLine quantity field.
type AssertExtendsType<TLeft, TRight>
  = [TLeft] extends [TRight] ? true : false;

function assert<T extends true>(_proof?: T): void {
  return;
}

assert<AssertExtendsType<
  Order['orderLines'] extends readonly unknown[] ? true : false,
  true
>>();

// At compile time, Order['orderLines'] is an array type resolved via the
// reference map. The assertion above confirms the resolution succeeded.
// Log the schema $id to show the cross-schema reference anchor.
console.log('OrderSchema $id:', 'urn:bookstore:Order');
console.log('reference map key:', 'urn:bookstore:OrderLine');
Output
Press Execute to run this example against the real library.

Without the reference map, items resolves to RefNotFoundType<'...'> at the element level — a compile-error brand, not a silent unknown. An unresolved cross-schema $ref is a compile error: thread the reference map (or register the target schema) so it resolves. A bare $ref to the schema's own $id still resolves without a map.

Example 4: CURIE-keyed cross-schema $ref resolution

A hash-namespace $id (https://ns#Class) is the idiomatic OWL form, and the recommended way to reference a sibling schema in that namespace is a CURIE $ref ('ns:Class'). The reference map key always matches the $ref string as authored — a CURIE $ref needs a CURIE-keyed map entry, an absolute-IRI $ref (as in Example 3) needs an absolute-IRI-keyed entry. Mixing the two (a CURIE $ref with an IRI-keyed map, or vice versa) is a map miss, which resolves to RefNotFoundType exactly as if no map had been supplied at all.

/**
 * InferType — Example: CURIE-keyed cross-schema $ref resolution.
 *
 * A hash-namespace `$id` (`https://ns#Class`) is the idiomatic OWL form, and
 * the recommended way to reference a sibling schema in that namespace is a
 * CURIE `$ref` (`'ns:Class'`) rather than the expanded IRI. When the `$ref`
 * is written as a CURIE, the reference map passed as InferType's second type
 * argument must be keyed by that exact CURIE string, not the expanded IRI —
 * the map key always matches the `$ref` string as authored.
 */

import type { InferType } from '../../../src/types/index.js';

const _BookGenreLabelSchema = {
  '$id': 'https://bookstore.example/ontology#BookGenreLabel',
  'type': 'string'
} as const;

const BookGenreSchema = {
  '$id': 'https://bookstore.example/ontology#BookGenre',
  'properties': { 'label': { '$ref': 'bk:BookGenreLabel' } },
  'required': ['label'],
  'type': 'object'
} as const;

// Reference map keyed by the CURIE exactly as written in $ref —
// 'bk:BookGenreLabel' — not 'https://bookstore.example/ontology#BookGenreLabel'.
type BookGenre = InferType<
  typeof BookGenreSchema,
  { 'bk:BookGenreLabel': typeof _BookGenreLabelSchema; }
>;

type AssertExtendsType<TLeft, TRight>
  = [TLeft] extends [TRight] ? true : false;

function assert<T extends true>(_proof?: T): void {
  return;
}

// The CURIE-keyed map resolved `label` to `string`, not RefNotFoundType.
assert<AssertExtendsType<BookGenre['label'], string>>();

console.log('BookGenreSchema $id:', BookGenreSchema.$id);
console.log('reference map key (CURIE):', 'bk:BookGenreLabel');
Output
Press Execute to run this example against the real library.

Troubleshooting: RefNotFoundType ({ kind: 'RefNotFound', unresolvedRef }) in an inferred type

If a property in an InferType result looks like { readonly kind: 'RefNotFound'; readonly unresolvedRef: '...' }, that is RefNotFoundType<'...'> — a deliberate compile-error brand json-tology emits when a cross-schema $ref has no matching entry in the reference map. It is not a sign that "inference degrades to unknown"; the type system is telling you the reference map is missing or mismatched.

Do not hand-roll a *Type by hand when you see RefNotFoundType. Adding a manually-written type to work around what looks like a broken inference defeats InferType: the hand-rolled type silently drifts from the schema the moment a property is renamed or added, because nothing re-checks it against the schema literal. Thread the reference map instead — same call, one extra type argument, and the type tracks the schema again.

/**
 * Anti-pattern: hand-rolling a type when InferType shows RefNotFoundType.
 *
 * Without a reference map, a cross-schema $ref that InferType cannot resolve
 * infers to RefNotFoundType<'...'> — `{ kind: 'RefNotFound'; unresolvedRef:
 * '...' }` — a deliberate compile-error brand, not a silent `unknown`.
 * Reading that shape as "inference degrades" and hand-rolling a replacement
 * type defeats InferType: the hand-rolled type silently drifts from the
 * schema the moment a property is added or renamed. The fix is to thread
 * the reference map, not to hand-roll a type.
 */

import type { InferType } from '../../../src/types/index.js';

const _BookGenreLabelSchema = {
  '$id': 'https://bookstore.example/ontology#BookGenreLabel',
  'type': 'string'
} as const;

const _BookGenreSchema = {
  '$id': 'https://bookstore.example/ontology#BookGenre',
  'properties': { 'label': { '$ref': 'bk:BookGenreLabel' } },
  'required': ['label'],
  'type': 'object'
} as const;

type AssertExtendsType<TLeft, TRight>
  = [TLeft] extends [TRight] ? true : false;

function assert<T extends true>(_proof?: T): void {
  return;
}

// ⊥ Without the reference map, `label` is RefNotFoundType<'bk:BookGenreLabel'>
// — { kind: 'RefNotFound'; unresolvedRef: 'bk:BookGenreLabel' }.
type UnresolvedLabel = InferType<typeof _BookGenreSchema>['label'];

assert<AssertExtendsType<
  UnresolvedLabel,
  { readonly 'kind': 'RefNotFound';
    readonly 'unresolvedRef': 'bk:BookGenreLabel'; }
>>();

// ⊥ Don't do this — hand-rolling a replacement type because the $ref "looks
// broken" defeats InferType and drifts from BookGenreSchema silently.
type BookGenreHandRolled = {
  // hand-rolled — comment claims "cross-file inference degrades to unknown"
  'label': string;
};

// ✓ Do this — thread the reference map, keyed by the $ref string as written.
type BookGenre = InferType<
  typeof _BookGenreSchema,
  { 'bk:BookGenreLabel': typeof _BookGenreLabelSchema; }
>;

const handRolled: BookGenreHandRolled = { 'label': 'Fantasy' };
const derived: BookGenre = { 'label': 'Fantasy' };

console.assert(handRolled.label === derived.label);
console.log('Unresolved $ref shape (RefNotFoundType): kind + unresolvedRef fields.');
console.log('Recommended: InferType + reference map, not a hand-rolled type.');
Output
Press Execute to run this example against the real library.

Searchable terms for this failure mode: RefNotFound, unresolvedRef, kind unresolvedRef. See also Runtime decoding across packages for the same pattern applied to a registry split across two packages, including why instantiate's return type cannot always be trusted across a package boundary even after the reference map is threaded correctly on the type side.

Comparison

ts
import type { InferType } from 'json-tology/types';

const CustomerSchema = { ... } as const;
type Customer = InferType<typeof CustomerSchema>;
ts
import { z } from 'zod';

const CustomerSchema = z.object({
  id:    z.string().uuid(),
  email: z.string().email(),
  name:  z.string(),
});
type Customer = z.infer<typeof CustomerSchema>;
ts
import { Type } from '@sinclair/typebox';

const CustomerSchema = Type.Object({
  id:    Type.String({ format: 'uuid' }),
  email: Type.String({ format: 'email' }),
  name:  Type.String(),
});
// TypeBox exposes Static<T> for type inference:
import type { Static } from '@sinclair/typebox';
type Customer = Static<typeof CustomerSchema>;
ts
// AJV validates at runtime but provides no compile-time type inference.
// Types must be declared separately and kept in sync manually.
interface Customer {
  id: string;
  email: string;
  name: string;
}
py
from pydantic import BaseModel, Field

class Customer(BaseModel):
    id: str
    email: str
    name: str

# Python type annotations ARE the schema  - no separate inference step.
ts
// Limitation: feature not directly supported in Valibot. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in io-ts. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.
  • Constraint Brands - format, string, numeric, array brands
  • InferSchemaType - explicit root control for sub-schema inference
  • Runtime decoding across packages - CURIE-keyed reference maps, and why instantiate's return type doesn't always survive a package boundary
  • Schemas - how schemas are registered

InferSchemaType

Lower-level inference with explicit Root and Refs parameters. Resolves $ref against the specified root schema.

Signature

/**
 * InferSchemaType — Signature
 *
 * The canonical declaration of InferSchemaType<T, TRoot, TReferences>:
 * lower-level inference with explicit root control. Resolves `$ref`
 * against the specified root schema. `InferType<T>` calls
 * `InferSchemaType<T, T>` internally — explicit use is only needed
 * when the sub-schema and the resolution root are different objects.
 */

import type {
  InferSchemaType
} from '../../../src/types/index.js';
import {
  BookSchema
} from '../bookstore/index.js';

// Surface declaration:
//
//   type MySubType = InferSchemaType<
//     typeof SubSchema,    // The sub-schema to infer
//     typeof RootSchema,   // Root schema providing $defs for $ref
//     RefMap               // Optional cross-schema reference map
//   >;

// When sub and root are the same, InferSchemaType behaves like InferType
// without the additional brand wrappers.
type BookViaSchema = InferSchemaType<
  typeof BookSchema,
  typeof BookSchema
>;
// Confirm the inferred type carries the expected isbn field.
const _bookVia: Partial<BookViaSchema> = {};

void _bookVia;

console.assert(typeof BookSchema.$id === 'string');
Output
Press Execute to run this example against the real library.

When to use

Use when you need to infer the type of a sub-schema that uses $ref: '#/$defs/...' pointing into a larger parent schema. InferType<T> calls InferSchemaType<T, T> automatically - explicit use is only needed when the sub-schema and the root are different objects.

Examples

Example 1: Infer a sub-schema type from $defs

/**
 * InferSchemaType — Example: Infer a sub-schema type from $defs.
 *
 * The sub-schema lives inside a parent's `$defs` block and references
 * nothing outside its own keyword set, but if it did, the root
 * provides the resolution scope. InferSchemaType takes the sub-schema
 * as `T` and the parent as `TRoot`.
 */

import type {
  InferSchemaType
} from '../../../src/types/index.js';

const _CatalogSchema = {
  '$defs': {
    'FeaturedBook': {
      'properties': {
        'badge': {
          'enum': [
            'bestseller',
            'new',
            'staff-pick'
          ],
          'type': 'string'
        },
        'isbn': { 'type': 'string' }
      },
      'required': [
        'isbn',
        'badge'
      ],
      'type': 'object'
    }
  },
  '$id': 'https://bookstore.example/Catalog',
  'properties': { 'featured': { '$ref': '#/$defs/FeaturedBook' } },
  'type': 'object'
} as const;

type FeaturedBook = InferSchemaType<
  typeof _CatalogSchema['$defs']['FeaturedBook'],
  typeof _CatalogSchema
>;
// { readonly isbn: string; readonly badge: 'bestseller' | 'new' | 'staff-pick' }

const pick: FeaturedBook = {
  'badge': 'staff-pick',
  'isbn': '9783522128001'
};

console.assert(pick.badge === 'staff-pick');
console.assert(pick.isbn === '9783522128001');
Output
Press Execute to run this example against the real library.

Comparison

ts
type Sub = InferSchemaType<typeof SubSchema, typeof RootSchema>;
ts
// Not applicable  - Zod types are derived per-schema, not from a root.
// Nested types are inferred via z.infer<typeof SubSchema>.
ts
// TypeBox infers from the sub-object directly using Static<T>.
// No explicit root concept.
ts
// Not applicable  - AJV provides no TypeScript inference.
py
# Not applicable  - Python uses class-based types, not JSON Pointer sub-schemas.
ts
// Limitation: feature not directly supported in Valibot. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in io-ts. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.

Constraint brands

json-tology surfaces JSON Schema constraint keywords as compile-time phantom brands. Two values with different constraints produce structurally incompatible TypeScript types.

The only way to obtain a branded value is through the validation API (instantiate, is, materialize, etc.), which enforces that data has passed runtime checks before being treated as a constrained type.

See Constraint Brands for the full reference, configuration flags, and structural narrowing features.

Examples

Example 1: Format brands prevent mixing email and UUID strings

/**
 * Constraint brands — Example: Format brands prevent mixing email and
 * UUID strings.
 *
 * Two `string` schemas with different `format` keywords resolve to
 * structurally incompatible TypeScript types — their `FormatBrand`
 * intersections differ. The only way to obtain a branded value is via
 * `instantiate`, which validates the format and applies the brand.
 */

import type { InferType } from '../../../src/types/index.js';
import {
  bookstoreEntities, CustomerIdSchema, EmailSchema
} from '../bookstore/index.js';

type CustomerId = InferType<typeof CustomerIdSchema>;
// string & FormatBrand<'uuid'>

type Email = InferType<typeof EmailSchema>;
// string & FormatBrand<'email'>

// TypeScript rejects mixing the two brands at compile time. Use
// `instantiate` to validate and brand a value:
const id: CustomerId = bookstoreEntities.instantiate(
  CustomerIdSchema,
  '09f8e7d6-c5b4-4321-9876-543210fedcba'
);
const email: Email = bookstoreEntities.instantiate(
  EmailSchema,
  'bastian@neverending.example'
);

// Both are strings at runtime …
console.assert(typeof id === 'string');
console.assert(typeof email === 'string');

// … but the brands keep them structurally distinct in the type system.
// const swap: CustomerId = email; // compile error — brand mismatch
Output
Press Execute to run this example against the real library.

Example 2: Integer range as literal union

/**
 * Constraint brands — Example: Integer range as literal union.
 *
 * `ReviewSchema.properties.rating` $refs RatingScoreSchema, which
 * declares `type: 'integer', minimum: 1, maximum: 5`. The inferred
 * `Review['rating']` resolves to the literal union `1 | 2 | 3 | 4 | 5`
 * — out-of-range values fail at compile time.
 */

import type { InferType } from '../../../src/types/index.js';
import type {
  BookstoreRefs, ReviewSchema
} from '../bookstore/index.js';

type Review = InferType<typeof ReviewSchema, BookstoreRefs>;
type Rating = Review['rating'];
// 1 | 2 | 3 | 4 | 5

// OK — within 1..5
const fine: Rating = 3;

// OK — within 1..5
const top: Rating = 5;

// const bad: Rating = 0; // compile error — 0 is not in 1..5
// const bad2: Rating = 6; // compile error — 6 is not in 1..5

// Rating is the literal union 1 | 2 | 3 | 4 | 5 — derived from
// RatingScoreSchema's minimum/maximum bounds. Only values in that range
// are assignable; out-of-range values fail at compile time.
console.log('fine rating:', fine);
console.log('top rating:', top);
Output
Press Execute to run this example against the real library.

Example 3: Disable brands for a project

Create a .d.ts anywhere in your tsconfig include path:

/**
 * Constraint brands — Example: Disable brands for a project.
 *
 * Create a `.d.ts` anywhere in your `tsconfig include` path and
 * augment the `JsonTologyTypeConfigInterface` module declaration with
 * `brands: false`. All `InferType` results revert to plain TypeScript
 * primitives. Runtime validation is unaffected.
 *
 * The block below is the literal contents of `json-tology.d.ts`:
 *
 *   declare module 'json-tology/types' {
 *     interface JsonTologyTypeConfigInterface {
 *       brands: false; // disables all phantom brands
 *     }
 *   }
 *
 * After adding the declaration, the inferred shape of the Email
 * primitive collapses from `string & FormatBrand<'email'>` to plain
 * `string`. The runtime call to `instantiate` still validates the
 * format — only the static type changes.
 */

import type { InferType } from '../../../src/types/index.js';
import type { EmailSchema } from '../bookstore/index.js';

// With brands enabled (default), the inferred type carries the
// FormatBrand<'email'> intersection.
// With brands disabled via the d.ts above, the same type collapses to
// plain string.
type Email = InferType<typeof EmailSchema>;

// In either configuration, Email is assignable to string.
type AssertExtendsType<TLeft, TRight>
  = [TLeft] extends [TRight] ? true : false;

function assert<T extends true>(_proof?: T): void {
  return;
}

assert<AssertExtendsType<Email, string>>();

const sample: Email = 'bastian@neverending.example' as Email;

console.assert(typeof sample === 'string');
Output
Press Execute to run this example against the real library.

All InferType results revert to plain TypeScript primitives. Runtime validation is unaffected.

Comparison

ts
// Brands are on by default.
// string & FormatBrand<'email'> is incompatible with string & FormatBrand<'uuid'>.
type Email = InferType<typeof EmailSchema>; // string & FormatBrand<'email'>
ts
// Zod supports .brand<'Email'>() for nominal typing.
const EmailSchema = z.string().email().brand<'Email'>();
type Email = z.infer<typeof EmailSchema>; // string & z.BRAND<'Email'>
// Must be applied manually per schema  - not automatic from JSON Schema keywords.
ts
// TypeBox does not generate phantom brands from JSON Schema constraints.
// All string schemas infer as string.
type Email = Static<typeof EmailSchema>; // string
ts
// AJV provides no type inference  - no brands possible.
py
# Pydantic v2 supports Annotated types for similar constraint propagation:
from pydantic import EmailStr
from typing import Annotated

class Customer(BaseModel):
    email: EmailStr  # validated as email at runtime, typed as str
ts
// Limitation: feature not directly supported in Valibot. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in io-ts. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.
  • Constraint Brands - full brand reference and configuration
  • Transform.brand - explicit nominal brands on schema $id

Mutability

Generated and inferred types are never readonly. Every schema-derived type surface — InferType, InferSchemaType, NominalSchemaType, MaterializedSchemaType, ParseOutputType (the return type of instantiate(), encode(), dump(), and friends), CanonicalShapeType/UnbrandType (the type materialize() accepts and a normalize transform's decode/encode boundary produces), and BrandOutputType — types its object properties and array/tuple elements as plain, mutable TypeScript: T[], not readonly T[] or ReadonlyArray<T>; { id: string }, not { readonly id: string }.

Mutability is the default. Readonly-ness is a decision an individual consumer makes at their own usage site — never something the library imposes on every consumer of a generated type. Concretely:

  • A function parameter can always be declared readonly T[] even when the canonical inferred type for T[] is a plain mutable array — TypeScript allows passing a mutable array anywhere a readonly array is expected, never the reverse. Narrowing mutable → readonly is free; a library that hands back readonly by default forecloses that choice and forces every consumer who needs to reassign or mutate the value (build it up in a loop, sort it in place, patch a field before re-validating) into a workaround.
  • This applies uniformly across every type-producing surface in the package: authoring a schema and reading InferType<typeof Schema> produces the same mutability convention as calling instantiate(), materialize(), encode(), or dump() and reading the value back. There is exactly one convention, not one for compile-time inference and a different one for runtime construction.
  • The convention also matches @studnicky/types's FromSchema and the wider TypeScript ecosystem's own json-schema-to-ts: a schema-inferred type is the canonical mutable shape. Consumers who need immutability — a Redux-style state slice, a value handed across a worker boundary, a defensive API boundary — opt in locally with their own readonly/Readonly<T> annotation rather than fighting the library's output type at every call site.

Compile-time constraint brands (FormatBrandType, MinItemsBrandType, and friends) are unaffected by this — they are phantom unique symbol markers used to make structurally-different constraints incompatible types, not readonly modifiers on real properties, so they carry no mutability implication either way.

Released under the MIT License.