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.
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:
{
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.
{
"manifestVersion": "1.0.0",
"version": "1.0.0",
"generatedAt": "2024-01-15T10:30:00Z",
"files": {
"content": {
"path": "/__typeowl/types/content.d.ts",
"hash": "a1b2c3d4",
"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
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);
}
}
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:
const getUsers = route.get('/api/users').returns<User[]>();
2. Type Resolution
Build Time
Referenced types are resolved and extracted with their full definitions:
interface User { id: string; name: string; role: Role; }
type Role = 'admin' | 'user' | 'guest';
3. Conflict Resolution
Build Time
When duplicate type names are found, TypeOwl uses structural comparison:
onConflict: 'error'
onConflict: 'rename'
Step 5: ApiEndpoints Type Map
TypeOwl automatically generates an ApiEndpoints interface that maps every
registered endpoint to its types:
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:
const users = await api.get('/api/users');
const user = await api.get('/api/users/1');
const newUser = await api.post('/api/users', {
email: 'alice@example.com',
name: 'Alice'
});
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:
typeSources: './src/types/'
extractAndRegister('./src/secrets/passwords.ts')
Environment Guards
Disable TypeOwl endpoints in production:
guard: {
enabled: 'development',
apiKey: process.env.TYPEOWL_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.