40 / 58
Как создать тип DeepReadonly<T>?
Полный ответ
Проблема с обычным Readonly:
TypeScript
interface State {
user: {
name: string;
roles: string[];
};
settings: {
theme: string;
};
}
const state: Readonly<State> = {
user: { name: "Alice", roles: ["admin"] },
settings: { theme: "dark" },
};
// state.user = { ... }; // ошибка — первый уровень защищён
state.user.name = "Bob"; // НЕТ ошибки — вложенный объект мутабелен!
state.user.roles.push("editor"); // НЕТ ошибки — массив мутабелен!
Рекурсивная реализация:
TypeScript
type DeepReadonly<T> = T extends Map<infer K, infer V>
? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
: T extends Set<infer U>
? ReadonlySet<DeepReadonly<U>>
: T extends ReadonlyArray<infer U>
? readonly DeepReadonly<U>[]
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
const deepState: DeepReadonly<State> = {
user: { name: "Alice", roles: ["admin"] },
settings: { theme: "dark" },
};
// deepState.user = { ... }; // ошибка
// deepState.user.name = "Bob"; // ошибка
// deepState.user.roles.push("editor"); // ошибка — readonly array
// deepState.settings.theme = "light"; // ошибка
Упрощённая версия для большинства случаев:
TypeScript
type DeepReadonly<T> = T extends (infer U)[]
? readonly DeepReadonly<U>[]
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
Использование с функциями:
TypeScript
// Функция, которая гарантирует неизменность входных данных
function processConfig(config: DeepReadonly<Config>): string {
// config.db.host = "other"; // ошибка компиляции
return `${config.db.host}:${config.db.port}`;
}
// Freeze на уровне рантайма + типа
function deepFreeze<T extends Record<string, unknown>>(obj: T): DeepReadonly<T> {
Object.freeze(obj);
for (const key of Object.keys(obj)) {
const value = obj[key];
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
deepFreeze(value as Record<string, unknown>);
}
}
return obj as DeepReadonly<T>;
}
const frozenConfig = deepFreeze({
api: { url: "https:// api.example.com", timeout: 5000 },
features: { darkMode: true },
});
// frozenConfig.api.url = "x"; // ошибка компиляции И рантайм ошибка
Реальные кейсы
Иммутабельное состояние в Redux:
TypeScript
interface AppState {
todos: Array<{ id: number; text: string; done: boolean }>;
filter: "all" | "active" | "done";
}
type ReadonlyState = DeepReadonly<AppState>;
function reducer(state: ReadonlyState, action: Action): ReadonlyState {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.text, done: false }],
}; // state.todos.push(...) — ошибка компиляции
default:
return state;
}
}
Ошибка: забыть про массивы:
TypeScript
// ❌ Только readonly на объекте — массив всё ещё мутабельный
type BadReadonly<T> = { readonly [K in keyof T]: T[K] };
type Test = BadReadonly<{ items: number[] }>;
// items: readonly number[] — нет! items: number[] — мутабельный
// ✅ DeepReadonly превращает number[] в readonly number[]
type Good = DeepReadonly<{ items: number[] }>; // items: readonly number[] — пуш невозможен
Резюме
DeepReadonly<T> рекурсивно добавляет readonly ко всем свойствам и превращает массивы в readonly массивы. Добавьте обработку Map, Set и Date для полноты. Используйте в паре с Object.freeze для защиты и на уровне типов, и в рантайме.