# 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:
- Backend exports runtime-validated endpoints via
tsup bundle
- Dedicated CLI extracts inline TS types into
.d.ts artefact
- 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