denoland/deno
Type checking
Active contributors: David Sherret, Nayeem Rahman, Bartek Iwańczuk
Purpose
Deno can type-check TypeScript before running it (deno check, deno run --check, the LSP, deno test's default check, …). Two implementations live side by side:
- The bundled
tsc— a snapshotted copy of the official TypeScript compiler running inside a dedicated JS isolate. tsgo— the newer TypeScript-Go rewrite, run as an external subprocess and spoken to over IPC.
The orchestration layer in cli/type_checker.rs (1,138 lines) and cli/tsc.rs decides which to use, marshals the module graph in, and surfaces diagnostics.
Directory layout
cli/
├── type_checker.rs # 1,138 lines: top-level orchestrator
├── tsc.rs # bundled tsc driver (snapshot + IPC)
├── tsc/ # the bundled tsc JS sources (snapshotted)
│ ├── 00_typescript.js # the literal TypeScript compiler bundle
│ └── …
├── lsp/tsc.rs # 7,137 lines: LSP-specific tsc bridge
└── lsp/ts_server.rs # tsc subprocess plumbing for the LSP
libs/typescript_go_client/ # IPC client for tsgo
└── lib.rs # protocol + runnerTwo paths
Path A — bundled tsc
cli/tsc/00_typescript.js is a snapshotted bundle of the official TypeScript compiler. At build time, cli/snapshot/ runs it through V8's snapshot machinery so that deno check doesn't have to re-parse the compiler at startup.
When type-checking, the CLI:
- Builds the module graph (see Module loading).
- Hands the graph to a child
JsRuntimerunning the snapshottedtsc. - Configures
tscto read source via custom callbacks (so it sees the graph the CLI built rather than going to disk). - Collects diagnostics, formats them, and exits with non-zero on errors.
This is the long-standing default. The driver is cli/tsc.rs; the LSP-specific equivalent is cli/lsp/tsc.rs (which uses the same compiler bundle but with editor-style request/response, including incremental updates).
Path B — tsgo
tsgo is the TypeScript team's Go rewrite of tsc. It runs as an out-of-process subprocess. The crate libs/typescript_go_client (libs/typescript_go_client/lib.rs) implements the IPC protocol:
- Spawn the
tsgobinary - Send the module graph + compiler options over stdio (length-prefixed JSON or similar)
- Stream diagnostics back
Adopting tsgo is incremental — at this snapshot of the codebase both paths are present and selectable. The motivation is that Go-tsc is dramatically faster for cold-start type checks.
Key abstractions
| Type | File | Role |
|---|---|---|
TypeChecker |
cli/type_checker.rs |
Top-level orchestrator; chooses tsc vs tsgo, builds the graph, formats diagnostics |
TypeCheckCache |
cli/type_checker.rs + cli/cache/ |
Keys on graph hash so repeat checks don't re-run tsc |
Tsc (in cli/tsc.rs) |
cli/tsc.rs |
Bundled-tsc driver: spawns the embedded JS isolate, calls into the snapshot |
TsServer (in cli/lsp/tsc.rs) |
cli/lsp/tsc.rs |
LSP-specific bridge with incremental updates |
TsgoClient (concept) |
libs/typescript_go_client/lib.rs |
IPC client for the tsgo subprocess |
Diagnostic |
TypeScript's own diagnostic type | Marshalled across the boundary; surfaced as deno errors |
How it works
graph TD
Cmd["deno check / deno run --check / LSP"] --> TC["TypeChecker<br/>cli/type_checker.rs"]
TC --> Graph["ModuleGraph<br/>(cli/graph_util.rs)"]
TC --> Decide{"tsc or tsgo?"}
Decide -->|tsc| TscDriver["cli/tsc.rs"]
TscDriver --> Snapshot["bundled tsc snapshot<br/>cli/tsc/00_typescript.js"]
Snapshot --> TscDiag["TS Diagnostic[]"]
Decide -->|tsgo| TsgoIpc["libs/typescript_go_client"]
TsgoIpc --> TsgoProc["tsgo subprocess"]
TsgoProc --> TsgoDiag["TS Diagnostic[]"]
TscDiag --> TC
TsgoDiag --> TC
TC --> Cache["TypeCheckCache<br/>cli/cache/"]
TC --> Format["formatted diagnostics"]
Format --> User["stderr / LSP publishDiagnostics"]Caching
Type checking is expensive. The CLI caches results keyed on a hash of:
- The module graph (every module's content hash)
- Compiler options (from
deno.jsoncompilerOptionsand CLI flags) - TypeScript /
tsgoversion
If the cache key matches, deno check exits in milliseconds without invoking the compiler. The cache lives under DENO_DIR/check/.
Updating the bundled TypeScript
Updating cli/tsc/00_typescript.js is a deliberate process documented in tools/update_typescript.md. Steps include:
- Replace the bundled
00_typescript.jsfrom a fresh build of TypeScript. - Update
cli/tsc/dts/if the lib*.d.tsfiles moved. - Re-run the snapshot generator (
cli/snapshot/). - Update spec tests for any output format changes (TypeScript's diagnostic messages do drift between minor versions).
Integration points
- CLI:
deno check→cli/tools/check.rs→TypeChecker(cli/type_checker.rs). - Run with check:
cli/tools/run/callsTypeCheckerbefore evaluation when--checkis on. - LSP: uses
cli/lsp/tsc.rsfor editor-friendly incremental checking; the runner-level type checker isn't used because the LSP needs its own caching strategy. - Compile / Bundle:
deno compileanddeno bundleinvoke the type checker before serializing the graph.
Entry points for modification
- Diagnostic message changes — typically come from TypeScript itself; changes to formatting live in
cli/type_checker.rs. - A new compiler option — add it to the CLI's
CheckFlags(cli/args/flags.rs), thread throughcli/type_checker.rs, and pass to whichever driver is selected. - Tsgo IPC protocol —
libs/typescript_go_client/lib.rs(and the matching server side, which lives outside this repo). - Cache invalidation —
cli/cache/for the disk format;TypeCheckCacheincli/type_checker.rsfor the keying logic.
Key source files
| File | Purpose |
|---|---|
cli/type_checker.rs |
Top-level orchestrator (1,138 lines) |
cli/tsc.rs |
Bundled-tsc driver (snapshot + IPC) |
cli/tsc/ |
The bundled tsc JS bundle and dts files |
cli/lsp/tsc.rs |
LSP-specific tsc bridge (7,137 lines) |
cli/lsp/ts_server.rs |
tsc subprocess plumbing |
cli/cache/ |
On-disk cache for type-check results |
libs/typescript_go_client/ |
tsgo IPC client crate |
tools/update_typescript.md |
Manual update procedure |
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.