← Назад к вопросам

Как сделать миграцию базы данных?

2.0 Middle🔥 111 комментариев
#JavaScript Core

Комментарии (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,
};

Ключевые моменты

  1. readonly - базовая иммутабельность для свойств
  2. Readonly<T> - утилита для всех свойств
  3. ReadonlyArray<T> - неизменяемые массивы
  4. DeepReadonly<T> - рекурсивная иммутабельность
  5. as const - заморозить объект с литеральными типами
  6. Классы с приватными полями - инкапсуляция
  7. Spread оператор (...) - создавать новые объекты
  8. Immer - удобная работа с иммутабельностью
  9. Pure функции - всегда возвращать новое состояние
  10. Redux и React контексты работают лучше с иммутабельными данными

Иммутабельность в TypeScript улучшает безопасность типов, облегчает отладку и предотвращает неожиданные мутации состояния.