Skip to content

Compose.intersection Compile-time + Runtime

Validation modes: Validation modes reference

Declaration. Creates a new allOf schema that combines multiple schemas. Data must satisfy every constituent schema simultaneously. TypeScript infers the intersection of all constituent types. The $id is set to newId.

Use this when data must satisfy multiple independent schemas - for example, an AuditedOrder must satisfy both Order constraints and Audit constraints including their respective required arrays. This is stronger than extend, which only merges properties into one flat object.

Don't use this when you only need to merge property definitions without additional required constraints (use extend - simpler, more predictable). Don't use it for union types (use discriminatedUnion).

Examples

Example 1: Add audit fields to Order

Both Order and Audit required arrays must be satisfied.

/**
 * Compose.intersection — Example 1: AuditedOrder = Order ∩ Audit
 * Demonstrates: allOf composition, all constituent schemas must pass
 *
 * AuditSchema and AuditedOrderSchema register onto the canonical bookstore
 * via `jt.set()` — no mini-registry. The order payload is
 * the canonical Bastian-orders-Neverending-Story fixture.
 */

import { Compose } from '../../../src/index.js';
import {
  aboxFixtures, createBookstoreDocRegistry,
  OrderSchema
} from '../bookstore/index.js';

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

const AuditSchema = {
  '$id': 'https://bookstore.example/Audit',
  'properties': {
    'createdAt': {
      'format': 'date-time',
      'type': 'string'
    },
    'updatedAt': {
      'format': 'date-time',
      'type': 'string'
    }
  },
  'required': [
    'createdAt',
    'updatedAt'
  ],
  'type': 'object'
} as const;

const AuditedOrderSchema = Compose.intersection(
  [
    OrderSchema,
    AuditSchema
  ] as const,
  'https://bookstore.example/AuditedOrder'
);

const jt2 = jt.set(AuditSchema).set(AuditedOrderSchema);

// Bastian's order without audit metadata — AuditSchema required fields not met.
const errors = jt2.validate(AuditedOrderSchema.$id, aboxFixtures.order);

console.assert(errors.length > 0);
console.log('AuditedOrder rejects missing audit fields:', errors.length, 'error(s)');

// All fields present — passes.
const valid = jt2.validate(AuditedOrderSchema.$id, {
  ...aboxFixtures.order,
  'createdAt': aboxFixtures.order.placedAt,
  'updatedAt': aboxFixtures.order.placedAt
});

console.assert(valid.length === 0);
console.log('AuditedOrder accepts order + audit fields:', valid.length === 0);
Output
Press Execute to run this example against the real library.

Example 2: Validation fails if any constituent schema fails

/**
 * Compose.intersection — Example 2: Validation fails if any constituent fails
 *
 * `AuditedOrderSchema` combines OrderSchema with an AuditSchema. The
 * Bastian order fixture (no audit metadata) fails because the Audit
 * required fields are missing.
 */

import { Compose } from '../../../src/index.js';
import {
  aboxFixtures, createBookstoreDocRegistry,
  OrderSchema
} from '../bookstore/index.js';

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

const AuditSchema = {
  '$id': 'https://bookstore.example/AuditFails',
  'properties': {
    'createdAt': {
      'format': 'date-time',
      'type': 'string'
    },
    'updatedAt': {
      'format': 'date-time',
      'type': 'string'
    }
  },
  'required': [
    'createdAt',
    'updatedAt'
  ],
  'type': 'object'
} as const;

const AuditedOrderSchema = Compose.intersection(
  [
    OrderSchema,
    AuditSchema
  ] as const,
  'https://bookstore.example/AuditedOrderFails'
);

const jt2 = jt.set(AuditSchema).set(AuditedOrderSchema);

// Missing createdAt and updatedAt — fails AuditSchema constraints.
const errors = jt2.validate(AuditedOrderSchema.$id, aboxFixtures.order);

console.assert(!errors.ok);
console.assert(errors.length > 0);
console.log('AuditedOrderFails rejects order without audit fields:', errors.length, 'error(s) — all constituents must pass');
Output
Press Execute to run this example against the real library.

Example 3: getDefaults on an intersection schema

Build on Compose.getDefaults - extracting defaults from an intersection walks each constituent.

/**
 * Compose.intersection — Example 3: getDefaults on an intersection schema
 *
 * `Compose.getDefaults` walks each constituent schema's `properties`
 * and returns the merged default values. The bookstore OrderSchema
 * has no top-level `default` keyword on its primitives, so the
 * extracted defaults are empty — exactly the documented behaviour.
 */

import { Compose } from '../../../src/index.js';
import { OrderSchema } from '../bookstore/index.js';

const defaults = Compose.getDefaults(OrderSchema);

// Order properties carry $ref to primitives but no top-level default keys,
// so getDefaults returns no recognised default fields.
console.assert(typeof defaults === 'object');
console.log('OrderSchema getDefaults (no declared defaults):', defaults);
Output
Press Execute to run this example against the real library.

ID collision prevention Compile-time

newId cannot collide with any input schema's $id. A collision surfaces an IntersectionIdCollisionType brand error at the call site.

/**
 * Anti-pattern: a `Compose.intersection` whose `newId` collides with one
 * of the input schemas' `$id`. The compiler raises an
 * `IntersectionIdCollisionType` brand at the call site so the mistake
 * fails before any runtime registration.
 */

import { Compose } from '../../../src/index.js';
import { BookSchema } from '../bookstore/index.js';

const AuditSchema = {
  '$id': 'https://bookstore.example/Audit',
  'properties': {
    'createdAt': {
      'format': 'date-time',
      'type': 'string'
    },
    'updatedAt': {
      'format': 'date-time',
      'type': 'string'
    }
  },
  'required': [
    'createdAt',
    'updatedAt'
  ],
  'type': 'object'
} as const;

// ✗ Runtime anti-pattern — `newId` collides with BookSchema.$id.
// The collision check fires at the type level when both schemas share the same $id.
const _Bad = Compose.intersection(
  [
    BookSchema,
    AuditSchema
  ] as const,
  'https://bookstore.example/Book'
);

console.log('Intersection $id collision anti-pattern: newId must not match any input schema $id | BookSchema.$id:', BookSchema.$id, '| _Bad.$id:', _Bad.$id);
Output
Press Execute to run this example against the real library.

Bad examples - what NOT to do

Anti-pattern 1: Using intersection when extend is simpler

/**
 * Compose.intersection — Anti-pattern 1: Using intersection when extend is simpler
 *
 * For simple property merging onto a base schema, `Compose.extend` is
 * the focused tool. Intersection is heavier and signals "all
 * constituent schemas must hold simultaneously" — overkill when the
 * additions don't have their own `required` constraints.
 */

import { Compose } from '../../../src/index.js';
import {
  BookSchema,
  createBookstoreDocRegistry
} from '../bookstore/index.js';

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

// ✓ Do this — extend is designed for property merging onto a base.
const ExtendedBookSchema = Compose.extend(
  BookSchema,
  { 'badge': { 'type': 'string' } } as const,
  'https://bookstore.example/ExtendedBookCorrect'
);

const jt2 = jt.set(ExtendedBookSchema);

const result = jt2.validate(ExtendedBookSchema.$id, {
  'authors': ['Michael Ende'],
  'badge': 'staff-pick',
  'inStock': true,
  'isbn': '9783522115056',
  'price': {
    'amount': 16.99,
    'currency': 'EUR'
  },
  'printStatus': 'inPrint',
  'title': 'Momo'
});

console.assert(result.ok);
console.log('Compose.extend is simpler for single property addition:', result.ok, '| schema:', ExtendedBookSchema.$id);
Output
Press Execute to run this example against the real library.

Comparison

ts
Compose.intersection([OrderSchema, AuditSchema] as const, 'https://bookstore.example/AuditedOrder')
// Produces allOf: [OrderSchema, AuditSchema]
// Both required arrays must be satisfied
ts
OrderSchema.and(AuditSchema)
// Or: z.intersection(OrderSchema, AuditSchema)
ts
import * as v from 'valibot';
v.intersect([OrderSchema, AuditSchema])
ts
import * as t from 'io-ts';
const AuditedOrder = t.intersection([OrderCodec, AuditCodec]);
// Both codecs must succeed for decode to return Right.
ts
import { Type } from '@sinclair/typebox';
Type.Intersect([OrderSchema, AuditSchema])
ts
const AuditedOrderSchema = {
  $id: 'https://bookstore.example/AuditedOrder',
  allOf: [OrderSchema, AuditSchema],
};
py
class AuditedOrder(Order, Audit):
    # Multiple inheritance achieves allOf semantics
    pass
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 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.
  • extend - simpler for just adding properties without separate required constraints
  • discriminatedUnion - for oneOf with type discriminator
  • partial - make the intersected result partially optional

See also

Released under the MIT License.