> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript Typecheck Performance Guide

> Running tsc --extendedDiagnostics on a cold build reveals:

**Last Updated:** 2026-04-28\
**Applies to:** Encore OS (v0.9.0-alpha, TypeScript 6.0.3, \~8,400 `.ts/.tsx` files)

***

## TL;DR — Numbers

| Scenario                                | Time         | Notes                                         |
| --------------------------------------- | ------------ | --------------------------------------------- |
| **Cold `npm run typecheck`**            | **\~8m 48s** | No `.tsbuildinfo` cache                       |
| Warm `npm run typecheck`                | \~12s        | Incremental cache hits                        |
| CI warm (Turbo cache hit)               | \~230ms      | Full skip via content-addressed cache         |
| **`tsgo` (TypeScript Go preview) cold** | **\~58s**    | 8.5× faster than `tsc`, no incremental needed |
| `tsgo` warm                             | \~3s         |                                               |

***

## Root Cause: What Makes This Codebase Slow

Running `tsc --extendedDiagnostics` on a cold build reveals:

```
Lines of TypeScript:   1,368,584
Files:                    10,721
Symbols:               9,162,192
Types:                 3,518,177
Instantiations:       31,066,718   ← the smoking gun
Memory used:           7,220,011K  ← 7.2 GB
Check time:              485.84s   ← 98% of total runtime
Total time:              494.18s
```

**Parse + Bind is only \~6 seconds. 98% of the time is spent in type checking.**

The 31 million type instantiations drive everything. Here is what causes them, ranked by impact:

### #1: The 107k-line generated Supabase `types.ts` (primary bottleneck)

`src/integrations/supabase/types.ts` is **3.4 MB** and defines a single `Database` type encoding **1,126 tables** × 3 operation shapes (Row/Insert/Update) = **3,378 table-type variants**, all nested inside deeply-parameterized generic helpers (`TablesInsert<T>`, `TablesUpdate<T>`, `Enums<T>`, etc.).

* **672 source files** directly import from it
* **1,866 uses** of `Database['public']['Tables']['...']['Row']` indexing
* **7,426 `.from(tableName)` calls** — every call instantiates the full `Database<>` generic tree against the table-name union

Every time the TypeScript checker encounters `.from('ce_activities')` it must:

1. Resolve `tableName` as `"ce_activities"` against the **1,039-member string union** of all table names
2. Distribute `Database['public']['Tables'][typeof tableName]` across all 1,126 branches
3. Check assignability for every instantiation

This is the classic "large union × heavy generics = exponential work" problem.

### #2: 317 files use type intersections (`&`) instead of interface `extends`

TypeScript's wiki explicitly calls this out: intersections are checked structurally every time (O(N²) in members), while `interface extends` results are cached. Files like `src/cores/gr/types/index.ts` (1,140 lines, 198 exports) frequently define types as `A & B & { ... }`.

### #3: 392 barrel `index.ts` files

Barrel files don't cause *check-time* overhead directly, but they dramatically increase the module graph that TypeScript must resolve and bind before checking begins. The biggest ones (`src/cores/gr/types/index.ts`, `src/cores/rh/types/index.ts`) re-export hundreds of types that may already be inlined elsewhere, forcing the checker to track them across module boundaries repeatedly.

### #4: Tests mixed into the Turbo input glob

`turbo.json` lists `tests/**/*.ts` and `tests/**/*.tsx` as inputs to the typecheck task, but `tsconfig.app.json` only `include`s `src/`. This means Turbo correctly invalidates its cache when test files change — even though `tsc` never actually type-checks them in the app project. A stray test file (`tests/utils/supabase-test-client.ts`) is included because it is transitively reachable from a `src/` import via path resolution.

### #5: No strict mode / inferred return types

With `strict: false` and `noImplicitAny: false`, TypeScript must infer return types for almost every function. Complex functions that return `supabase.from(...)` chains have inferred return types that cascade into many re-instantiations of the Database generics.

***

## Recommendations — Prioritized

### Priority 1 — Immediate (hours of effort, high impact)

#### 1a. Migrate to `tsgo` (TypeScript 7 Go native preview) for local dev and pre-commit

The TypeScript team has ported the compiler to Go (`@typescript/native-preview`). Tested on this exact codebase:

|             | Time     |
| ----------- | -------- |
| `tsc` cold  | 8m 48s   |
| `tsgo` cold | **58s**  |
| `tsgo` warm | **\~3s** |

This is a **pre-release CLI** (`7.0.0-dev.*`) and should **not** replace `tsc` in the authoritative CI gate yet — `tsgo` currently has minor compatibility gaps. However, it is safe to offer it as an opt-in developer speedup:

```jsonc theme={null}
// package.json — add alongside existing scripts
"typecheck:fast": "tsgo --noEmit --project tsconfig.app.json",
```

Install as an optional devDependency:

```bash theme={null}
npm install --save-dev --save-optional @typescript/native-preview
```

Update the pre-commit hook to use `typecheck:fast` for the interactive developer workflow, keeping `typecheck` (plain `tsc`) in CI. This alone cuts pre-commit wait time from \~12s (warm `tsc`) to \~3s.

**Risk:** `tsgo` may miss a small number of errors that `tsc` catches. CI must keep `tsc` as the authoritative gate. As `tsgo` reaches stability (expected TypeScript 7.0 stable), flip CI to `tsgo` as well.

#### 1b. Add `verbatimModuleSyntax: true` to `tsconfig.app.json`

This flag makes all type-only imports require `import type`, which:

* Eliminates the compiler's need to determine at emit time whether an import is value or type
* Reduces the number of files that must be "checked together" vs checked in isolation
* Is already enforced by the Biome linter rule `useImportType` — this makes it a compiler-enforced invariant too

```jsonc theme={null}
// tsconfig.app.json compilerOptions
"verbatimModuleSyntax": true,
```

**Compatibility note:** `allowJs: true` (set in root `tsconfig.json`) is incompatible with `verbatimModuleSyntax` in some edge cases. Since `tsconfig.app.json` does not enable `allowJs`, this is safe.

#### 1c. Narrow the Turbo `typecheck` input glob to exclude test files

Currently `turbo.json` lists `tests/**/*.ts` and `tests/**/*.tsx` as inputs, which means any test file change invalidates Turbo's typecheck cache even though `tsconfig.app.json` only checks `src/`. Change it:

```json theme={null}
"inputs": [
  "src/**/*.ts",
  "src/**/*.tsx",
  "tsconfig.json",
  "tsconfig.app.json",
  "tsconfig.node.json"
]
```

The `tests/**` globs that were previously listed have been removed. `turbo.json` does not support JSON comments, so this note lives here instead. This allows Turbo to skip the typecheck entirely on PRs that only touch test files, which is a common pattern.

***

### Priority 2 — Medium (days of effort, very high long-term impact)

#### 2a. Split `src/integrations/supabase/types.ts` into per-core type modules

The 107k-line monolithic `types.ts` is the single biggest driver of the 31M instantiations. Each `Database['public']['Tables']['x']` access forces TypeScript to evaluate every table branch.

**Recommended approach:**

1. Generate per-core type slice files (e.g., `src/integrations/supabase/types.ce.ts`, `types.hr.ts`, etc.) containing only the tables owned by each core.
2. Each core imports from its own slice file instead of the monolithic one.
3. Reduce the `Database` type to a minimal "plumbing" type used only by the Supabase client itself.

A custom code-generation step can produce these from the Supabase schema:

```bash theme={null}
# Example: generate per-core type slices
npx supabase gen types typescript --local > src/integrations/supabase/types.ts
npx tsx scripts/database/split-types-by-core.ts  # new script to be authored
```

**Expected impact:** Reduces union breadth from 1,126 tables to \~50–150 tables per core, cutting instantiation count by 7-15×.

#### 2b. Replace `type X = A & B` patterns with `interface X extends A, B`

In files like `src/cores/gr/types/index.ts`, `src/cores/rh/types/index.ts`, and `src/platform/permissions/constants.ts`, convert intersection types to interface extension wherever possible:

```typescript theme={null}
// Before (slow — re-checked structurally every time)
type UserWithPermissions = User & { permissions: Permission[] };

// After (fast — result cached after first check)
interface UserWithPermissions extends User {
  permissions: Permission[];
}
```

Run a codemod to identify candidates:

```bash theme={null}
npx ts-migrate-types --pattern "type * = * & *" src/
```

Focus first on the largest type definition files (by line count) as those have the highest payoff.

#### 2c. Add explicit return type annotations to complex functions

Functions that call `supabase.from(...)` chains without return type annotations force TypeScript to re-infer the full generic return type on every call site. Adding explicit `Promise<SomeRow[]>` or `PostgrestQueryBuilder<...>` annotations breaks the inference chain:

```typescript theme={null}
// Before: inferred return type triggers 31M instantiations
async function getUsers(orgId: string) {
  return supabase.from('pf_users').select('*').eq('organization_id', orgId);
}

// After: explicit type — checker stops at the annotation
async function getUsers(orgId: string): Promise<{ data: PfUser[] | null; error: PostgrestError | null }> {
  return supabase.from('pf_users').select('*').eq('organization_id', orgId);
}
```

The `docs:comments:audit:changed` script already enforces TSDoc on exported APIs. This is the same principle applied to return types of functions that interact with the Supabase client.

***

### Priority 3 — Architectural (weeks of effort, maximum long-term impact)

#### 3a. TypeScript Project References with `composite: true`

Break the monolithic `tsconfig.app.json` into per-core projects that reference each other. This is the TypeScript team's recommended approach for large codebases.

```
tsconfig.json (solution file — references all)
├── tsconfig.platform.json  (src/platform/**)
├── tsconfig.core-hr.json   (src/cores/hr/**)
├── tsconfig.core-fa.json   (src/cores/fa/**)
... (one per core)
└── tsconfig.routes.json    (src/routes/**)
```

With `tsc -b` (build mode):

* Each sub-project produces a `.d.ts` declaration file (not the full source types)
* Downstream projects check against the **declaration**, not the full source
* Changed files only re-check their sub-project and any downstream projects that directly depend on them

**Constraints for this codebase:**

* `composite: true` requires `declaration: true`, which is incompatible with `noEmit: true` — you would need to emit `.d.ts` files into a temp location (e.g., `node_modules/.cache/tsc-declarations/`)
* The existing architecture enforcement (no core-to-core imports) maps cleanly to project reference boundaries
* Estimated setup effort: 2–3 days to author the per-core configs and validate the dependency graph
* Expected outcome: `tsc -b --incremental` cold time drops from \~9min to \~1–2min; warm stays \~10s

This is the highest-effort but most structurally sound long-term fix.

#### 3b. Migrate to `tsgo` (TypeScript 7 Go) as the primary CI type-checker

Once TypeScript 7.0 stable is released (expected late 2026), migrate the `typecheck` CI step from `tsc` to `tsgo`. As demonstrated above, this is a near drop-in replacement that delivers \~8-10× speedup with no code changes.

Track readiness at: [https://github.com/microsoft/typescript-go](https://github.com/microsoft/typescript-go)

***

## Quick-Win Configuration Changes (Apply Now)

These changes can be made immediately with **zero risk**:

### 1. Add `typecheck:fast` script using `tsgo`

```jsonc theme={null}
// package.json
"typecheck:fast": "tsgo --noEmit --project tsconfig.app.json",
```

### 2. Add `verbatimModuleSyntax` to `tsconfig.app.json`

```jsonc theme={null}
// tsconfig.app.json
"verbatimModuleSyntax": true,
```

### 3. Fix the Turbo input glob for typecheck

```jsonc theme={null}
// turbo.json — "//#typecheck"
"inputs": [
  "src/**/*.ts",
  "src/**/*.tsx",
  "tsconfig.json",
  "tsconfig.app.json",
  "tsconfig.node.json"
]
```

### 4. Pre-commit hook: use `typecheck:fast` for developer iteration

In `.husky/pre-commit` or the lint-staged config, replace `npm run typecheck` with `npm run typecheck:fast` for the interactive pre-commit check. Keep `npm run typecheck` in CI.

***

## Measurement Commands

Use these to track progress:

```bash theme={null}
# Cold typecheck baseline (delete cache first)
rm -rf node_modules/.cache/tsc
time npm run typecheck

# Cold tsgo baseline
TSGO=/path/to/tsgo
time $TSGO --noEmit --project tsconfig.app.json

# Extended diagnostics (shows instantiation count)
NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit -p tsconfig.app.json --extendedDiagnostics

# Performance trace (open in Perfetto: https://ui.perfetto.dev)
NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit -p tsconfig.app.json --generateTrace /tmp/tsc-trace
npx @typescript/analyze-trace /tmp/tsc-trace

# Warm incremental check
time npm run typecheck

# File count in compilation
npx tsc --listFilesOnly -p tsconfig.app.json | wc -l
```

***

## Summary Table

| Recommendation                        | Effort   | Impact                                | Risk                          |
| ------------------------------------- | -------- | ------------------------------------- | ----------------------------- |
| Add `typecheck:fast` with `tsgo`      | 1 hour   | ★★★★★ — 8.5× cold speedup             | Low — CI keeps `tsc`          |
| Fix Turbo input glob                  | 30 min   | ★★★ — skip on test-only PRs           | None                          |
| Add `verbatimModuleSyntax`            | 1 hour   | ★★ — reduces redundant checking       | Low                           |
| Split `types.ts` by core              | 2–3 days | ★★★★★ — eliminates 31M instantiations | Medium — requires gen tooling |
| Convert `&` to `extends`              | 1–2 days | ★★★ — caches intersection results     | Low                           |
| Explicit return types on Supabase fns | 2–3 days | ★★★★ — breaks inference cascade       | Low                           |
| Full project references               | 1 week   | ★★★★★ — structural + incremental      | High — large config change    |
| Upgrade to `tsgo`/TS7 in CI           | Future   | ★★★★★ — 10× with no code changes      | Wait for stable               |

***

## References

* [TypeScript Performance Wiki](https://github.com/microsoft/TypeScript/wiki/Performance)
* [TypeScript Go (tsgo)](https://github.com/microsoft/typescript-go)
* [`@typescript/native-preview` on npm](https://www.npmjs.com/package/@typescript/native-preview)
* [TypeScript Project References](https://www.typescriptlang.org/docs/handbook/project-references.html)
* [Supabase type generation docs](https://supabase.com/docs/guides/api/rest/generating-types)
