How Deno Handles TypeScript Type Checking: A Deep Dive into the Compiler Pipeline
Deno performs TypeScript type checking through an integrated three-stage pipeline that combines module graph resolution with a bundled TypeScript compiler running in a dedicated thread, eliminating the need for external tsc binaries.
Deno’s approach to TypeScript type checking differs fundamentally from Node.js workflows that rely on separate compiler installations. In the denoland/deno repository, the runtime embeds the TypeScript compiler directly and orchestrates type checking through Rust-based module resolution and a JavaScript-based language service. This architecture enables Deno to type-check code while respecting import maps, permissions, and Deno-specific module resolution rules.
The Three-Stage Type Checking Pipeline
Deno’s type checking process follows a strict pipeline defined in cli/graph_container.rs. When you run deno check, the system progresses through three distinct phases before emitting diagnostics.
Stage 1: Collecting Module Specifiers
The process begins with MainModuleGraphContainer::collect_specifiers, which resolves the file patterns you provide into concrete ModuleSpecifier URLs. This function handles glob expansion, respects .gitignore patterns, and applies tsconfig.json include/exclude rules to filter the final list of files requiring type checking.
Stage 2: Loading the Module Graph
Once specifiers are collected, MainModuleGraphContainer::check_specifiers acquires a mutable permit on the global ModuleGraph and invokes ModuleLoadPreparer::prepare_module_load. This step resolves all imports across the dependency tree, fetches remote modules if necessary, and performs permission checks using the container from runtime/deno_permissions.rs. The result is a fully resolved module graph ready for TypeScript compilation.
Stage 3: Running the TypeScript Server
The final stage spawns the TypeScript compiler in a separate thread. Inside prepare_module_load, Deno initializes TsJsServer (defined in cli/lsp/tsc.rs), which creates a communication channel and spawns run_tsc_thread. This thread loads three static JavaScript assets bundled into the binary: 99_main_compiler.js, 97_ts_host.js, and 98_lsp.js (declared in cli/tsc/mod.rs as MAIN_COMPILER_SOURCE, TS_HOST_SOURCE, and LSP_SOURCE).
The TypeScript language service processes GetDiagnostics requests, resolves modules through the op_load host function, and returns structured diagnostic data that the CLI formats and prints.
Key Components of the Type Checking Architecture
Understanding Deno’s type checking requires familiarity with several core files that orchestrate the process.
CLI Entry Point: cli/tools/check.rs
When you execute deno check, the implementation in cli/tools/check.rs parses command-line flags, initializes the CliFactory, and delegates to the graph container’s checking methods. This file bridges user input with the internal type checking pipeline.
Module Graph Container: cli/graph_container.rs
This file houses MainModuleGraphContainer, the central authority for module resolution and type checking. It manages the ModuleGraph state, handles specifier collection, and coordinates between the Rust-based resolution logic and the JavaScript-based TypeScript server.
TypeScript Server Implementation: cli/lsp/tsc.rs
The TsJsServer struct and associated run_tsc_thread function live here. This file implements the request/response protocol (TscRequest, Request) that marshals data between the Rust main thread and the TypeScript compiler thread. It also handles URL normalization for diagnostics.
Static Compiler Assets: cli/tsc/mod.rs
This module embeds the TypeScript compiler as static strings. The MAIN_COMPILER_SOURCE, TS_HOST_SOURCE, and LSP_SOURCE constants contain the bundled JavaScript that powers the type checking service, ensuring Deno operates without external tsc dependencies.
Practical Usage and Code Examples
Command-Line Type Checking
The simplest way to trigger Deno’s type checking pipeline is through the deno check command:
# Type-check specific files
deno check src/main.ts
# Type-check using glob patterns
deno check src/**/*.ts
# Type-check with specific permissions
deno check --allow-read src/main.ts
Programmatic Type Checking in Rust
The following example demonstrates how Deno’s internal APIs orchestrate type checking, mirroring the implementation in cli/tools/check.rs:
use deno_cli::graph_container::MainModuleGraphContainer;
use deno_cli::args::Flags;
use std::sync::Arc;
async fn type_check_files(file_patterns: Vec<String>) -> Result<(), deno_core::error::AnyError> {
// Initialize CLI flags and factory
let flags = Arc::new(Flags::default());
let factory = deno_cli::factory::CliFactory::from_flags(flags);
// Obtain the module graph container
let graph_container = factory.main_module_graph_container().await?;
// Collect module specifiers from file patterns
let specifiers = graph_container
.collect_specifiers(&file_patterns, Default::default())?;
// Execute type checking via the TS server
graph_container
.check_specifiers(&specifiers, Default::default())
.await
}
Internal TypeScript Server Thread
The run_tsc_thread function (invoked internally) initializes the V8 isolate and loads the bundled compiler assets:
fn run_tsc_thread(
request_rx: UnboundedReceiver<Request>,
performance: Arc<Performance>,
specifier_map: Arc<TscSpecifierMap>,
enable_tracing: Arc<AtomicBool>,
) {
// Initialize V8 isolate for the TypeScript compiler
// Load static assets: MAIN_COMPILER_SOURCE, TS_HOST_SOURCE, LSP_SOURCE
// Start event loop processing TscRequest::GetDiagnostics, etc.
}
Summary
Deno handles TypeScript type checking through a sophisticated pipeline that eliminates external compiler dependencies:
- Module Resolution:
MainModuleGraphContainer::collect_specifiersincli/graph_container.rsresolves file patterns into concrete module specifiers while respecting.gitignoreandtsconfig.jsonrules. - Graph Preparation:
check_specifiersacquires a mutable permit on theModuleGraph, resolves dependencies viaModuleLoadPreparer::prepare_module_load, and enforces permissions throughruntime/deno_permissions.rs. - Compiler Execution: A dedicated thread runs the bundled TypeScript compiler (loaded from static assets in
cli/tsc/mod.rs) viaTsJsServerincli/lsp/tsc.rs, processing diagnostics through theop_loadhost function and returning structured results to the CLI.
Frequently Asked Questions
How does Deno type checking differ from running tsc directly?
Deno embeds the TypeScript compiler as static JavaScript assets (99_main_compiler.js, 97_ts_host.js, 98_lsp.js) rather than shelling out to a separate tsc binary. According to the denoland/deno source code, the TsJsServer in cli/lsp/tsc.rs manages the compiler in a dedicated thread, allowing Deno to integrate type checking with its native module resolution, permission system, and import maps—features that standard tsc cannot handle natively.
Can I skip type checking when running Deno scripts?
Yes. By default, Deno does not type-check code during deno run unless explicitly requested with the --check flag. This design choice improves startup performance, as the runtime transpiles TypeScript to JavaScript using SWC without performing full type analysis. When you need strict validation, use deno check to invoke the full pipeline described in cli/tools/check.rs and cli/graph_container.rs.
How does Deno handle tsconfig.json during type checking?
Deno respects tsconfig.json settings during the specifier collection phase. The MainModuleGraphContainer::collect_specifiers function in cli/graph_container.rs parses tsconfig.json to apply include and exclude patterns when expanding file globs. However, Deno uses its own TypeScript compiler assets bundled in cli/tsc/mod.rs, so certain compiler options that assume traditional Node.js module resolution may behave differently or be ignored in favor of Deno’s native resolution logic.
What permissions are required for Deno type checking?
Type checking requires --allow-read permissions to access local source files and potentially --allow-net if your module graph includes remote imports. The ModuleLoadPreparer::prepare_module_load function in cli/graph_container.rs integrates with runtime/deno_permissions.rs to validate these permissions during graph construction. If you run deno check without sufficient permissions, the process fails during the module resolution phase before the TypeScript compiler in cli/lsp/tsc.rs begins its analysis.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →