Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как сделать объект иммутабельным в TypeScript
Иммутабельность - это принцип когда данные не могут быть изменены после создания. TypeScript предоставляет несколько способов для создания иммутабельных объектов. Это улучшает безопасность кода и облегчает отслеживание изменений.
1. Тип readonly для свойств
// Простое использование readonly
interface User {
readonly id: number;
readonly name: string;
readonly email: string;
}
const user: User = {
id: 1,
name: 'John',
email: 'john@example.com',
};
// ОШИБКА: Cannot assign to 'name' because it is a read-only property
// user.name = 'Jane';
// Но можно переопределить весь объект
const updatedUser: User = { ...user, name: 'Jane' };
console.log(updatedUser); // { id: 1, name: 'Jane', email: 'john@example.com' }
2. Тип Readonly<T>
// Readonly<T> делает все свойства readonly
interface UserMutable {
id: number;
name: string;
email: string;
age: number;
}
type UserImmutable = Readonly<UserMutable>;
const user: UserImmutable = {
id: 1,
name: 'John',
email: 'john@example.com',
age: 30,
};
// ОШИБКА: все свойства readonly
// user.name = 'Jane';
// user.age = 31;
// Но можно создать новый объект
const updatedUser: UserImmutable = { ...user, name: 'Jane', age: 31 };
3. Тип ReadonlyArray<T>
// ReadonlyArray предотвращает мутации массива
const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];
// ОШИБКИ: методы push, pop, splice недоступны
// numbers.push(6);
// numbers[0] = 10;
// numbers.splice(0, 1);
// Можно создавать новый массив
const newNumbers = [...numbers, 6]; // [1, 2, 3, 4, 5, 6]
const updated = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]
const filtered = numbers.filter(n => n > 2); // [3, 4, 5]
// Альтернативный синтаксис
const items: readonly string[] = ['a', 'b', 'c'];
// items.push('d'); // ОШИБКА
4. Глубокая иммутабельность (Recursive Readonly)
// Проблема: Readonly<T> не делает вложенные объекты readonly
interface Address {
street: string;
city: string;
country: string;
}
interface User {
name: string;
address: Address;
}
type UserReadonly = Readonly<User>;
const user: UserReadonly = {
name: 'John',
address: { street: '123 Main St', city: 'NYC', country: 'USA' },
};
// ОШИБКА: название readonly
// user.name = 'Jane';
// НО ЭТО РАБОТАЕТ! (глубокие свойства не readonly)
user.address.street = '456 Oak Ave'; // Мутация возможна!
// РЕШЕНИЕ: Глубокая иммутабельность
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type UserImmutable = DeepReadonly<User>;
const user: UserImmutable = {
name: 'John',
address: { street: '123 Main St', city: 'NYC', country: 'USA' },
};
// ОШИБКА: даже вложенные свойства readonly
// user.address.street = '456 Oak Ave';
5. Полезные типы утилит
// Собственные типы утилит
// ReadonlyMap - для работы с Map
type ReadonlyMap<K, V> = {
get(key: K): V | undefined;
has(key: K): boolean;
readonly size: number;
entries(): IterableIterator<readonly [K, V]>;
keys(): IterableIterator<K>;
values(): IterableIterator<V>;
};
// Делаем все свойства и вложенные readonly рекурсивно
type Immutable<T> = {
readonly [K in keyof T]: T[K] extends (infer U)[]
? readonly Immutable<U>[]
: T[K] extends object
? Immutable<T[K]>
: T[K];
};
interface Product {
id: number;
name: string;
tags: string[];
details: {
description: string;
price: number;
};
}
type ImmutableProduct = Immutable<Product>;
const product: ImmutableProduct = {
id: 1,
name: 'Laptop',
tags: ['electronics', 'computers'],
details: {
description: 'High performance laptop',
price: 999,
},
};
// Все mutations запрещены
// product.name = 'Desktop';
// product.tags.push('new-tag');
// product.details.price = 1299;
6. Работа с иммутабельными объектами
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface UserState {
user: {
id: number;
name: string;
email: string;
};
isLoading: boolean;
errors: string[];
}
type ImmutableUserState = DeepReadonly<UserState>;
// Функция для обновления иммутабельного состояния
function updateUserName(
state: ImmutableUserState,
newName: string
): ImmutableUserState {
return {
...state,
user: {
...state.user,
name: newName,
},
};
}
function addError(
state: ImmutableUserState,
error: string
): ImmutableUserState {
return {
...state,
errors: [...state.errors, error],
};
}
let state: ImmutableUserState = {
user: { id: 1, name: 'John', email: 'john@example.com' },
isLoading: false,
errors: [],
};
state = updateUserName(state, 'Jane');
state = addError(state, 'Network error');
console.log(state);
// {
// user: { id: 1, name: 'Jane', email: 'john@example.com' },
// isLoading: false,
// errors: ['Network error']
// }
7. Использование as const для литеральных типов
// as const делает объект полностью readonly
const config = {
api: {
baseUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
},
features: {
darkMode: true,
notifications: true,
},
} as const;
// config.api.baseUrl = 'https://other.com'; // ОШИБКА
// config.features.darkMode = false; // ОШИБКА
type Config = typeof config; // Тип заморозиться с литеральными значениями
// Типы:
type BaseUrl = typeof config.api.baseUrl; // 'https://api.example.com'
type Timeout = typeof config.api.timeout; // 5000
8. Классы с приватными полями
// Класс может скрыть состояние и предоставить только методы
class ImmutableUser {
private readonly _id: number;
private readonly _name: string;
private readonly _email: string;
constructor(id: number, name: string, email: string) {
this._id = id;
this._name = name;
this._email = email;
}
get id(): number {
return this._id;
}
get name(): string {
return this._name;
}
get email(): string {
return this._email;
}
// Метод возвращает НОВЫЙ объект вместо модификации
updateName(newName: string): ImmutableUser {
return new ImmutableUser(this._id, newName, this._email);
}
updateEmail(newEmail: string): ImmutableUser {
return new ImmutableUser(this._id, this._name, newEmail);
}
toJSON() {
return {
id: this._id,
name: this._name,
email: this._email,
};
}
}
let user = new ImmutableUser(1, 'John', 'john@example.com');
// Нельзя изменить
// user.id = 2; // ОШИБКА
// user.name = 'Jane'; // ОШИБКА
// Создаём новый объект вместо модификации
user = user.updateName('Jane');
user = user.updateEmail('jane@example.com');
console.log(user.toJSON());
// { id: 1, name: 'Jane', email: 'jane@example.com' }
9. Библиотеки для иммутабельности
// Immer - библиотека для удобной работы с иммутабельностью
import { produce } from 'immer';
interface State {
user: {
name: string;
age: number;
};
items: Array<{ id: number; done: boolean }>;
}
const state: State = {
user: { name: 'John', age: 30 },
items: [
{ id: 1, done: false },
{ id: 2, done: false },
],
};
// Используем draft для "мутирования", но результат новый
const newState = produce(state, (draft) => {
draft.user.name = 'Jane';
draft.user.age = 31;
draft.items[0].done = true;
});
console.log(state === newState); // false (новый объект)
console.log(state.user === newState.user); // false
console.log(state.items[0] === newState.items[0]); // false
console.log(state.items[1] === newState.items[1]); // true (не изменился)
10. Практический пример с Redux
interface AppState {
readonly users: ReadonlyArray<{
readonly id: number;
readonly name: string;
readonly email: string;
}>;
readonly isLoading: boolean;
readonly error: string | null;
}
// Action
type UserAction =
| { type: 'ADD_USER'; payload: { id: number; name: string; email: string } }
| { type: 'REMOVE_USER'; payload: { id: number } }
| { type: 'UPDATE_USER'; payload: { id: number; name: string } }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null };
// Reducer - должен быть pure и вернуть новое состояние
function appReducer(state: AppState, action: UserAction): AppState {
switch (action.type) {
case 'ADD_USER':
return {
...state,
users: [...state.users, action.payload],
};
case 'REMOVE_USER':
return {
...state,
users: state.users.filter((u) => u.id !== action.payload.id),
};
case 'UPDATE_USER':
return {
...state,
users: state.users.map((u) =>
u.id === action.payload.id
? { ...u, name: action.payload.name }
: u
),
};
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
default:
return state;
}
}
const initialState: AppState = {
users: [],
isLoading: false,
error: null,
};
Ключевые моменты
- readonly - базовая иммутабельность для свойств
- Readonly<T> - утилита для всех свойств
- ReadonlyArray<T> - неизменяемые массивы
- DeepReadonly<T> - рекурсивная иммутабельность
- as const - заморозить объект с литеральными типами
- Классы с приватными полями - инкапсуляция
- Spread оператор (...) - создавать новые объекты
- Immer - удобная работа с иммутабельностью
- Pure функции - всегда возвращать новое состояние
- Redux и React контексты работают лучше с иммутабельными данными
Иммутабельность в TypeScript улучшает безопасность типов, облегчает отладку и предотвращает неожиданные мутации состояния.