Skip to content

Compose.pick and Compose.omit

Validation modes: Validation modes reference

pick and omit are inverse operations for creating schema projections. Both return new schema objects - input schemas are never mutated.


Compose.pick {#compose-pick} Compile-time + Runtime

Declaration. Creates a new schema containing only the specified property keys. The required array is filtered to include only keys that were in the original required and appear in the keys argument. Non-picked required fields are dropped. TypeScript infers a type with only the picked properties.

Use this when you need a schema that exposes only a subset of fields - for API projections, list-view summaries, or public interfaces that should not expose all internal fields. This is the runtime equivalent of TypeScript's Pick<T, K>.

Don't use this when you need to remove specific fields while keeping the rest (use omit). Don't use it when you want to add fields (use extend).

Examples

Example 1: Book catalog summary - only display fields

/**
 * Compose.pick / omit — Example 1: BookSummary and PublicBook
 * Demonstrates: pick keeps fields, omit removes fields, required adjusted
 *
 * Book is now a Compose.subClassOf(BibliographicRecordSchema, …) — it is an
 * allOf composition, so `Compose.pick` sees only Book's OWN (retail) keys:
 * annotations, inStock, price, printStatus, ratings, stockLevel. Bibliographic
 * fields (isbn, title, authors, publishedOn) live on BibliographicRecordSchema.
 *
 * Pick the bibliographic summary from BibliographicRecordSchema; omit the
 * inventory field from BookSchema (retail view). Both lessons remain intact.
 *
 * Derived schemas register onto the canonical bookstore via
 * `jt.set()`. Every validate/instantiate call goes
 * through the same registry the rest of the docs reference, using
 * the canonical Bastian-orders-Neverending-Story fixture data.
 */

import { Compose } from '../../../src/index.js';
import {
  aboxFixtures, BibliographicRecordSchema, 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();

// pick — select only the bibliographic identity fields from BibliographicRecordSchema.
// isbn and title are NOT on BookSchema's own properties (they live on the base),
// so we target the base schema directly.
const BookSummarySchema = Compose.pick(
  BibliographicRecordSchema,
  [
    'isbn',
    'title'
  ] as const,
  'https://bookstore.example/BookSummary'
);

// omit — derive a public-facing Book view by removing the operational inventory field.
const PublicBookSchema = Compose.omit(
  BookSchema,
  ['inStock'] as const,
  'https://bookstore.example/PublicBook'
);

const jt2 = jt.set(BookSummarySchema).set(PublicBookSchema);

// BookSummary — only picked fields survive
const summary = jt2.instantiate(BookSummarySchema, {
  'isbn': aboxFixtures.rareBook.isbn,
  'title': aboxFixtures.rareBook.title
});

console.assert(!('authors' in summary));
console.assert(summary.isbn === aboxFixtures.rareBook.isbn);
console.log('BookSummary picked keys:', Object.keys(summary));

// PublicBook — inStock removed, printStatus still required
const pub = jt2.validate(PublicBookSchema.$id, {
  'authors': aboxFixtures.rareBook.authors,
  'isbn': aboxFixtures.rareBook.isbn,
  'price': aboxFixtures.rareBook.price,
  'printStatus': aboxFixtures.rareBook.printStatus,
  'title': aboxFixtures.rareBook.title
});

console.assert(pub.length === 0);
console.log('PublicBook valid without inStock:', pub.length === 0, '| omitted inStock from properties');
Output
Press Execute to run this example against the real library.

Example 2: Customer card for embedding in order responses

/**
 * Compose.pick — Example 2: Customer card for embedding in order responses
 *
 * Picks only display fields off the canonical CustomerSchema to embed
 * inside Order responses without leaking the customer's full address
 * book.
 */

import { Compose } from '../../../src/index.js';
import {
  aboxFixtures, createBookstoreDocRegistry,
  CustomerSchema
} 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 CustomerCardSchema = Compose.pick(
  CustomerSchema,
  [
    'customerId',
    'name',
    'email'
  ] as const,
  'https://bookstore.example/CustomerCard'
);

const jt2 = jt.set(CustomerCardSchema);

const card = jt2.instantiate(CustomerCardSchema.$id, {
  'customerId': aboxFixtures.customer.customerId,
  'email': aboxFixtures.customer.email,
  'name': aboxFixtures.customer.name
}) as Record<string, unknown>;

console.assert(card.customerId === aboxFixtures.customer.customerId);
console.assert(card.name === 'Bastian Balthazar Bux');
console.assert(!('addresses' in card));
console.log('CustomerCard picked fields:', Object.keys(card), '| addresses omitted:', !('addresses' in card));
Output
Press Execute to run this example against the real library.

Example 3: Build sub-schema for partial validation (builds on subschemaAt)

/**
 * Compose.pick — Example 3: Single-field sub-schema for blur validation
 *
 * Pick a single field from the canonical ReviewSchema and register it
 * onto the bookstore. The derived schema validates the rating slot in
 * isolation — useful for incremental field-level form validation.
 */

import { Compose } from '../../../src/index.js';
import {
  createBookstoreDocRegistry,
  ReviewSchema
} 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 ReviewRatingSchema = Compose.pick(
  ReviewSchema,
  ['rating'] as const,
  'https://bookstore.example/ReviewRating'
);

const jt2 = jt.set(ReviewRatingSchema);

// A 5 passes — within 0..5.
const okResult = jt2.validate(ReviewRatingSchema.$id, { 'rating': 5 });

console.assert(okResult.ok);
console.log('ReviewRating validates rating=5:', okResult.ok);

// A 6 fails — exceeds the canonical rating cap.
const overResult = jt2.validate(ReviewRatingSchema.$id, { 'rating': 6 });

console.assert(!overResult.ok);
console.log('ReviewRating rejects rating=6:', !overResult.ok, '(exceeds max 5)');
Output
Press Execute to run this example against the real library.

Argument validation Compile-time

keys are bound to keyof properties. Passing a key that does not exist in the source schema's properties is a compile-time error rather than a silent empty-properties result.

/**
 * Anti-pattern: calling `Compose.pick` with a key that does not exist in
 * the source schema's `properties`. The compiler narrows `keys` to
 * `keyof properties`, so the unknown key is rejected at the call site.
 *
 * Book is now a Compose.subClassOf(BibliographicRecordSchema, …) — it is an
 * allOf composition without a flat top-level `properties` object. Picking
 * from a composed schema would bypass the key-narrowing guard. We target
 * BibliographicRecordSchema directly: it has a concrete `properties` object,
 * so TypeScript can narrow `keys` to its known property names and reject any
 * unknown key at the call site. The anti-pattern is identical; only the
 * source schema changes to one with top-level properties.
 */

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

// ✗ Compile error — 'nonExistent' is not a key of BibliographicRecordSchema.properties.
const _Bad = Compose.pick(
  BibliographicRecordSchema,
  [
    'isbn',
    // @ts-expect-error 'nonExistent' is not a key of BibliographicRecordSchema.properties
    'nonExistent'
  ] as const,
  'https://bookstore.example/BookIsbnOnly'
);

console.log('pick unknown key anti-pattern: compile-time error for keys not in BibliographicRecordSchema.properties | valid keys:', Object.keys(BibliographicRecordSchema.properties), '| _Bad.$id:', _Bad.$id);
Output
Press Execute to run this example against the real library.

Bad examples - what NOT to do

Anti-pattern 1: Forgetting as const on the keys array

/**
 * Compose.pick — Anti-pattern 1: Forgetting `as const` on the keys array
 *
 * Without `as const`, the keys array widens to `string[]` and
 * TypeScript loses the literal types — pick can no longer narrow the
 * inferred property set. Always pass keys as a `const` tuple.
 *
 * Book is now a Compose.subClassOf(BibliographicRecordSchema, …) — isbn and
 * title are bibliographic fields that live on BibliographicRecordSchema, not
 * on Book's own properties. We target BibliographicRecordSchema directly so
 * the keys are valid; the `as const` lesson is unchanged.
 */

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

// ✓ Do this — `as const` preserves literal types so pick narrows correctly.
const BookSummarySchema = Compose.pick(
  BibliographicRecordSchema,
  [
    'isbn',
    'title'
  ] as const,
  'https://bookstore.example/BookSummaryConst'
);

const summaryId: string = BookSummarySchema.$id;

console.assert(summaryId.endsWith('BookSummaryConst'));
console.log('pick with as const preserves literal key types:', Object.keys(BookSummarySchema.properties));
Output
Press Execute to run this example against the real library.

Comparison

ts
Compose.pick(BookSchema, ['isbn', 'title', 'price'] as const, 'https://bookstore.example/BookSummary')
ts
BookSchema.pick({ isbn: true, title: true, price: true })
ts
import * as v from 'valibot';
v.pick(BookSchema, ['isbn', 'title', 'price'])
ts
import * as t from 'io-ts';
// Limitation: io-ts has no built-in pick. Reconstruct the codec from the
// fields you want, or use t.intersection of a subset codec.
const BookSummary = t.type({
  isbn:  BookCodec.props.isbn,
  title: BookCodec.props.title,
  price: BookCodec.props.price,
});
ts
import { Type } from '@sinclair/typebox';
// TypeBox has Type.Pick:
Type.Pick(BookSchema, ['isbn', 'title', 'price'])
ts
// Manual construction:
const BookSummary = {
  $id: 'BookSummary',
  type: 'object',
  properties: { isbn: BookSchema.properties.isbn, title: BookSchema.properties.title, price: BookSchema.properties.price },
  required: BookSchema.required?.filter(k => ['isbn', 'title', 'price'].includes(k)),
};
py
book.model_dump(include={'isbn', 'title', 'price'})
# Or define a separate model class:
class BookSummary(BaseModel):
    isbn: str
    title: str
    price: float
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.
  • omit - inverse: keep everything except specified keys
  • extend - add new properties
  • partial - make all properties optional

Compose.omit {#compose-omit} Compile-time + Runtime

Declaration. Creates a new schema with the specified property keys removed from properties. Removed keys are also dropped from required. TypeScript infers a type without the omitted properties.

Use this when you need to remove specific fields while keeping the rest - for example, stripping currency from Book for a region-normalized API, or removing addresses from Customer for a public profile endpoint. This is the runtime equivalent of TypeScript's Omit<T, K>.

Don't use this when you need to keep only specific fields (use pick). Don't use it when you want to add fields (use extend).

Examples

Example 1: Public book without internal currency field

/**
 * Compose.omit — Example 1: Public book without the internal printStatus
 *
 * Drops a single field from BookSchema for a region-normalised public
 * feed. The omitted field is removed from both `properties` and
 * `required`; everything else carries through.
 */

import { Compose } from '../../../src/index.js';
import {
  aboxFixtures, 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();

const PublicBookSchema = Compose.omit(
  BookSchema,
  ['printStatus'] as const,
  'https://bookstore.example/PublicBookNoStatus'
);

const jt2 = jt.set(PublicBookSchema);

const errors = jt2.validate(PublicBookSchema.$id, {
  'authors': aboxFixtures.rareBook.authors,
  'inStock': aboxFixtures.rareBook.inStock,
  'isbn': aboxFixtures.rareBook.isbn,
  'price': aboxFixtures.rareBook.price,
  'title': aboxFixtures.rareBook.title
  // printStatus omitted — schema does not require it
});

console.assert(errors.ok);
console.log('PublicBookNoStatus validates without printStatus:', errors.ok, '| omitted fields:', ['printStatus']);
Output
Press Execute to run this example against the real library.

Example 2: Order summary without line items

/**
 * Compose.omit — Example 2: Order summary without line items
 *
 * Drops the `orderLines` array from the canonical OrderSchema to produce a
 * compact summary suitable for dashboard rows. The derived type still
 * carries `orderId`, `customerId`, `orderTotal`, `placedAt`, and the shipping
 * address.
 */

import { Compose } from '../../../src/index.js';
import type { InferType } from '../../../src/types/index.js';
import {
  aboxFixtures, createBookstoreDocRegistry,
  OrderSchema
} from '../bookstore/index.js';
import type { BookstoreRefs } 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 OrderSummarySchema = Compose.omit(
  OrderSchema,
  ['orderLines'] as const,
  'https://bookstore.example/OrderSummary'
);

type OrderSummary = InferType<typeof OrderSummarySchema, BookstoreRefs>;

const jt2 = jt.set(OrderSummarySchema);

// Instantiate the full order to get branded field values, then project the
// OrderSummary view (every field except orderLines) from them.
const order = jt2.instantiate(OrderSchema.$id, aboxFixtures.order);
const summary: OrderSummary = {
  'customerId': order.customerId,
  'orderId': order.orderId,
  'orderTotal': order.orderTotal,
  'placedAt': order.placedAt,
  'shippingAddress': order.shippingAddress
};

const result = jt2.validate(OrderSummarySchema.$id, summary);

console.assert(result.ok);
console.assert(!('orderLines' in summary));
console.log('OrderSummary fields:', Object.keys(summary), '| orderLines omitted:', !('orderLines' in summary));
Output
Press Execute to run this example against the real library.

Example 3: Build a derived schema from a retrieved schema (builds on get)

/**
 * Compose.omit — Example 3: Derive a schema from one already in the registry
 *
 * `jt.registry.get(id)` retrieves a previously
 * registered schema. We narrow it with `Compose.omit` and register
 * the derivation back onto the same registry.
 */

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();

const retrieved = jt.registry.get(BookSchema.$id);

if (retrieved !== undefined) {
  const BookWithoutStockSchema = Compose.omit(
    retrieved as typeof BookSchema,
    ['inStock'] as const,
    'https://bookstore.example/BookWithoutStock'
  );

  const jt2 = jt.set(BookWithoutStockSchema);

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

  console.assert(result.ok);
  console.log('BookWithoutStock derived from registry and validates without inStock:', result.ok);
}
Output
Press Execute to run this example against the real library.

Argument validation Compile-time

keys are bound to keyof properties, the same constraint pick uses. Passing a key that does not exist in the source schema's properties is a compile-time error rather than a silent no-op.

Comparison

ts
Compose.omit(CustomerSchema, ['addresses'] as const, 'https://bookstore.example/CustomerPublic')
ts
CustomerSchema.omit({ addresses: true })
ts
import * as v from 'valibot';
v.omit(CustomerSchema, ['addresses'])
ts
import * as t from 'io-ts';
// Limitation: io-ts has no built-in omit. Rebuild the codec from the fields
// you want to keep.
const { addresses: _drop, ...rest } = CustomerCodec.props;
const CustomerPublic = t.type(rest);
ts
import { Type } from '@sinclair/typebox';
Type.Omit(CustomerSchema, ['addresses'])
ts
// Manual  - copy schema, delete key from properties and required:
const { addresses: _, ...props } = CustomerSchema.properties;
const req = CustomerSchema.required?.filter(k => k !== 'addresses') ?? [];
const CustomerPublic = { ...CustomerSchema, properties: props, required: req };
py
customer.model_dump(exclude={'addresses'})
# Or define a derived model:
class CustomerPublic(BaseModel):
    id: str
    email: str
    name: str
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.
  • pick - keep only specified fields
  • partial - make remaining fields optional after omit
  • extend - add new properties

See also

Released under the MIT License.