先记住这个答案
declare global 用于从外部模块内部向全局作用域增加或增强声明,补丁文件通常用 export {} 等方式建立模块语境。应使用业务专属名称,只描述确实由宿主或初始化代码提供的能力,并按真实生命周期决定字段是否可选。扩展 Window 不会在 Node 中创建 window,也不会自动把属性写到浏览器全局对象上。多个声明中的同名属性需要兼容,不能靠重复声明覆盖旧类型;共享库尤其应谨慎引入全局增强,避免让所有消费者被迫接受不属于其运行环境的假设。
- 全局增强影响整个类型程序,需要收紧命名和范围
- 声明不会初始化全局对象或保证属性总是存在
- 重复成员要兼容,跳过声明检查不能修复契约冲突
模块文件怎样有意进入全局作用域
普通外部模块中的接口属于该模块自身,直接写 interface Window 不会自然扩展浏览器的全局 Window。使用 declare global 可以明确表达这是全局补充,文件外层的 export {} 则建立合法模块语境,避免随手散落全局声明。
示例把启动配置放在专属属性中,并保留可选标记,因为并非每个页面都保证已经注入。消费者需要先检查属性再读取配置。Readonly 约束静态写法,不会冻结运行时对象,也不会证明地址内容已经通过业务校验。
// types/app-window.d.ts
export {};
declare global {
interface Window {
__INTERVIEW_APP_CONFIG__?: Readonly<{ apiBase: string }>;
}
}这个声明只让类型程序认识 Window 上的可选属性。它不会创建属性值,不能替代浏览器启动时的真实注入;调用者仍需处理 undefined,也不能因为名字出现在类型里就在服务端访问 window。
全局命名与声明合并需要集中维护
把全局属性叫 config、data 或 utils 很容易与其他脚本冲突。可以把相关配置收拢到一个业务专属属性下,减少散落键,但仍要控制其内容和初始化责任。不要因为方便就把模块内部工具函数全部暴露成全局能力。
同名 Window 属性在不同声明中必须保持兼容,不能在一个文件写字符串、另一个文件写对象,并期待后者覆盖前者。类型包或测试环境可能带入额外全局声明,出现冲突时应追踪实际来源和版本,避免只通过跳过库检查让错误暂时消失。
运行环境隔离比类型名称更重要
浏览器、服务端和测试运行器可能使用不同全局对象。把所有环境声明放进同一配置会让编辑器允许某些在当前环境根本不存在的 API。应按项目边界组织类型输入,公共模块尽量接收显式依赖,减少对全局状态的隐式要求。
如果配置来自外部脚本或动态数据,还需要验证内容并处理未就绪状态。全局增强适合描述已经确定的接入契约,不是绕过数据解析的捷径。库作者引入全局补丁时,也应确保消费者能明确选择是否加载,避免仅导入一个工具就改变整个项目的类型环境。
容易答错的地方
- declare global 会把值写到 globalThis 上
- 它只改变静态可见的声明,实际对象赋值、脚本加载与初始化仍由运行时代码承担;没有真实赋值时属性仍可能不存在。
- 重复声明相同属性时最后一份类型生效
- 声明合并不是覆盖顺序,冲突类型会带来诊断或不可靠契约;应统一来源和定义,不能依赖文件排列顺序解决语义冲突。
面试官还会怎么问?
能把全局属性声明为必填来省掉判断吗?
只有所有相关运行路径都能保证读取前初始化才合理。若页面、测试或服务端存在例外,应保留缺失可能或改用显式初始化接口。
为什么浏览器类型检查通过,SSR 仍报 window 未定义?
类型环境和运行环境不同。DOM 声明让编译器认识 window,但服务端未提供这个对象;需要按实际执行边界安排访问,而不是补更多声明。
扩展全局数组原型是否也用 declare global?
类型上可以描述真实扩展,但还需实现、加载顺序与兼容性保障。公共库随意修改全局原型会影响所有代码,通常应优先考虑普通工具函数。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。