How to share TS types between frontend and backend when both lives on different repos?

# The Type Sync Struggle: Sharing TypeScript Types Across Repositories

## Summary
A recurring challenge emerged in multiple projects when attempting to share TypeScript types between independently versioned frontend/backend repositories. Custom-built solutions using tooling like TypeOwl provided partial relief through HTTP-based type synchronization, but introduced DX friction by requiring manual schema declarations and diverging from natural coding patterns.

## Root Cause
* **Fundamental decoupling**: Complete separation of concerns between frontend/backend repos breaks type system assumptions
* **Compiler limitations**: TypeScript's type system operates per-project without native cross-repo awareness
* **Architecture gaps**: Common solutions require tight deployment coupling that contradicts repo separation requirements
* **Abstraction leakage**: Generated types often fail to preserve contextual metadata (validation rules, endpoint semantics)

## Why This Happens in Real Systems
* Independent team velocity requires separate version control workflows
* Infrastructure constraints enforce deployment isolation (e.g. serverless vs static hosting)
* Different release cycles create version drift between interfaces
* Monorepos introduce scaling/complexity tradeoffs that become unacceptable at larger orgs
* Generated types often lose subtle behavioral contracts (auth rules, error formats)

## Real-World Impact
* Interface mismatch bugs caught late in development cycle
* Manual synchronization overhead doubling type maintenance
* Fragile deployments causing runtime schema discrepancies
* Documentation drift between actual implementation and types
* Velocity reduction due to constant manual validation layers

## Example Code
The TypeOwl approach requiring explicit schema declaration:
```typescript
// server/types.ts
export const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email()
});

// client/api.ts
import { UserSchema } from "@server/types";

const response = await fetch("/api/user");
const data = await response.json();
const parsed = UserSchema.parse(data); // Manual validation needed 👇
// Ideal: Direct `User` type without dual declarations

How Senior Engineers Fix It

  • Artifact publishing: Versioned type packages via private NPM registry
  • Contract-first APIs: Generate types/swagger from OpenAPI specs
  • RPC refinement: Extend tRPC/ts-rest with custom serialization layers
  • Metaprogramming: Create mono-directional type flow:
    1. Backend exports runtime-validated endpoints via tsup bundle
    2. Dedicated CLI extracts inline TS types into .d.ts artefact
    3. Frontend consumes via version-pinned artifact imports
  • Architecture shift: Hybrid monorepo with selective repos (shared types via pnpm workspaces)
  • Automated sync: Git hooks that enforce type compatibility during PRs

Why Juniors Miss It

  • Expect compile-time magic without runtime implications
  • Underestimate versioning complexity in distributed systems
  • Confuse type safety with data validation requirements
  • Presume framework conventions over explicit contracts
  • Focus on local DX rather than cross-team compatibility constraints