发布带类型的包
自己维护 npm 包时,目标是用户 npm i your-lib 之后不用装 @types/xxx 就有完整提示。
最小配置
{
"name": "my-lib",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": ["dist"]
}四个关键字段:
| 字段 | 作用 | 给谁看 |
|---|---|---|
types | 老式类型入口兜底 | 旧版 TS / 老工具 |
exports["."].types | 现代类型入口 | TS 4.7+ / Node |
main | CJS 入口 | Node require |
module | ESM 入口 | 打包器(非标准但广泛支持) |
exports 里的 types 必须放在第一位。条件是按顺序匹配的,放在 import / require 后面就永远轮不到。
生成声明文件
{
"compilerOptions": {
"declaration": true,
"declarationMap": true, // 点击跳转回源码,而不是跳进 .d.ts
"emitDeclarationOnly": true, // 只出声明,JS 交给别的工具
"outDir": "dist",
"rootDir": "src"
}
}完整构建流程通常是两条腿:
{
"scripts": {
"build:types": "tsc -p tsconfig.build.json",
"build:js": "tsup src/index.ts --format esm,cjs --dts false",
"build": "npm run build:types && npm run build:js"
}
}或者直接用 tsup / unbuild 这类工具,它们内置了 .d.ts 生成(底层还是 tsc 或 rollup-plugin-dts)。
多入口
每个子路径导出都要单独生成声明:
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./utils": {
"types": "./dist/utils.d.ts",
"import": "./dist/utils.js"
}
}
}忘了声明某个子路径,用户 import 'my-lib/utils' 会直接失败(Node 的 exports 是白名单)。
打包声明:要不要 bundle
两种策略:
| 策略 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 不打包 | tsc 直接输出,目录结构和源码一致 | 简单、跳转准确 | 用户要能解析到所有内部 .d.ts |
| 打包成一个 | 用 rollup-plugin-dts / tsup --dts 合并成单个 .d.ts | 一份文件、无内部路径泄漏 | 依赖类型必须可解析 |
小库推荐打包成单文件;大库(有复杂内部引用)保持目录结构。
打包时注意:外部依赖不要打进去,只打包自己的类型。
暴露类型给用户
用户需要能 import type { Config } from 'my-lib',所以入口要 export type:
// src/index.ts
export { createClient } from './client'
export type { Config, ClientOptions } from './types'
export type { Result, StreamEvent } from './stream'用 export type 而不是 export,配合 verbatimModuleSyntax 更干净。
版本兼容声明
如果你的类型依赖某个最低 TS 版本:
{
"typesVersions": {
"<4.7": {
"*": ["./dist/legacy/*"]
}
}
}多数情况下不需要。更常见的是声明支持的 Node / TS 版本:
{
"engines": { "node": ">=18" }
}发布前自检清单
- [ ]
npm pack --dry-run看看实际会上传哪些文件(.d.ts在里面吗?) - [ ] 确认
files或.npmignore不会把dist排除掉 - [ ] 用一个全新的测试项目
npm i本地产物,确认不用装@types - [ ] 测两种导入方式:
import和require - [ ] 测
moduleResolution三种配置下的解析(Bundler / NodeNext / Node10) - [ ] 确认
exports里types在第一位
本地验证的最快方式
# 在包目录
npm pack
# 在测试项目
npm i ../my-lib/my-lib-1.0.0.tgz然后写一个 test.ts 尝试导入,看有没有提示。
常见问题
1. 用户报"找不到类型"
先确认三件事:
types/exports.types指向的文件真的存在(在npm pack的产物里)- 那个
.d.ts里真的export了你用到的名字 - 用户的
moduleResolution能理解exports字段(Node10 不行)
2. 提示跳到 any
通常是依赖的类型没解析到:
import type { Foo } from 'another-lib' // another-lib 没被用户安装如果你的 .d.ts 里 import 了一个 devDependency 的类型,用户那边就解析不到。解决:把这类依赖移到 dependencies,或者把那个类型内联进来。
3. skipLibCheck 掩盖了问题
你自己开发时开了 skipLibCheck,.d.ts 的错误不报,但用户那边(如果没开)会报错。发布前临时关掉跑一次:
tsc --noEmit --skipLibCheck false4. 声明文件和实现不同步
永远用 tsc 自动生成 .d.ts,不要手写。手写的一定会和实现漂移。
用工具简化
- tsup:零配置,内置 dts
- unbuild(unjs):rollup + dts,适合库
- publint:检查 package.json 的字段是否正确
- arethetypeswrong.github.io:在线检查你的包在各种配置下的类型解析结果
npx publint这两个工具能在发布前抓出 90% 的配置问题。
下一步
- 回到工程实践概览