Skip to content

Shared Types Guide

This guide explains how to manage shared TypeScript types across the Coaching App packages.

Table of Contents

Overview

The shared package contains TypeScript types, interfaces, and utilities used across all packages (backend, frontend, mobile).

Benefits

  • Single source of truth for data models
  • Type safety across frontend and backend
  • Code reuse for common utilities
  • Consistent data structures

Current Approach

Manual type definitions shared between the Hono backend and Expo frontend.

Package Structure

shared/
├── src/
│   ├── types/
│   │   ├── User.ts
│   │   ├── Session.ts
│   │   ├── Booking.ts
│   │   └── index.ts
│   ├── utils/
│   │   ├── pricing.ts
│   │   ├── validation.ts
│   │   └── index.ts
│   ├── constants/
│   │   └── index.ts
│   └── index.ts
├── package.json
└── tsconfig.json

Defining Types

Basic Types

shared/src/types/User.ts

/**
 * User roles in the system
 */
export enum UserRole {
  PLATFORM_ADMIN = 'platform_admin', // System-wide admin
  ORGANIZATION_ADMIN = 'organization_admin', // Org-level admin
  MANAGER = 'manager', // Full access within organization
  COACH = 'coach', // Session management
  TEACHER = 'teacher', // Browse and book sessions
}

/**
 * User account status
 */
export type UserStatus = 'active' | 'inactive' | 'suspended' | 'archived';

/**
 * User entity
 */
export interface User {
  id: string;
  email: string;
  firstName?: string;
  lastName?: string;
  role: UserRole;
  status?: UserStatus;
  avatar?: string;
  createdAt: string;
  updatedAt: string;
}

/**
 * User creation payload
 */
export interface CreateUserInput {
  email: string;
  password: string;
  first_name: string;
  last_name: string;
  role: UserRole;
}

/**
 * User update payload
 */
export interface UpdateUserInput {
  first_name?: string;
  last_name?: string;
  avatar?: string;
}

shared/src/types/Session.ts

import type { User } from './User';

export type SessionType = 'standard' | 'premium';
export type SessionStatus = 'available' | 'booked' | 'completed' | 'cancelled';

export interface Session {
  id: string;
  title: string;
  description: string;
  duration: number; // minutes
  price: number;
  type: SessionType;
  status: SessionStatus;
  date: string; // ISO 8601
  start_time: string; // ISO 8601
  end_time: string; // ISO 8601
  coach_id: string | User; // Can be ID or populated object
  max_participants: number;
  created_at: string;
  updated_at: string;
}

export interface CreateSessionInput {
  title: string;
  description: string;
  duration: number;
  type: SessionType;
  date: string;
  start_time: string;
  coach_id: string;
  max_participants?: number;
}

export interface UpdateSessionInput {
  title?: string;
  description?: string;
  duration?: number;
  date?: string;
  start_time?: string;
  max_participants?: number;
}

Complex Types

shared/src/types/Booking.ts

import type { User } from './User';
import type { Session } from './Session';

export type BookingStatus = 'pending' | 'confirmed' | 'cancelled';

export interface Booking {
  id: string;
  session_id: string | Session;
  user_id: string | User;
  status: BookingStatus;
  notes?: string;
  created_at: string;
  updated_at: string;
}

export interface CreateBookingInput {
  session_id: string;
  user_id: string;
  notes?: string;
}

export interface UpdateBookingInput {
  status?: BookingStatus;
  notes?: string;
}

/**
 * Booking with populated relations
 */
export interface BookingWithRelations extends Omit<Booking, 'session_id' | 'user_id'> {
  session_id: Session;
  user_id: User;
}

Exporting Types

shared/src/types/index.ts

// Re-export all types
export * from './User';
export * from './Session';
export * from './Booking';

// Export type utilities
export type ID = string;
export type Timestamp = string; // ISO 8601
export type Nullable<T> = T | null;
export type Optional<T> = T | undefined;

Using Shared Types

In Backend (Hono Routes)

backend/src/routes/sessions/sessions.router.ts

import { Hono } from 'hono';
import type { CreateSessionInput } from '@coaching-app/shared';
import { requireAuth } from '../../middleware/auth';
import { db } from '../../db';

const app = new Hono();

app.post('/create', requireAuth, async (c) => {
  const input = await c.req.json<CreateSessionInput>();
  const session = await db.insert(sessionsTable).values(input).returning();
  return c.json(session[0]);
});

export default app;

In Frontend (Expo Web)

frontend/src/api/sessions.ts

import type { Session, CreateSessionInput } from '@coaching-app/shared';
import { apiClient } from './client';

export async function createSession(input: CreateSessionInput): Promise<Session> {
  const response = await apiClient.post<{ data: Session }>('/sessions', input);
  return response.data.data;
}

export async function getSessions(): Promise<Session[]> {
  const response = await apiClient.get<{ data: Session[] }>('/sessions');
  return response.data.data;
}

frontend/src/components/SessionCard.tsx

import React from 'react';
import type { Session } from '@coaching-app/shared';

interface SessionCardProps {
  session: Session;
  onBook: (sessionId: string) => void;
}

export function SessionCard({ session, onBook }: SessionCardProps) {
  return (
    <div className="session-card">
      <h3>{session.title}</h3>
      <p>{session.description}</p>
      <p>{session.duration} minutes - ${session.price}</p>
      <button onClick={() => onBook(session.id)}>
        Book Session
      </button>
    </div>
  );
}

In Mobile (React Native)

mobile/src/screens/SessionListScreen.tsx

import React from 'react';
import { FlatList, View } from 'react-native';
import type { Session } from '@coaching-app/shared';
import { SessionCard } from '../components/SessionCard';

export function SessionListScreen() {
  const [sessions, setSessions] = React.useState<Session[]>([]);

  return (
    <FlatList<Session>
      data={sessions}
      renderItem={({ item }) => <SessionCard session={item} />}
      keyExtractor={(item) => item.id}
    />
  );
}

Type Generation Workflow

Workflow

  1. Update the Drizzle schema in backend/src/db/schema/
  2. Create a matching shared type in shared/src/types/
    export interface YourResource {
      id: string;
      fieldName: string;
    }
    
  3. Export from index
    export * from './YourResource';
    
  4. Use in packages
    import type { YourResource } from '@coaching-app/shared';
    
  5. Run type check across all workspaces
    pnpm type-check
    

Keeping Types in Sync

When adding a field:

  1. Add the column to the Drizzle schema in backend/src/db/schema/
  2. Run pnpm db:generate && pnpm db:migrate
  3. Update the corresponding shared type in shared/src/types/
  4. Run pnpm type-check and fix any type errors

When removing a field:

  1. Remove from Drizzle schema and create a migration
  2. Remove from the shared TypeScript type
  3. Find and update usages: grep -rn "fieldName" .

Best Practices

1. Use Descriptive Names

// ❌ Bad
export interface S {
  t: string;
  d: number;
}

// ✅ Good
export interface Session {
  title: string;
  duration: number;
}

2. Document Complex Types

/**
 * Represents a coaching session booking
 *
 * @property session_id - Reference to the session being booked
 * @property user_id - User making the booking
 * @property status - Current status of the booking
 */
export interface Booking {
  session_id: string;
  user_id: string;
  status: BookingStatus;
}

3. Use Union Types for Enums

// ✅ Good: Type-safe string literals
export type SessionStatus = 'available' | 'booked' | 'completed';

// ❌ Avoid: Plain strings
export interface Session {
  status: string;
}

4. Create Input/Output Types

// Input type (for creation)
export interface CreateSessionInput {
  title: string;
  duration: number;
}

// Full type (from database)
export interface Session extends CreateSessionInput {
  id: string;
  created_at: string;
  updated_at: string;
}

// Update type (partial)
export interface UpdateSessionInput {
  title?: string;
  duration?: number;
}

5. Use Utility Types

// Pick specific fields
export type SessionSummary = Pick<Session, 'id' | 'title' | 'date'>;

// Omit fields
export type SessionWithoutTimestamps = Omit<Session, 'created_at' | 'updated_at'>;

// Make all fields optional
export type PartialSession = Partial<Session>;

// Make all fields required
export type RequiredSession = Required<Session>;

6. Handle Relations

// ID reference or populated object
export interface Booking {
  session_id: string | Session;
  user_id: string | User;
}

// Or use separate types
export interface BookingWithIds {
  session_id: string;
  user_id: string;
}

export interface BookingWithRelations {
  session_id: Session;
  user_id: User;
}

Shared Utilities

shared/src/utils/pricing.ts

import type { SessionType } from '../types';

export function calculateSessionPrice(params: {
  duration: number;
  type: SessionType;
  memberTier?: 'basic' | 'premium';
}): number {
  const basePrice = params.type === 'premium' ? 150 : 100;
  const pricePerMinute = basePrice / 60;
  const totalPrice = pricePerMinute * params.duration;

  if (params.memberTier === 'premium') {
    return totalPrice * 0.8; // 20% discount
  }

  return totalPrice;
}

shared/src/utils/validation.ts

export function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

export function isValidDuration(duration: number): boolean {
  return duration >= 30 && duration <= 180 && duration % 15 === 0;
}

Future: Automated Type Generation

Types are defined manually to match the Drizzle schema in backend/src/db/schema/.

Future workflow:

  1. Define tables in backend/src/db/schema/
  2. Run yarn generate:types
  3. Types automatically generated in shared/src/types/generated/
  4. Import and use in all packages

Benefits:

  • Always in sync with database schema
  • No manual maintenance
  • Catch breaking changes early

Troubleshooting

Type Errors After Updating Shared Package

# Rebuild shared package
yarn workspace @coaching-app/shared build

# Reinstall dependencies
yarn install

# Clear TypeScript cache
rm -rf */tsconfig.tsbuildinfo

# Type check all packages
yarn type-check

Cannot Import from '@coaching-app/shared'

Check package.json:

{
  "name": "@coaching-app/shared",
  "main": "src/index.ts",
  "types": "src/index.ts"
}

Check tsconfig.json paths:

{
  "compilerOptions": {
    "paths": {
      "@coaching-app/shared": ["../shared/src"],
      "@coaching-app/shared/*": ["../shared/src/*"]
    }
  }
}

Type Mismatches

Ensure TypeScript types match PostgreSQL / Drizzle column types:

Drizzle Column TypeScript Type
String string
Text string
Integer number
Float number
Boolean boolean
DateTime string (ISO 8601)
UUID string
JSON Record<string, any> or specific interface
Many-to-One string (ID) or object
One-to-Many string[] or object[]

Resources