Shared Types Guide¶
This guide explains how to manage shared TypeScript types across the Coaching App packages.
Table of Contents¶
- Overview
- Package Structure
- Defining Types
- Using Shared Types
- Type Generation Workflow
- Best Practices
- Future: Automated Type Generation
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;
}
Related Types¶
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¶
- Update the Drizzle schema in
backend/src/db/schema/ - Create a matching shared type in
shared/src/types/ - Export from index
- Use in packages
- Run type check across all workspaces
Keeping Types in Sync¶
When adding a field:
- Add the column to the Drizzle schema in
backend/src/db/schema/ - Run
pnpm db:generate && pnpm db:migrate - Update the corresponding shared type in
shared/src/types/ - Run
pnpm type-checkand fix any type errors
When removing a field:
- Remove from Drizzle schema and create a migration
- Remove from the shared TypeScript type
- 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:
- Define tables in
backend/src/db/schema/ - Run
yarn generate:types - Types automatically generated in
shared/src/types/generated/ - 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:
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[] |