TypeScript 第四周 BOSS 战 — typed-utils 发布准备 + 第 1 个月毕业总结
三周前你手写了 15+ 个工具类型(第 2 周 BOSS 战),本周给它们装上了生产级配置:严格 tsconfig(Day 22)、双格式模块产物(Day 23)、声明文件(Day 24)、类型测试(Day 26)、工具链门禁(Day 27)。今天是毕业典礼:审计 package.json → 构建双格式产物 → npm pack 演练 → 完成"随时可发布"的完整包。然后是重头戏——第 1 个月(TypeScript 深入)的完整复盘:四个星期、28 天、从一个
let到一条完整类型工程流水线的全部路径。
目录
- 一、战斗目标与验收清单
- 二、第一步:package.json 全面审计
- 三、第二步:构建双格式产物
- 四、第三步:npm pack 演习
- 五、第四步:发布前终检
- 六、第 1 个月毕业总结:四周全景复盘
- 七、毕业博客写作指南
- 八、常见坑点与最佳实践
- 九、自测挑战
- 十、总结与知识图谱
一、战斗目标与验收清单
今日目标:把 typed-utils 推到"npm publish 只差一个账号"的状态
(真的发布是可选任务——重点是"具备随时发布的能力")
- [ ] package.json 审计通过(本包元信息 + 双格式入口 + files 白名单)
- [ ] 构建产出 dist/(ESM + CJS + .d.ts 三件套)
- [ ]
npm pack --dry-run产物清单干净(无源码泄漏、无多余文件) - [ ] 本地 link 验证(另一个项目里真的能用)
- [ ] 四个周的掌握度自评 + 薄弱点重修计划
- [ ] 毕业博客:《TypeScript 深入:一个月从语法到工程的完整路径》
二、第一步:package.json 全面审计
2.1 身份信息(用户在 npm 搜索页看到的)
{
// ===== 身份五要素 =====
"name": "typed-utils", // ⚠ 先去 npmjs.com 查重!
"version": "0.1.0", // 0.x = API 未稳定的约定
"description": "类型安全的工具类型库:深度只读、路径访问、元组操作等 15+ 工具类型",
// ↑ 一句话说清"是什么、给谁用"(搜索权重的一部分)
"keywords": ["typescript", "utility-types", "type-challenges", "types"],
"license": "MIT",
"author": "你的名字 <you@example.com>",
// ===== 仓库与问题追踪 =====
"repository": {
"type": "git",
"url": "git+https://github.com/you/typed-utils.git"
},
"bugs": "https://github.com/you/typed-utils/issues"
}
2.2 入口配置(本周知识的集大成)
{
// ===== 类型入口(Day 24) =====
"types": "./dist/index.d.ts",
// ===== 双格式入口(Day 23) =====
"exports": {
".": {
"types": "./dist/index.d.ts", // ⚠ 永远第一位(条件匹配顺序)
"import": "./dist/index.mjs", // ESM 用户走这里
"require": "./dist/index.cjs" // CJS 用户走这里
}
},
// ===== 老字段兜底(旧工具链的用户) =====
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
// ===== 发布白名单(Day 23 的 exports 是导入白名单,这是包内容白名单) =====
"files": ["dist"],
// ===== 模块声明 =====
"type": "module", // .js/.mjs 默认 ESM;.cjs 显式 CJS
"sideEffects": false // 纯类型+纯函数 → 允许 tree-shaking 全力摇树
}
2.3 依赖分类审计
{
// typed-utils 是纯类型库 —— dependencies 应该是空的!
"dependencies": {}, // ⚠ 运行时零依赖 = 类型库的美德
"devDependencies": {
"typescript": "^5.6.0",
"tsd": "^0.31.0",
"vitest": "^2.0.0",
"eslint": "^9.0.0",
"typescript-eslint": "^8.0.0",
"prettier": "^3.0.0",
"husky": "^9.0.0",
"lint-staged": "^15.0.0"
}
// 审计原则:
// - dependencies:用户运行时【必需】的(发布后会被一起安装)
// - devDependencies:构建/测试用的(用户不装)
// - ⚠ 类型库最常见的事故:把 typescript 放进 dependencies
// → 用户项目被强塞一份 TS(版本可能与他们项目冲突)
}
2.4 工程脚本全景
{
"scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit",
"lint": "eslint .",
"format": "prettier --write .",
"test": "vitest run",
"test:types": "tsd",
"build": "npm run build:esm && npm run build:cjs",
"build:esm": "tsc -p tsconfig.build.json --module ESNext --outDir dist/esm",
"build:cjs": "tsc -p tsconfig.build.json --module CommonJS --outDir dist/cjs",
"prepack": "npm run build", // ⭐ npm pack/publish 前自动构建(见第四节)
"prepare": "husky"
}
}
三、第三步:构建双格式产物
3.1 为什么类型库也要双格式
类型库的产物 = .d.ts(类型)+ 少量运行时代码(如果你写了函数工具)
- 纯类型(type/interface 导出)→ 编译后只剩 .d.ts,js 文件是空壳
- 但只要有一个运行时函数 → ESM/CJS 双格式的需求就真实存在
- 双格式的成本很低(两条 tsc 命令),收益是全部用户通吃
3.2 构建配置(tsconfig.build.json,Day 22 的伏笔)
// tsconfig.build.json
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"declaration": true, // 产出 .d.ts —— 类型库的产品本体
"declarationMap": true, // .d.ts.map:用户可以"跳转到源码"
"sourceMap": true,
"module": "ESNext" // 先出 ESM(命令行可覆盖)
},
"include": ["src"] // 排除 tests —— 老坑点重申
}
3.3 入口文件的模块约定
// src/index.ts —— 公开 API 的唯一出口
export type { Flatten, DeepReadonly, Get, Last, Union } from "./types/tuple.js";
export type { HubTopic, PayloadOf } from "./types/topic.js";
// ↑ 纯类型导出显式用 type(Day 23 的 isolatedModules 规范)
// 有运行时函数则正常导出:
// export { createValidator } from "./runtime/validator.js";
3.4 产物结构核验
dist/
├── esm/ # ESM 产物
│ ├── index.js #(空壳或运行时实现)
│ └── index.d.ts
├── cjs/ # CJS 产物
│ ├── index.js
│ └── index.d.ts
├── index.mjs # 复制自 esm/index.js
├── index.cjs # 复制自 cjs/index.js
└── index.d.ts # 复制自 esm/index.d.ts
// 简单方案:构建后用几个 copy 命令整理入口(package.json 指向根级文件)
// 进阶方案:tsup 一步到位(esm/cjs/dts 三合一)—— 了解即可,先掌握手动原理
四、第三步:npm pack 演习
4.1 npm pack --dry-run:发布彩排
npm pack --dry-run
# 输出产物清单(这就是即将进入 .tgz 的全部内容):
# npm notice
# npm notice 📦 typed-utils@0.1.0
# npm notice === Tarball Contents ===
# npm notice 1.1kB dist/index.mjs
# npm notice 0.8kB dist/index.cjs
# npm notice 3.2kB dist/index.d.ts
# npm notice 1.5kB LICENSE
# npm notice 2.0kB README.md
# npm notice === Tarball Details ===
# npm notice name: typed-utils
# npm notice version: 0.1.0
# npm notice total files: 5
4.2 清单审计要点
✅ 必须在包里的:
- dist/ 全部产物
- README.md(npm 页面的门面 —— 没有它的包等于没有说明书)
- LICENSE
❌ 绝不能泄漏的(files 白名单没写它们就不会进):
- src/ 源码 → 检查清单里有没有 src/
- tests/ 测试文件
- tsconfig*.json
- .eslintrc / .prettierrc
- node_modules(npm 默认排除,但 files 写错可能误伤)
⚠ 特殊文件永远包含(白名单拦不住):
- package.json、README、LICENSE / main 入口文件
4.3 prepack:自动化构建
{
"scripts": {
"prepack": "npm run build"
// npm 在 pack/publish 前自动执行 —— 忘记构建导致"发布旧产物"的经典事故
// 从此根除(CI 缓存场景要留意它的执行成本)
}
}
4.4 本地验证:npm link 模拟用户
# 在 typed-utils 目录:
npm link
# 全局注册了一个软链接
# 在另一个测试项目(比如 playground/):
npm link typed-utils
// playground/test.ts —— 完整模拟用户视角:
import type { Flatten, DeepReadonly } from "typed-utils";
type A = Flatten<readonly [1, 2, 3]>; // 1 | 2 | 3?
type B = DeepReadonly<{ a: { b: 1 } }>; // 嵌套 readonly?
// 验收三问:
// 1. 有没有类型提示(exports.types 链路通了吗)
// 2. 提示的内容对不对(.d.ts 的内容正确吗)
// 3. import type 与普通 import 都正常吗(Day 23 知识的真实验证)
五、第四步:发布前终检
发布前 60 秒 checklist(真实按这个顺序走一遍):
□ git status 干净(所有变更已提交)
□ npm run typecheck 通过
□ npm test 通过(vitest + tsd 全绿)
□ npm run lint 无 error(warning 有交代)
□ npm run build 成功,dist 产物齐全
□ npm pack --dry-run 清单审计(上节的 ❌ 项为零)
□ npm link 验证:下游项目提示正常
□ version 遵循 semver(首个版本 0.1.0)
□ README 的安装/导入示例【亲手复制跑通】
□ CHANGELOG.md 记录了 0.1.0(哪怕只有一行)
全部打勾 = 具备发布能力 ✅
(真的 npm publish 是可选项:npm login + npm publish —— 发了记得可以
npm unpublish 包名@版本 在 72 小时内撤回)
六、第 1 个月毕业总结:四周全景复盘
6.1 四周路径图
第 1 个月:TypeScript 深入(Day 1-28)
第 1 周 类型系统基础(Day 1-7)
基础类型 → interface/type → 联合/交叉 → 类型收窄/判别联合
→ 枚举/字面量 → keyof/typeof/索引访问 → BOSS:智能工厂类型建模
第 2 周 泛型(Day 8-14)
泛型函数 → 约束 → 泛型接口/类 → 内置工具类型
→ 条件类型/infer → 映射类型/模板字面量 → BOSS:typed-utils 诞生 ⭐
第 3 周 类型体操(Day 15-21)
入门方法论 → 元组 → 递归 → 字符串 → 对象 → 协变逆变
→ BOSS:30+ 题通关(Permutation)
→ 产出:四大套路口诀 + Equal/Expect 判题体系
第 4 周 工程化(Day 22-28)★ 本周
tsconfig → 模块系统 → 声明文件 → 装饰器
→ 类型测试 → 工具链 → BOSS:typed-utils 毕业为可发布包
6.2 四周掌握度自评表
| 能力域 | 自评内容 | 分数(1-5) | 薄弱则重修 |
|---|---|---|---|
| 类型语法 | 收窄/判别联合/枚举选型 | ☐ | Day 4-5 |
| 泛型思维 | 约束/推断/工具类型手写 | ☐ | Day 8-11 |
| 类型体操 | medium 独立解题 | ☐ | Day 16-19 对应天 |
| 变型理论 | 协变逆变推导 | ☐ | Day 20 |
| 工程配置 | tsconfig 每个开关的意义 | ☐ | Day 22 |
| 模块系统 | 互操作报错 30 秒定位 | ☐ | Day 23 |
| 声明文件 | 给任意 JS 库补类型 | ☐ | Day 24 |
| 装饰器 | 四类签名 + 工厂 + 元数据 | ☐ | Day 25 |
| 类型测试 | tsd 三断言的运用 | ☐ | Day 26 |
| 工具链 | 从零搭规范三件套 | ☐ | Day 27 |
规则:低于 3 分的能力域 → 下周抽一天重修对应 Day(文档都在,重做练习即可)。
6.3 关键里程碑验证(自问自答)
Q1:为什么泛型比 any 强?
A1:any 是"失忆",泛型是"占位待填"——类型信息全程流动(Day 8 心法)
Q2:分配律什么时候触发?
A2:裸类型参数 + 条件类型(T extends U 时 T 拆开判断)——包裹 [T] 即阻止(Day 21 Permutation)
Q3:函数参数为什么逆变?
A3:漏斗规则——宽进窄出才安全:接收 Animal 的函数才能安全处理 Dog(Day 20)
Q4:import type 解决什么问题?
A4:单文件编译器不用猜"该不该擦除这条 import"(Day 23)
Q5:为什么类型库需要类型测试?
A5:类型回归是编译期事故,运行时测试零发现(Day 26 的 readonly 案例)
每题都能不假思索答出 = 这一个月真的学到手了。
七、毕业博客写作指南
标题:《TypeScript 深入:一个月从语法到工程的完整路径》
推荐大纲:
## 一、起点与终点
- Day 1 的我:分不清 interface 和 type
- Day 28 的我:拥有一个带完整质量流水线的可发布类型库
## 二、四周爬坡路径(每周一图)
- 第 1 周:从"标注"到"收窄"——类型开始参与逻辑
- 第 2 周:从"重复"到"工厂"——泛型让类型可复用(typed-utils 诞生)
- 第 3 周:从"使用"到"创造"——30+ 道体操题的方法论
- 第 4 周:从"个人"到"工程"——配置/模块/测试/工具链
## 三、五个认知跃迁(月度最值钱的五句话)
1. 类型是逻辑:收窄与判别联合让分支代码自己证明自己
2. 泛型是类型的函数:占位待填,而不是失忆的 any
3. 类型体操的终点是业务:HubTopic 拦截 100 个运行时 bug
4. 配置是防御工事:strict 八开关各守一道防线
5. 质量要流水线化:从保存到 CI 的四级门禁
## 四、踩坑 Top5 与修复
(从四天的"坑点"章节里挑你最痛的五个)
## 五、typed-utils 的诞生记
- 第 2 周的手写 15 个工具类型
- 第 4 周的类型测试 + 双格式构建 + 发布审计
- (附)npm pack 清单截图
## 六、下个月的路线
Canvas 2D + 实时通信(WebSocket/MQTT)——类型能力将全部用于实战
八、常见坑点与最佳实践
坑点 1:发布后才发现 types 没排第一
症状:用户 install 后"找不到声明文件",但你本地明明正常
原因:本地 TS 从 node_modules 源码猜到了类型;用户只拿到 dist
修复:exports.types 永远第一 + npm link 用【空项目】验证(不能靠源码就近猜测)
坑点 2:types 字段指向了不存在的文件
npm pack 不校验 types 指向的文件是否存在!
终检时必须 ls dist/ 逐个核对 exports 里写的每个路径
坑点 3:忘了 prepack 导致发旧产物
症状:npm 上的版本没有最新修复
原因:手动 build 后又改了代码,publish 时 dist 是旧的
修复:prepack 脚本自动构建(2.4 节)—— 让"忘记"在物理上不可能
坑点 4:semver 语义用错
0.x.y 阶段:minor 变化也可以有 breaking(npm 对 0.x 的约定)
1.0.0 之后:必须严格 semver ——
breaking → major(删导出、改类型形状)
新功能 → minor
修复 → patch
类型库特别提醒:类型收紧( narrowing)对下游就是 breaking!
Flatten 返回从 any 变具体类型 = patch?不,是 major(下游可能依赖了 any 的宽容)
坑点 5:毕业总结变成流水账
❌ 按天罗列"Day 1 学了 X,Day 2 学了 Y"
✅ 按【认知跃迁】组织:"我原来以为 X,四周后理解成 Y"
毕业博客的价值在于记录思维的转变,不是课程的目录
九、自测挑战
挑战 1(基础):字段对号入座
package.json 的以下字段分别解决用户的什么问题?
a. exports(含 types/import/require 三条件)
b. files
c. sideEffects: false
d. dependencies vs devDependencies
e. prepack
挑战 2(进阶):产物排错
用户反馈:import { Flatten } from "typed-utils" 提示
"Module 'typed-utils' has no exported member 'Flatten'"
但你源码里明明导出了。列出 5 个可能的检查点
(提示:从 exports 链路 → .d.ts 内容 → 构建时序 逐环排查)
挑战 3(实验):完整发布演练
1. npm pack --dry-run → 审计清单(本文 4.2 的 ❌ 项为零)
2. npm pack → 解压 .tgz(tar -tzf)核对内容
3. npm link + 空项目验证三问(4.4 节)
4. (可选)npm publish → npm unpublish 撤回演练
全程截图,作为博客素材
挑战 4(论文级):月度毕业答辩
对着 typed-utils 的最终形态,向橡皮鸭做 10 分钟毕业答辩:
1. 这个库解决什么问题?(每个工具类型一个场景)
2. 它的质量防线有几道?各守什么?
3. 如果下周要发 1.0.0,还差什么?
4. 四周里哪个认知跃迁对你影响最大?为什么?
十、总结与知识图谱
第 4 周 BOSS 战(Day 28)
│
├── package.json 审计
│ ├── 身份五要素(name/version/description/keywords/license)
│ ├── 双格式入口(exports:types → import → require)
│ ├── files 发布白名单
│ └── 纯类型库的依赖美德(dependencies 为空)
│
├── 双格式构建
│ ├── declaration: true(.d.ts 是产品本体)
│ ├── ESM + CJS 两条 tsc 命令
│ └── 产物入口整理(index.mjs/.cjs/.d.ts)
│
├── npm pack 演习
│ ├── --dry-run 清单审计(无 src/tests/配置泄漏)
│ ├── prepack 自动构建(消灭"发旧产物")
│ └── npm link 空项目验证(用户视角三问)
│
└── 第 1 个月毕业
├── 四周路径:语法 → 泛型 → 体操 → 工程
├── 十能力域自评 + 薄弱重修
├── 五个认知跃迁(毕业博客的骨架)
└── 下一站:Canvas 2D + 实时通信(第 2 个月)
一句话总结:第 1 个月的旅程是一条完整的抛物线——从"给变量贴标签"(类型标注)到"给类型写工厂"(泛型体操),再到"给类型配质量流水线"(工程化);typed-utils 是这条抛物线的物证:第 2 周它只有 15 个手写类型,今天它拥有类型测试、双格式产物、提交门禁和一份干净的 pack 清单——这就是"从会写类型到会做类型工程"的距离,而你走完了。
下月预告:第 2 个月进入 Canvas 2D + 实时通信——画板(坐标系/路径/贝塞尔)、图片滤镜(像素操作)、流程图编辑器(进阶)、WebSocket/MQTT 实时数据推送。你四周练出的每一个类型(判别联合守卫事件、模板字面量约束 topic、泛型封装消息总线)都将在工业可视化场景里真刀真枪地服役。