ToolkitX
知识库工具箱

TS 项目实战

工程配置、声明文件、严格模式

25min·进阶

01. 从零搭建 TypeScript 项目

搭建一个 TypeScript 项目不光是装个包那么简单。现代 TS 项目推荐配套: 1. Node.js 项目——ts-node 或 tsx 直接运行 TS 文件,省去先编译再运行的步骤。 2. 前端项目——Vite 或 Next.js 自带 TS 支持,创建项目时就选 TS 模板。 3. 构建输出——tsc 编译成 JS 输出到 dist/ 目录。 4. 类型检查——tsc --noEmit 只检查不输出,放到 CI 和 pre-commit hook 里。 5. 格式化和 lint——Prettier + ESLint(typescript-eslint)保持代码风格一致。
bash
# Node.js 项目
mkdir my-project && cd my-project
npm init -y
npm install -D typescript @types/node tsx
tsc --init

# 运行 TS 文件
npx tsx src/index.ts

# 前端项目
npm create vite@latest my-app -- --template react-ts

# 只检查类型
npx tsc --noEmit

02. tsconfig.json 详细配置指南

一个生产级别的 tsconfig.json: target——编译输出到哪个 JS 版本。Node.js 18 推荐 ES2022。 module——模块系统。Node.js 用 commonjs 或 nodenext,前端打包项目用 ESNext。 moduleResolution——模块解析策略。node 或 bundler。 rootDir / outDir——源码和输出分开。 esModuleInterop——让默认导出和命名导出互操作更顺畅。 skipLibCheck——跳过 .d.ts 文件的类型检查(快很多)。 forceConsistentCasingInFileNames——文件名大小写一致(Windows 上区分,Mac 上不区分,不开容易出问题)。 declaration: true——生成 .d.ts 类型声明文件(如果你要发布为 npm 包)。 sourceMap: true——生成 source map,调试时能看到原始 TS 代码。
json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

03. 声明文件 .d.ts——给 JS 加类型

.d.ts 文件是纯类型声明文件——不包含运行时代码,只告诉 TypeScript「这个模块/对象的类型长这样」。 什么时候需要写 .d.ts: 1. 你用了一个没有官方类型的 JS 库——写 .d.ts 文件声明模块类型。 2. 你在项目中定义了全局类型或全局变量。 3. 你发布 npm 包——写 .d.ts 让用户获得类型提示。 declare module 'module-name'——给一个 JS 模块声明类型。如果只用到模块的少数几个方法,只声明你需要的就行。 declare global——声明全局变量。例如 declare var __VERSION__: string。 全局类型文件 types/*.d.ts——把常用的全局类型(如 API 响应的通用包装)放这里,不用到处 import。
typescript
// types/global.d.ts
// 声明一个没有类型的 JS 模块
declare module 'legacy-lib' {
  export function doSomething(input: string): number;
}

// 声明全局变量
declare var __VERSION__: string;
declare var __BUILD_TIME__: number;

// 声明静态资源导入
// declare module '*.svg' {
//   const content: React.FC<React.SVGProps<SVGSVGElement>>;
//   export default content;
// }

declare module '*.css' {
  const content: Record<string, string>;
  export default content;
}

04. ESLint + Prettier 的 TS 配置

TypeScript 的 lint 以前用 TSLint,现在已经废弃。现在都用 ESLint + @typescript-eslint。 核心包:eslint、@typescript-eslint/parser、@typescript-eslint/eslint-plugin。 Prettier 负责格式化(缩进、引号、分号),ESLint 负责代码质量(未使用的变量、错误的类型断言、detect async 里的 await 缺失)。两者分工明确。 eslint-config-prettier——关闭 ESLint 里跟 Prettier 冲突的规则。确保顺序:ESLint 先跑,Prettier 后格式化。 CI 里加上 npx eslint 和 npx prettier --check,以及 npx tsc --noEmit。三道关过不了不能合并。
bash
# 安装
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
npm install -D prettier eslint-config-prettier

# .eslintrc.json
{
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ]
}

# package.json 的 lint 脚本
{
  "scripts": {
    "lint": "eslint 'src/**/*.{ts,tsx}'",
    "format": "prettier --write 'src/**/*.{ts,tsx}'",
    "typecheck": "tsc --noEmit"
  }
}
CI 管线里 typecheck、lint、format check 三个都跑一遍,确保代码质量。pre-commit hook 只跑 lint-staged(快),完整检查交给 CI。

05. 生产环境的 TS 最佳实践

1. strict: true 从项目第一天就开启。后面开会有几百个报错很难改。 2. 避免 any——用 unknown + 类型守卫替代。as any 越多 TS 的价值越低。 3. 类型收窄——联合类型变量用 if/switch 缩窄类型范围,不要到处 as 断言。 4. 泛型约束——extends 约束泛型参数,不要写成裸 T。 5. 给异步函数声明返回类型——避免意外返回 Promise<void> 而是 Promise<SpecificType>。 6. import type——只用到的类型用 import type 导入,编译后不留下 import 语句。 7. 善用 enum 替代(union type / as const object)——不用生成额外运行时代码的方案。
typescript
// 好的 TS 实践

// 1. 避免 any
function safe(data: unknown) {
  if (typeof data === 'string') {
    return data.toUpperCase();
  }
  return null;
}

// 2. import type——只导类型不导值
import type { User } from './types';
import { getUser } from './api';  // 这个是值,正常 import

// 3. as const 替代 enum
// const STATUS = {
//   ACTIVE: 'active',
//   INACTIVE: 'inactive'
// } as const;
// type Status = typeof STATUS[keyof typeof STATUS];
import type 在编译后会被完全删除——不会出现在输出的 JS 里。用这能避免循环依赖和减少打包体积。

知识测验

1/5正确 0

esModuleInterop 解决什么问题?