# How Deno Handles TypeScript Type Checking: A Deep Dive into the Compiler Pipeline

> Discover how Deno handles TypeScript type checking using its efficient three-stage pipeline Integrated compiler and module graph resolution. Learn more.

- Repository: [Deno/deno](https://github.com/denoland/deno)
- Tags: deep-dive
- Published: 2026-02-26

---

**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`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/99_main_compiler.js), [`97_ts_host.js`](https://github.com/denoland/deno/blob/main/97_ts_host.js), and [`98_lsp.js`](https://github.com/denoland/deno/blob/main/98_lsp.js) (declared in [`cli/tsc/mod.rs`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/cli/tools/check.rs)

When you execute `deno check`, the implementation in [`cli/tools/check.rs`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/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:

```bash

# 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`](https://github.com/denoland/deno/blob/main/cli/tools/check.rs):

```rust
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:

```rust
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_specifiers` in [`cli/graph_container.rs`](https://github.com/denoland/deno/blob/main/cli/graph_container.rs) resolves file patterns into concrete module specifiers while respecting `.gitignore` and [`tsconfig.json`](https://github.com/denoland/deno/blob/main/tsconfig.json) rules.
- **Graph Preparation**: `check_specifiers` acquires a mutable permit on the `ModuleGraph`, resolves dependencies via `ModuleLoadPreparer::prepare_module_load`, and enforces permissions through [`runtime/deno_permissions.rs`](https://github.com/denoland/deno/blob/main/runtime/deno_permissions.rs).
- **Compiler Execution**: A dedicated thread runs the bundled TypeScript compiler (loaded from static assets in [`cli/tsc/mod.rs`](https://github.com/denoland/deno/blob/main/cli/tsc/mod.rs)) via `TsJsServer` in [`cli/lsp/tsc.rs`](https://github.com/denoland/deno/blob/main/cli/lsp/tsc.rs), processing diagnostics through the `op_load` host 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`](https://github.com/denoland/deno/blob/main/99_main_compiler.js), [`97_ts_host.js`](https://github.com/denoland/deno/blob/main/97_ts_host.js), [`98_lsp.js`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/cli/tools/check.rs) and [`cli/graph_container.rs`](https://github.com/denoland/deno/blob/main/cli/graph_container.rs).

### How does Deno handle [`tsconfig.json`](https://github.com/denoland/deno/blob/main/tsconfig.json) during type checking?

Deno respects [`tsconfig.json`](https://github.com/denoland/deno/blob/main/tsconfig.json) settings during the specifier collection phase. The `MainModuleGraphContainer::collect_specifiers` function in [`cli/graph_container.rs`](https://github.com/denoland/deno/blob/main/cli/graph_container.rs) parses [`tsconfig.json`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/cli/graph_container.rs) integrates with [`runtime/deno_permissions.rs`](https://github.com/denoland/deno/blob/main/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`](https://github.com/denoland/deno/blob/main/cli/lsp/tsc.rs) begins its analysis.