阶段一 · 语言概览与环境

JS/TS 介绍与工具链环境配置

系列导航: TypeScript 基础与核心语法

阶段一 · 语言概览与环境(1 篇): JS/TS 介绍与工具链环境配置
阶段二 · 类型系统基础(3 篇): 变量声明与基础类型 → 函数与参数类型 → 对象类型, 接口与类型别名
阶段三 · 类型系统进阶(3 篇): 联合类型与字面量类型 → 泛型 → 类型收窄与类型守卫
阶段四 · 面向对象与模块(2 篇): 类与继承 → 模块系统(ESM vs CJS)
阶段五 · 异步与运行时(2 篇): 事件循环与 Promise → async/await
阶段六 · DOM 与浏览器 API(2 篇): DOM 操作与事件机制 → fetch, Storage, History

一句话总结

TypeScript = JavaScript + 静态类型系统. 对有 Go/Rust 经验的人来说, 直接从 TS 开始比先学 JS 再加类型更顺手.
本篇装好工具链, 跑通第一个 TS 项目, 建立开发环境.

JavaScript 是什么

JavaScript 最初是浏览器里的脚本语言, 现在通过 Node.js 也能跑在服务端. 和后端语言对比几个核心差异:

特性 Go / Rust / Java JavaScript
类型系统 静态, 编译期检查 动态, 运行时才知道类型
并发模型 多线程 / goroutine / tokio 单线程 + 事件循环
执行方式 编译为二进制 / 字节码 解释执行(JIT 优化)
包管理 go mod / cargo / maven npm / pnpm
入口 main 函数 脚本从第一行开始执行

几个关键点:

  1. 动态类型: 变量不绑定类型, let x = 1; x = "hello" 合法. 这是 TS 要解决的问题.
  2. 单线程事件循环: 没有多线程竞争, 但所有 I/O 都是异步回调. 类似 Go 里只有一个 goroutine 但配合 epoll 做非阻塞 I/O.
  3. 原型继承: 不是经典的 class-based OOP(虽然 ES6 加了 class 语法糖), 底层是原型链.

Go 的并发 = 多个工人(goroutine)同时干活, 通过 channel 协调.
JS 的并发 = 一个工人 + 一个任务队列, 工人做完手头的活就从队列取下一个, 永远不会被阻塞在 I/O 上.

TypeScript 是什么

TypeScript 是 JavaScript 的超集: 所有合法的 JS 代码都是合法的 TS 代码, TS 只是在 JS 基础上加了类型标注和编译期检查.

┌─────────────────────────────┐
│ TypeScript │
│ ┌───────────────────────┐ │
│ │ JavaScript │ │
│ │ (运行时真正执行的) │ │
│ └───────────────────────┘ │
│ + 类型标注 │
│ + 接口 / 泛型 / 枚举 │
│ + 编译期类型检查 │
└─────────────────────────────┘

TS 编译器(tsc)做的事: 检查类型 → 擦除类型标注 → 输出纯 JS. 运行时没有任何类型信息, 零运行时开销.

为什么直接学 TS 而不是先学 JS

有 Go/Rust 经验意味着习惯了编译器帮忙抓错. 裸写 JS 时 IDE 提示极弱, 参数传错类型要跑起来才能发现.
TS 的类型系统比 Go 灵活(支持联合类型, 泛型约束, 条件类型), 比 Rust 简单(没有生命周期), 上手门槛不高.
学 TS 的过程中 JS 语法自然就会了, 不需要分两步.

运行环境: 浏览器 vs Node.js

JS/TS 有两个主要运行环境:

环境 提供的额外 API 类比
浏览器 DOM, BOM, fetch, Web Storage 前端 UI 渲染
Node.js fs, path, http, child_process 后端服务, CLI 工具

两者共享同一套语言规范(ECMAScript), 但各自提供不同的"标准库". 本系列前期用 Node.js 跑 TS 学语法, 后期再涉及浏览器 API.

Go 编译出的二进制能在 Linux/macOS/Windows 跑, 但 syscall 层不同.
JS 也一样: 语言相同, 但浏览器和 Node.js 提供的底层 API 不同.

工具链全景

工具 职责 后端类比
Node.js JS 运行时(V8 引擎 + 系统 API) JVM / Go runtime
pnpm 包管理, 依赖安装 cargo / go mod
tsc TS → JS 编译器, 类型检查 rustc / javac
Vite 开发服务器 + 生产构建打包 cargo build(但带 HMR 热更新)
ESLint 代码规范检查 golangci-lint / clippy
Prettier 代码格式化 gofmt / rustfmt

开发阶段的典型流程:

编写 .ts 文件
│
▼
tsc 类型检查(报错在 IDE 里实时显示)
│
▼
tsx / ts-node 直接运行(开发阶段, 不手动编译)
│
▼
Vite 构建(生产阶段, 打包为浏览器可用的 JS)

环境安装

Node.js: 通过版本管理器安装

不要用系统包管理器直接装 Node.js, 用版本管理器方便切换版本. 两种主流方案:

工具 实现 启动速度 适合场景
fnm Rust 原生二进制 极快(< 1ms) 新环境首选
nvm bash 脚本 较慢(~200ms) 传统方案, 团队已有统一用 nvm 时跟随

方案一: fnm(推荐)

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

# 重新加载 shell 配置
source ~/.zshrc # 或 source ~/.bashrc

# 安装最新 LTS 版本
fnm install --lts
fnm use --lts

# 验证
node --version # v22.x.x
npm --version # 10.x.x

fnm 的 shell 集成会在 cd 进目录时自动读取 .node-version 或 .nvmrc 文件切换版本, 和 nvm 的项目版本锁定行为兼容.

方案二: nvm

# 安装 nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# 重新加载 shell 配置
source ~/.zshrc # 或 source ~/.bashrc

# 验证 nvm 自身
nvm --version

# 安装最新 LTS 版本
nvm install --lts

# 查看已安装的版本列表
nvm ls

# 切换版本
nvm use 22 # 使用 v22.x
nvm use --lts # 切回 LTS

# 设置默认版本(新终端自动使用)
nvm alias default 22

# 验证
node --version
npm --version

nvm 常用操作:

# 安装指定版本
nvm install 20.11.0

# 项目锁定版本: 在项目根目录创建 .nvmrc
echo "22" > .nvmrc

# 之后进入项目目录执行
nvm use # 自动读取 .nvmrc 中的版本

nvm 的启动延迟

nvm 是纯 bash 脚本, 每次打开新终端都要加载约 200ms. 如果觉得 shell 启动明显变慢, 可以改用 fnm 或配置 nvm 的懒加载:

# ~/.zshrc 中用懒加载替代直接 source
export NVM_DIR="$HOME/.nvm"
alias nvm="unalias nvm; [ -s \"$NVM_DIR/nvm.sh\" ] && . \"$NVM_DIR/nvm.sh\"; nvm"

pnpm: 替代 npm 的包管理器

# 用 Node.js 自带的 corepack 启用(推荐)
corepack enable
corepack prepare pnpm@latest --activate

# 或者独立安装
npm install -g pnpm

# 验证
pnpm --version

pnpm 相比 npm 的优势: 硬链接去重(节省磁盘), 严格的依赖隔离(不允许幽灵依赖), 速度更快. 类比 Go modules 的 GOMODCACHE 全局缓存机制.

TypeScript

Node.js 的包有两种安装方式:

方式 命令 安装位置 调用方式
全局安装 pnpm add -g typescript 系统目录, 所有项目共享 直接敲 tsc
本地安装 pnpm add -D typescript 项目的 node_modules/ npx tsc 或 scripts

项目内用本地版本的原因: 不同项目可能依赖不同 TS 版本. 本地安装的版本锁定在 pnpm-lock.yaml 里, 团队和 CI 保证一致. 全局版本不受项目管控, 换台机器就可能不一样.

类比: Rust 项目用 rust-toolchain.toml 锁定编译器版本, 而不是依赖系统全局装的那个 rustc. 思路相同.

# 全局安装(可选, 用于在任意目录快速跑 tsc)
pnpm add -g typescript
tsc --version

# 项目内安装(推荐, 版本跟着项目走)
pnpm add -D typescript
pnpm exec tsc --version

后续建项目时我们用本地安装, 全局装不装都行.

第一个 TypeScript 项目

# 创建项目目录
mkdir ts-hello && cd ts-hello

# 初始化 package.json(类似 cargo init 生成 Cargo.toml)
pnpm init

# 安装 TypeScript 为开发依赖
pnpm add -D typescript

# 生成 tsconfig.json(类似 cargo init 生成的项目配置)
pnpm exec tsc --init

# 创建源码目录(tsconfig 默认的 rootDir)
mkdir src

npx vs pnpm exec

npx 是 npm 自带的命令执行工具. 如果项目 package.json 声明了 "packageManager": "pnpm@...", npm 会检测到不匹配并报错:
Invalid name "pnpm" does not match "npm" for "packageManager"

用 pnpm 的项目中, 用 pnpm exec 替代 npx 执行本地安装的命令:

pnpm exec tsc --init   # 等价于 npx tsc --init
pnpm tsc --init # 简写, 效果相同

找不到任何输入

tsc --init 后如果 IDE 提示 "在配置文件中找不到任何输入", 是因为 tsconfig.json 里配了 rootDir 但对应目录下还没有 .ts 文件.
创建 src/main.ts 后报错消失. 这不影响编译, 只是 IDE 的实时检查发现没有可处理的文件.

创建 src/main.ts:

function greet(name: string): string {
return `Hello, ${name}!`;
}

const message = greet("TypeScript");
console.log(message);

编译并运行:

# 编译: ts → js
pnpm exec tsc

# 运行编译产物
node dist/main.js
# 输出: Hello, TypeScript!

项目结构:

ts-hello/
├── package.json ← 项目元信息 + 依赖声明(类似 Cargo.toml)
├── pnpm-lock.yaml ← 锁文件(类似 Cargo.lock)
├── tsconfig.json ← TS 编译器配置
├── node_modules/ ← 依赖安装目录(类似 target/)
├── src/
│ └── main.ts ← 源码
└── dist/
└── main.js ← tsc 编译产物(outDir 指定)

tsconfig.json 关键配置

tsc --init 生成的配置项非常多, 大部分保持默认即可. 关键的几个:

{
"compilerOptions": {
// 编译目标: 输出的 JS 版本. es2022 覆盖现代 Node.js
"target": "es2022",

// 模块系统: Node.js 用 nodenext, 浏览器项目用 esnext
"module": "nodenext",

// 启用严格模式(强烈建议). 包含 strictNullChecks, noImplicitAny 等
"strict": true,

// 编译输出目录
"outDir": "./dist",

// 源码根目录
"rootDir": "./src",

// 允许导入 JSON 文件
"resolveJsonModule": true,

// 跳过 node_modules 的类型检查(加速编译)
"skipLibCheck": true
},
"include": ["src/**/*"]
}
配置项 作用 后端类比
target 输出 JS 的版本(语法降级) rustc 的 --edition
module 模块解析策略 Go 的 module mode
strict 启用全部严格检查 clippy 的 pedantic 级别
outDir 编译产物输出目录 target/ 目录

推荐: 永远开启 strict

strict: true 是 TS 的最大价值所在. 关掉它等于放弃了一半的类型安全, 还不如写 JS.
遇到类型报错时修复它, 而不是关闭检查.

开发体验: 免编译直接运行

每次改代码都手动 tsc + node 太慢. 用 tsx 可以直接运行 .ts 文件:

# 安装 tsx(TypeScript Execute)
pnpm add -D tsx

# 直接运行, 不需要先编译
pnpm exec tsx src/main.ts
# 输出: Hello, TypeScript!

# 监听模式: 文件改动自动重新运行(类似 cargo watch)
pnpm exec tsx watch src/main.ts

tsx 底层用 esbuild 做即时转译, 速度极快(毫秒级). 它不做类型检查, 只负责把 TS 转成 JS 后运行. 类型检查交给 IDE 实时提示或单独跑 tsc --noEmit.

tsx vs ts-node

ts-node 是老牌方案, 但配置复杂且启动慢(基于 tsc). tsx 是新方案, 零配置, 基于 esbuild, 推荐使用.
两者定位相同: 开发阶段直接运行 TS, 生产环境仍然编译为 JS 再运行.

package.json 脚本配置

{
"scripts": {
"dev": "tsx watch src/main.ts",
"build": "tsc",
"start": "node dist/main.js",
"typecheck": "tsc --noEmit"
}
}
pnpm dev        # 开发: 实时运行
pnpm build # 构建: 编译为 JS
pnpm start # 生产: 运行编译产物
pnpm typecheck # 仅类型检查, 不输出文件

VSCode 配置建议

TS 开发几乎必须用 VSCode(或同类 IDE), 因为类型系统的价值有一半体现在 IDE 提示上.

必装扩展:

  • TypeScript 本身: VSCode 内置, 无需额外安装
  • ESLint: 代码规范实时提示
  • Prettier: 保存时自动格式化
  • Error Lens: 把错误信息直接显示在代码行内(强烈推荐)

.vscode/settings.json 推荐配置:

{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"typescript.preferences.importModuleSpecifier": "relative"
}

VSCode 之于 TS 开发, 相当于 GoLand 之于 Go 开发: 语言服务(补全, 跳转, 重构)是核心生产力工具, 不是可选项.

快速回顾

  • JavaScript: 动态类型, 单线程事件循环, 浏览器和 Node.js 两个运行环境
  • TypeScript: JS 超集 + 静态类型, 编译后擦除类型, 零运行时开销
  • 工具链: fnm(Node 版本管理) + pnpm(包管理) + tsc(类型检查) + tsx(开发运行)
  • tsconfig.json: 永远开 strict: true, target 和 module 根据运行环境选择
  • 开发流程: 写 .ts → IDE 实时类型检查 → tsx 直接运行 → 生产时 tsc 编译

动手练习

  1. 装环境: 用 fnm 安装 Node.js, 用 corepack 启用 pnpm, 验证版本号.
  2. 建项目: 创建一个 TS 项目, 写一个函数计算两个数的和, 用 tsx 运行.
  3. 故意犯错: 把函数参数类型改成 number, 然后传入字符串, 观察 IDE 和 tsc 分别怎么报错.
  4. 编译产物对比: 看 tsc 编译出的 .js 文件和源 .ts 文件的区别, 理解"类型擦除".
  5. 配置 scripts: 在 package.json 里配好 dev/build/start 三个脚本, 体验完整的开发-构建-运行流程.