type DeepReadonly<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> };
const cfg: DeepReadonly<Config> = load();
// cfg.server.port = 8080; // TS2540
Object.freeze(cfg.server);

给配置对象标了 readonly,别处给 config.server.port 赋值时 tsc 一点提示都没有,跑起来端口真的被改了。改外层字段才会报 TS2540。先别急着动参数,把报错原文和时间点记下来,顺序反了容易白折腾。

原因不复杂,readonly 是浅层的,它只约束当前这一层的属性,值本身如果是对象,里面的字段仍然可写。要连内层一起锁住,每一层都得声明只读,或者用递归映射类型。

围绕「readonly 深层只读」排查时,命令行跑一遍比在面板里点十次有用,面板会替你做掉不少隐式操作。动手前把原文件复制一份带时间戳,出问题立刻换回去,比现场分析快得多。

一次只改一处,改完立刻验证,几个改动叠在一起,出问题就说不清是谁导致的。

处理上按这个顺序:把嵌套对象单独抽成接口 每层字段都标 readonly;或者用 DeepReadonly 映射类型 一次性递归处理;运行时还要防就递归调用 Object.freeze;确认冻结之后没有代码依赖后续修改。

改完只确认一件事:内层赋值也报 TS2540 运行时修改被忽略 读配置的代码不受影响。

作者 9823189