跳转至

第 1 章:TS 环境搭建与基础配置

学习时间:2 小时 | 难度:⭐ 入门 | 前置知识:JavaScript 基础、Node.js 命令行


本章概览

本章将带你从零搭建一个生产级 TypeScript 开发环境,深入理解 tsconfig.json 每个重要选项的含义,并写出你的第一个有意义的 TypeScript 程序。

学习目标:

  • 安装 Node.js 22 + pnpm,理解 tsxtsc 的区别
  • 掌握 tsconfig.json 的核心配置选项
  • 理解 TypeScript 编译流程:.ts → 类型检查 → .js
  • 配置 VS Code 获得最佳开发体验
  • 理解 strict 模式的全部含义

1.1 安装与工具链

Node.js 22 + pnpm

Bash
# 推荐使用 fnm(快速 Node 版本管理器)安装 Node.js
# Windows
winget install Schniz.fnm

# macOS / Linux
curl -fsSL https://fnm.vercel.app/install | bash

# 安装并使用 Node.js 22 LTS(2025 年长期支持版)
fnm install 22
fnm use 22
node --version   # v22.x.x

# 安装 pnpm(比 npm 快 2-3 倍,磁盘占用少 70%)
corepack enable pnpm
pnpm --version   # 9.x.x

创建第一个 TypeScript 项目

Bash
mkdir learn-typescript && cd learn-typescript
pnpm init                          # 生成 package.json

# 安装核心工具链
pnpm add -D typescript             # TypeScript 编译器(类型检查)
pnpm add -D @types/node            # Node.js 类型定义
pnpm add -D tsx                    # 直接运行 .ts 文件(开发时用)

# 生成 tsconfig.json
npx tsc --init

tsx vs tsc:开发与生产的区别

工具 用途 速度 类型检查
tsx 开发时直接运行 .ts 极快(esbuild) ❌ 不检查
tsc 类型检查 + 编译 .js 较慢 ✅ 完整
tsc --noEmit 仅类型检查,不生成文件 中等 ✅ 完整
esbuild 生产构建 极快 ❌ 不检查
Bash
# 开发时:直接运行(快速迭代)
npx tsx src/index.ts

# CI/CD 中:类型检查(发现错误)
npx tsc --noEmit

# 生产构建:esbuild 打包
npx esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js

最佳实践:开发用 tsx,提交前用 tsc --noEmit 检查,部署用 esbuild


1.2 tsconfig.json 深度解析

这是一个适合现代项目的 tsconfig.json 完整配置:

JSON
{
  "compilerOptions": {
    // ── 目标与模块系统 ──────────────────────────────
    "target": "ES2022",           // 编译后 JS 的语法版本(支持顶层 await、class fields)
    "lib": ["ES2022"],            // 可用的内置类型库(DOM 项目加 "DOM")
    "module": "NodeNext",         // 模块系统(Node.js 22 用 NodeNext,浏览器用 ESNext)
    "moduleResolution": "NodeNext", // 模块解析策略(必须与 module 匹配)

    // ── 输出配置 ────────────────────────────────────
    "outDir": "./dist",           // 编译后文件输出目录
    "rootDir": "./src",           // 源码根目录
    "declaration": true,          // 生成 .d.ts 类型声明文件(库开发时必须)
    "declarationMap": true,       // 生成 .d.ts.map(支持 "Go to Definition" 跳转源码)
    "sourceMap": true,            // 生成 Source Map(调试时映射到原始 .ts 行号)

    // ── 严格模式(强烈推荐全部开启)────────────────
    "strict": true,               // 开启所有严格检查(等于下面 5 项全部 true)
    // "strictNullChecks": true,  // (strict 已包含)null/undefined 不能赋值给其他类型
    // "strictFunctionTypes": true, // (strict 已包含)函数参数逆变检查
    // "noImplicitAny": true,     // (strict 已包含)禁止隐式 any
    // "strictPropertyInitialization": true, // (strict 已包含)类属性必须初始化
    // "strictBindCallApply": true, // (strict 已包含)bind/call/apply 类型检查

    // ── 额外质量检查 ────────────────────────────────
    "noUnusedLocals": true,       // 未使用的局部变量报错
    "noUnusedParameters": true,   // 未使用的函数参数报错
    "noImplicitReturns": true,    // 函数所有分支必须有 return
    "noFallthroughCasesInSwitch": true, // switch-case 必须有 break/return
    "noUncheckedIndexedAccess": true,   // 数组/对象索引访问返回 T | undefined(更安全)
    "exactOptionalPropertyTypes": true, // 可选属性不能显式赋值 undefined

    // ── 路径与兼容性 ────────────────────────────────
    "esModuleInterop": true,      // 允许 import fs from 'fs'(而非 import * as fs)
    "allowSyntheticDefaultImports": true, // 允许没有 default export 的模块用 default 导入
    "resolveJsonModule": true,    // 允许 import data from './data.json'
    "skipLibCheck": true,         // 跳过 node_modules 中 .d.ts 文件的类型检查(加快速度)
    "forceConsistentCasingInFileNames": true, // 文件名大小写必须一致(跨平台兼容)

    // ── 路径别名(可选,需配合 tsx/esbuild 插件)──
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]            // 使用 @/utils 代替 ../../utils
    }
  },
  "include": ["src/**/*"],        // 包含哪些文件参与编译
  "exclude": [
    "node_modules",               // 永远排除
    "dist",                       // 排除输出目录
    "**/*.test.ts"                // 测试文件单独用 vitest 的 tsconfig 处理
  ]
}

strict 模式的重要性

TypeScript
// ❌ 不开启 strictNullChecks 的危险代码
function getLength(str: string | null) {
  return str.length; // 运行时 TypeError:Cannot read properties of null
}

// ✅ 开启 strictNullChecks 后,编译时就报错
function getLength(str: string | null) {
  return str.length; // 错误:Object is possibly 'null'
  // 必须处理 null:
}

function getLengthSafe(str: string | null): number {
  if (str === null) return 0;
  return str.length; // 这里 TypeScript 知道 str 一定是 string
}

// noUncheckedIndexedAccess 的威力
const arr = [1, 2, 3];
const item = arr[10];          // item 类型是 number | undefined(而非 number)
console.log(item.toFixed(2));  // 错误:item 可能 undefined,必须先判断

1.3 VS Code 最佳配置

必装扩展

  • TypeScript Error Translator:把晦涩的 TS 错误翻译成人话
  • Pretty TypeScript Errors:更美观的类型错误显示
  • ESLint:代码质量检查(配合 typescript-eslint
  • Prettier:代码格式化

.vscode/settings.json

JSON
{
  // 使用项目本地的 TypeScript 版本(而非 VS Code 内置版本)
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true,

  // 保存时自动修复 ESLint 错误
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.organizeImports": "explicit"
  },

  // 保存时用 Prettier 格式化
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",

  // TypeScript 语言服务优化
  "typescript.preferences.importModuleSpecifier": "relative",
  "typescript.suggest.autoImports": true,
  "typescript.inlayHints.parameterNames.enabled": "literals",
  "typescript.inlayHints.variableTypes.enabled": true
}

1.4 基础类型速览

TypeScript
// ── 原始类型 ────────────────────────────────────────
let name: string = "Alice";          // 字符串
let age: number = 30;               // 数字(整数与浮点数统一为 number)
let active: boolean = true;          // 布尔值
let nothing: null = null;            // null(strictNullChecks 下独立类型)
let missing: undefined = undefined;  // undefined(同上)
let unique: symbol = Symbol("id");   // Symbol(唯一值)
let big: bigint = 9007199254740993n; // BigInt(超大整数)

// ── 字面量类型:值就是类型 ──────────────────────────
type Direction = "up" | "down" | "left" | "right"; // 只能是这四个值
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;           // 1~6 的整数

// ── 数组类型 ────────────────────────────────────────
const nums: number[] = [1, 2, 3];         // 语法 1
const strs: Array<string> = ["a", "b"];   // 语法 2(泛型写法,完全等价)
const pairs: [string, number][] = [["Alice", 30]]; // 元组数组

// ── 元组(Tuple):固定长度和类型的数组 ─────────────
const point: [number, number] = [10, 20]; // x, y 坐标
const entry: [string, number] = ["age", 30]; // 键值对

// ── 对象类型 ────────────────────────────────────────
const user: { name: string; age: number; email?: string } = {
  name: "Alice",
  age: 30,
  // email 是可选的,可以不写
};

// ── any 与 unknown:都能接受任意值,但差别巨大 ──────
let dangerous: any = "hello"; // any 关闭类型检查,危险!
dangerous = 42;               // 可以赋任意值
dangerous.toUpperCase();      // 不会报错,但运行时可能崩溃

let safe: unknown = "hello";  // unknown 是安全的 any
// safe.toUpperCase();        // 错误!必须先做类型检查
if (typeof safe === "string") {
  safe.toUpperCase();         // OK:已经确认是 string
}

// ── never:永远不会到达的类型 ─────────────────────
function fail(msg: string): never {
  throw new Error(msg); // 函数永远不会正常返回
}

function exhaustiveCheck(x: never): never {
  throw new Error(`Unhandled case: ${x}`);
}

1.5 类型推断:让 TypeScript 帮你省力

TypeScript 非常聪明,很多时候不需要手动写类型注解:

TypeScript
// TypeScript 自动推断变量类型
const message = "Hello World";  // 推断为 string(常量更精确:推断为 "Hello World")
let count = 0;                  // 推断为 number
const arr = [1, 2, 3];          // 推断为 number[]

// 函数返回值推断
function add(a: number, b: number) {
  return a + b; // 推断返回 number(不必写 : number)
}

// 解构赋值保持类型
const { name, age } = { name: "Alice", age: 30 };
// name: string, age: number — 自动推断

// 何时需要手动注解:
// 1. 函数参数(无法推断)
// 2. 声明变量但稍后赋值
// 3. 想要比推断更宽泛或更窄的类型
let later: string; // 声明先不赋值
later = "now";

// 4. 对象字面量需要明确约束
const config: { port: number; host: string } = {
  port: 3000,
  host: "localhost",
};

1.6 第一个完整程序:命令行 TODO 管理器

TypeScript
// src/todo.ts
// 综合运用本章所学:类型、接口、推断、null 检查

interface Todo {
  id: number;
  title: string;
  done: boolean;
  createdAt: Date;
}

// 模拟数据库(开启 noUncheckedIndexedAccess 后,访问结果是 Todo | undefined)
const todos: Todo[] = [];
let nextId = 1;

// 创建 Todo
function addTodo(title: string): Todo {
  const todo: Todo = {
    id: nextId++,
    title,
    done: false,
    createdAt: new Date(),
  };
  todos.push(todo);
  return todo;
}

// 标记完成
function completeTodo(id: number): boolean {
  const todo = todos.find((t) => t.id === id); // Todo | undefined
  if (todo === undefined) return false;         // 处理 undefined
  todo.done = true;
  return true;
}

// 列出所有未完成项
function listPending(): Todo[] {
  return todos.filter((t) => !t.done);
}

// 格式化输出(模板字面量类型)
function formatTodo(todo: Todo): string {
  const status = todo.done ? "✅" : "⬜";
  return `${status} [${todo.id}] ${todo.title}`;
}

// 使用
addTodo("学习 TypeScript 类型系统");
addTodo("完成第 1 章练习");
addTodo("搭建 Next.js 全栈项目");
completeTodo(1);

console.log("=== 待办事项 ===");
listPending().forEach((t) => console.log(formatTodo(t)));

// 运行:npx tsx src/todo.ts

🏋️ 本章练习

  1. 配置实验:关闭 tsconfig.json 中的 noUncheckedIndexedAccess,观察数组访问的类型变化
  2. 推断练习:写 5 个函数,不显式标注返回类型,让 TypeScript 推断,然后用鼠标悬停确认推断结果
  3. 严格模式体验:写一个接受 string | null 参数的函数,在不处理 null 的情况下调用方法,观察编译错误

📌 本章小结

要点 说明
工具链 tsx(开发)、tsc --noEmit(检查)、esbuild(构建)
tsconfig strict: true 必开,noUncheckedIndexedAccess 强推
原始类型 string/number/boolean/null/undefined/symbol/bigint
特殊类型 any(不安全)、unknown(安全)、never(不可达)
类型推断 让 TS 自动推断,只在必要时手动注解

下一章:深入类型系统——接口、类型别名、枚举、联合类型与交叉类型。


TypeScript 5.8 · Node.js 22 · 2025