@repo/types
TypeScript types, utilities, and branded types for type-safe development. Prevent ID mixing bugs at compile time. Result types instead of exceptions. Generic utilities for safer code. Works across the entire monorepo.
Quick Start
Add type safety in 5 minutes:
pnpm add @repo/typesBranded types, Result types, API response types included. Skip to Quick Start →
Why @repo/types?
TypeScript allows mixing userId and postId strings. A function expecting userId accepts postId by mistake.
Exceptions make error handling unpredictable (did you catch all errors?). any types bypass type checking. Nested
objects need partial types for updates. IDE shows complex intersection types as unreadable messes.
@repo/types solves this with branded types for ID safety, Result types for predictable errors, and utility types for better DX.
Production-ready with compile-time ID validation, exception-free error handling, API response types, and deep utility types.
Use cases
- Prevent ID mixing bugs — Branded types catch wrong IDs at compile time
- Exception-free error handling — Result types make errors explicit and type-safe
- Type-safe API responses — Standardized response and error types
- Deep partial/readonly types — Update nested objects safely
- Better IDE type displays — Prettify complex types for readability
How it works
@repo/types provides branded types and utilities:
import { Brand, Result, Ok, Err } from "@repo/types";
// Branded types prevent ID mixing
type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;
function getUser(id: UserId) {
return fetch(`/api/users/${id}`);
}
const userId = "user_123" as UserId;
const postId = "post_456" as PostId;
getUser(userId); // ✅ Works
getUser(postId); // ❌ Compile error!
// Result types replace exceptions
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return Err("Division by zero");
return Ok(a / b);
}
const result = divide(10, 2);
if (result.ok) {
console.log(result.value); // 5 (type-safe!)
}Uses TypeScript's type system to create nominal types (branded types), discriminated unions (Result), and advanced mapped types (DeepPartial, Prettify).
Key features
Branded types — Compile-time ID validation prevents mixing different ID types
Result types — Exception-free error handling with type-safe success/error
API response types — Standardized types for API responses, errors, pagination
Deep utilities — DeepPartial, DeepReadonly, ValueOf, Prettify for complex types
Type guards — Runtime type checking with isString, isNumber, isArray
Type-safe — Full TypeScript support with strict mode compatibility
Quick Start
1. Install the package
pnpm add @repo/types2. Create branded types for IDs
import { Brand } from "@repo/types";
export type UserId = Brand<string, "UserId">;
export type PostId = Brand<string, "PostId">;
export type CommentId = Brand<string, "CommentId">;import type { UserId } from "#/types/ids";
export function getUser(id: UserId) {
return fetch(`/api/users/${id}`);
}
// Usage
const userId = "user_123" as UserId;
await getUser(userId); // ✅ Works
const postId = "post_456" as PostId;
await getUser(postId); // ❌ Type error at compile time!3. Use Result types for error handling
import { Result, Ok, Err, type AsyncResult } from "@repo/types/result";
export async function fetchUser(id: string): AsyncResult<User, ApiError> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return Err({
code: response.status,
message: "Failed to fetch user"
});
}
const user = await response.json();
return Ok(user);
} catch (error) {
return Err({
code: 500,
message: "Network error"
});
}
}
// Usage - errors are explicit and type-safe
const result = await fetchUser("123");
if (result.ok) {
console.log(result.value.name); // user is type-safe
} else {
console.error(result.error.message); // error is type-safe
}4. Import API response types
import type { ApiResponse, ApiError, PaginatedResponse } from "@repo/types/api";
// Standard response
export type UserResponse = ApiResponse<User>;
// Error response
export type ErrorResponse = ApiError;
// Paginated response
export type PostsResponse = PaginatedResponse<Post>;import type { UserResponse, ErrorResponse } from "#/types/api";
export async function GET(request: Request): Promise<Response> {
const response: UserResponse = {
data: user,
timestamp: Date.now()
};
return Response.json(response);
}That's it! You now have compile-time ID safety, exception-free error handling, and type-safe API responses.
Use utility types for complex scenarios
DeepPartial for nested updates, Prettify for readable types:
import type { DeepPartial, Prettify } from "@repo/types";
// Update nested objects safely
type UserUpdate = DeepPartial<User>;
// Readable intersection types
type UserWithPosts = Prettify<User & { posts: Post[] }>;Technical Details
For Developers: Technical implementation details
Overview
| Property | Value |
|---|---|
| Location | packages/types |
| Purpose | Type safety, branded types, utility types |
Export Paths
| Path | Description |
|---|---|
@repo/types | All types |
@repo/types/brand | Branded type utilities |
@repo/types/api | API response types |
@repo/types/result | Result type utilities |
Branded Types
Compile-Time Safety
Branded types catch ID mixing bugs at compile time, not runtime. This eliminates an entire class of bugs.
Prevent mixing different ID types:
import { Brand } from "@repo/types";
// highlight-start
type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;
// highlight-end
function getUser(id: UserId) {
// ...
}
const userId = "user_123" as UserId;
const postId = "post_456" as PostId;
getUser(userId); // ✅ Works
// highlight-next-line
getUser(postId); // ❌ Type error!Creating Branded Types
import { createBrand } from "@repo/types/brand";
const UserId = createBrand<string, "UserId">();
const PostId = createBrand<string, "PostId">();
// Create values
const userId = UserId.create("user_123");
const postId = PostId.create("post_456");
// Validate at runtime
if (UserId.is(value)) {
// value is UserId
}Result Type
Exception-Free Error Handling
Use Result types instead of throwing exceptions for predictable error handling and better type inference.
Type-safe error handling without exceptions:
import { Result, Ok, Err } from "@repo/types/result";
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
// highlight-next-line
return Err("Division by zero");
}
// highlight-next-line
return Ok(a / b);
}
const result = divide(10, 2);
// highlight-start
if (result.ok) {
console.log(result.value); // 5
} else {
console.error(result.error); // "Division by zero"
}
// highlight-endAsync Result
import { AsyncResult } from "@repo/types/result";
async function fetchUser(id: string): AsyncResult<User, ApiError> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return Err({ code: response.status, message: "Failed to fetch" });
}
const user = await response.json();
return Ok(user);
} catch (error) {
return Err({ code: 500, message: "Network error" });
}
}Result Utilities
import { Result, map, flatMap, unwrapOr } from "@repo/types/result";
const result = Ok(5);
// Map over success value
const doubled = map(result, (n) => n * 2); // Ok(10)
// Chain results
const chained = flatMap(result, (n) => (n > 0 ? Ok(n.toString()) : Err("Negative")));
// Unwrap with default
const value = unwrapOr(result, 0); // 5 or 0 if errorAPI Types
API Response
import type { ApiResponse, ApiError, PaginatedResponse } from "@repo/types/api";
// Standard response
const response: ApiResponse<User> = {
data: user,
timestamp: Date.now()
};
// Error response
const error: ApiError = {
code: "NOT_FOUND",
message: "User not found",
details: { userId: "123" }
};
// Paginated response
const paginated: PaginatedResponse<Post> = {
data: posts,
pagination: {
page: 1,
pageSize: 10,
total: 100,
totalPages: 10
}
};Utility Types
DeepPartial
import type { DeepPartial } from "@repo/types";
interface User {
name: string;
profile: {
bio: string;
avatar: string;
};
}
// All properties optional, including nested
type PartialUser = DeepPartial<User>;DeepReadonly
import type { DeepReadonly } from "@repo/types";
const config: DeepReadonly<Config> = {
api: { url: "https://..." }
};
config.api.url = "new"; // ❌ Type errorNullable
import type { Nullable, NonNullable } from "@repo/types";
type MaybeString = Nullable<string>; // string | null | undefined
type DefiniteString = NonNullable<MaybeString>; // stringValueOf
import type { ValueOf } from "@repo/types";
const STATUS = {
PENDING: "pending",
ACTIVE: "active",
INACTIVE: "inactive"
} as const;
type Status = ValueOf<typeof STATUS>; // "pending" | "active" | "inactive"Prettify
import type { Prettify } from "@repo/types";
type UserWithPosts = User & { posts: Post[] };
// Shows expanded type in IDE
type PrettyUserWithPosts = Prettify<UserWithPosts>;Type Guards
import { isString, isNumber, isObject, isArray } from "@repo/types";
function process(value: unknown) {
if (isString(value)) {
return value.toUpperCase();
}
if (isNumber(value)) {
return value * 2;
}
if (isArray(value)) {
return value.length;
}
}Custom Type Guards
import { createTypeGuard } from "@repo/types";
interface User {
id: string;
email: string;
}
const isUser = createTypeGuard<User>(
(value) => typeof value === "object" && value !== null && "id" in value && "email" in value
);
if (isUser(data)) {
// data is User
}Related Packages
- @repo/utils - Utility functions
- @repo/config - TypeScript configurations