Deno-First Type Checking: Why typeCheck false in dnt Isn't What You Think

deno typescript monorepo

TL;DR: In a Deno-first monorepo, disabling type checking in dnt (Deno to Node Transform) isn't a type safety gapβ€”it's a deliberate optimization. The real type checking happens at the Deno layer, not the npm build layer.


The Confusion

When auditing a Deno-first monorepo, you might encounter this pattern in build_npm.ts files:

await build({
  typeCheck: false, // 😱 Is this disabling type safety?
  // ...
});

Seeing typeCheck: false across multiple packages can trigger alarm bells:

  • "Are we shipping untyped code?"
  • "How are we catching type errors?"
  • "Is this technical debt?"

The answer requires understanding the two-layer type checking architecture in a Deno-first repo.


The Two-Layer Architecture

Layer 1: Deno Type Checking (Source of Truth)

In a Deno-first monorepo, each package has a Moon task for type checking:

# packages/core/moon.yml
tasks:
  typecheck:
    command: deno
    args:
      - check
      - src/**/*.ts

This runs Deno's native TypeScript checker with:

  • Full strict: true mode (configured in deno.json)
  • Access to Deno's type definitions
  • Resolution via import maps

This is where type errors are caught.

Layer 2: dnt Build (Transformation)

The dnt build step transforms Deno code to Node.js-compatible npm packages:

// packages/core/build_npm.ts
await build({
  entryPoints: ["./mod.ts"],
  outDir: "./npm",
  typeCheck: false,  // Skip redundant re-check
  declaration: true, // Generate .d.ts for Node consumers
  // ...
});

When typeCheck: false, dnt skips running its internal TypeScript compiler for validation, trusting that the source was already validated by deno check.


The Data Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     DEVELOPMENT TIME                             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                  β”‚
β”‚  src/*.ts  ──────►  deno check  ──────►  βœ… Type errors caught  β”‚
β”‚                     (strict: true)                               β”‚
β”‚                                                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       BUILD TIME                                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                  β”‚
β”‚  src/*.ts  ──────►  dnt build  ──────►  npm/                    β”‚
β”‚                     (typeCheck: false)   β”œβ”€β”€ esm/*.js           β”‚
β”‚                                          β”œβ”€β”€ esm/*.d.ts         β”‚
β”‚                                          └── package.json       β”‚
β”‚                                                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    NODE.JS CONSUMPTION                           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                  β”‚
β”‚  Node app  ──────►  imports npm/  ──────►  tsc validates        β”‚
β”‚  (Next.js,          (file: protocol)       against .d.ts        β”‚
β”‚   Trigger)                                                       β”‚
β”‚                                                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Why This Works

1. Deno's Type Checker Is Authoritative

Deno uses a modified version of TypeScript's compiler that:

  • Understands Deno's module resolution
  • Respects import maps
  • Enforces strict mode by default

If deno check passes, the code is type-safe.

2. dnt's Type Checking Is Redundant (Usually)

When dnt runs with typeCheck: true, it:

  1. Compiles the source again using its internal TS compiler
  2. May produce different errors due to Node.js type definitions
  3. Can choke on valid Deno patterns (like Deno.* APIs)

Since the source was already validated, this is redundant work.

3. Build Speed Matters

Disabling type checking in dnt saves 5-15 seconds per package. In a monorepo with 20+ packages, this adds up:

Configuration Build Time
typeCheck: true ~4-5 minutes
typeCheck: false ~1-2 minutes

When typeCheck: false IS a Problem

The optimization becomes a liability when:

1. Declaration Generation Is Disabled

// ❌ PROBLEM: No types for Node consumers
await build({
  typeCheck: false,
  declaration: false,  // ← This is the real issue
});

If declaration: false, Node.js apps importing the package get no type hintsβ€”they're effectively using any.

2. Type Stubs Replace Real Types

// ❌ PROBLEM: Type safety defeated by stubs
const stubTypesContent = `export type AppType = any;`;
Deno.writeTextFileSync(originalTypesPath, stubTypesContent);

Some packages use any stubs during build to work around cross-package type resolution. This defeats the purpose of type safety.

3. Deno Check Isn't Actually Running

If the CI pipeline or pre-commit hooks skip deno check, the assumption breaks:

# ❌ PROBLEM: No type checking at all
build-npm:
  command: deno run -A build_npm.ts
  # No deps on typecheck task!

Fix: Ensure build-npm depends on typecheck:

# βœ… CORRECT: Type check before build
build-npm:
  command: deno run -A build_npm.ts
  deps:
    - ~:typecheck  # Run type check first

Verification Checklist

To ensure your Deno-first repo maintains type safety:

βœ… Deno Type Checking

# Should pass with no errors
moon run :typecheck

βœ… Declaration Generation

// In each build_npm.ts
await build({
  declaration: true,  // Must be true
  // ...
});

βœ… No any Stubs

# Search for type stubs
grep -r "export type.*= any" packages/*/build_npm.ts
# Should return empty or have documented exceptions

βœ… Build Depends on Typecheck

# In moon.yml for packages that produce npm artifacts
build-npm:
  deps:
    - ~:typecheck

The Complete Picture

Layer Tool Purpose Should Fail Build?
Source validation deno check Catch type errors βœ… Yes
npm transformation dnt build Generate Node.js code Only on syntax errors
Node consumption tsc --noEmit Validate against .d.ts βœ… Yes

Common Misunderstandings

"But what if dnt generates wrong types?"

dnt's type generation is mechanicalβ€”it transforms TypeScript to TypeScript. If the source types are correct (validated by deno check), the output types will be correct.

The exception is when using Deno-specific types (like Deno.HttpClient) that don't exist in Node.js. These require shims:

await build({
  shims: {
    deno: true,  // Adds @deno/shim-deno for Deno.* APIs
    crypto: true, // Uses Node's crypto
  },
});

"Shouldn't we check types twice for safety?"

Double-checking sounds safer, but it introduces:

  1. Build time bloat: 2-3x longer builds
  2. False negatives: dnt's checker may pass when Deno's fails
  3. Configuration drift: Two TypeScript configs to maintain

Trust the source of truth (Deno) and validate the output separately (Node apps' tsc).

"What about runtime type errors?"

TypeScript only catches compile-time errors. Runtime type errors (wrong API responses, invalid JSON, etc.) require runtime validation:

import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
});

// Runtime validation
const user = UserSchema.parse(apiResponse);

This is orthogonal to typeCheck: false in dnt.


Recommended Configuration

For Most Packages

// build_npm.ts
await build({
  entryPoints: ["./mod.ts"],
  outDir: "./npm",
  
  // Type checking
  typeCheck: false,   // Trust deno check
  declaration: true,  // Generate .d.ts for consumers
  
  // Shims for Node.js compatibility
  shims: {
    crypto: true,
  },
  
  // Compiler options
  compilerOptions: {
    lib: ["ES2022", "DOM"],
    target: "ES2022",
    skipLibCheck: true,
  },
});

For Packages with Deno APIs

await build({
  // ...
  shims: {
    deno: true,     // Shim Deno.* APIs
    crypto: true,
  },
});

Moon Task Configuration

# moon.yml
tasks:
  typecheck:
    command: deno check mod.ts
    inputs:
      - src/**/*.ts
      - mod.ts
      - deno.json

  build-npm:
    command: deno run -A build_npm.ts
    deps:
      - ~:typecheck  # Enforce type checking before build
    inputs:
      - src/**/*.ts
      - build_npm.ts
    outputs:
      - npm/

Key Takeaways

  1. typeCheck: false in dnt is an optimization, not a gapβ€”if deno check runs first
  2. The real issue is declaration: falseβ€”this removes type hints for Node consumers
  3. Ensure build tasks depend on typecheck tasksβ€”don't skip the source of truth
  4. Watch for any stubsβ€”they defeat type safety silently
  5. Trust the two-layer architectureβ€”Deno validates source, Node apps validate consumption

Further Reading


This article emerged from an audit of a polyglot monorepo where typeCheck: false appeared in multiple packages. Initial concern about type safety gaps led to a deeper understanding of the Deno-first type checking strategy.