先记住这个答案
声明生成需要把导出 API 的类型写成消费者可以解析的 .d.ts。如果推断结果依赖外部模块中无法命名或无法公开引用的符号,就可能出现 TS4023 等诊断,即使普通实现类型检查通过。应从报错的导出值追踪其推断类型,判断内部细节是否真的属于公共契约;可通过明确的公共接口收窄暴露范围,或让确实需要公开的符号具备合法引用路径。不是所有未 export 的辅助类型都会报错,也不能把这个问题简单理解为类里使用了 private 关键字。
- 声明生成额外要求公开类型能够被表达和解析
- 先判断内部类型是否真的应该进入公共 API
- 修复后检查生成声明并用独立消费者验证
外部唯一符号为何会进入推断结果
对象上带有唯一符号键时,推断类型会保留这个键的身份。另一个模块导出包装对象后,公开类型也可能间接包含该符号。如果符号没有可供外部使用的名称,编译器就难以在包装模块的声明中表达这份结构。
下面 model.ts 内部的 key 未导出,index.ts 又把完整 model 结构推断到 wrapper。普通类型检查可以理解它们之间的关系,但开启声明生成时 wrapper 会触发 TS4023。不要把错误出现的位置误认为真正定义问题的文件位置。
// file: model.ts
const key = Symbol('private');
export const model = { id: 'u1', [key]: 1 };
// file: index.ts
import { model } from './model';
export const wrapper = { model };两个文件参与同一程序时实现类型检查可以通过,但 wrapper 的声明需要描述来自另一个模块的私有唯一符号。示例没有使用类 private 成员,问题来自导出类型的跨模块可表达性。
用明确公共接口隐藏不必要的内部细节
如果消费者只需要 id,就不该让符号键成为公共契约的一部分。给导出值加一个准确的公共类型注解,可以保留真实需要的字段,同时避免把内部结构泄漏到声明中。这是设计边界,不是把结果强行断言成不相干类型。
下面只替换 index.ts,model.ts 保持原实现。PublicModel 在当前文件中作为辅助接口,可以随生成声明一起保留,并不要求所有辅助类型都必须单独 export。消费者能读取 wrapper.model.id,但不能再通过这个公开类型依赖内部符号键。
// file: index.ts
import { model } from './model';
interface PublicModel { id: string; }
export const wrapper: { model: PublicModel } = { model };显式注解要求实际 model 满足公开字段,同时收窄导出契约。编译器可以在 index.d.ts 中保留辅助接口并声明 wrapper,不需要把外部私有符号写进这个入口的公共形状。
修复需要兼顾产物完整性与消费方式
如果符号确实是用户需要的 API,应考虑显式导出和合法导入引用,让声明能准确表达其身份。具体修复取决于推断结构和模块路径,不能把所有 TS4023 都归结为缺少某一句 import。应缩小复现并检查当前编译器的实际输出。
用 any、关闭声明生成或忽略错误都可能让发布问题继续存在。构建失败后还要注意输出目录中可能有部分旧产物或部分新产物,不能把它们一起发布。完成修复应重新生成声明,并在消费项目中验证所承诺的公开字段和错误使用边界。
容易答错的地方
- 所有没有 export 的接口都会导致私有名称错误
- 编译器可以在某些声明文件中保留本地辅助类型,示例修复就使用了这种方式;关键是最终导出类型能否被正确表达和引用。
- skipLibCheck 可以可靠解决声明生成失败
- 它主要影响声明文件检查范围,不会替导出 API 创造缺失的名称和引用路径;应修复实际公共类型边界,而不是隐藏诊断。
面试官还会怎么问?
显式公共类型注解会损失推断信息吗?
会有意限制消费者可见的细节。应确认被隐藏信息本来就不属于公开契约;若用户确实需要它,则应设计合法可引用的公开类型。
为什么直接导出 model 没错,包装后却报错?
原模块可以在自己的声明里保留私有符号,而包装模块需要跨模块表达它。类型能否序列化取决于声明所在上下文,不只取决于值本身。
必须把所有内部类型都导出才能发布吗?
不必,应只公开需要承诺的契约。过度导出会增加兼容负担,优先检查明确公共接口能否准确描述用户真正需要的能力。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。