OneApp Docs
PackagesCore

@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/types

Branded 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/types

2. Create branded types for IDs

types/ids.ts
import { Brand } from "@repo/types";

export type UserId = Brand<string, "UserId">;
export type PostId = Brand<string, "PostId">;
export type CommentId = Brand<string, "CommentId">;
services/user.ts
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

services/api.ts
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

types/api.ts
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>;
app/api/users/route.ts
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

PropertyValue
Locationpackages/types
PurposeType safety, branded types, utility types

Export Paths

PathDescription
@repo/typesAll types
@repo/types/brandBranded type utilities
@repo/types/apiAPI response types
@repo/types/resultResult 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-end

Async 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 error

API 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 error

Nullable

import type { Nullable, NonNullable } from "@repo/types";

type MaybeString = Nullable<string>; // string | null | undefined
type DefiniteString = NonNullable<MaybeString>; // string

ValueOf

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
}

On this page