☰
t3code:让类型安全贯穿全流程的TypeScript开发基线
2026/10/26 16:45:01 网站建设 项目流程

t3code这个项目,最开始只是我给自己整理的一套TypeScript开发基线,后来在团队里推广之后,还真把好多人从“配置地狱”里捞了出来。如果你也经历过从零搭项目时tsconfig、ESLint、Prettier、husky一坨配置来回折腾的日子,这个项目应该能给你一些直接的参考。它不是某个脚手架全家桶,而是把TypeScript从语言、工具链到工程化流程重新梳理了一遍的实践方案,核心目标就一句话:让类型安全贯穿开发全流程,让代码规范变成自动化的习惯。

在动手之前我先把项目拆成了四层来看:语言层的编译配置、规范层的lint与格式化、体验层的开发与测试工具、流程层的提交与CI校验。每个阶段都有明确的对象和方法论,这样无论你拿到的是全新空目录还是历史遗留乱项目,都能照着这套思路自上而下理顺。尤其适合前端、Node端和全栈方向的朋友拿来做基线。

1. 整体设计与思路拆解

1.1 t3code要解决的真实痛点

我见过太多项目倒在“配置”这一步上:同事拉下仓库跑不起来,原来是tsconfig里路径别名压根没配;新同学写了个接口调用,ESLint报了几十个红色波浪线却不知道怎么修;两三个项目之间复制粘贴的ESLint规则互相冲突,谁都不敢动;更常见的是没有pre-commit钩子,格式化全靠人肉,代码风格随缘。

这些问题表面上是“工具不好用”,本质上是没有把配置当成工程的一部分来设计。t3code一开始的目标就是消灭这些“隐性成本”,用一套统一的、可解释的配置,把技术基础设施踩平。具体来说就是:编译阶段管住类型,静态检查管住代码质量,格式化管住风格,Git钩子和CI管住流程,四个环节各司其职、互相补充。

1.2 设计原则:Type-first、约定大于配置

t3code这个名字,浓缩了三个关键词:Type-safe、Tooling、Template。也就是“以类型安全为核心、以工具链为支撑、以模板化交付为手段”。我不喜欢把项目命名搞得很玄,但确实想通过这个名字提醒自己,也提醒使用者:在引入任何框架、任何运行时特性之前,先回答好“类型这件事做扎实了没有”。

这正是t3code和市面上一些全家桶脚手架最大的区别。很多脚手架把重点放在“一键生成页面”和“内置状态管理”上,配置只是附带品;t3code反过来,配置才是主角。它走的是“约定优于配置”的路子:约定统一的目录结构、统一的命名方式、统一的脚本入口,然后把约定固化到配置和自动检查里,减少团队讨论成本。

另外有个思路值得分享:t3code采用“渐进式增强”而非“全量标配”。最小可用集只包含TypeScript、ESLint、Prettier和基础的Git钩子;当项目确实需要测试、Mock、路径别名、CI时再逐层叠加。这样做的好处很明显——小型脚本项目不会被一堆工具拖慢速度,大型项目又能向上兼容,不会回头重构。

2. 核心细节解析与实操要点

2.1 TypeScript配置逐项解读

tsconfig.json是整个项目的“地基”,这里面的每一个选项都不是凭空加上的。我在t3code里最看重三个地方。

第一个是strict。很多人问“为什么一定要开严格模式”,我的回答是:TypeScript最大的价值就是静态类型检查,不开strict等于把汽车买回来但只骑自行车。strict模式下,null、undefined、隐式any都会被严肃对待,前期确实会有阵痛,但救回来的都是上线前的低级错误。团队规模越大,这个选项越不能关。

第二个是moduleResolution。新一代工具链比如Vite、tsx、Vitest都基于ESM,所以t3code里我直接使用"moduleResolution": "Bundler",配合"module": "ESNext",让TypeScript理解“通过打包器解析模块”的语义。这比传统的Node10或Node16要省心,而且对路径别名、JSON导入的支持也更好。

第三个是路径别名paths。项目稍微一复杂,到处都是../../../utils/format这种串,看着就头疼。t3code统一把@/*指向src/*,同时会在ESLint、Vitest里做对应的解析配置,保证编译、检查、测试三套工具都认识同一个别名。

下面这份是我目前生产级别项目在用的精简版本:

{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "lib": ["ES2022", "DOM", "DOM.Iterable"], "strict": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, "noUncheckedIndexedAccess": true, "baseUrl": ".", "paths": { "@/*": ["src/*"] }, "outDir": "dist", "sourceMap": true, "declaration": true }, "include": ["src", "tests", "configs", "eslint.config.js"] }

这里特别注意两个容易被忽略的选项:isolatedModules关系到单文件转译工具(比如esbuild、tsx)能否正常工作,不开的话遇到const enum这类语法会在转译层炸掉;noUncheckedIndexedAccess则会让数组下标和对象取值变成可空类型,倒逼你处理边界情况。这段配置里的每一项踩过坑,后面会展开讲。

2.2 ESLint 9.x flat config:从rc时代到扁平化

ESLint在9.x正式切换到了扁平化配置(flat config),t3code当然跟进。旧版的.eslintrc依赖层层继承和插件注册,新项目用一个eslint.config.js就能说清楚全部规则,这不仅是写法变化,更解决了两个长期问题:一是原生支持ESM,二是去掉了--ext和ignore的困扰,忽略文件直接在配置里用ignores字段声明。

t3code的ESLint配置核心围绕typescript-eslint这个生态展开。新版中我建议直接用项目服务模式(projectService),而不是手动指定tsconfig.json路径。这是从旧版parserOptions.project演进过来的能力,能自动用最接近当前文件的tsconfig做类型感知规则检查,既省心又规避了“多个tsconfig解析串台”的老毛病。

import eslint from '@eslint/js' import tseslint from 'typescript-eslint' import prettier from 'eslint-config-prettier' export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommended, prettier, { files: ['**/*.ts'], languageOptions: { parserOptions: { projectService: true } }, rules: { '@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }] } }, { ignores: ['dist/', 'node_modules/', 'coverage/'] } )

这里有个设计考量:为什么不把prettier直接装成ESLint插件?因为格式化规则和代码质量规则本质是两类问题,合并在一起只会让ESLint越来越臃肿、越来越慢。t3code里把eslint-config-prettier放在末尾,把所有可能与Prettier冲突的格式类规则关掉,让Prettier专心做排版。

2.3 格式化与编辑器体验的一致性

代码风格统一这件事,光靠人的意志力绝对不行,必须靠工具。t3code里Prettier的配置遵循一组相对稳定的默认值,但我会显式声明endOfLine: 'lf'。你可能以为这不重要,直到有人在Windows上提交了一堆CRLF换行,diff里满屏红色,才追悔莫及。

export default { semi: false, singleQuote: true, trailingComma: 'all', printWidth: 100, tabWidth: 2, endOfLine: 'lf' }

有了Prettier,还需要在编辑器层面配合。t3code会在项目根目录放一个.editorconfig和.vscode/settings.json,前者让任何编辑器都保持同样的缩进和字符集,后者强制VS Code在保存时自动执行格式化。两者组合下来,本地开发几乎不会再有“我这边格式化完,推上去就冲突”的情况。

这里还要单独说一句:Prettier的trailingComma: 'all'会让ESM时代的导入语句更整洁,也降低后续增加字段时diff的噪声。很多团队纠结这个要不要开,我的建议是直接开,配合import/order整理排序,提交记录会好看很多。

2.4 开发与测试基础设施

开发期跑TypeScript,t3code默认推荐tsx而不是ts-node。原因很现实:ts-node在ESM项目里配置繁琐,还需要额外处理loader兼容;tsx基于esbuild,启动快,对ESM支持直接,实测下来几乎没有需要额外调的场景。配合tsx watch,开发体验完全够用。

测试方面选Vitest,而不是Jest。Vitest不仅能直接读tsconfig里的路径别名,还天然支持ESM和TS,不需要额外配置babel-jest或者ts-jest那套折腾。t3code里的测试配置非常薄,一个vitest.config.ts就搞定:

import { defineConfig } from 'vitest/config' import { resolve } from 'node:path' export default defineConfig({ test: { include: ['tests/**/*.test.ts'], environment: 'node', globals: true }, resolve: { alias: { '@': resolve(__dirname, 'src') } } })

这套组合对新人也很友好,不需要了解一堆历史包袱,装完就能写测试。而且Vitest的断言风格和Jest几乎一致,迁移成本也低。

3. 实操过程与核心环节实现

3.1 从空目录搭建一套骨架

有了前面的设计思路,现在落实到实际搭建。t3code的目录结构并不复杂,但每一层的归属都说得清楚:

my-project/ ├── configs/ │ ├── tsconfig.base.json │ └── vitest.config.ts ├── src/ │ ├── index.ts │ ├── utils/ │ └── types/ ├── tests/ │ └── index.test.ts ├── .husky/ │ └── pre-commit ├── .vscode/ │ └── settings.json ├── .editorconfig ├── .gitignore ├── eslint.config.js ├── package.json ├── prettier.config.js └── tsconfig.json

为什么要单独搞一个configs/目录?因为很多项目实际存在“多份tsconfig”的情况:构建用一份精简配置,类型检查用一份严格配置。把它们集中起来,避免根目录被配置文件占满,同时可以借助extends让子配置继承公共基线。例如tsconfig.base.json里放绝对通用的选项,tsconfig.json只做路径映射和包含目录的组装。

{ "extends": "./configs/tsconfig.base.json", "compilerOptions": { "paths": { "@/*": ["src/*"] } }, "include": ["src", "tests"] }

这样的模式非常适合中大型项目:基础配置只改一处,全项目同步生效。

3.2 安装依赖与脚本体系设计

依赖管理我默认用pnpm。不是因为追新,而是pnpm对ESM和monorepo支持得最稳,磁盘占用也小。初始化命令就三板斧:

pnpm init pnpm add -D typescript eslint @eslint/js typescript-eslint prettier eslint-config-prettier tsx vitest husky lint-staged

依赖装好之后,package.json的scripts才是日常开发真正接触的入口。t3code里的脚本要求做到“语义自解释”,让任何人不用看文档都大概知道怎么运行。

{ "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc -p tsconfig.json", "type-check": "tsc --noEmit -p tsconfig.json", "lint": "eslint .", "format": "prettier --write .", "format:check": "prettier --check .", "test": "vitest run", "test:watch": "vitest" } }

你可能会问,type-check和build都调用了tsc,区别在哪?build会产出dist目录,用来发布;type-check只做类型检查、不产出任何文件,适合在CI和pre-commit中快速跑一遍,几秒钟就能反馈。实际项目中我把它们放在同一个流程里,先type-check再build,任何类型错误都不会进入产物。

3.3 配置文件逐段落地

接下来是t3code的精髓:所有配置必须可解释、可维护。ESLint配置上面已经给过,这里重点说下Prettier、EditorConfig与Husky之间的交接。

.editorconfig内容很简单,却能在Vim、Sublime、VS Code之间保持一致:

root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true

.vscode/settings.json主要是触发保存格式化,同时让ESLint接管自动修复:

{ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "typescript.tsdk": "node_modules/typescript/lib" }

其中typescript.tsdk这个设置容易被忽略:它让VS Code使用项目本地安装的TypeScript版本,而不是编辑器内置的全局版本。如果你在项目里用了某个TS新特性,本地版本和全局版本不一致时,这一步能省掉一堆“明明代码没错,编辑器却报错”的乌龙。

Husky部分,新版Husky直接在项目根目录放.husky/pre-commit文件:

npx lint-staged

然后在package.json里配置lint-staged的匹配规则。我只在钩子里格式化并检查“本次改动的文件”,而不是全量检查,这能把pre-commit的耗时控制在几百毫秒内。

{ "lint-staged": { "*.{ts,tsx}": [ "eslint --fix", "prettier --write" ] } }

顺序上先ESLint再Prettier比较合理,因为ESLint的--fix可能改完代码风格后产生新的格式差异,Prettier再兜底一次。

3.4 用CI把规范固化成硬门槛

本地钩子总归可能被--no-verify跳过,所以t3code最后一步是把同样的检查复制到CI里,形成硬门槛。GitHub Actions的配置写起来不复杂:

name: CI on: push: branches: [main] pull_request: jobs: verify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 9 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile - name: Lint run: pnpm lint - name: Type check run: pnpm type-check - name: Test run: pnpm test - name: Build run: pnpm build

这里有个容易被忽略的细节:--frozen-lockfile。如果没有它,CI可能会悄悄更新依赖版本,造成本地能过、CI却失败的情况。锁定lockfile后,每次依赖变化都走commit流程,依赖变更有了可追溯性。克隆仓库、提交分支、PR检查,整套流程就是开发者的日常高速公路。

4. 常见问题与排查技巧实录

4.1 类型系统的三座大山

第一座大山是第三方库缺少类型声明。现在的TS生态比以前好太多,但总有年代久远的包没带类型也没人补DefinitelyTyped。t3code的处理方式是在src/types目录放一个global.d.ts,针对性地做本地声明,而不是无脑写any。

declare module 'some-untyped-package' { export function doSomething(input: string): Promise<void> export const VERSION: string }

这样既让TypeScript不再报错,也让调用方看到明确的函数签名。比declare module 'some-untyped-package'直接裸露一个any要严谨得多。

第二座大山是泛型推导卡壳。尤其是封装API请求、事件总线这类高复用模块时,泛型套多了容易把自己绕晕。我的经验是:宁可拉开代码写明确类型,也不要硬凑绝妙的单行泛型。代码是给人读的,其次才是让机器满意。

第三座大山是null和undefined的地狱。开了strict之后,maybeNull?.value这种链式访问会逼着你处理每一个可能的空值。说实话,这正是strict的意义所在,但这些检查在长期演进的代码里会逐渐膨胀。t3code的实践是:数据入口做收窄,内部业务逻辑尽量保证非空语义,不要在每一层都重复判空。

4.2 工具链之间的隐形冲突

最常见的一个冲突就是ESLint和Prettier对同一段代码给出相反意见。其实解决方式很简单:把eslint-config-prettier放在规则的末尾,让所有格式类规则闭嘴。但很多人没注意顺序,配置一长串插件之后随便追加,结果冲突依旧。记住,prettier的配置在所有“规则/插件”之后才有效。

另一个冲突发生在“路径别名”上。代码里写了@/utils,TypeScript编译没问题,ESLint跑也没问题,结果Vitest一执行就报模块找不到。原因很简单——三套工具各认各的路径,必须在ESLint配置里配置import/resolver,在Vitest里配置resolve.alias。t3code把这些配置集中在一页文档里,目的就是防止这类“三缺一”的隐患。

还有一类问题来自Node版本不统一。比如TS 5.5正式支持了const类型参数,但CI镜像里的Node还停留在16,构建时语法错误一堆。t3code的经验是统一用.nvmrc或CI里的node-version固定大版本,同时把tsconfig里的target调到与Node版本匹配的ES版本。

4.3 高频异常排查速查表

下面这张表是我在实际使用t3code过程中遇到的问题集锦,几乎每周都能派上用场:

异常现象可能原因快速处理
Cannot find module '@/xxx'运行时没有别名映射检查tsconfig paths、打包器/测试工具alias、ESLint resolver
ESLint报parserOptions.project警告旧版typescript-eslint配置过时改成projectService: true并升级依赖
提交时pre-commit没触发Husky未安装或钩子无执行权限重新执行npx husky init,检查.husky/pre-commit是否有x权限
Windows上代码全部CRLF缺少endOfLine配置或git autocrlfPrettier加endOfLine: 'lf',.gitattributes统一
Vitest能跑但不识别TS类型未配置resolve.alias或environment按上文vitest.config.ts补齐
isolatedModules报const enum错误单文件转译不支持将const enum改为普通enum或as const对象
CI安装依赖报错锁文件冲突lockfile与package.json不一致本地pnpm install后提交新的lockfile

这张表的素材都很朴素,但每一条都对应我真实踩过的坑,尤其是Husky权限问题。新版的Husky在Windows和Linux上表现不太一样,如果发现钩子没生效,先别急着搜配置,跑一下ls -l .husky/pre-commit看有没有执行权限,十有八九是这里。

4.4 几个值得坚持的“笨办法”

最后聊几句方法论上的体会。t3code这套东西能跑起来并长期复用,靠的不是某个天才设计,而是几个“笨办法”:所有配置必须能解释为什么存在,不用的插件不要装,临时绕过问题的代码必须打上TODO标记。

我见过太多项目在ESLint配置里躺着一推废弃规则,在tsconfig里开着互相矛盾的特性。这种“僵尸配置”比没有配置更危险,因为后来者根本不敢动。t3code里我坚持做配置即文档,每个文件开头用注释写明用途、适用场景和变更记录。看起来多花了几分钟,但在半年后回来看自己的项目时,省下的是成倍的“考古时间”。

另外,建议每一个使用t3code的团队,都至少指定一位“配置责任人”,负责依赖升级、规则调整和配置评审。这不是权力集中,而是防止每个人各改一处,最后配置之间彼此打架。这样一个简单的角色分工,反而能让工具链长期保持可用,而不是半年后敌我不分。

我个人在实际操作中还有一个习惯:每次跑通一个新人项目的初始化流程,都会顺手把“从零搭建”这个过程记下来,形成一篇速查笔记。因为这能倒逼你把所有手动步骤不断精简,直到任何人在文档指导下都能十分钟内复制整个开发环境。这才是我做t3code真正想看到的局面——工具不该是门槛,而应该是每个开发者脚下扎实的地基。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询