Skip to content

Getting Started ​

Ready to use Models in your project? This guide will walk you through installation, basic usage, and common patterns to get you productive quickly.

Installation ​

Requirements ​

TypeScript: Version 5.3.0 or higher is required. We recommend using the latest stable version for the best experience and type safety.

Configuration: Make sure you have "strict": true (or at least "strictNullChecks": true) enabled in your tsconfig.json:

json
{
  "compilerOptions": {
    "strict": true
  }
}

Install the Package ​

You can install @uni-ts/model using your preferred package manager:

sh
$ npm add @uni-ts/model
sh
$ pnpm add @uni-ts/model
sh
$ yarn add @uni-ts/model
sh
$ bun add @uni-ts/model

If you want to handle model validation errors safely without throwing exceptions, you can also install @uni-ts/result:

sh
$ npm add @uni-ts/model @uni-ts/result
sh
$ pnpm add @uni-ts/model @uni-ts/result
sh
$ yarn add @uni-ts/model @uni-ts/result
sh
$ bun add @uni-ts/model @uni-ts/result

Your First Model ​

As you saw in the Introduction, a Model combines data structure and business logic in one place. Let's create your first model using Zod:

typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
const
User
=
createModel
(
z
.
object
({
name
:
z
.
string
().
min
(1),
email
:
z
.
string
().
email
(),
age
:
z
.
number
().
int
().
min
(0),
}), ); // Get the TypeScript type type
User
=
InferModelOutput
<typeof
User
>;

📦 Bring Your Own Validation Library

While all examples in the documentation use Zod, you can choose any validation library that supports Standard Schema instead.

Validation Methods ​

Models provide multiple ways to validate data, each suited for different scenarios:

Type Guards with is() ​

Use is() to safely check unknown data and narrow its type:

typescript
import { 
User
} from './user';
export function
processUserData
(
data
: unknown) {
if (
User
.
is
(
data
)) {
// TypeScript knows data is User type
console
.
log
(`Processing user: ${
data
.
name
}`);
return
data
;
} throw new
Error
('Invalid user data');
}
typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
export type
User
=
InferModelOutput
<typeof
User
>;
export const
User
=
createModel
(
z
.
object
({
name
:
z
.
string
().
min
(1),
email
:
z
.
email
(),
age
:
z
.
number
().
int
().
min
(0),
}), );

Validation with from() ​

When you have data that should match your model's input type, use from():

typescript
import { 
User
} from './user';
const
user
=
User
.
from
({
name
: 'John Doe',
email
: '[email protected]',
age
: 25,
});
console
.
log
(
user
.
name
); // "John Doe"
typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
export type
User
=
InferModelOutput
<typeof
User
>;
export const
User
=
createModel
(
z
.
object
({
name
:
z
.
string
().
min
(1),
email
:
z
.
email
(),
age
:
z
.
number
().
int
().
min
(0),
}), );

Validation with cast() ​

When working with completely unknown data (like API responses), use cast():

typescript
import { 
User
} from './user';
export async function
fetchUser
(
id
: string) {
const
response
= await
fetch
(`/api/users/${
id
}`);
const
data
: unknown = await
response
.
json
();
// cast() validates unknown data return
User
.
cast
(
data
);
}
typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
export type
User
=
InferModelOutput
<typeof
User
>;
export const
User
=
createModel
(
z
.
object
({
name
:
z
.
string
().
min
(1),
email
:
z
.
email
(),
age
:
z
.
number
().
int
().
min
(0),
}), );

đź’ˇ from vs cast

Use from() when you're confident about the data structure, and cast() when working with external or unknown data sources.

Accessing the Schema ​

Every model exposes its underlying validation schema through the .schema property. It's useful when you need to compose schemas or use them with other libraries.

typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
import {
Todo
} from './todo';
export type
TodoList
=
InferModelOutput
<typeof
TodoList
>;
export const
TodoList
=
createModel
(
z
.
object
({
id
:
z
.
int
().
positive
(),
name
:
z
.
string
().
min
(1),
todos
:
z
.
array
(
Todo
.
schema
), // âś… Use the underlying schema
}), );
typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
export type
Todo
=
InferModelOutput
<typeof
Todo
>;
export const
Todo
=
createModel
(
z
.
object
({
id
:
z
.
int
().
positive
(),
title
:
z
.
string
().
min
(1),
completed
:
z
.
boolean
().
default
(false),
}), );
typescript
import { 
TodoList
} from './todo-list';
const
todoList
=
TodoList
.
from
({
id
: 1,
name
: 'My Tasks',
todos
: [
{
id
: 1,
title
: 'Learn TypeScript',
completed
: true },
{
id
: 2,
title
: 'Build an app',
completed
: false },
], });

Extending Models ​

Models become even more powerful when you integrate them with business rules. You can use the second argument when defining a model to add custom functionality.

typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
export type
Product
=
InferModelOutput
<typeof
Product
>;
export const
Product
=
createModel
(
z
.
object
({
name
:
z
.
string
().
min
(1),
price
:
z
.
number
().
positive
(),
stock
:
z
.
number
().
int
().
min
(0),
}), {
isInStock
,
canBeSold
,
}, ); function
isInStock
(
product
:
Product
) {
return
product
.
stock
> 0;
} function
canBeSold
(
product
:
Product
,
quantity
: number) {
return
product
.
stock
>=
quantity
&&
quantity
> 0;
}
typescript
import { 
Product
} from './definition';
const
laptop
=
Product
.
from
({
name
: 'Gaming Laptop',
price
: 1299.99,
stock
: 5,
});
Product
.
isInStock
(
laptop
); // true
Product
.
canBeSold
(
laptop
, 3); // true

Working with Different Libraries ​

Models work with any validation library that supports the Standard Schema interface. Here are examples for some popular libraries:

typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
export type
Product
=
InferModelOutput
<typeof
Product
>;
export const
Product
=
createModel
(
z
.
object
({
name
:
z
.
string
().
min
(1),
price
:
z
.
number
().
positive
(),
category
:
z
.
enum
(['electronics', 'clothing', 'books']),
}), );
typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import * as
v
from 'valibot';
export type
Product
=
InferModelOutput
<typeof
Product
>;
export const
Product
=
createModel
(
v
.
object
({
name
:
v
.
pipe
(
v
.
string
(),
v
.
minLength
(1)),
price
:
v
.
pipe
(
v
.
number
(),
v
.
minValue
(0)),
category
:
v
.
picklist
(['electronics', 'clothing', 'books']),
}), );
typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
type
} from 'arktype';
export type
Product
=
InferModelOutput
<typeof
Product
>;
export const
Product
=
createModel
(
type
({
name
: 'string>0',
price
: 'number>0',
category
: "'electronics'|'clothing'|'books'",
}), );

All have the same interface regardless of the underlying validation library!

Branded Types for Primitives ​

When modeling primitive values like emails or user IDs, use branded types to enforce validation at the type level:

typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
// Create branded type type
Email
=
InferModelOutput
<typeof
Email
>;
const
Email
=
createModel
(
z
.
email
().
brand
('Email'));
// Function can now require already validated data function
sendWelcomeEmail
(
to
:
Email
) {
// No need to validate - email is guaranteed to be valid
console
.
log
(`Sending welcome email to ${
to
}`);
} // Usage requires validation const
email
=
Email
.
from
('[email protected]');
sendWelcomeEmail
(
email
);
typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
// Create branded type type
UserId
=
InferModelOutput
<typeof
UserId
>;
const
UserId
=
createModel
(
z
.
uuid
().
brand
('UserId'));
// Function can now require already validated data function
getUser
(
id
:
UserId
) {
// No need to validate - ID is guaranteed to be a valid UUID return
fetch
(`/api/users/${
id
}`);
} // Usage requires validation const
userId
=
UserId
.
from
('123e4567-e89b-12d3-a456-426614174000');
getUser
(
userId
);

Branded Types Across Libraries ​

typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
export type
Email
=
InferModelOutput
<typeof
Email
>;
export const
Email
=
createModel
(
z
.
email
().
brand
('Email'));
export type
UserId
=
InferModelOutput
<typeof
UserId
>;
export const
UserId
=
createModel
(
z
.
uuid
().
brand
('UserId'));
typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import * as
v
from 'valibot';
export type
Email
=
InferModelOutput
<typeof
Email
>;
export const
Email
=
createModel
(
v
.
pipe
(
v
.
string
(),
v
.
email
(),
v
.
brand
('Email')),
); export type
UserId
=
InferModelOutput
<typeof
UserId
>;
export const
UserId
=
createModel
(
v
.
pipe
(
v
.
string
(),
v
.
uuid
(),
v
.
brand
('UserId')),
);
typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
type
} from 'arktype';
export type
Email
=
InferModelOutput
<typeof
Email
>;
export const
Email
=
createModel
(
type
('string.email#Email'));
export type
UserId
=
InferModelOutput
<typeof
UserId
>;
export const
UserId
=
createModel
(
type
('string.uuid#UserId'));

Safe Error Handling ​

If you prefer Result types over exceptions, use the safe model creators from @uni-ts/model/safe. In such case, remember to have @uni-ts/result installed in your project.

typescript
import { 
createSafeModel
} from '@uni-ts/model/safe';
import {
isOk
} from '@uni-ts/result';
import {
z
} from 'zod';
const
User
=
createSafeModel
(
z
.
object
({
name
:
z
.
string
().
min
(1),
email
:
z
.
email
(),
age
:
z
.
number
().
int
().
min
(0),
}), ); // Safe validation returns Result instead of throwing const
result
=
User
.
from
({
name
: 'John',
email
: 'invalid',
age
: 25 });
if (
isOk
(
result
)) {
console
.
log
('User created:',
result
.
data
);
} else {
console
.
error
('Validation failed:',
result
.
error
);
}

Safe Model Variants ​

@uni-ts/model/safe provides three variants of safe model creators, so you can choose the one that fits your coding style.

typescript
import { 
createSafeModel
} from '@uni-ts/model/safe';
import {
z
} from 'zod';
const
User
=
createSafeModel
(
z
.
object
({
name
:
z
.
string
().
min
(1),
email
:
z
.
email
(),
age
:
z
.
number
().
int
().
min
(0),
}), ); // Only safe methods available const
user1
=
User
.
from
(
data
);
const
user2
=
User
.
cast
(
data
);
typescript
import { 
createSafeFirstModel
} from '@uni-ts/model/safe';
import {
z
} from 'zod';
const
User
=
createSafeFirstModel
(
z
.
object
({
name
:
z
.
string
().
min
(1),
email
:
z
.
email
(),
age
:
z
.
number
().
int
().
min
(0),
}), ); // Safe by default, unsafe methods with prefix const
user1
=
User
.
from
(
data
);
const
user2
=
User
.
cast
(
data
);
const
user3
=
User
.
unsafeFrom
(
data
);
const
user4
=
User
.
unsafeCast
(
data
);
typescript
import { 
createUnsafeFirstModel
} from '@uni-ts/model/safe';
import {
z
} from 'zod';
const
User
=
createUnsafeFirstModel
(
z
.
object
({
name
:
z
.
string
().
min
(1),
email
:
z
.
email
(),
age
:
z
.
number
().
int
().
min
(0),
}), ); // Unsafe by default, safe methods with prefix const
user1
=
User
.
from
(
data
);
const
user2
=
User
.
cast
(
data
);
const
user3
=
User
.
safeFrom
(
data
);
const
user4
=
User
.
safeCast
(
data
);

Working with APIs ​

Models shine when working with external APIs. Here's a common pattern for usage with data fetching.

typescript
import { 
User
} from './user';
import {
fetchUser
} from './user-service';
const
user
= await
fetchUser
('123e4567-e89b-12d3-a456-426614174000');
if (
User
.
isNew
(
user
)) {
console
.
log
(`Welcome ${
user
.
name
}!`);
}
typescript
import { 
ApiUser
} from './api-user';
export async function
fetchUser
(
id
: string) {
const
response
= await
fetch
(`/api/users/${
id
}`);
const
apiData
= await
response
.
json
();
// Validate API response const
apiUser
=
ApiUser
.
cast
(
apiData
);
// Convert to domain model return
ApiUser
.
toUser
(
apiUser
);
}
typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
import {
User
} from './user';
// API response model export type
ApiUser
=
InferModelOutput
<typeof
ApiUser
>;
export const
ApiUser
=
createModel
(
z
.
object
({
id
:
z
.
uuid
(),
first_name
:
z
.
string
(),
last_name
:
z
.
string
(),
email
:
z
.
email
(),
created_at
:
z
.
number
().
int
().
min
(0),
}), {
toUser
,
}, ); function
toUser
(
apiUser
:
ApiUser
) {
return
User
.
from
({
id
:
apiUser
.
id
,
name
: `${
apiUser
.
first_name
} ${
apiUser
.
last_name
}`,
email
:
apiUser
.
email
,
createdAt
: new
Date
(
apiUser
.
created_at
),
}); }
typescript
import { 
createModel
, type
InferModelOutput
} from '@uni-ts/model';
import {
z
} from 'zod';
// Domain model with business logic export type
User
=
InferModelOutput
<typeof
User
>;
export const
User
=
createModel
(
z
.
object
({
id
:
z
.
uuid
().
brand
('UserId'),
name
:
z
.
string
().
min
(1),
email
:
z
.
email
().
brand
('Email'),
createdAt
:
z
.
date
(),
}), {
isNew
,
}, ); function
isNew
(
user
:
User
) {
const
daysSinceCreation
=
(
Date
.
now
() -
user
.
createdAt
.
getTime
()) / (1000 * 60 * 60 * 24);
return
daysSinceCreation
<= 7;
}