先记住这个答案
对于确实存在但缺少类型的 JavaScript 包,可以在被项目读取的脚本式 .d.ts 文件中写 declare module "包名",并在块内描述真实导出。包名要匹配消费者的模块说明符,函数签名和默认或具名导出也要与运行时一致。文件顶层的 import 或 export 会使同样的语法转为模块增强语境,不能随手加 export {} 后仍当作新建环境模块。只写空的 declare module 会把该模块放宽为 any,适合明确受控的过渡,不能等同于完成了类型建模。
- 环境模块声明描述已有运行模块的缺失类型
- 顶层是否为模块会改变 declare module 的含义
- 空声明消除报错的同时也失去调用检查
声明块内部写实际可用的导出
如果消费者调用 greet(name, options),需要检查 name 的类型、options 是否可选,以及结果是否同步返回字符串。不能根据函数名字猜它一定返回 Promise,也不能把 require 导出对象和 ESM 默认导出随意互换。
示例只覆盖一个具名函数与选项接口,文件本身没有顶层 import 或 export。消费者在另一个实现文件中正常导入 greet,编译器就能按这里的参数契约检查调用;传入不存在的风格值应被拒绝。
// types/tiny-greeting.d.ts
// 本文件顶层不添加 import 或 export。
declare module 'tiny-greeting' {
export interface GreetingOptions {
style?: 'short' | 'formal';
}
export function greet(name: string, options?: GreetingOptions): string;
}模块说明符 tiny-greeting 对应消费者的导入名称,GreetingOptions 是这个模块的导出类型。若实际库采用其他导出形式,应调整声明;不能仅为了让当前 import 写法通过而编造默认导出。
文件作用域为何会改变语法含义
脚本式声明文件中的字符串模块块可以定义环境模块;文件变成外部模块后,同样的 declare module 通常用于增强可解析的已有模块。给无类型库补声明时添加一个顶层类型导入,可能导致原来的声明不再按预期提供整个模块类型。
需要引用其他类型时,可以把类型导入放到环境模块块内,或使用合适的 import 类型表达式,保留外层脚本语境。还要核对项目模块检测和实际编译输入,不能只看文件扩展名就断定它一定采用想要的作用域。
避免把缺类型和路径错误混在一起
先用实际运行或打包解析确认模块存在,再检查项目是否包含这份 .d.ts。路径拼写、包子路径、exports 条件或错误 tsconfig 都可能造成导入问题。为错误包名加声明会隐藏定位线索,却不能提供运行时实现。
临时空声明虽然可以推进迁移,但调用会失去参数和返回值检查,应记录适用范围并逐步补齐。完成声明后最好放入正确与错误调用样本,并在真实库版本下验证返回行为,防止声明和实现各自通过却互不一致。
容易答错的地方
- 所有 .d.ts 都应该无条件加 export {}
- 外部模块标记会改变环境模块声明的语境,适用于全局增强等场景,但不是所有声明文件的统一写法;要先明确当前文件负责哪种契约。
- declare module 可以修复包没有安装的问题
- 它只提供类型信息,运行时解析仍可能失败;应先确认依赖和路径正确,再补真正缺失的类型,不要把两个问题混在一起。
面试官还会怎么问?
包的子路径也自动受到根模块声明覆盖吗?
不一定,导入说明符是匹配依据。使用 tiny-greeting/extra 等子路径时,需要对应的类型入口或合适声明,不能假设根包声明覆盖所有路径。
可以只声明项目实际用到的函数吗?
可以先提供经过核对的最小范围,但应让未声明导出保持可见的缺失提示,避免用宽索引或 any 假装整个库都已经完整建模。
已有类型但漏了一个接口字段时怎么办?
应评估模块增强或上游修复,保留原有导出。重新写一个环境模块覆盖原契约,可能让其他原本可用的成员丢失类型信息。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。