TypeScript 从零基础到精通(七):从配置到全栈项目落地

摘要 :本系列最后一篇文章将带你走进真实的 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 运行时环境的类型定义,避免出现 windowdocument 等找不到的错误
outDir 输出目录 "./dist""./lib" 编译后的 .js.d.ts.map 文件存放目录
rootDir 源码根目录 "./src" 编译器会保留 rootDir 下的目录结构输出到 outDir
strict 开启所有严格检查 true 强烈推荐开启 ,它会同时启用 noImplicitAnystrictNullChecks 等核心类型安全选项
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 区分 undefinednull 与普通值,避免"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 进程。

注意 :需要先安装 concurrentlynodemon

pnpm add -D concurrently nodemon

3.2 集成 Webpack

Webpack 是一个功能强大的模块打包工具,通过 ts-loaderbabel-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)能力,无需打包即可直接提供源代码;在生产构建时则使用 esbuildRollup 进行优化打包。

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.jsonpaths

  • Vite 插件 :许多 Webpack 插件没有直接对应,但 Vite 有丰富的插件生态(@vitejs/plugin-reactvite-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: truecheckJs: 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,但如果开启了 noImplicitAnystrict 包含它),编译器会报错。

解决方案:

  • 显式添加类型注解:为函数参数、变量明确写出类型。

    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.jsonpnpm-lock.yaml 复制到生产服务器。

在生产服务器上执行 pnpm install --prod

使用 node dist/index.js 启动应用,或用 PM2 等进程管理器守护。

8.2 发布 npm 包(库项目)

  • 确保 package.jsonmain 指向编译后的入口文件,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 发布。


如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。

相关推荐
秋天的一阵风1 小时前
✨ 代码秒跳转、自动补全?全靠 LSP 和 AST!
前端·后端·ai编程
如果超人不会飞2 小时前
TinyVue Checkbox复选框组件使用指南
前端·vue.js
程序员小淞2 小时前
写一个行政区划下拉选组件(异步+搜索)
前端
星栈2 小时前
用 Rust + Makepad 做一个 JSON 查看器:从零到能用的全过程
前端·rust
yijianace2 小时前
Python爬虫实战:分页爬取 + 详情页采集 + CSV存储
前端·爬虫·python
十九画生2 小时前
从同步到异步:重新理解 JavaScript 的执行机制
javascript
想吃火锅10052 小时前
【前端手撕】防抖节流
前端
半个落月2 小时前
JavaScript 同步异步与 Promise 详解 —— 从 Event Loop 到手写 sleep
javascript
MemoriKu2 小时前
Flutter 相册 APP 视频模态稳定化实战:从视频抽帧、Embedding 元数据到 Android 真机启动修复
android·开发语言·前端·flutter·架构·音视频·embedding