源码里不能改的属性,发布成 .d.ts 后却能赋值:检查嵌套 readonly 是否丢失
源码拒绝的赋值在包消费者中却通过时,检查声明生成是否遗漏嵌套 readonly。说明 const 类型参数与引用组合的具体条件,以及声明侧验证。
类型包在自己的源码中会报 TS2540: Cannot assign ... because it is a read-only property,调用方安装发布包后,同样的赋值却通过了。先比较源码检查结果与实际发布的 .d.ts,不要马上给下游补 Readonly 掩盖声明生成差异。
已知具体形态涉及 TypeScript 7.0.2、7.1.0-dev.20260826.1 的原生声明生成,macOS arm64环境;相同结构在5.9.3、6.0.3生成了预期修饰符。这个问题没有运行时异常也可能产生影响:发布声明允许了源码本来不允许的修改。
const 类型参数的推断,与声明输出应当一致
下面用一个只用于类型检查的辅助函数声明说明结构;它不提供运行时实现,不能直接拿去执行:
const leaf = { value: 1 } as const;
declare function define<const T>(input: T): T;
export const schema = define({
properties: { first: leaf, second: leaf },
});
const 类型参数让对象字面量采用接近 as const 的推断方式。这里需要检查的不是 leaf.value,而是外层 properties.first、properties.second 能否被重新赋值。
受影响声明可能保留 properties 本身和叶子 value 的 readonly,却丢掉中间这一层:
export declare const schema: {
readonly properties: {
first: { readonly value: 1 };
second: { readonly value: 1 };
};
};
这样下游不能改 value,却能替换 first,即使源项目原本拒绝同一赋值。只检查最内层字段,容易漏掉真正丢失的限制。
这一具体结构的共同点是嵌套对象的成员值全来自已有引用。不能把它概括成所有 const 类型参数失效,更不能认为 readonly 在JavaScript运行时冻结了对象;它提供的是类型约束。
将只读意图明确放进声明来源
对于确实应当整段只读的字面量,可以先显式使用 const 断言,再交给已有辅助函数:
const definition = {
properties: { first: leaf, second: leaf },
} as const;
export const schema = define(definition);
这里沿用前例的 leaf 与 define 类型说明,实际项目仍使用自己的函数实现。不要为避开问题随意增加一个无业务用途的新字段;虽然改变字面量形态可能影响推断,却会把排错手段混进公开数据结构。
若API需要更明确的公开类型,也可以在导出边界使用项目已有的只读接口,但要逐层表达需要的限制。Readonly<T>只处理一层,不能假定给最外层套上它就等于递归只读。类型断言还不会验证运行时数据,不能用它替代原来辅助函数的结构检查。
验证要站在安装者的一侧
保留旧 .d.ts 作为差异依据,用项目原有声明构建命令生成新文件,检查相关中间层的 readonly。随后在独立消费项目中从包入口导入,而不是直接指向源文件,再做原来应被禁止的赋值检查。
如果源码与消费者实际读的是两份不同路径,先核对 types、条件导出和打包清单,避免审查了一份正确的声明,发布的却是另一份旧文件。类型检查缓存也应与真实产物区分。
截至2026年10月20日,已读取的关闭记录没有给出能够绑定的修复提交或正式修复版号,不能把“问题已关闭”解释为升级任意新版本即可解决。可先保留显式只读声明,或在兼容评估后使用已经验证符合预期的声明生成工具组合;不应因此关闭下游类型检查。


