为什么 TS

  • 静态类型 = 编译期发现错(少跑、少崩)
  • IDE 智能提示(自动补全 + 跳转)
  • 重构友好
  • 团队代码"自文档化"

2026 新项目几乎都用 TS——Node 也不例外。

装 + 配

npm install -D typescript @types/node
npx tsc --init                            # 生成 tsconfig.json

跑 TS 的几种方式

工具 特点
tsx(推荐) 直接 tsx app.ts,无配置
ts-node 老牌,配置略多
tsc + node 编译后跑(生产部署用)
Node 22+ 原生 --experimental-strip-types(实验性)
bun / deno 原生支持 TS

tsx(最方便)

npm install -D tsx
npx tsx app.ts                    # 直接跑
npx tsx watch app.ts              # 文件变化自动重启

package.json:

{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

推荐 tsconfig

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,

    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

关键字段:

字段 含义
target 编译到哪个 JS 版本
module 模块系统(NodeNext 自动跟随 package.json type
strict 总是开 —— 一组严格检查
skipLibCheck 跳过 node_modules 类型检查(提速)
outDir 编译输出目录

基础语法

// 变量
const name: string = 'Alice';
const age: number = 30;
const active: boolean = true;
const tags: string[] = ['a', 'b'];

// 对象
interface User {
    id: number;
    name: string;
    email?: string;        // 可选
}

const u: User = { id: 1, name: 'Alice' };

// 函数
function add(a: number, b: number): number {
    return a + b;
}

// 箭头
const greet = (name: string): string => `Hi, ${name}`;

// Union 类型
type Status = 'pending' | 'done' | 'failed';
let s: Status = 'done';

// 泛型
function first<T>(arr: T[]): T | undefined {
    return arr[0];
}
first([1, 2, 3]);          // 类型 number
first(['a', 'b']);          // 类型 string

// Promise
async function getUser(id: number): Promise<User> {
    const r = await fetch(`/users/${id}`);
    return r.json();
}

interface vs type

interface User {
    name: string;
}

type User = {
    name: string;
};

二者大部分时候等价。微小区别:

  • interface 可重复声明合并;type 不行
  • type 能定义 union / primitive 别名;interface 不行

常用规则:对象 / 类用 interface;其他用 type

实战:Express + TS

import express, { Request, Response } from 'express';

const app = express();
app.use(express.json());

interface CreateUserBody {
    name: string;
    email: string;
}

app.post('/users', (req: Request<{}, {}, CreateUserBody>, res: Response) => {
    const { name, email } = req.body;        // 自动类型 string
    res.json({ id: 1, name, email });
});

app.listen(3000);

第三方类型

# 多数库自带 .d.ts
npm install express

# 没自带的去 DefinitelyTyped 装
npm install -D @types/lodash @types/cors

迁移 JS 项目

渐进策略:

1. 装 TS + 配 tsconfig(allowJs: true)
2. 把入口文件改成 .ts
3. 逐个文件改后缀 .js → .ts,修类型错
4. 全部改完后关 allowJs
5. 开 strict 模式逐步增强

不用一夜全改——TS 允许 .js.ts 共存。

实战提示

any 是逃生口(少用)

const data: any = JSON.parse(text);    // 失去类型检查
// 更好:
interface Data { ... }
const data = JSON.parse(text) as Data;

unknown 比 any 安全

function parse(s: string): unknown {
    return JSON.parse(s);
}

const data = parse(text);
// data 是 unknown,必须 narrow 才能用
if (typeof data === 'object' && data && 'name' in data) {
    console.log(data.name);
}

satisfies(精准类型)

type Config = { host: string; port: number };

const config = {
    host: 'localhost',
    port: 8080,
} satisfies Config;
// config 类型精确(不被 widening),又被检查符合 Config

部署

开发用 tsx watch;生产先编译再跑:

# 构建阶段
npm run build              # = tsc

# 部署阶段
node dist/index.js

或在 Docker 里多阶段构建:

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/index.js"]

  • TS 不在运行时检查——as Type 是程序员的承诺,错了运行时仍崩
  • 第三方库类型可能滞后或错——@ts-ignore 临时跳过,但永远不该是 PR 心态
  • tsc 慢 → 用 tsx / swc / esbuild 提速
  • 生产不要直接跑 .ts——用 tsc 编译或 esbuild bundle

下一篇:工具链。