Type Inference

Derive TypeScript types directly from your schema — no duplicate interfaces, no manual synchronization.

InferType

▶ Open in Playground

Use InferType<typeof MySchema> to extract the TypeScript type from any schema. The inferred type updates automatically when you change the schema — no manual synchronization needed.

import { object, string, number, array, boolean, type InferType } from '@cleverbrush/schema';

const UserSchema = object({
  name:     string().minLength(2),
  email:    string().email(),
  age:      number().min(0).optional(),
  isActive: boolean(),
  tags:     array(string())
});

type User = InferType<typeof UserSchema>;
// {
//   name: string;
//   email: string;
//   age?: number | undefined;
//   isActive: boolean;
//   tags: string[];
// }

Optional & Nullable

.optional() adds undefined to the type and removes the field from required keys. .nullable() adds null. .nullish() adds both.

import { string, type InferType } from '@cleverbrush/schema';

type A = InferType<typeof string().optional()>;
// string | undefined

type B = InferType<typeof string().nullable()>;
// string | null

type C = InferType<typeof string().nullish()>;
// string | null | undefined

Default Values Narrow the Type

When .default() is applied to an optional schema, undefined is removed from the inferred type — because the default value will always fill in.

import { number, type InferType } from '@cleverbrush/schema';

const Port = number().optional().default(3000);
type Port = InferType<typeof Port>;
// number  (not number | undefined)

Branded Types

Use .brand() to create nominal types that prevent accidental mixing of structurally identical values.

import { string, type InferType } from '@cleverbrush/schema';

const UserId = string().uuid().brand('UserId');
const OrderId = string().uuid().brand('OrderId');

type UserId = InferType<typeof UserId>;
// string & { __brand: 'UserId' }

type OrderId = InferType<typeof OrderId>;
// string & { __brand: 'OrderId' }

function getUser(id: UserId) { /* ... */ }

const uid = UserId.validate('...').object;
const oid = OrderId.validate('...').object;

getUser(uid); // ✓ compiles
getUser(oid); // ✗ TypeScript error — OrderId is not assignable to UserId

Union & Discriminated Union Inference

Unions produce TypeScript union types. When each branch has a literal-typed discriminator field, the inferred type is a proper discriminated union — enabling switch narrowing.

import { union, object, string, number, type InferType } from '@cleverbrush/schema';

const Shape = union(
  object({ kind: string().equals('circle'), radius: number() })
).or(
  object({ kind: string().equals('rect'), width: number(), height: number() })
);

type Shape = InferType<typeof Shape>;
// { kind: 'circle'; radius: number }
// | { kind: 'rect'; width: number; height: number }

function area(s: Shape) {
  switch (s.kind) {
    case 'circle': return Math.PI * s.radius ** 2; // s narrowed to circle
    case 'rect':   return s.width * s.height;       // s narrowed to rect
  }
}

Readonly Inference

.readonly() wraps the inferred type in Readonly<> or ReadonlyArray<> — a type-level-only constraint that prevents mutation at compile time.

import { object, array, string, number, type InferType } from '@cleverbrush/schema';

type User = InferType<typeof object({ name: string(), age: number() }).readonly()>;
// Readonly<{ name: string; age: number }>

type Tags = InferType<typeof array(string()).readonly()>;
// ReadonlyArray<string>