Types vs Schemas
Side by side
Same shape, two spellings. Both isProductA and isProductB are identical validators under the hood.
// packages/examples/src/guide/types-vs-schemas-side-by-side.ts
// Option A — a plain TypeScript type. Fastest path, nothing extra to write.
type Product = {
id: number;
name: string;
tags: string[];
status: 'draft' | 'live';
};
const isProductA = createValidate<Product>();
// packages/examples/src/guide/types-vs-schemas-side-by-side.ts
// Option B — the RT.* builders, if you like the Zod / TypeBox feel.
const productSchema = RT.object({
id: TF.number(),
name: TF.string(),
tags: RT.array(TF.string()),
status: RT.union([RT.literal('draft'), RT.literal('live')]),
});
// Recover the TypeScript type from the schema whenever you need it.
type ProductFromSchema = Static<typeof productSchema>;
const isProductB = createValidate(productSchema);
The pure type is the fast path. There's nothing to write but the type you already have. The schema is a real value you can store in a variable, pass to a function, or build up piece by piece.
Get the type back with Static
A schema is a value, but sometimes you want the TypeScript type it stands for. Static<typeof schema> hands it back:
// packages/examples/src/guide/types-vs-schemas-static.ts
import * as TF from 'ts-runtypes/formats';
import {type Static} from 'ts-runtypes';
import * as RT from 'ts-runtypes/schema';
// Build a schema as a value you can pass around, store, or compose.
const address = RT.object({
street: TF.string(),
city: TF.string(),
zip: TF.string(),
});
// Static<typeof schema> hands you the TypeScript type back.
type Address = Static<typeof address>;
// Now `Address` is a normal type — use it anywhere.
const home: Address = {street: '1 Infinite Loop', city: 'Cupertino', zip: '95014'};
export {address, home};
export type {Address};
So you never write a shape twice. Start from a type and reflect it, or start from a schema and Static your way to the type. Either direction gives you one source of truth.
Mix them freely
They're the same thing, so you can use both in the same file: a plain type here, a schema there, even nesting one kind of thinking inside the other.
// packages/examples/src/guide/types-vs-schemas-mixed.ts
import * as TF from 'ts-runtypes/formats';
import {createValidate} from 'ts-runtypes';
import * as RT from 'ts-runtypes/schema';
// Mix both in one file — a pure type nested inside a schema and back again.
type Money = {amount: number; currency: 'USD' | 'EUR'};
// A schema that references the plain type via RT.* leaves.
const invoice = RT.object({
id: TF.string(),
lines: RT.array(
RT.object({
sku: TF.string(),
total: RT.object({amount: TF.number(), currency: RT.union([RT.literal('USD'), RT.literal('EUR')])}),
})
),
});
const isMoney = createValidate<Money>();
const isInvoice = createValidate(invoice);
export {isMoney, isInvoice};
Either way you end up at the same validator, the same JSON codec, the same mock. Pick whichever reads better to you.