摘要 :本系列最后一篇文章将带你走进真实的 TypeScript 工程化世界。从 tsconfig.json 的每一个重要选项讲起,到如何与 Webpack、Vite 等构建工具集成,再到如何将一个现有的 JavaScript 项目逐步迁移到 TypeScript,以及调试技巧、代码规范配置。最后,我们会用一个小型实战项目(基于 Node.js 的命令行工具)将前面七篇的所有知识串联起来,让你真正具备独立开发 TypeScript 项目的能力。
一、前言
在前几篇文章中,我们从 TypeScript 的诞生背景、环境搭建开始,系统学习了基础类型、函数与接口、面向对象编程、泛型与高级类型、类型声明与模块化。每一篇都配有大量示例和练习,相信你已经能够熟练地使用 TypeScript 编写类型安全的代码。
然而,理论与实践之间还有"最后一公里":如何在一个真实项目中配置 TypeScript?如何与构建工具(Webpack、Vite)协作?如何保证代码质量、调试和部署?这些工程化问题往往是初学者从"会写"到"能干活"的最大障碍。
本文将一一解答这些问题。我们会先深入剖析 tsconfig.json 的每个关键选项,然后介绍与主流构建工具的集成方式,接着讨论代码规范、调试技巧,以及如何将一个现有的 JavaScript 项目平滑迁移到 TypeScript。最后,通过一个完整的命令行待办事项工具,将前面所有知识点串联起来。
二、核心配置文件:tsconfig.json
tsconfig.json 是 TypeScript 项目的核心配置文件,它告诉 TypeScript 编译器如何处理项目中的 .ts 文件。该文件通常放在项目根目录,执行 tsc 命令时,编译器会自动向上查找该文件,并根据其中的配置进行编译。
2.1 生成 tsconfig.json
在项目根目录执行以下命令,可以生成一个带有大量注释的默认配置:
tsc --init

生成的默认配置会包含几乎所有可用选项(以注释形式呈现),你可以根据实际需求取消注释并修改。
2.2 顶层选项
tsconfig.json 的顶层字段用于控制编译的范围和行为,常用的有:
TypeScript
// 文件名:tsconfig.json(顶层字段示例)
{
"compilerOptions": { /* 编译选项(最常用) */ },
"include": ["src/**/*"], // 指定要编译的文件或目录
"exclude": ["node_modules", "dist"], // 排除的文件或目录
"extends": "./base.json", // 继承另一个配置文件(适合多项目共享配置)
"files": ["main.ts"], // 指定一个精确的文件列表(少用,一般用 include)
"references": [{ "path": "./shared" }] // 项目引用,用于 monorepo 或代码分割
}
-
include/exclude:支持 glob 通配符(*匹配任意字符,**匹配任意子目录)。注意:exclude只能排除include中包含的文件,不能排除未包含的文件。 -
extends:可以继承另一个配置文件的全部内容,再在当前文件中覆盖部分选项。非常适合团队统一基础配置。 -
references:用于构建大型项目时拆分多个子项目,可实现增量编译和更快的构建速度。
2.3 关键编译选项详解
compilerOptions 是配置的核心,下面列出最常用、最重要的选项,并给出推荐值:
| 选项 | 含义 | 推荐值 | 说明 |
|---|---|---|---|
target |
编译后的 JS 版本 | "ES2020"(Node 18+)或 "ES6"(兼容旧浏览器) |
现代 Node.js 环境支持 ES2020,浏览器环境需根据兼容性要求选择 |
module |
模块系统 | "commonjs"(Node)、"ESNext"(打包工具) |
Node.js 后端使用 commonjs,前端使用 ESNext 让打包工具处理 |
lib |
运行时库(类型定义) | ["ES2020", "DOM"](前端)或 ["ES2020"](Node) |
指定 TypeScript 运行时环境的类型定义,避免出现 window、document 等找不到的错误 |
outDir |
输出目录 | "./dist" 或 "./lib" |
编译后的 .js、.d.ts、.map 文件存放目录 |
rootDir |
源码根目录 | "./src" |
编译器会保留 rootDir 下的目录结构输出到 outDir |
strict |
开启所有严格检查 | true |
强烈推荐开启 ,它会同时启用 noImplicitAny、strictNullChecks 等核心类型安全选项 |
esModuleInterop |
兼容 CommonJS 和 ES 模块 | true |
允许通过 import * as fs from 'fs' 导入 CommonJS 模块,并允许默认导入 |
skipLibCheck |
跳过声明文件检查 | true |
加速编译,因为你无法修复第三方库的类型错误 |
forceConsistentCasingInFileNames |
强制文件名大小写一致 | true |
避免在 macOS(大小写不敏感)上开发,部署到 Linux(大小写敏感)时出现路径错误 |
declaration |
生成 .d.ts 声明文件 |
库项目为 true,应用项目 false |
如果要发布 npm 包供他人使用,必须开启 |
sourceMap |
生成 .map.js 映射文件 |
true |
便于调试,错误堆栈可以映射回原始 .ts 代码行号 |
noImplicitAny |
禁止隐式 any |
true(属于 strict) |
要求所有函数参数、变量必须有明确类型或能被自动推断 |
strictNullChecks |
严格空值检查 | true(属于 strict) |
区分 undefined、null 与普通值,避免"Cannot read property of undefined"错误 |
resolveJsonModule |
允许导入 .json 文件 |
true |
方便读取配置文件或静态数据 |
allowJs |
允许编译 .js 文件 |
迁移项目中为 true |
逐步迁移 JavaScript 到 TypeScript 时使用 |
checkJs |
对 .js 文件进行类型检查(基于 JSDoc) |
可选 true |
配合 allowJs 使用,即使不写 TS 也能获得部分类型保护 |
一个典型的 Node.js 后端配置示例
TypeScript
// 文件名:tsconfig.json(Node.js 后端)
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
一个典型的前端(React/Vue)配置示例
TypeScript
// 文件名:tsconfig.json(前端项目)
{
"compilerOptions": {
"target": "ES2015",
"module": "ESNext",
"lib": ["ES2015", "DOM"],
"jsx": "react-jsx", // React 17+ 使用新的 JSX 转换
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true,
"moduleResolution": "node", // 使用 Node.js 模块解析策略
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
2.4 生成声明文件(用于库开发)
如果你要开发一个 npm 包供其他 TypeScript 项目使用,必须生成 .d.ts 声明文件。在 compilerOptions 中添加:
TypeScript
// 文件名:tsconfig.json(库项目)
{
"compilerOptions": {
"declaration": true, // 生成 .d.ts
"declarationMap": true, // 辅助跳转到原始源码(需要配合 sourceMap)
"outDir": "./lib"
}
}
然后在 package.json 中指定类型入口:
TypeScript
// 文件名:package.json(部分)
{
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": ["lib"]
}
三、与构建工具集成
在实际项目中,我们很少直接运行 tsc 然后手动执行 node 命令。通常会结合构建工具(Webpack、Vite)或进程管理工具(nodemon、tsx)来获得更好的开发体验。
3.1 使用 tsc 单独编译(简单场景)
对于小型 Node.js 项目或库,直接使用 tsc 已经足够。在 package.json 中添加脚本:
TypeScript
// 文件名:package.json(scripts 部分)
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "concurrently \"tsc --watch\" \"nodemon dist/index.js\""
}
-
tsc --watch:监听文件变化,自动重新编译。 -
nodemon dist/index.js:当dist/目录下的文件变化时,自动重启 Node 进程。
注意 :需要先安装
concurrently和nodemon:pnpm add -D concurrently nodemon
3.2 集成 Webpack
Webpack 是一个功能强大的模块打包工具,通过 ts-loader 或 babel-loader 可以处理 TypeScript 文件。
安装依赖
TypeScript
pnpm add -D webpack webpack-cli ts-loader typescript
配置 webpack.config.js
TypeScript
// 文件名:webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.ts',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
resolve: {
extensions: ['.ts', '.js'],
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
devtool: 'source-map',
devServer: {
static: './dist',
port: 3000,
hot: true,
},
};
使用 babel-loader 的方案
Babel 7+ 支持 TypeScript 语法剥离(@babel/preset-typescript),但 Babel 不进行类型检查,因此需要单独运行 tsc --noEmit 来检查类型。
TypeScript
pnpm add -D @babel/core @babel/preset-env @babel/preset-typescript babel-loader
在 webpack.config.js 中,将 ts-loader 替换为:
TypeScript
// 文件名:webpack.config.js(使用 babel-loader)
{
test: /\.tsx?$/,
use: 'babel-loader',
exclude: /node_modules/,
}
并在项目根目录创建 babel.config.json:
TypeScript
// 文件名:babel.config.json
{
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
]
}
然后在 package.json 中添加类型检查脚本:
TypeScript
// 文件名:package.json(scripts 部分)
"scripts": {
"type-check": "tsc --noEmit"
}
3.3 集成 Vite
什么是 Vite?
Vite(法语意为"快速")是一个由 Vue.js 作者尤雨溪开发的现代前端构建工具。它解决了传统打包工具(如 Webpack)在开发环境下启动缓慢、热更新迟钝的问题。Vite 在开发阶段利用浏览器原生 ES 模块 (ESM)能力,无需打包即可直接提供源代码;在生产构建时则使用 esbuild 和 Rollup 进行优化打包。
Vite 为什么快?
-
开发服务器启动极快 :Vite 将应用中的模块分为"依赖"和"源码"两类。依赖(如 React、lodash)通常不变,Vite 使用 esbuild 预构建它们,esbuild 是用 Go 语言编写的,比 JavaScript 打包器快 10-100 倍。源码则通过浏览器原生的
<script type="module">按需加载,不进行打包。 -
热更新(HMR)速度快:当编辑一个文件时,Vite 只精确替换该模块,而不是重新构建整个依赖图。得益于 ESM 的天然边界,HMR 更新速度与页面规模解耦。
-
生产构建使用 Rollup:Rollup 成熟稳定,插件生态丰富,能输出高度优化的静态资源。
Vite 对 TypeScript 的支持
Vite 默认支持 .ts 文件,但它只做语法转译,不做类型检查 。Vite 内部调用 esbuild 将 TypeScript 编译为 JavaScript,这个过程非常快(因为 esbuild 只剥离类型,不检查类型)。类型检查需要单独运行 tsc --noEmit。
创建 Vite + TypeScript 项目
TypeScript
pnpm create vite my-app --template react-ts # 或 vue-ts
cd my-app
pnpm install
生成的项目中,src/main.tsx(或 .vue)可以直接编写 TypeScript,vite.config.ts 配置文件本身也是 TypeScript 文件。
添加类型检查脚本
由于 Vite 不执行类型检查,我们必须在 package.json 中添加一个独立的类型检查命令,并建议在 CI 或构建前运行:
TypeScript
// 文件名:package.json(scripts 部分)
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"type-check": "tsc --noEmit"
}
-
tsc --noEmit:只进行类型检查,不输出 JS 文件。 -
在
build命令中先做类型检查再构建,确保不会发布带有类型错误的代码。
配置 tsconfig.json 与 Vite 配合
使用 Vite 时,tsconfig.json 需要注意以下几点:
-
"module": "ESNext":让 TypeScript 输出 ESM 语法,Vite 可直接处理。 -
"moduleResolution": "node":使用 Node.js 模块解析策略。 -
"target": "ES2015"或更高:现代浏览器支持良好。 -
"jsx":根据框架设置("react-jsx"或"preserve")。
TypeScript
// 文件名:tsconfig.json(Vite + React 典型配置)
{
"compilerOptions": {
"target": "ES2015",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
注意 "noEmit": true 和 "isolatedModules": true ------ 因为 Vite 负责转译输出,TypeScript 只需做类型检查。
Vite 与其他工具的对比
| 特性 | Vite | Webpack | tsc |
|---|---|---|---|
| 开发服务器启动速度 | 极快(预构建+ESM) | 慢(全量打包) | 不适用 |
| 热更新速度 | 快(模块级替换) | 中等(依赖 HMR 配置) | 不适用 |
| 生产构建 | Rollup(成熟) | 高度可配置 | 仅输出单文件,不适合浏览器 |
| TypeScript 支持 | 转译(esbuild),不检查类型 | 通过 loader 转译 + 可选类型检查 | 完整编译和检查 |
| 配置复杂度 | 低 | 高 | 低 |
使用 Vite 的注意事项
-
类型检查分离 :务必在 CI 或 pre-commit hook 中运行
tsc --noEmit,否则可能遗漏类型错误。 -
环境变量 :Vite 使用
import.meta.env而不是process.env。 -
路径别名 :需要在
vite.config.ts中配置resolve.alias,并同步修改tsconfig.json的paths。 -
Vite 插件 :许多 Webpack 插件没有直接对应,但 Vite 有丰富的插件生态(
@vitejs/plugin-react、vite-plugin-vue等)。

打开 src/App.tsx,修改里面的文字,保存,浏览器立即刷新

3.4 Node.js 后端开发热重载
方案一:ts-node + nodemon
TypeScript
pnpm add -D ts-node nodemon
package.json:
TypeScript
// 文件名:package.json(scripts 部分)
"scripts": {
"dev": "nodemon --exec ts-node src/index.ts"
}
可选配置 nodemon.json:
TypeScript
// 文件名:nodemon.json
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.spec.ts"],
"exec": "ts-node src/index.ts"
}
方案二:tsx(推荐)
TypeScript
npm install -D tsx
package.json:
TypeScript
// 文件名:package.json(scripts 部分)
"scripts": {
"dev": "tsx watch src/index.ts"
}
tsx 基于 esbuild,速度比 ts-node 快很多,且无需额外配置。
在 src/index.ts 中写一个简单的 HTTP 服务或循环打印:
let count = 0;
setInterval(() => {
console.log(`Tick ${count++}`);
}, 2000);
运行 npm run dev,终端每秒打印一次。现在修改 count 的初始值,保存后,进程自动重启,观察打印变化。这是后端开发的标准姿势。

四、代码质量与规范
4.1 ESLint + TypeScript
安装依赖
TypeScript
pnpm add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

创建 .eslintrc.js
TypeScript
// 文件名:.eslintrc.js
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
env: {
node: true,
es2020: true,
},
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-console': 'off',
},
};
添加 lint 脚本
TypeScript
// 文件名:package.json(scripts 部分)
"scripts": {
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix"
}
4.2 Prettier
安装
TypeScript
pnpm add -D prettier

创建 .prettierrc
TypeScript
// 文件名:.prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"endOfLine": "lf"
}
创建 .prettierignore
TypeScript
// 文件名:.prettierignore
dist
node_modules
coverage
*.log
添加格式化脚本
TypeScript
// 文件名:package.json(scripts 部分)
"scripts": {
"format": "prettier --write \"src/**/*.ts\""
}
4.3 解决 ESLint 与 Prettier 的冲突
安装 eslint-config-prettier 来关闭 ESLint 中与 Prettier 重叠的规则:
TypeScript
pnpm add -D eslint-config-prettier

修改 .eslintrc.js,将 prettier 放在 extends 数组的最后:
TypeScript
// 文件名:.eslintrc.js(修改后)
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
五、从 JavaScript 迁移到 TypeScript
5.1 迁移策略:渐进式
不要试图一次性重写整个项目。推荐的迁移路线图如下:
在项目根目录添加 tsconfig.json ,设置 allowJs: true、checkJs: false(可选),outDir: "./dist"。
修改构建流程 :将原来的 JavaScript 构建命令替换为 TypeScript 编译(如 tsc 或 Webpack 支持 TypeScript)。确保原有 JS 代码依然能正常运行。
逐个文件重命名 :将 .js 文件改为 .ts(React 项目则改为 .tsx)。从叶子节点(工具函数、API 调用等)开始,逐步向上层组件扩散。
修复类型错误 :每改一个文件,修复编译错误。如果某些依赖没有类型定义,安装 @types/* 或使用 declare module 临时解决。
开启严格模式 :当所有文件都迁移完成后,将 strict 设为 true,修复所有隐式 any 和空值相关错误。
5.2 处理隐式 any
迁移过程中最常见的错误是"隐式 any"。当 TypeScript 无法推断一个变量或参数的类型时,它会默认视为 any,但如果开启了 noImplicitAny(strict 包含它),编译器会报错。
解决方案:
-
显式添加类型注解:为函数参数、变量明确写出类型。
TypeScript// 修复前(隐式 any) function multiply(a, b) { return a * b; } // 修复后 function multiply(a: number, b: number): number { return a * b; } -
定义一个接口 :对于复杂对象,创建
interface描述其形状。 -
使用类型断言 (不推荐作为长期方案):
const user = data as User; -
临时放宽检查 :在单个变量前加
// @ts-ignore(尽量少用)或在函数参数后加: any。
5.3 允许 JS 与 TS 共存
在迁移期间,tsconfig.json 必须配置:
TypeScript
// 文件名:tsconfig.json(迁移期配置)
{
"compilerOptions": {
"allowJs": true,
"checkJs": false, // 可选,如果设置为 true,会对 JS 文件基于 JSDoc 进行类型检查
"outDir": "./dist"
},
"include": ["src/**/*.ts", "src/**/*.js"]
}
这样,.js 文件也会被 TypeScript 编译器处理并输出到 dist,你可以逐个重命名为 .ts 而不中断项目运行。
5.4 使用 JSDoc 为 JS 文件提供类型(可选)
如果团队成员对 TypeScript 还不熟悉,或者不想立即改变源代码,可以在 JS 文件中使用 JSDoc 注释来提供类型信息。开启 checkJs: true 后,TypeScript 会根据这些注释进行类型检查。
TypeScript
// 文件名:someFile.js(使用 JSDoc)
/**
* 计算两个数的和
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}
/**
* @typedef {Object} User
* @property {number} id
* @property {string} name
*/
/** @type {User} */
const currentUser = { id: 1, name: 'Alice' };
5.5 处理第三方库缺少类型定义
许多老旧的 npm 包没有提供 TypeScript 类型声明。你可以:
查找 @types/ 作用域下的社区类型定义:pnpm add -D @types/pkgname。
如果不存在,可以自己编写一个简单的声明文件(.d.ts):
TypeScript
// 文件名:types/legacy-lib.d.ts
declare module 'legacy-lib' {
export function doSomething(input: string): number;
}
临时使用 any 规避:const lib = require('legacy-lib') as any;
六、调试 TypeScript
6.1 确保生成 Source Map
在 tsconfig.json 中开启:
TypeScript
// 文件名:tsconfig.json(确保 sourceMap 为 true)
{
"compilerOptions": {
"sourceMap": true
}
}

6.2 VS Code 调试(编译后调试)
点击 VS Code 左侧的"运行和调试"图标(或按 Ctrl+Shift+D)。
点击"创建 launch.json 文件",选择 Node.js。
修改生成的 launch.json:
TypeScript
// 文件名:.vscode/launch.json(编译后调试配置)
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/src/index.ts",
"preLaunchTask": "tsc: build",
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"sourceMaps": true
}
]
}
6.3 直接调试 TypeScript(无需预编译)
使用 ts-node 配合调试器,可以直接在 .ts 文件上调试。
修改 launch.json,添加一个新的配置:
TypeScript
// 文件名:.vscode/launch.json(直接调试 TypeScript)
{
"type": "node",
"request": "launch",
"name": "Debug with ts-node",
"runtimeArgs": ["-r", "ts-node/register"],
"args": ["${workspaceFolder}/src/index.ts"],
"cwd": "${workspaceFolder}",
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
"skipFiles": ["<node_internals>/**"]
}
七、实战项目:命令行待办事项工具
现在,我们将运用前面学到的所有知识,构建一个简单的命令行 Todo 工具(CLI)。
7.1 项目初始化
TypeScript
mkdir ts-todo-cli
cd ts-todo-cli
pnpm init
pnpm add -D typescript @types/node ts-node nodemon
pnpm add commander
pnpm add -D @types/commander
tsc --init


修改 tsconfig.json。
TypeScript
// 文件名:tsconfig.json(Node.js 后端)
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
创建目录结构:
TypeScript
ts-todo-cli/
├── src/
│ ├── types.ts
│ ├── storage.ts
│ ├── todoService.ts
│ └── index.ts
├── data/ # 存储 todos.json(运行时自动生成)
├── dist/ # 编译输出(自动生成)
├── tsconfig.json
└── package.json
7.2 定义 Todo 类型
TypeScript
// 文件名:src/types.ts
export interface Todo {
id: number;
content: string;
completed: boolean;
createdAt: Date;
}
7.3 实现文件存储模块
TypeScript
// 文件名:src/storage.ts
import fs from 'fs';
import path from 'path';
import { Todo } from './types';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const DATA_FILE = path.join(__dirname, '..', 'data', 'todos.json');
export function loadTodos(): Todo[] {
if (!fs.existsSync(DATA_FILE)) {
return [];
}
const data = fs.readFileSync(DATA_FILE, 'utf-8');
const parsed = JSON.parse(data) as Todo[];
return parsed.map(todo => ({
...todo,
createdAt: new Date(todo.createdAt),
}));
}
export function saveTodos(todos: Todo[]): void {
const dir = path.dirname(DATA_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(DATA_FILE, JSON.stringify(todos, null, 2));
}
7.4 实现业务逻辑
TypeScript
// src/todoService.ts
import { Todo } from './types.js';
import { loadTodos, saveTodos } from './storage.js';
export function listTodos(): void {
const todos = loadTodos();
if (todos.length === 0) {
console.log('暂无待办事项,使用 "todo add <内容>" 添加一条。');
return;
}
console.log('待办事项列表:');
todos.forEach((todo: Todo) => { // ✅ 显式类型注解
const status = todo.completed ? '✓' : '○';
const dateStr = todo.createdAt.toLocaleString();
console.log(`${status} [${todo.id}] ${todo.content} (创建于 ${dateStr})`);
});
}
export function addTodo(content: string): void {
const todos = loadTodos();
const newId = todos.length > 0 ? Math.max(...todos.map((t: Todo) => t.id)) + 1 : 1; // ✅ 显式类型
const newTodo: Todo = {
id: newId,
content,
completed: false,
createdAt: new Date(),
};
todos.push(newTodo);
saveTodos(todos);
console.log(`✅ 已添加: "${content}" (ID: ${newId})`);
}
export function completeTodo(id: number): void {
const todos = loadTodos();
const todo = todos.find((t: Todo) => t.id === id); // ✅ 显式类型
if (!todo) {
console.error(`❌ 未找到 ID 为 ${id} 的待办事项`);
process.exit(1);
}
if (todo.completed) {
console.log(`ℹ️ 待办事项 "${todo.content}" 已经是完成状态。`);
return;
}
todo.completed = true;
saveTodos(todos);
console.log(`✅ 已完成: "${todo.content}"`);
}
export function deleteTodo(id: number): void {
const todos = loadTodos();
const index = todos.findIndex((t: Todo) => t.id === id); // ✅ 显式类型
if (index === -1) {
console.error(`❌ 未找到 ID 为 ${id} 的待办事项`);
process.exit(1);
}
const deleted = todos.splice(index, 1)[0];
saveTodos(todos);
console.log(`🗑️ 已删除: "${deleted.content}"`);
}
7.5 实现 CLI 入口
TypeScript
// 文件名:src/index.ts
import { Command } from 'commander';
import { listTodos, addTodo, completeTodo, deleteTodo } from './todoService';
const program = new Command();
program
.name('todo')
.description('一个简单的 TypeScript 待办事项命令行工具')
.version('1.0.0');
program
.command('list')
.alias('ls')
.description('列出所有待办事项')
.action(() => {
listTodos();
});
program
.command('add <content>')
.description('添加新的待办事项')
.action((content: string) => {
addTodo(content);
});
program
.command('complete <id>')
.alias('done')
.description('标记指定 ID 的待办为完成')
.action((id: string) => {
const parsedId = parseInt(id, 10);
if (isNaN(parsedId)) {
console.error('❌ ID 必须是数字');
process.exit(1);
}
completeTodo(parsedId);
});
program
.command('delete <id>')
.alias('rm')
.description('删除指定 ID 的待办')
.action((id: string) => {
const parsedId = parseInt(id, 10);
if (isNaN(parsedId)) {
console.error('❌ ID 必须是数字');
process.exit(1);
}
deleteTodo(parsedId);
});
if (process.argv.length === 2) {
program.help();
}
program.parse();
7.6 编译与本地测试
添加 npm 脚本
TypeScript
// 文件名:package.json(scripts 部分)
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
}
测试
TypeScript
pnpm dev add "学习 TypeScript"
pnpm dev add "写一篇技术文章"
pnpm dev list
pnpm dev complete 1
pnpm dev delete 2

7.7 完善为可发布的 npm 包
修改 package.json:
TypeScript
// 文件名:package.json(添加 bin 和 files 字段)
{
"name": "ts-todo-cli",
"version": "1.0.0",
"description": "一个简单的命令行待办事项工具,使用 TypeScript 编写",
"main": "dist/index.js",
"bin": {
"todo": "dist/index.js"
},
"files": ["dist", "data"],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"keywords": ["todo", "cli", "typescript"],
"author": "Your Name",
"license": "MIT"
}
然后运行:
TypeScript
pnpm build

八、部署与发布
8.1 应用项目部署(非 CLI)
对于普通的 Node.js Web 应用,部署流程通常是:
在 CI/CD 环境中执行 pnpm install。
运行 pnpm build(即 tsc)。
将 dist/ 目录和 package.json、pnpm-lock.yaml 复制到生产服务器。
在生产服务器上执行 pnpm install --prod。
使用 node dist/index.js 启动应用,或用 PM2 等进程管理器守护。
8.2 发布 npm 包(库项目)
-
确保
package.json中main指向编译后的入口文件,types指向.d.ts文件。 -
使用
prepublishOnly脚本自动构建。 -
使用
npm version patch/minor/major更新版本并打 tag。 -
运行
npm publish --access public。
8.3 生产环境注意事项
-
关闭 source map :设置
sourceMap: false或删除.map文件。 -
启用增量编译 :在
tsconfig.json中设置incremental: true。 -
使用
--noEmit分离类型检查 :CI 中可先运行tsc --noEmit,再编译。
九、总结
我们学习了:
-
tsconfig.json的核心配置:给出了后端和前端的典型配置模板。 -
构建工具集成 :
tsc、Webpack、Vite(详细解释其原理、优缺点、与 TypeScript 配合的注意事项)、Node.js 热重载。 -
代码质量保障:ESLint、Prettier、Husky + lint-staged。
-
JavaScript 迁移策略:渐进式迁移、共存配置、JSDoc。
-
调试技巧:source map、VS Code 调试配置。
-
实战项目:完整的 CLI 工具,包含类型定义、存储、业务逻辑、命令行入口、npm 发布。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。