Type Inference
Derive TypeScript types directly from your schema — no duplicate interfaces, no manual synchronization.
InferType
▶ Open in PlaygroundUse 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 | undefinedDefault 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 UserIdUnion & 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>