Chapter 02

TypeScript 与 ES 模块

原生 TypeScript、零配置运行、ES 模块导入与 import maps 全解析

原生 TypeScript:无需任何配置

Deno 最广受好评的特性之一:直接运行 .ts 文件,无需安装 ts-node、无需 tsconfig.json、无需任何预处理步骤。

Node.js — 需要配置
# 安装依赖
npm install -D typescript ts-node
# 或安装 tsx
npm install -D tsx

# 创建 tsconfig.json
npx tsc --init

# 运行
npx tsx server.ts
Deno 2 — 开箱即用
# 无需任何安装
# 无需 tsconfig.json
# 直接运行
deno run app.ts

# 带权限运行
deno run --allow-net app.ts

Deno 的 TypeScript 是"类型擦除":Deno 运行 TypeScript 时仅剥离类型注解,不进行类型检查。类型错误不会阻止程序运行。你需要单独运行 deno check app.ts 来执行类型检查(等同于 tsc --noEmit)。

deno.json 中的 TypeScript 配置

Deno 通过 deno.jsoncompilerOptions 字段来配置 TypeScript,与 tsconfig.json 的字段基本一致:

// deno.json
{
  "compilerOptions": {
    "strict": true,              // 严格模式(推荐)
    "noImplicitAny": true,      // 禁止隐式 any
    "strictNullChecks": true,   // 严格 null 检查
    "lib": ["deno.window"],       // 包含 Deno 全局 API 类型
    "jsx": "react-jsx",           // JSX 支持(用于 Fresh)
    "jsxImportSource": "preact"  // JSX 运行时
  }
}

lib 选项说明

lib 值适用场景
deno.window默认,Deno 主线程(包含 Deno 全局、Web API)
deno.workerDeno Web Worker 环境
deno.ns仅 Deno 命名空间 API(不含 Web API)
dom浏览器 DOM API(当你写前端代码时)

ES 模块:Deno 的模块系统

Deno 从一开始就只支持 ES 模块(ESM),没有 CommonJS。这是 Deno 与 Node.js 最显著的设计差异之一。

四种导入方式

1. 相对路径导入(必须带扩展名)

// main.ts
import { add } from "./math.ts";  // 必须写 .ts 扩展名
import type { Config } from "./types.ts";

console.log(add(1, 2));

扩展名是必须的:Node.js 允许省略扩展名(require('./math')),但 Deno 严格遵循 Web 标准,必须写完整扩展名。这使模块解析更明确,也与浏览器行为一致。

2. URL 导入(远程模块)

// 从 URL 直接导入(Deno 1.x 风格,仍然支持)
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";

// Deno 会自动下载、缓存模块
// 缓存位置:~/.cache/deno/deps/

3. npm: 前缀(npm 包)

// 使用 npm 包,无需 npm install
import { Hono } from "npm:hono@4";
import express from "npm:express@4";
import { z } from "npm:zod@3";

// Deno 会自动解析 npm 包,无需 node_modules

4. node: 前缀(Node.js 内置模块)

// 兼容 Node.js 内置模块
import { readFileSync } from "node:fs";
import path from "node:path";
import { EventEmitter } from "node:events";
import { createHash } from "node:crypto";

// Deno 2 完整实现了 Node.js 兼容层

5. jsr: 前缀(JSR 注册表)

// 从 JSR 导入(推荐方式)
import { assertEquals } from "jsr:@std/assert@^1.0.0";
import { join } from "jsr:@std/path@^1.0.0";
import { walk } from "jsr:@std/fs@^1.0.0";

import maps:统一管理依赖

import maps 让你为长 URL 或版本号定义别名,统一管理所有依赖版本。在 deno.jsonimports 字段定义:

// deno.json
{
  "imports": {
    // JSR 包
    "@std/assert": "jsr:@std/assert@^1.0.0",
    "@std/path":   "jsr:@std/path@^1.0.0",
    "@std/fs":     "jsr:@std/fs@^1.0.0",

    // npm 包
    "hono":        "npm:hono@^4.0.0",
    "zod":         "npm:zod@^3.22.0",

    // 路径别名(目录映射需以 / 结尾)
    "@/":          "./src/"
  }
}
// 代码中使用别名,不再需要写版本号
import { assertEquals } from "@std/assert";
import { Hono } from "hono";
import { UserService } from "@/services/user.ts";

顶层 await

Deno 原生支持顶层 await(Top-level await),无需将异步代码包裹在 async 函数中:

// fetch-data.ts — 顶层 await,无需 async 包装
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users: Array<{ id: number; name: string }> = await response.json();

console.log(`获取到 ${users.length} 位用户:`);
for (const user of users.slice(0, 3)) {
  console.log(`  - ${user.name}`);
}

// 运行:deno run --allow-net fetch-data.ts

实战:无需编译的 TypeScript 脚本

下面用一个完整例子展示 Deno TypeScript 脚本的能力:读取 CSV、处理数据、输出报告。

// report.ts — 零依赖、原生 TypeScript 脚本
import { parse } from "jsr:@std/csv@^1.0.0";

interface SalesRecord {
  date: string;
  product: string;
  amount: number;
}

// 顶层 await 读取文件
const csvText = await Deno.readTextFile("./sales.csv");
const records = parse(csvText, {
  skipFirstRow: true,
  columns: ["date", "product", "amount"],
}) as SalesRecord[];

// 按产品聚合销售额
const totals = records.reduce((acc, r) => {
  acc[r.product] = (acc[r.product] ?? 0) + Number(r.amount);
  return acc;
}, {} as Record<string, number>);

// 排序输出
const sorted = Object.entries(totals)
  .sort(([, a], [, b]) => b - a);

console.log("=== 销售报告 ===");
for (const [product, total] of sorted) {
  console.log(`${product.padEnd(20)} ¥${total.toLocaleString()}`);
}

// deno run --allow-read report.ts

本章小结:Deno 的 TypeScript 支持是开箱即用的——不需要 tsconfig.json,不需要 ts-node,直接 deno run xxx.ts。模块系统完全基于 ES 模块标准,支持 URL、jsr:、npm:、node: 四种导入方式。import maps 统一管理依赖版本。顶层 await 让异步脚本写起来如呼吸般自然。