手把手教你从零搭建TypeScript项目:从tsconfig配置到项目目录结构,完整避坑指南(新手必看)
我当时的坑
说实话,我第一次碰TypeScript的时候,脑子是懵的。tsconfig.json 那一大坨配置,strict、esModuleInterop、target、module……看着就头疼。目录结构更是乱七八糟,src 里塞了一堆文件,dist 也不知道是干嘛的。
后来踩了无数次坑,终于摸出一套比较稳妥的搭建设计。今天就把这套东西整理出来,尽量用大白话讲清楚,让新手少绕弯路。
第一步:项目初始化,别急着写代码
很多新手上来就 npm init,然后立刻开始写 .ts 文件。这是错误的。
正确的姿势是:先想清楚项目是做什么的,再决定用什么工具链。
TypeScript 项目常见的使用场景大概分这几类:
| 场景 | 推荐工具链 |
|---|---|
| 网页应用(前端) | Vite / Next.js / Webpack |
| Node.js 后端服务 | Express / Nest.js / Fastify |
| 通用工具库 | Rollup / esbuild |
| 纯脚本/自动化 | ts-node / tsx |
如果你只是学习 TypeScript 语法,建议从Node.js 后端或通用库开始,因为它们对构建工具链的要求相对简单,更容易理解底层原理。
假设我们做一个简单的 Node.js CLI 工具,命名为 my-cli。
# 创建目录并进入
mkdir my-cli && cd my-cli
# 初始化 package.json(全部用默认值先跳过)
npm init -y
此时你的项目目录是空的,只有一个 package.json。
第二步:安装 TypeScript 和相关依赖
这里有一个新手最容易踩的坑:不要全局安装 TypeScript。
全局安装会导致不同项目用不同版本的 TS,最后混乱不堪。正确做法是项目本地安装。
# 安装 TypeScript(开发依赖)
npm install -D typescript
# 安装 Node.js 类型定义(必须装,否则 Node 内置 API 没有类型提示)
npm install -D @types/node
装完之后,检查一下版本:
npx tsc --version
# 输出:Version 5.x.x 就说明安装成功了
为什么要装
@types/node?TypeScript 默认不知道
process、console、fs这些 Node.js 内置 API 的类型。如果不装@types/node,你写console.log的时候可能会报类型错误,非常困扰。
第三步:生成并理解 tsconfig.json
这是整个文章最核心的部分。TypeScript 的项目配置全靠这个文件。
先让它自动生成:
npx tsc --init
这会在项目根目录生成一个 tsconfig.json,里面密密麻麻全是注释掉的配置项。别慌,我们一个个来理解。
3.1 核心配置项逐个讲解
我用一个经过验证的最小可用配置作为起点,每个字段都配上通俗解释:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInImports": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
下面逐个解释,为什么这么配:
target:编译输出的 JavaScript 版本
"target": "ES2022"
- 决定 TypeScript 编译后输出什么版本的 JavaScript
- 你的代码里用了哪些新语法(比如
??、?.、Array.prototype.at()),target至少要等于或高于那个特性 introduced 的版本 - 选 ES2022 的理由:现代 Node.js(18+)和浏览器都完全支持,同时不会过于超前导致兼容性问题
新手常见错误:把
target设成ES3或ES5,结果代码里用了箭头函数还报错。
module 和 moduleResolution:模块系统怎么选
"module": "NodeNext",
"moduleResolution": "NodeNext"
这是最容易搞混的一对配置。简单说:
module:决定编译出来的代码用什么模块化语法(CommonJS 还是 ES Module)moduleResolution:决定 TypeScript 怎么解析import语句里的路径
| 配置组合 | 适用场景 |
|---|---|
NodeNext + NodeNext |
Node.js 项目首选,支持 .ts、.mts、.cts 扩展名,完美支持 ES Module |
CommonJS + Node |
老项目,用 require() 的场景 |
ESNext + Bundler |
前端项目,配合 Vite/Webpack 等打包工具 |
ESNext + Node16 |
Node.js 项目,使用 .mjs/.cjs 文件扩展名 |
为什么推荐
NodeNext?它是 TypeScript 4.7+ 引入的,专为 Node.js 设计。支持最新的 Node.js 模块解析规则,同时兼容 CommonJS 和 ES Module。
lib:告诉 TypeScript 哪些 API 可用
"lib": ["ES2022"]
- 指定项目可以使用哪些 JavaScript/TypeScript 内置 API 的类型定义
- Node.js 内置 API 的类型由
@types/node提供,不需要在这里写 - 如果你的项目还需要浏览器 API,可以加
"DOM"
outDir 和 rootDir:输入输出路径
"outDir": "./dist",
"rootDir": "./src"
rootDir:TypeScript 源代码所在的目录outDir:编译后的 JavaScript 文件输出到哪里- 这两个是相对的,TypeScript 会保持
src里的目录结构原样输出到dist
src/
index.ts → dist/
utils/ index.js
helper.ts utils/
helper.js
strict:开启所有严格检查(强烈推荐)
"strict": true
这是最重要的一条配置。开启后,TypeScript 会启用以下所有严格检查:
strictNullChecks ← 最关键,防止 undefined/null 乱飞
noImplicitAny ← 禁止隐式 any 类型
strictFunctionTypes ← 更严格的函数类型检查
strictBindCallApply ← 严格检查 bind/call/apply
strictPropertyInitialization ← 要求类属性必须初始化
noImplicitReturns ← 函数所有路径都必须有返回值
noFallthroughCasesInSwitch ← 禁止 switch 穿透
新手吐槽:开启
strict: true之后,初期会报一堆错,很痛苦。但这是值得的,因为早期暴露的问题比运行时报错好处理一万倍。
esModuleInterop:解决 import 风格不一致的问题
"esModuleInterop": true
这个配置专门解决一个问题:当你要引入一个 CommonJS 模块(用 module.exports 导出的)时,TypeScript 可能会报错。
开启后,TypeScript 会自动处理以下两种导入风格的兼容:
// CommonJS 风格导入(以前会报错)
import _ from 'lodash';
// ES Module 风格导入
import { readFile } from 'fs';
简单记:只要你的项目引入了第三方库,几乎一定需要这个配置。
skipLibCheck:跳过类型检查第三方库
"skipLibCheck": true
- TypeScript 默认会检查
node_modules里所有.d.ts类型声明文件 - 但这既浪费时间,又容易因为第三方库的类型定义不严谨而报错
- 开启后跳过检查,只检查你自己写的代码
实际影响:几乎不会有问题,放心开。
forceConsistentCasingInImports:强制导入路径大小写一致
"forceConsistentCasingInImports": true
在 Windows 上,文件系统不区分大小写(./Src 和 ./src 是同一个文件夹),但在 Linux/Mac 上区分。这个配置确保你的导入路径在所有平台上都一致,避免”在我电脑上能跑,在你电脑上报找不到模块”的经典问题。
resolveJsonModule:允许导入 JSON 文件
"resolveJsonModule": true
当你需要 import config from './config.json' 时,必须开启这个。默认不开启,会报类型错误。
declaration 和 declarationMap:生成类型声明文件
"declaration": true,
"declarationMap": true
declaration: true:为每个.ts文件生成对应的.d.ts类型声明文件declarationMap: true:生成.d.ts.map映射文件,方便 IDE 跳转到源码
这对发布 npm 包非常重要。如果不生成 .d.ts,使用者就享受不到类型提示了。
sourceMap:生成 Source Map
"sourceMap": true
- 将编译后的
.js文件映射回原始的.ts文件 - 调试时可以在 Chrome DevTools / VS Code 中直接看到 TypeScript 源码
- 排查线上问题时,能定位到具体的
.ts文件而非编译后的.js
3.2 include 和 exclude
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
include:告诉 TypeScript 只编译src目录下的文件exclude:明确排除不需要的目录
为什么
exclude已经默认有node_modules和dist,还要显式写?因为有时候你会有额外的目录需要排除,比如
tests、docs、__tests__等。显式写出来更清晰,也方便后续扩展。
第四步:设计项目目录结构
类型安全的项目,目录结构直接影响可维护性。以下是我推荐的结构:
my-cli/
├── src/ # 源代码目录
│ ├── index.ts # 入口文件
│ ├── types/ # 类型定义
│ │ ├── common.ts
│ │ └── api.ts
│ ├── utils/ # 工具函数
│ │ ├── logger.ts
│ │ └── validator.ts
│ ├── services/ # 业务逻辑
│ │ └── userService.ts
│ └── handlers/ # 请求处理器 / CLI 命令处理
│ └── userHandler.ts
├── tests/ # 测试文件
│ ├── index.test.ts
│ └── utils/
│ └── logger.test.ts
├── dist/ # 编译输出(不要手动维护)
├── node_modules/ # 依赖(不要手动维护)
├── .gitignore # Git 忽略规则
├── package.json
├── package-lock.json
└── tsconfig.json
为什么这样设计?
src/ 下按功能分层,而不是按文件类型分。很多教程喜欢把文件按 .ts、.js、.css 分类放,这是错误的做法。
正确做法是按业务领域分组:
types/:只放类型定义,其他文件import这些类型utils/:通用工具函数,不依赖业务逻辑services/:核心业务逻辑,操作数据handlers/:处理外部输入(HTTP 请求、CLI 命令等),调用 services
这样的好处是:改动一个功能时,你知道去哪里找相关代码。
第五步:编写入口文件和基本代码
在 src/index.ts 写一个最简单的入口:
import { getUser } from './services/userService.js';
import { logger } from './utils/logger.js';
async function main() {
try {
logger.info('应用启动');
const user = await getUser(1);
logger.info(`获取用户: ${user.name}`);
} catch (error) {
logger.error(`启动失败: ${error}`);
process.exit(1);
}
}
main();
注意文件扩展名:当
module: "NodeNext"时,导入路径必须带.js扩展名,即使源文件是.ts。这是 Node.js 的 ES Module 规范要求。
src/utils/logger.ts:
export const logger = {
info: (message: string): void => {
console.log(`[INFO] ${new Date().toISOString()} - ${message}`);
},
error: (message: string): void => {
console.error(`[ERROR] ${new Date().toISOString()} - ${message}`);
},
};
src/services/userService.ts:
export interface User {
id: number;
name: string;
email: string;
}
const mockUsers: User[] = [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' },
];
export async function getUser(id: number): Promise<User> {
const user = mockUsers.find((u) => u.id === id);
if (!user) {
throw new Error(`用户 ${id} 不存在`);
}
return user;
}
第六步:配置 npm scripts
打开 package.json,添加这些脚本:
{
"name": "my-cli",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist"
},
"devDependencies": {
"typescript": "^5.5.0",
"@types/node": "^20.14.0"
},
"dependencies": {}
}
关键说明:
| 脚本 | 用途 |
|---|---|
build |
编译 TypeScript → JavaScript |
dev |
开发时用,直接运行 .ts 文件,热重载 |
start |
生产环境用,运行编译后的 .js |
type-check |
检查类型错误但不输出文件 |
clean |
清理编译产物 |
"type": "module"的作用:告诉 Node.js 这个项目使用 ES Module 语法,这样import/export才能正常工作。如果这个字段不存在,Node.js 默认用 CommonJS,import会报错。
tsx是什么? 它是ts-node的现代化替代品,专门为 ES Module 环境设计,速度更快,支持更好。如果要用
tsx,安装一下:> npm install -D tsx > ``` --- ## 第七步:编译并运行 ```bash # 1. 类型检查(不生成文件,只检查错误) npm run type-check # 2. 编译 npm run build # 3. 运行编译后的代码 npm start # 4. 开发时直接运行(自动热重载) npm run dev
编译完成后,dist/ 目录下会有:
dist/
index.js ← 编译后的 JS
index.d.ts ← 类型声明
index.d.ts.map ← 类型映射
index.js.map ← 源码映射
utils/
logger.js
logger.d.ts
services/
userService.js
userService.d.ts
第八步:常见坑和避坑指南
坑 1:import 路径忘记加 .js 扩展名
// ❌ 错误(NodeNext 模式下会报错)
import { getUser } from './services/userService';
// ✅ 正确
import { getUser } from './services/userService.js';
原因:ES Module 规范要求导入路径必须有明确的扩展名。TypeScript 编译时会保留这个扩展名,Node.js 运行时需要它来找到正确的文件。
坑 2:strict: true 开启后 undefined 报错
let name: string;
console.log(name); // ❌ 报错:变量 'name' 未初始化
解决:要么初始化,要么用可选类型:
// 方案 1:初始化
let name: string = '';
// 方案 2:可选类型
let name?: string;
坑 3:JSON 文件导入报错
import config from './config.json'; // ❌ 报错
解决:确保 tsconfig.json 里开了 resolveJsonModule: true,同时:
// 用 as const 或者定义类型
import config from './config.json' with { type: 'json' };
坑 4:Windows 和 Linux 路径大小写不一致
你在 Windows 上写 import { foo } from './Src/foo',代码能跑。但部署到 Linux 服务器后,./Src/foo 和实际目录 ./src/foo 不匹配,直接报 Cannot find module。
解决:
- 在
tsconfig.json里开启forceConsistentCasingInImports: true - 统一使用小写路径
- 在 CI/CD 流程中加入类型检查
坑 5:忘记在 package.json 里设置 "type": "module"
这是 Node.js + ES Module 项目最常见的新手坑。没有这个字段,import 语句会报 SyntaxError。
坑 6:编译后 dist/ 目录没有被 Git 忽略
dist/ 和 node_modules/ 都应该加到 .gitignore:
# .gitignore
node_modules/
dist/
*.tsbuildinfo
如果不忽略,你的 Git 仓库会充满编译产物,体积爆炸,而且每次改代码都要提交一堆无意义的 .js 文件。
坑 7:@types/node 版本和 Node.js 运行环境不匹配
如果你的生产环境是 Node.js 18,但装了 @types/node@22,可能会引入一些不存在的 API 类型,导致你在本地开发时用了 Node 18 不支持的特性,上线后报错。
解决:@types/node 的版本尽量和生产环境的 Node.js 大版本保持一致。
第九步:如何发布为 npm 包
如果你的项目最终要发布到 npm,需要在 tsconfig.json 里确保以下配置正确:
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
然后在 package.json 里指定入口:
{
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
}
发布前的检查清单:
# 1. 类型检查
npm run type-check
# 2. 编译
npm run build
# 3. 检查 dist/ 目录内容
ls -la dist/
# 4. 预览包内容(确认没有意外文件)
npm pack --dry-run
完整的 tsconfig.json 参考模板
把上面所有讲解整合起来,给你一个开箱即用的模板:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInImports": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}
根据你的实际需求,可以做以下调整:
| 需求 | 调整方式 |
|---|---|
| 用 Jest 写测试 | 在 include 加 "tests/**/*",并配置独立的 tsconfig.test.json |
| 用 ES Module 但不用 NodeNext | 改为 module: "ESNext",moduleResolution: "Bundler" |
| 纯浏览器项目 | lib 加 "DOM",moduleResolution 改为 "Bundler" |
| 想支持老版本 Node.js(14) | target 改为 "ES2020",module 改为 "CommonJS" |
总结
TypeScript 项目搭建看起来复杂,但核心就三件事:选对配置、分清目录、写对导入路径。
- 配置上,
strict: true+NodeNext模块系统是最稳妥的起点 - 目录上,按功能分层而不是按文件类型分层
- 导入路径上,记得加
.js扩展名,记得在package.json里设"type": "module"
把这几点记牢,剩下的就是写代码时让 TypeScript 帮你 catching bug 了。
