Skip to content

Utility Types Compile-time

This page covers utility types for working with schema-derived types: deprecated key extraction, enum unions, exhaustive checks, and default alignment guards. All examples use the bookstore domain. See Schemas for how schemas are registered.

See also Primary inference, Range types.

All schema-derived types on this page — like every generated type in json-tology — are mutable by default; see Mutability.

json-tology exports five utility types for working with schema-derived types. Each has its own section below.

TypePurpose
DeprecatedKeysType<T>Extract keys marked deprecated: true
NonDeprecatedSchemaType<T>Omit deprecated properties from inferred type
EnumValuesType<T>Extract enum values as a TS union
ExhaustiveType<T>Enforce exhaustive switch/case at compile time
DefaultAlignedType<T>never when declared defaults mismatch their declared types

DeprecatedKeysType<T>

Declaration. Extracts the union of property keys marked deprecated: true from an object schema literal. Returns never when no properties carry the annotation.

Use this when you want compile-time visibility into which properties of a schema are deprecated - for example, to build a lint utility, produce a typed exclusion list, or assert that a particular key is (or is not) deprecated.

Don't use this when you only need the filtered object type at a call site; use NonDeprecatedSchemaType<T> instead. DeprecatedKeysType<T> gives you the key names, not the pruned object shape.

Signature

/**
 * DeprecatedKeysType — Signature
 *
 * The canonical declaration of DeprecatedKeysType<T>: extracts the
 * union of property keys marked `deprecated: true` from an object
 * schema literal. Returns `never` when no properties carry the
 * annotation.
 */

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

// Type declaration mirrors the canonical export in src/types/Infer.ts:
//
// export type DeprecatedKeysType<T>
//   = T extends { readonly 'properties': infer P }
//     ? { [K in keyof P & string]: P[K] extends { readonly 'deprecated': true } ? K : never
//       }[keyof P & string]
//     : never;

const _BookV1Schema = {
  '$id': 'https://bookstore.example/BookV1',
  'properties': {
    'isbn': { 'type': 'string' },
    'legacySku': {
      'deprecated': true,
      'type': 'string'
    },
    'title': { 'type': 'string' }
  },
  'required': [
    'isbn',
    'title'
  ],
  'type': 'object'
} as const;

type DeprecatedBookKeys = DeprecatedKeysType<typeof _BookV1Schema>;

// 'legacySku' is the only deprecated key in the schema above
const deprecatedKey: DeprecatedBookKeys = 'legacySku';

console.log('DeprecatedKeysType<BookV1Schema>:', deprecatedKey);
console.log('schema property count:', Object.keys(_BookV1Schema.properties).length, '| deprecated count: 1');
Output
Press Execute to run this example against the real library.

Examples

Example 1: Extract deprecated keys from a schema

/**
 * Utility types — Example 1: pointer paths and required-key extraction
 *
 * Demonstrates the type-level helpers that operate on a registered
 * schema literal: extracting required-key unions, computing property
 * pointer paths, and inferring keyof view. All compile-time only.
 */

import type {
  DeepPropertyPathsType, ExhaustiveType, PropertyPathsType
} from '../../../src/types/Infer.js';
import type {
  CustomerSchema, OrderSchema
} from '../bookstore/index.js';

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

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

// PropertyPathsType — direct properties only.
type CustomerPaths = PropertyPathsType<typeof CustomerSchema>;

assert<AssertEqualType<CustomerPaths extends string ? true : false, true>>();

// DeepPropertyPathsType — recursive pointer paths.
type OrderDeep = DeepPropertyPathsType<typeof OrderSchema>;

assert<AssertEqualType<OrderDeep extends string ? true : false, true>>();

// ExhaustiveType — a discriminated-union exhaustion marker. It accepts only
// `never`, so feeding it the residual of an enum after every literal has been
// excluded statically proves the switch handled every case. Here all three
// PrintStatus literals are excluded, leaving `never`.
type PrintStatusLiterals = 'inPrint' | 'limitedRun' | 'outOfPrint';
type HandledResidual = Exclude<PrintStatusLiterals, 'inPrint' | 'limitedRun' | 'outOfPrint'>;
type PrintStatusExhaustive = ExhaustiveType<HandledResidual>;

assert<AssertEqualType<[PrintStatusExhaustive] extends [never] ? true : false, true>>();


// Log the utility type shapes at runtime — showing what the type system exposes.
const customerPathSample: CustomerPaths = 'customerId';
const orderDeepSample: OrderDeep = 'orderLines';

console.log('PropertyPathsType<CustomerSchema> sample path:', customerPathSample);
console.log('DeepPropertyPathsType<OrderSchema> sample path:', orderDeepSample);
// PrintStatusExhaustive is `ExhaustiveType<never>` = `never` (no runtime value);
// the type-level assertion above is its proof that every case is handled.
console.log('ExhaustiveType resolves to never — all PrintStatus cases handled.');
Output
Press Execute to run this example against the real library.

Example 2: Compile-time assertion that a key is deprecated

/**
 * DeprecatedKeysType — Example 2: Compile-time assertion that a key is deprecated
 *
 * Demonstrates using DeprecatedKeysType to assert at compile time whether
 * a particular property is marked deprecated in a schema.
 */

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

const _BookV1Schema = {
  '$id': 'https://bookstore.example/BookV1',
  'properties': {
    'isbn': { 'type': 'string' },
    'legacySku': {
      'deprecated': true,
      'type': 'string'
    },
    'title': { 'type': 'string' }
  },
  'required': [
    'isbn',
    'title'
  ],
  'type': 'object'
} as const;

type DeprecatedBookKeys = DeprecatedKeysType<typeof _BookV1Schema>;
// 'legacySku'

// Compile-time guard - narrows to never if the key is not deprecated.
// OK: 'legacySku' is in DeprecatedBookKeys
const deprecatedKey: DeprecatedBookKeys = 'legacySku';

console.log('DeprecatedKeysType<BookV1Schema>:', deprecatedKey, '(only key with deprecated: true)');
Output
Press Execute to run this example against the real library.

Bad examples

Anti-pattern 1: Manual string union

/**
 * Anti-pattern: Manual string union for deprecated keys.
 *
 * Hand-rolling the union of deprecated property names drifts from the
 * schema the moment a second field is marked deprecated. Use
 * `DeprecatedKeysType<T>` instead — it stays in sync with the schema
 * literal at compile time.
 */

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

const _BookV1Schema = {
  '$id': 'https://bookstore.example/BookV1',
  'properties': {
    'isbn': { 'type': 'string' },
    'legacySku': {
      'deprecated': true,
      'type': 'string'
    },
    'title': { 'type': 'string' }
  },
  'required': [
    'isbn',
    'title'
  ],
  'type': 'object'
} as const;

// ⊥ Don't do this — manual string union drifts from the schema.
type DeprecatedBookKeysManual = 'legacySku';

// ✓ Do this — derived from the schema literal, stays in sync.
type DeprecatedBookKeysCorrect = DeprecatedKeysType<typeof _BookV1Schema>;

// Both happen to resolve to 'legacySku' today, but only the derived
// form survives a second `deprecated: true` annotation tomorrow.
const manualKey: DeprecatedBookKeysManual = 'legacySku';
const derivedKey: DeprecatedBookKeysCorrect = 'legacySku';

console.log('manual union:', manualKey, '(drifts if schema changes)');
console.log('derived union:', derivedKey, '(stays in sync with schema literal)');
Output
Press Execute to run this example against the real library.

Anti-pattern 2: Using it where NonDeprecatedSchemaType is the right tool

/**
 * Anti-pattern: Reaching for `Omit<…, DeprecatedKeysType<T>>` when
 * `NonDeprecatedSchemaType<T>` is the right tool.
 *
 * `DeprecatedKeysType<T>` gives you the key names. If what you actually
 * want is the filtered object shape, ask for it directly with
 * `NonDeprecatedSchemaType<T>` — it composes the same `Omit` for you
 * and propagates the schema's deep inference rules.
 */

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

const _BookV1Schema = {
  '$id': 'https://bookstore.example/BookV1',
  'properties': {
    'isbn': { 'type': 'string' },
    'legacySku': {
      'deprecated': true,
      'type': 'string'
    },
    'title': { 'type': 'string' }
  },
  'required': [
    'isbn',
    'title'
  ],
  'type': 'object'
} as const;

// ⊥ Don't do this — you want the filtered type, not just the key names.
type SafeBookHandRolled = Omit<
  InferType<typeof _BookV1Schema>,
  DeprecatedKeysType<typeof _BookV1Schema>
>;

// ✓ Do this — single utility expresses intent.
type SafeBook = NonDeprecatedSchemaType<typeof _BookV1Schema>;

// Both resolve to the same object shape today.
const sample: SafeBook = {
  'isbn': '9783522128001',
  'title': 'Die unendliche Geschichte'
};
const sampleAlt: SafeBookHandRolled = sample;

console.assert(sampleAlt.isbn === '9783522128001');

console.log('SafeBook (NonDeprecatedSchemaType) keys:', Object.keys(sample).join(', '));
console.log('both shapes equivalent:', sampleAlt.isbn === sample.isbn);
Output
Press Execute to run this example against the real library.

Comparison

ts
type Deprecated = DeprecatedKeysType<typeof BookV1Schema>;
// 'legacySku'  - compile-time union of deprecated key names
ts
// Zod v3.24+ supports .deprecated() on individual fields.
// There is no built-in type-level utility to extract deprecated key names.
// Introspection is runtime-only via schema._def.
import { z } from 'zod';
const BookV1 = z.object({
  isbn:      z.string(),
  legacySku: z.string().deprecated(),
});
// No equivalent of DeprecatedKeysType  - cannot extract 'legacySku' at type level.
ts
// TypeBox supports { deprecated: true } as a JSON Schema annotation.
// No built-in utility type extracts deprecated key names  - annotation is metadata only.
import { Type, Static } from '@sinclair/typebox';
const BookV1 = Type.Object({
  isbn:      Type.String(),
  legacySku: Type.String({ deprecated: true }),
});
// Static<typeof BookV1> includes legacySku  - no compile-time filtering.
ts
// AJV is a runtime validator  - no TypeScript type-level extraction of deprecated keys.
// Maintain the list manually.
py
# Pydantic v2 supports Field(deprecated=True) for runtime introspection.
# Returns a deprecation warning at access time, not a type-level key union.
from pydantic import BaseModel, Field

class BookV1(BaseModel):
    isbn: str
    legacy_sku: str = Field(default=None, deprecated=True)

# model_fields['legacy_sku'].metadata contains the deprecation annotation.
# No equivalent of DeprecatedKeysType  - introspection is runtime only.
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.
  • NonDeprecatedSchemaType - obtain the pruned object type rather than the key union
  • InferType - full object type including deprecated properties

See also


NonDeprecatedSchemaType<T>

Declaration. Derives the TypeScript object type for a schema literal with all properties marked deprecated: true omitted. Delegates to InferSchemaType<T> and applies Omit<…, DeprecatedKeysType<T>>.

Use this when you want a type that represents the "current" shape of an object after stripping legacy fields - for example, for API response types or view models that should never surface deprecated properties.

Don't use this when you need to read or write deprecated properties (e.g. migration code that must still handle them). Use InferType<T> directly when you need access to all properties.

Signature

/**
 * NonDeprecatedSchemaType — Signature
 *
 * The canonical declaration of NonDeprecatedSchemaType<T>: derives the
 * TypeScript object type for a schema literal with all properties
 * marked `deprecated: true` omitted. Delegates to InferSchemaType<T>
 * and applies `Omit<…, DeprecatedKeysType<T>>`.
 */

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

// Type declaration mirrors the canonical export in src/types/Infer.ts:
//
// export type NonDeprecatedSchemaType<T, TRoot = T, TReferences = Record<never, never>>
//   = T extends { readonly 'properties': unknown; readonly 'type': 'object' }
//     ? SimplifyType<Omit<InferSchemaType<T, TRoot, TReferences>, DeprecatedKeysType<T>>>
//     : InferSchemaType<T, TRoot, TReferences>;

const _BookV1Schema = {
  '$id': 'https://bookstore.example/BookV1',
  'properties': {
    'isbn': { 'type': 'string' },
    'legacySku': {
      'deprecated': true,
      'type': 'string'
    },
    'title': { 'type': 'string' }
  },
  'required': [
    'isbn',
    'title'
  ],
  'type': 'object'
} as const;

// legacySku is omitted from the inferred object shape.
type BookView = NonDeprecatedSchemaType<typeof _BookV1Schema>;

const view: BookView = {
  'isbn': '9783522128001',
  'title': 'Die unendliche Geschichte'
};

console.assert(view.isbn === '9783522128001');
console.assert(!('legacySku' in view));

console.log('BookView (NonDeprecatedSchemaType) keys:', Object.keys(view).join(', '));
console.log('isbn:', view.isbn, '| legacySku present:', 'legacySku' in view);
Output
Press Execute to run this example against the real library.

Examples

Example 1: Schema with a deprecated field

/**
 * NonDeprecatedSchemaType — Example 1: Schema with a deprecated field
 *
 * Demonstrates the difference between InferType (includes all props)
 * and NonDeprecatedSchemaType (omits deprecated props).
 */

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

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

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

const _BookV1Schema = {
  '$id': 'https://bookstore.example/BookV1',
  'properties': {
    'isbn': { 'type': 'string' },
    'legacySku': {
      'deprecated': true,
      'type': 'string'
    },
    'title': { 'type': 'string' }
  },
  'required': [
    'isbn',
    'title'
  ],
  'type': 'object'
} as const;

type BookV1Full = InferType<typeof _BookV1Schema>;
// { readonly isbn: string; readonly title: string; readonly legacySku?: string }

type BookV1Current = NonDeprecatedSchemaType<typeof _BookV1Schema>;
// { readonly isbn: string; readonly title: string }
//  - legacySku is gone

assert<AssertEqualType<
  BookV1Full['legacySku'] extends string | undefined ? true : false,
  true
>>();

assert<AssertEqualType<
  'legacySku' extends keyof BookV1Current ? true : false,
  false
>>();


// Runtime demonstration: the filtered type omits legacySku.
const bookCurrent: BookV1Current = {
  'isbn': '9783522128001',
  'title': 'Die unendliche Geschichte'
};
const bookFull: BookV1Full = {
  'isbn': '9783522128001',
  'legacySku': 'OLD-NES-001',
  'title': 'Die unendliche Geschichte'
};

console.log('InferType (full) keys:', Object.keys(bookFull).join(', '));
console.log('NonDeprecatedSchemaType keys:', Object.keys(bookCurrent).join(', '), '(legacySku omitted)');
Output
Press Execute to run this example against the real library.

Example 2: Using as a return type for a view layer function

/**
 * NonDeprecatedSchemaType — Example: Using as a return type for a view
 * layer function.
 *
 * The view function accepts an already-validated book and returns the
 * object with deprecated fields stripped. The return type annotation
 * signals to callers that they should not depend on deprecated fields.
 */

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

const _BookV1Schema = {
  '$id': 'https://bookstore.example/BookV1',
  'properties': {
    'isbn': { 'type': 'string' },
    'legacySku': {
      'deprecated': true,
      'type': 'string'
    },
    'title': { 'type': 'string' }
  },
  'required': [
    'isbn',
    'title'
  ],
  'type': 'object'
} as const;

type BookV1 = InferType<typeof _BookV1Schema>;
type BookV1View = NonDeprecatedSchemaType<typeof _BookV1Schema>;

function toBookView(book: BookV1): BookV1View {
  // Strip the deprecated property in the view layer. The return type
  // annotation prevents downstream code from depending on legacySku.
  const {
    'legacySku': _, ...rest
  } = book;

  return rest;
}

const stored: BookV1 = {
  'isbn': '9783522128001',
  'legacySku': 'OLD-NES-001',
  'title': 'Die unendliche Geschichte'
};

const view = toBookView(stored);

console.assert(view.isbn === stored.isbn);
console.assert(view.title === stored.title);
console.assert(!('legacySku' in view));

console.log('stored (BookV1) keys:', Object.keys(stored).join(', '));
console.log('view (BookV1View) keys:', Object.keys(view).join(', '), '(legacySku stripped)');
Output
Press Execute to run this example against the real library.

Bad examples

Anti-pattern 1: Manual Omit with a string literal

/**
 * Anti-pattern: Manual `Omit` with a string literal.
 *
 * Hand-typing the keys to omit drifts the moment another property is
 * marked `deprecated: true`. The schema knows which keys are
 * deprecated — let `NonDeprecatedSchemaType<T>` derive the filtered
 * shape from it.
 */

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

const _BookV1Schema = {
  '$id': 'https://bookstore.example/BookV1',
  'properties': {
    'isbn': { 'type': 'string' },
    'legacySku': {
      'deprecated': true,
      'type': 'string'
    },
    'title': { 'type': 'string' }
  },
  'required': [
    'isbn',
    'title'
  ],
  'type': 'object'
} as const;

// ⊥ Don't do this — the Omit list goes stale on the next deprecation.
type BookV1ManualCurrent = Omit<InferType<typeof _BookV1Schema>, 'legacySku'>;

// ✓ Do this — derived from the schema; new deprecations propagate.
type BookV1Current = NonDeprecatedSchemaType<typeof _BookV1Schema>;

const fresh: BookV1Current = {
  'isbn': '9783522128001',
  'title': 'Die unendliche Geschichte'
};
const manual: BookV1ManualCurrent = fresh;

console.assert(manual.isbn === fresh.isbn);

console.log('NonDeprecatedSchemaType keys:', Object.keys(fresh).join(', '));
console.log('both shapes have same isbn:', manual.isbn === fresh.isbn, '(derived form auto-tracks future deprecations)');
Output
Press Execute to run this example against the real library.

Comparison

ts
type BookV1Current = NonDeprecatedSchemaType<typeof BookV1Schema>;
// Automatically omits all properties where deprecated: true
ts
// Zod v3 has no built-in type-level filter for deprecated fields.
// Runtime workaround: parse then strip manually.
// No compile-time equivalent.
ts
// TypeBox has no built-in type-level filter for deprecated annotations.
// Static<T> always includes all declared properties regardless of deprecated metadata.
ts
// Not applicable  - AJV is a runtime validator with no type inference.
// There is no TypeScript-level equivalent.
py
# Pydantic v2 supports model_dump(exclude_deprecated=True) for runtime serialization.
# There is no compile-time type that omits deprecated fields.
from pydantic import BaseModel, Field

class BookV1(BaseModel):
    isbn: str
    legacy_sku: str | None = Field(default=None, deprecated=True)

data = BookV1(isbn='9780000000001', legacy_sku='OLD-1')
data.model_dump(exclude_deprecated=True)
# {'isbn': '9780000000001'}  - but the static type still includes legacy_sku
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.
  • DeprecatedKeysType - extract just the deprecated key names
  • InferType - full type including deprecated properties

See also


EnumValuesType<T>

Declaration. Extracts the union of enum values from a schema literal. Works on any schema shape that carries an enum array; returns never when no enum is declared.

Use this when you have a registered or imported schema with an enum constraint and need the corresponding TypeScript union for switch statements, function parameters, or component props. Prefer this over hand-typing 'USD' | 'EUR' | ... because the union stays in sync with the schema literal.

Don't use this when the schema is dynamic at runtime (loaded from a file or remote URL) - type-level extraction requires the schema as a const literal at compile time. For runtime-only enum lists, use schema.enum (the array value) directly.

Signature

/**
 * EnumValuesType — Signature
 *
 * The canonical declaration of EnumValuesType<T>: extracts the union of
 * `enum` values from a schema literal. Works on any schema shape that
 * carries an `enum` array; returns `never` when no `enum` is declared.
 */

import type { EnumValuesType } from '../../../src/types/index.js';
import type { PrintStatusSchema } from '../bookstore/index.js';

// Type declaration mirrors the canonical export in src/types/Infer.ts:
//
// export type EnumValuesType<T>
//   = T extends { readonly 'enum': ReadonlyArray<infer V> } ? V : never;

// PrintStatusSchema declares `enum: ['inPrint', 'limitedRun', 'outOfPrint']`.
type PrintStatus = EnumValuesType<typeof PrintStatusSchema>;
// 'inPrint' | 'limitedRun' | 'outOfPrint'

const statuses: PrintStatus[] = [
  'inPrint',
  'limitedRun',
  'outOfPrint'
];

console.log('EnumValuesType<PrintStatusSchema>:', statuses.join(' | '));
console.log('sample value:', statuses[0]);
Output
Press Execute to run this example against the real library.

Examples

Example 1: Currency enum from an inline schema

/**
 * EnumValuesType — Example 1: Currency enum from an inline schema.
 *
 * Demonstrates extracting a union of literal strings from a schema's
 * `enum` array. The union stays in sync with the schema literal at
 * compile time.
 */

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

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

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

const currencies: Currency[] = [
  'USD',
  'EUR',
  'GBP',
  'JPY'
];

console.log('EnumValuesType<CurrencySchema>:', currencies.join(' | '));
console.log('sample value:', currencies[1], '(derived from schema enum array, stays in sync)');
Output
Press Execute to run this example against the real library.

Example 2: With ExhaustiveType for an exhaustive switch

/**
 * EnumValuesType + ExhaustiveType — Exhaustive switch with compile-time safety
 *
 * Demonstrates pairing EnumValuesType with ExhaustiveType to ensure
 * all enum cases are handled; adding a case without handling it
 * becomes a compile error.
 */

import type {
  EnumValuesType, ExhaustiveType
} from '../../../src/types/index.js';

const _CurrencySchema = {
  'enum': [
    'USD',
    'EUR',
    'GBP'
  ],
  'type': 'string'
} as const;

type Currency = EnumValuesType<typeof _CurrencySchema>;

function currencySymbol(cur: Currency): string {
  switch (cur) {
    case 'EUR': return '€';
    case 'GBP': return '£';
    case 'USD': return '$';
    default: {
      // Adding 'JPY' to the enum without adding a case here becomes a compile error
      const _: ExhaustiveType<typeof cur> = cur;

      return _;
    }
  }
}

// Demonstrate the exhaustive switch for all three enum values.
console.log('USD ->', currencySymbol('USD'));
console.log('EUR ->', currencySymbol('EUR'));
console.log('GBP ->', currencySymbol('GBP'));
Output
Press Execute to run this example against the real library.

Example 3: As a function parameter type

/**
 * EnumValuesType — Example: As a function parameter type.
 *
 * Using EnumValuesType as a function argument keeps the parameter
 * surface in sync with the schema's `enum` declaration. Adding a new
 * currency to the schema automatically widens the parameter type.
 */

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

const _CurrencySchema = {
  'enum': [
    'USD',
    'EUR',
    'GBP'
  ],
  'type': 'string'
} as const;

type Currency = EnumValuesType<typeof _CurrencySchema>;

function formatPrice(amount: number, currency: Currency): string {
  const symbols: Record<Currency, string> = {
    'EUR': '€',
    'GBP': '£',
    'USD': '$'
  };

  return `${symbols[currency]}${amount.toFixed(2)}`;
}

const eur = formatPrice(19.99, 'EUR');
const gbp = formatPrice(19.99, 'GBP');

console.assert(eur === '€19.99');
console.assert(gbp === '£19.99');

console.log('formatPrice(19.99, "EUR"):', eur);
console.log('formatPrice(19.99, "GBP"):', gbp);
console.log('formatPrice(19.99, "USD"):', formatPrice(19.99, 'USD'));
Output
Press Execute to run this example against the real library.

Bad examples

Anti-pattern 1: Hand-rolled duplicate union

/**
 * Anti-pattern: Hand-rolled duplicate union.
 *
 * Typing the union by hand drifts from the schema the instant someone
 * adds a new value. Derive the union from the schema with
 * `EnumValuesType<typeof Schema>` so the type follows the data.
 */

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

const _CurrencySchema = {
  'enum': [
    'USD',
    'EUR',
    'GBP'
  ],
  'type': 'string'
} as const;

// ⊥ Don't do this — drifts from CurrencySchema.enum the moment 'JPY' is added.
type CurrencyManual = 'EUR' | 'GBP' | 'USD';

// ✓ Do this — derived from the schema literal.
type Currency = EnumValuesType<typeof _CurrencySchema>;

const manual: CurrencyManual = 'USD';
const derived: Currency = 'USD';

// Both resolve to the same runtime value; the difference is that
// CurrencyManual will drift when a new currency is added to the schema,
// while Currency tracks the schema literal automatically.
console.log('manual (hand-rolled):', manual);
console.log('derived (schema-driven):', derived);
Output
Press Execute to run this example against the real library.

Anti-pattern 2: Unsafe index access

/**
 * Anti-pattern: Unsafe index access on `enum`.
 *
 * `(typeof Schema)['enum'][number]` breaks when the enum is not a
 * const array and ignores edge cases that EnumValuesType handles
 * (single-element enums, mixed types). Use the utility — it has been
 * designed for these shapes.
 */

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

const _CurrencySchema = {
  'enum': [
    'USD',
    'EUR',
    'GBP'
  ],
  'type': 'string'
} as const;

// ⊥ Don't do this — fragile and edge-case-sensitive.
type CurrencyUnsafe = (typeof _CurrencySchema)['enum'][number];

// ✓ Do this — robust across enum shapes.
type Currency = EnumValuesType<typeof _CurrencySchema>;

const unsafe: CurrencyUnsafe = 'USD';
const derived: Currency = 'USD';

// Both resolve to the same runtime value; the difference is that
// CurrencyUnsafe via index access is fragile for non-const arrays and
// mixed-type enums, while EnumValuesType handles those shapes correctly.
console.log('unsafe (index access):', unsafe);
console.log('derived (EnumValuesType):', derived);
Output
Press Execute to run this example against the real library.

Comparison

ts
type Currency = EnumValuesType<typeof CurrencySchema>;
// 'USD' | 'EUR' | 'GBP'  - derived from schema.enum at compile time
ts
import { z } from 'zod';
const Currency = z.enum(['USD', 'EUR', 'GBP']);
type Currency = z.infer<typeof Currency>;
// 'USD' | 'EUR' | 'GBP'  - Zod owns both schema and type
ts
import { Type } from '@sinclair/typebox';
import type { Static } from '@sinclair/typebox';

const Currency = Type.Union([
  Type.Literal('USD'),
  Type.Literal('EUR'),
  Type.Literal('GBP'),
]);
type Currency = Static<typeof Currency>;
// 'USD' | 'EUR' | 'GBP'
ts
// AJV is a runtime validator  - no type-level extraction.
// Maintain the union manually:
type Currency = 'USD' | 'EUR' | 'GBP';
py
# Python uses Literal for enum-style unions:
from typing import Literal
Currency = Literal['USD', 'EUR', 'GBP']

# Or extract from a model field annotation at runtime:
# typing.get_args(model.model_fields['currency'].annotation)
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.
  • ExhaustiveType - pair with EnumValuesType for exhaustive switch checks

See also


ExhaustiveType<T>

Declaration. A compile-time marker type that accepts only never. Use it in the default branch of a switch statement to enforce that all union members are handled. If a new case is added to the union without a corresponding case clause, the type check fails.

Use this when you are switching over an EnumValuesType<T> union (or any discriminated union) and want the TypeScript compiler to flag missing cases. The pattern is identical to the standard "exhaustive check" idiom used across the TypeScript ecosystem - ExhaustiveType<T> is a named alias that makes the intent explicit.

Don't use this when the union is intentionally open (you want a fallthrough default). The utility is for closed, fully-enumerated unions only.

Signature

/**
 * ExhaustiveType — Signature
 *
 * The canonical declaration of ExhaustiveType<T>: a compile-time
 * marker type that accepts only `never`. Use it in the `default`
 * branch of a switch statement to enforce that all union members are
 * handled. Adding a case to the union without a corresponding `case`
 * clause causes a compile error.
 */

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

// Type declaration mirrors the canonical export in src/types/Infer.ts:
//
// export type ExhaustiveType<T extends never> = T;

type Color = 'gold' | 'silver';

function describeColor(color: Color): string {
  switch (color) {
    case 'gold': return 'lustrous yellow';
    case 'silver': return 'cool grey';
    default: {
      // Adding 'bronze' to Color without a case here becomes a compile error.
      const _: ExhaustiveType<typeof color> = color;

      return _;
    }
  }
}

console.assert(describeColor('gold') === 'lustrous yellow');
console.assert(describeColor('silver') === 'cool grey');
Output
Press Execute to run this example against the real library.

Examples

Example 1: Exhaustive switch over a Review rating

/**
 * IntegerRangeType — Star rating range
 *
 * Demonstrates using IntegerRangeType to produce a literal union
 * for a bounded integer range, here 1–5 for star ratings.
 */

import type {
  ExhaustiveType, IntegerRangeType
} from '../../../src/types/index.js';

type StarRating = IntegerRangeType<1, 5>;
// 1 | 2 | 3 | 4 | 5

function ratingLabel(rating: StarRating): string {
  switch (rating) {
    case 1: return 'Poor';
    case 2: return 'Fair';
    case 3: return 'Good';
    case 4: return 'Very Good';
    case 5: return 'Excellent';
    default: {
      const _: ExhaustiveType<typeof rating> = rating;

      return _;
    }
  }
}

// Demonstrate the exhaustive switch across the literal union.
const ratings: StarRating[] = [
  1,
  2,
  3,
  4,
  5
];

for (const rating of ratings) {
  console.log(`rating ${rating} ->`, ratingLabel(rating));
}
Output
Press Execute to run this example against the real library.

Example 2: Pairing with EnumValuesType for a string enum

/**
 * ExhaustiveType — Example: Pairing with EnumValuesType for a string
 * enum.
 *
 * Switch over an OrderStatus union derived from an `enum` schema. The
 * default branch uses ExhaustiveType to assert the switch is total.
 * Adding 'refunded' to the schema without a matching case becomes a
 * compile error.
 */

import type {
  EnumValuesType, ExhaustiveType
} from '../../../src/types/index.js';

const _OrderStatusSchema = {
  'enum': [
    'pending',
    'confirmed',
    'shipped',
    'delivered',
    'cancelled'
  ],
  'type': 'string'
} as const;

type OrderStatus = EnumValuesType<typeof _OrderStatusSchema>;

function describeStatus(status: OrderStatus): string {
  switch (status) {
    case 'cancelled': return 'Order cancelled';
    case 'confirmed': return 'Confirmed, preparing shipment';
    case 'delivered': return 'Delivered';
    case 'pending': return 'Awaiting confirmation';
    case 'shipped': return 'In transit';
    default: {
      // Adding 'refunded' to the enum without a case here becomes a compile error.
      const _: ExhaustiveType<typeof status> = status;

      return _;
    }
  }
}

console.assert(describeStatus('pending') === 'Awaiting confirmation');
console.assert(describeStatus('shipped') === 'In transit');
Output
Press Execute to run this example against the real library.

Bad examples

Anti-pattern 1: Using never directly instead of the named alias

/**
 * Anti-pattern: Using `never` directly instead of the named alias.
 *
 * Both `const _: never = s;` and `const _: ExhaustiveType<typeof s> = s;`
 * compile identically — the runtime behaviour is the same. The named
 * alias is preferred because it communicates intent: "this default
 * branch exists to enforce exhaustiveness."
 */

import type {
  EnumValuesType, ExhaustiveType
} from '../../../src/types/index.js';

const _StatusSchema = {
  'enum': [
    'pending',
    'shipped'
  ],
  'type': 'string'
} as const;

type Status = EnumValuesType<typeof _StatusSchema>;

function describeBare(status: Status): string {
  switch (status) {
    case 'pending': return 'Awaiting confirmation';
    case 'shipped': return 'In transit';
    default: {
      // Works, but intent is less clear.
      const _: never = status;

      return _;
    }
  }
}

function describeNamed(status: Status): string {
  switch (status) {
    case 'pending': return 'Awaiting confirmation';
    case 'shipped': return 'In transit';
    default: {
      // ✓ Prefer the named form — communicates exhaustiveness intent.
      const _: ExhaustiveType<typeof status> = status;

      return _;
    }
  }
}

console.assert(describeBare('pending') === describeNamed('pending'));
Output
Press Execute to run this example against the real library.

Anti-pattern 2: Omitting the default branch entirely

/**
 * Anti-pattern: Omitting the default branch entirely.
 *
 * With both cases of the current union handled and no default branch,
 * the function compiles. The catch is that adding a new value to the
 * schema's `enum` (and therefore to the derived union) re-introduces
 * an implicit fallthrough — and without the `ExhaustiveType` check in
 * a default branch, the compiler cannot warn about it.
 */

import type {
  EnumValuesType, ExhaustiveType
} from '../../../src/types/index.js';

const _StatusSchema = {
  'enum': [
    'pending',
    'shipped'
  ],
  'type': 'string'
} as const;

type Status = EnumValuesType<typeof _StatusSchema>;

// ⊥ Don't do this — no default exhaustiveness check.
// Adding a new union member (e.g. 'cancelled') silently falls through
// to the implicit gap; the compiler emits no error.
function describeUnsafe(status: Status): string {
  switch (status) {
    case 'pending': return 'Awaiting confirmation';
    case 'shipped': return 'In transit';
    default: return 'unknown';
  }
}

// ✓ Do this — adding 'cancelled' to the enum without a case here is
// caught by the compiler.
function describeSafe(status: Status): string {
  switch (status) {
    case 'pending': return 'Awaiting confirmation';
    case 'shipped': return 'In transit';
    default: {
      const _: ExhaustiveType<typeof status> = status;

      return _;
    }
  }
}

console.assert(describeUnsafe('pending') === describeSafe('pending'));
console.assert(describeUnsafe('shipped') === describeSafe('shipped'));
Output
Press Execute to run this example against the real library.

Comparison

ts
default: {
  const _: ExhaustiveType<typeof s> = s;
  return _;
}
// ExhaustiveType<T> is an alias for `T extends never`  - a named version of the
// standard TypeScript exhaustiveness idiom.
ts
// Pure TypeScript pattern  - not Zod-specific.
// Every TS codebase reimplements it; the standard form is:
default: {
  const _: never = s;
  return _;
}
ts
// Same pure TypeScript pattern  - not TypeBox-specific.
default: {
  const _exhaustiveCheck: never = s;
  return _exhaustiveCheck;
}
ts
// Same pure TypeScript pattern  - AJV does not affect type narrowing.
default: {
  const _: never = s;
  return _;
}
py
# Python's match/case with a wildcard arm and assert_never from typing:
from typing import assert_never

match status:
    case 'pending':   ...
    case 'confirmed': ...
    case _:
        assert_never(status)  # type error if the match is not exhaustive
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.
  • EnumValuesType - the closed union to switch over
  • IntegerRangeType - integer literal unions that pair naturally with exhaustive checks

See also


DefaultAlignedType<T>

Declaration. Compile-time guard that resolves to the schema type T when all properties with default values have defaults that match their declared type, and resolves to never otherwise. Checks string, boolean, integer, and number fields; unrecognised property shapes pass through.

Use this when you want a compile-time assertion that schema defaults are type-correct - for example, as a generic constraint on a function that registers schemas, ensuring a misconfigured schema is caught before it reaches runtime.

Don't use this when you only want runtime validation of defaults; the schema compiler already validates defaults at registration time. DefaultAlignedType<T> is a static analysis utility, not a replacement for runtime checks.

Signature

/**
 * DefaultAlignedType — Signature
 *
 * The canonical declaration of DefaultAlignedType<T>: compile-time
 * guard that resolves to the schema type `T` when every property with
 * a `default` value carries a default that matches its declared
 * `type`, and resolves to `never` otherwise. Checks `string`,
 * `boolean`, `integer`, and `number`; unrecognised property shapes
 * pass through.
 */

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

// Type declaration mirrors the canonical export in src/types/Infer.ts:
//
// export type DefaultAlignedType<T>
//   = T extends { readonly 'properties': infer TP }
//     ? CheckPropertyDefaultsType<TP> extends true ? T : never
//     : T;

const _AlignedSchema = {
  'properties': {
    'currency': {
      'default': 'USD',
      'type': 'string'
    },
    'inStock': {
      'default': true,
      'type': 'boolean'
    }
  },
  'type': 'object'
} as const;

type Aligned = DefaultAlignedType<typeof _AlignedSchema>;
// typeof _AlignedSchema — defaults are aligned, the schema passes through.

const aligned: Aligned = _AlignedSchema;

// The schema passes through the guard: its default values match their
// declared types, so Aligned resolves to the schema literal itself.
console.log('aligned schema $id (no $id here):', typeof aligned);
console.log('currency default:', aligned.properties.currency.default);
console.log('inStock default:', aligned.properties.inStock.default);
Output
Press Execute to run this example against the real library.

Examples

Example 1: A well-aligned schema passes through

/**
 * DefaultAlignedType — Example 1: A well-aligned schema passes through.
 *
 * Every property with a `default` carries a value whose type matches
 * the declared `type`. DefaultAlignedType resolves to the schema type
 * unchanged.
 */

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

const _BookSchema = {
  '$id': 'https://bookstore.example/Book',
  'properties': {
    'currency': {
      'default': 'USD',
      'type': 'string'
    },
    'inStock': {
      'default': true,
      'type': 'boolean'
    },
    'price': {
      'exclusiveMinimum': 0,
      'type': 'number'
    }
  },
  'required': ['price'],
  'type': 'object'
} as const;

type AlignedBook = DefaultAlignedType<typeof _BookSchema>;
// typeof _BookSchema — the schema passes through.

const aligned: AlignedBook = _BookSchema;

// The schema resolves to AlignedBook (the schema literal itself) because
// every default matches its declared type: 'USD' is a string, true is boolean.
console.log('schema $id:', aligned.$id);
console.log('currency default:', aligned.properties.currency.default);
console.log('inStock default:', aligned.properties.inStock.default);
Output
Press Execute to run this example against the real library.

Example 2: A misaligned default resolves to never

/**
 * DefaultAlignedType — Example 2: A misaligned default resolves to
 * `never`.
 *
 * `currency` declares `type: 'string'` but provides `default: 42`.
 * DefaultAlignedType detects the mismatch at compile time and resolves
 * to `never`, marking the schema as not safe to consume directly.
 */

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

const _BadSchema = {
  'properties': {
    'currency': {
      'default': 42,
      'type': 'string'
    }
  },
  'type': 'object'
} as const;

type MisalignedBook = DefaultAlignedType<typeof _BadSchema>;
// never — default 42 is not assignable to 'string'.

// Compile-time assertion: MisalignedBook is `never`.
type AssertEqualType<TLeft, TRight>
  = [TLeft] extends [TRight] ? [TRight] extends [TLeft] ? true : false : false;

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

assert<AssertEqualType<MisalignedBook, never>>();

// MisalignedBook resolves to never at compile time: the schema has
// default: 42 for a property declared type: 'string', which is a mismatch.
// At runtime the schema object still exists; the guard only affects the type.
console.log('schema type property:', _BadSchema.properties.currency.type);
console.log('schema default (mismatched):', _BadSchema.properties.currency.default);
Output
Press Execute to run this example against the real library.

Example 3: Using as a generic constraint on a registration helper

/**
 * DefaultAlignedType — Example 3: Using as a generic constraint on a
 * registration helper.
 *
 * The helper accepts only schemas whose `default` values match their
 * declared types. A misaligned schema fails the constraint at the
 * call site, before any runtime code executes.
 */

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

const _BookSchema = {
  '$id': 'https://bookstore.example/AlignedBook',
  'properties': {
    'currency': {
      'default': 'USD',
      'type': 'string'
    },
    'inStock': {
      'default': true,
      'type': 'boolean'
    }
  },
  'type': 'object'
} as const;

function registerChecked<T>(schema: DefaultAlignedType<T>): void {
  // DefaultAlignedType<T> ensures the schema never reaches this
  // function when its defaults are misaligned — the call site itself
  // becomes a compile error in that case.
  // doc example with synthetic fixture schemas (strict-graph default does not throw because no inline duplicates)
  const jt = JsonTology.create({ 'baseIri': 'https://bookstore.example' });

  jt.registry.set(schema as Record<string, unknown>);
}

registerChecked(_BookSchema);

// The helper accepted the schema — DefaultAlignedType<T> resolved to
// the schema literal (not never) because every default matches its type.
console.log('registered schema $id:', _BookSchema.$id);
console.log('currency default:', _BookSchema.properties.currency.default);
console.log('inStock default:', _BookSchema.properties.inStock.default);
Output
Press Execute to run this example against the real library.

Bad examples

Anti-pattern 1: Runtime-only default validation

/**
 * Anti-pattern: Relying solely on runtime to catch misaligned
 * defaults.
 *
 * Registering a misaligned schema raises a runtime error — but the
 * mismatch is detectable at compile time. Wrap registration in a
 * helper constrained by `DefaultAlignedType<T>` and the misalignment
 * surfaces in the editor before it ever reaches `set()`.
 */

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

const _BadSchema = {
  '$id': 'https://bookstore.example/BadBook',
  'properties': {
    'currency': {
      'default': 42,
      'type': 'string'
    }
  },
  'type': 'object'
} as const;

// ⊥ Don't do this — runtime-only detection.
// jt.set(_BadSchema); // throws at registration time

// ✓ Do this — catch the misalignment at compile time.
type GuardedBadSchema = DefaultAlignedType<typeof _BadSchema>;
// never — DefaultAlignedType refuses the misaligned schema.

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

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

assert<AssertEqualType<GuardedBadSchema, never>>();

// GuardedBadSchema resolves to never: default 42 is incompatible with
// type 'string'. The schema object still exists at runtime; the guard
// prevents it from reaching a registration function without compile error.
console.log('schema $id:', _BadSchema.$id);
console.log('problematic default (42, type string):', _BadSchema.properties.currency.default);
Output
Press Execute to run this example against the real library.

Comparison

ts
type AlignedBook = DefaultAlignedType<typeof BookSchema>;
// typeof BookSchema  - passes through when all defaults match declared types
// never  - when any default is misaligned
// Concept specific to json-tology's compile-time validation of `default` values.
ts
// Zod .default() takes a value and infers its type from the schema.
// Type mismatches are caught at the z.default() call site, not via a utility type.
import { z } from 'zod';
const BookSchema = z.object({
  currency: z.string().default('USD'),  // type-safe: default must be string
  // z.string().default(42) → TypeScript error at definition
});
// No equivalent of DefaultAlignedType  - Zod's API enforces it structurally.
ts
// TypeBox accepts { default: value } as metadata but does not validate
// that the value matches the declared type at compile time.
// No equivalent of DefaultAlignedType.
ts
// AJV validates defaults at runtime (via ajv-defaults plugin).
// No compile-time equivalent.
py
# Pydantic v2 validates default values against field types at class definition time.
# A type-mismatched default raises a ValidationError at import time  - no separate utility needed.
from pydantic import BaseModel

class Book(BaseModel):
    currency: str = 'USD'  # OK
    # price: int = 'free'  # ValidationError at class definition  - caught early
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.
  • InferType - infer the full object type once defaults are known to be aligned
  • Schemas - declaring default values on properties

See also

Released under the MIT License.