How TypeOwl Works

A deep dive into the engine behind type synchronization

TypeOwl Architecture
Backend
src/types/*.ts
type User = {...}
type Blog = {...}
TypeOwl Extractor
• Parse AST
• Extract types
• Generate .d.ts
/__typeowl
manifest.json
types/*.d.ts
HTTP
(dev, CI/CD, or on-demand)
Frontend
typeowl.config.ts
resolvers: [...]
output: .typeowl
TypeOwl Client
• Fetch manifest
• Compare hashes
• Download changed
.typeowl/
index.d.ts
content.d.ts
endpoints.d.ts

Step 1: Type Extraction

TypeOwl parses your TypeScript files using the TypeScript compiler API to extract type definitions. This happens at runtime when the server starts, not at build time.

src/types/User.ts (your code)
export type User = { id: string; email: string; name: string; role: 'admin' | 'user' | 'guest'; };

The extractor walks the AST and converts type nodes into an internal representation:

Internal TypeDefinition
{ kind: 'object', properties: { id: { type: { kind: 'primitive', value: 'string' } }, email: { type: { kind: 'primitive', value: 'string' } }, name: { type: { kind: 'primitive', value: 'string' } }, role: { type: { kind: 'union', types: [ { kind: 'literal', value: 'admin' }, { kind: 'literal', value: 'user' }, { kind: 'literal', value: 'guest' }, ] } }, } }

Step 2: Manifest Generation

TypeOwl generates a lightweight JSON manifest that describes all available type files. Each file is identified by a content hash for efficient change detection.

/__typeowl/manifest.json
{ "manifestVersion": "1.0.0", "version": "1.0.0", "generatedAt": "2024-01-15T10:30:00Z", "files": { "content": { "path": "/__typeowl/types/content.d.ts", "hash": "a1b2c3d4", // MD5 hash of file content "exports": ["User", "Blog", "Product"] }, "endpoints": { "path": "/__typeowl/types/endpoints.d.ts", "hash": "e5f6g7h8", "exports": ["ApiEndpoints"] } }, "endpoints": { "GET /api/users": { "response": "User[]" }, "POST /api/users": { "body": "CreateUser", "response": "User" } } }

Step 3: Hash Comparison (Incremental Sync)

This is where TypeOwl shines. Instead of downloading all type files every time, the client compares hashes to detect changes:

1

Fetch Manifest

Client fetches the small manifest JSON (~1KB) from /__typeowl

2

Compare Hashes

Compare each file's hash with locally cached hashes from previous sync

3

Download Changed Only

Only fetch files where the hash differs — skip unchanged files entirely

4

Update Cache

Save new hashes to local cache for next comparison

Without Hash Comparison

  • Download all 10 type files every sync
  • ~50KB transferred each time
  • Slow watch mode (constant re-downloads)
  • Wastes bandwidth and time

With Hash Comparison

  • Download only 1 changed file
  • ~1KB manifest + 5KB changed file
  • Instant sync when nothing changed
  • Efficient watch mode
Hash comparison algorithm
function getChangedFiles(manifest, cachedManifest) { const changed = []; for (const [domain, fileRef] of Object.entries(manifest.files)) { const cachedHash = cachedManifest.localHashes[domain]; if (cachedHash !== fileRef.hash) { changed.push(domain); // Hash mismatch = file changed } } return changed; }

Step 4: TypeChecker Extraction

TypeOwl uses the TypeScript Compiler API (TypeChecker) to extract types from your code:

1. Route Scanning Build Time

TypeChecker scans your route definitions and extracts generic type arguments:

// Your code const getUsers = route.get('/api/users').returns<User[]>(); // TypeChecker extracts: // - Path: '/api/users' // - Method: GET // - Response type: User[]

2. Type Resolution Build Time

Referenced types are resolved and extracted with their full definitions:

// TypeChecker follows type references interface User { id: string; name: string; role: Role; } type Role = 'admin' | 'user' | 'guest'; // Both User and Role are extracted to content.d.ts

3. Conflict Resolution Build Time

When duplicate type names are found, TypeOwl uses structural comparison:

// Same structure = merged silently // Different structure = handled by onConflict config onConflict: 'error' // Throw error with suggestion onConflict: 'rename' // Auto-rename: User -> User1

Step 5: ApiEndpoints Type Map

TypeOwl automatically generates an ApiEndpoints interface that maps every registered endpoint to its types:

.typeowl/index.d.ts (generated)
export interface ApiEndpoints { 'GET /api/users': { response: User[] }; 'GET /api/users/:id': { params: IdParams; response: User | null }; 'POST /api/users': { body: CreateUserInput; response: User }; 'DELETE /api/users/:id': { params: { id: string }; response: { success: boolean } }; }

This enables building fully typed API clients:

Type-safe API client
// TypeScript knows exactly what each endpoint returns! const users = await api.get('/api/users'); // User[] const user = await api.get('/api/users/1'); // User const newUser = await api.post('/api/users', { email: 'alice@example.com', name: 'Alice' }); // User - body is type-checked!

Security Model

TypeOwl includes several security features:

Type Source Restriction

The typeSources config option limits which files can have types extracted. Only files within allowed paths are accessible:

// Only types from ./src/types/ can be extracted typeSources: './src/types/' // Attempting to extract from elsewhere throws an error extractAndRegister('./src/secrets/passwords.ts') // ❌ Error: Type extraction not allowed from this path

Environment Guards

Disable TypeOwl endpoints in production:

guard: { enabled: 'development', // Disabled when NODE_ENV=production apiKey: process.env.TYPEOWL_API_KEY // Optional API key }
When do types get synced? You decide! Sync during development, in CI/CD pipelines, or on-demand before deploying. Once synced and committed, types are baked into your codebase — no runtime dependency on TypeOwl endpoints. This enables independent frontend and backend deploys.

Performance Characteristics

Operation Typical Time Network
Initial sync (10 types) ~200ms ~50KB
Incremental sync (no changes) ~50ms ~1KB
Incremental sync (1 file changed) ~100ms ~6KB
Type extraction (100 types) ~500ms
Hash generation (MD5) ~1ms
Pro Tip Type extraction happens once when the server starts. After that, generated types are cached in memory and served instantly.