Skip to content

Shared Types

Overview

The @pos/shared-types package is the single source of truth for TypeScript interfaces used across the entire system. It is consumed by the HQ server, store server, web client, and all other packages.

Package Structure

packages/shared-types/
  src/
    index.ts           # Re-exports everything
    product.ts         # Product, Department, TaxRate, TaxGroup, Supplier, etc.
    sale.ts            # Sale, SaleItem, SalePayment, SaleStatus
    user.ts            # User, Permission, Role, RolePermission, UserStoreRole
    store.ts           # Store, StoreConfig, StoreInventory, StoreDashboardEntry
    sync.ts            # SyncOutboxEntry, SyncLog, SyncEnvelope, SyncEntityType
    api.ts             # ApiResponse, PaginatedResponse, LoginRequest/Response, SyncDashboard types
    inventory.ts       # Inventory types
    transfer.ts        # Transfer types
    register.ts        # Register session types
    purchase-order.ts  # PurchaseOrder, PurchaseOrderItem
    audit.ts           # Audit log types
    worksheet.ts       # Worksheet, WorksheetItem, WorksheetStore
    sales-rep.ts       # SalesRep type
    module.ts          # ModuleDefinition
  tsconfig.json
  package.json

What Goes in Shared Types

Put in shared-types:

  • Interfaces that represent database entities (Product, Sale, User, etc.)
  • API request/response shapes (LoginRequest, ApiResponse, etc.)
  • Enum-like union types (SaleStatus, SyncDirection, WorksheetStatus)
  • Types used by more than one package or app

Keep inline (do NOT put in shared-types):

  • Zod schemas (validation is server-specific)
  • Drizzle table definitions (ORM-specific)
  • Vue component props/emits types
  • Internal implementation types used by only one module

Key Types

ApiResponse<T>

The standard response wrapper used by all API endpoints:

typescript
export interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
}

PaginatedResponse<T>

Extension for paginated list endpoints:

typescript
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
}

Entity Interfaces

Every database entity has a corresponding TypeScript interface. These interfaces match the database column types with the numeric conventions applied:

typescript
export interface Product {
  id: string;              // UUID
  sku: string;
  barcode: string | null;
  name: string;
  description: string | null;
  departmentId: string | null;
  costPrice: number;       // cents (integer)
  sellPrice: number;       // cents (integer)
  taxGroupId: string | null;
  isActive: boolean;
  trackStock: boolean;
  updatedAt: string;       // ISO timestamp (serialized from Date)
  syncVersion: number;
}

Note that updatedAt is string (not Date) because dates are JSON-serialized as ISO strings over the API.

Sync Types

typescript
export type SyncEntityType =
  | 'product' | 'department' | 'tax_rate' | 'supplier'
  | 'user' | 'role' | 'permission' | 'role_permission' | 'user_store_role'
  | 'sale' | 'inventory_adjustment' | 'transfer' | 'register_session'
  | 'monthly_closing' | 'product_supplier' | 'sales_rep'
  | 'store_price' | 'purchase_order' | 'purchase_order_item';

export interface SyncEnvelope {
  id: string;
  storeId: string;
  entityType: SyncEntityType;
  entityId: string;
  payload: unknown;
  version: number;
  timestamp: string;
}

How TypeScript Catches API Drift

Because the HQ server, store server, web client, and backend adapter all import from the same @pos/shared-types package, TypeScript's type system catches mismatches at build time.

Example: Adding a Field

If you add a new required field to the Product interface:

typescript
export interface Product {
  // ... existing fields ...
  minOrderQuantity: number;  // NEW
}

The following will fail at typecheck:

  1. HQ server routes that return products without the new field
  2. Store server routes that return products without the new field
  3. Web client views that destructure products without handling the new field
  4. Backend adapter tests that mock products without the new field

This forces you to update all consumers in a single commit, preventing API drift.

Build-Time Verification

The deploy script runs tsc --noEmit for all packages and apps before building. If any type mismatch exists, the deploy fails before any Docker image is built:

bash
# deploy.sh pipeline
pnpm --filter shared-types exec tsc --noEmit   # Shared types must compile
pnpm --filter hq-server exec tsc --noEmit      # HQ server must match types
pnpm --filter store-server exec tsc --noEmit   # Store server must match types
cd apps/web-client && npx vue-tsc --noEmit     # Web client must match types

Consuming Shared Types

In Server Code

typescript
import type { Product, ApiResponse } from '@pos/shared-types';

app.get('/', async (): Promise<ApiResponse<Product[]>> => {
  const products = await db.select().from(productsTable);
  return { success: true, data: products };
});

In Vue Components

typescript
import type { Product } from '@pos/shared-types';

const products = ref<Product[]>([]);

In Backend Adapter

typescript
import type { ApiResponse } from '@pos/shared-types';

const result = await adapter.execute<Product[]>('get_products');
// result is typed as ApiResponse<Product[]>

Adding a New Type

  1. Create or update the type file in packages/shared-types/src/
  2. Export it from packages/shared-types/src/index.ts
  3. Build the package: pnpm --filter shared-types build
  4. Use it in consuming packages (they will pick it up via the workspace link)

The monorepo workspace configuration ensures all apps reference the local version of @pos/shared-types (not a published npm version).