Импорт настроек использует небольшой рекурсивный merge. Типы параметров выглядят аккуратно, но описывают лишь то, чего ждёт программа. Цикл проходит по тем ключам, которые реально есть в разобранном файле.
type Settings = { theme: "light" | "dark"; fontSize: number };
function merge(target: any, src: any): void {
for (const key of Object.keys(src)) {
const v = src[key];
if (typeof v === "object" && v !== null) {
target[key] ??= {};
merge(target[key], v);
} else {
target[key] = v;
}
}
}
const saved: Partial<Settings> = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, saved);
console.log(({} as any).isAdmin); // true
JSON.parse определяет __proto__ как обычное собственное свойство, и Object.keys его возвращает. Дальше target[key] на обычном объекте читает унаследованный аксессор и получает Object.prototype, ??= его не трогает, потому что это не nullish, а рекурсивный вызов записывает isAdmin в прототип, общий для всех объектов. Аннотация Partial<Settings> прошла без единого слова, ведь JSON.parse возвращает any.
Есть два исправления на выбор. Пропускать опасные имена ключей внутри цикла или строить каждый уровень цели через Object.create(null), чтобы не было аксессора, на который можно наткнуться. Второе кажется основательнее, но тогда у каждого объекта, который получает код ниже, нет toString. Кто-нибудь долго жил с версией на null-прототипе?