Возьмем типичный компонент React, который извлекает данные из внешнего источника и обновляет пользовательский интерфейс в зависимости от состояния сети.

На первый взгляд этот код может показаться идеальным, и на самом деле он отобразит компонент без ошибок.

// Users.tsx
import { useState, useEffect } from "react";

export const REQUEST_STATUS = {
  idle: "IDLE",
  pending: "PENDING",
  success: "SUCCESS",
  error: "ERROR",
};

async function fetchUsers(): Promise<{ name: string; id: number }[]> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([{ name: "User 1", id: 1 }]);
    }, 2000);
  });
}

export function App() {
  const [networkState, setNetworkState] = useState(REQUEST_STATUS.idle);
  const [users, setUsers] = useState<{ name: string; id: number }[]>([]);

  useEffect(() => {
    setNetworkState(REQUEST_STATUS.pending);
    // or setNetworkState("IDLE"); // valid
    const getUsers = async () => {
      const users = await fetchUsers();
      setUsers(users);
      setNetworkState(REQUEST_STATUS.success);
    };
    getUsers();
  }, []);

  return (
    <div>
      {networkState === "PENDING" ? (
        <p>Fetching data...</p>
      ) : (
        networkState === "SUCCESS" && (
          <li>
            {users?.map((user) => (
              <ul key={user.id}> {user.name} </ul>
            ))}
          </li>
        )
      )}
      {networkState === "ERROR" && (
        <div>An error occured while fetching users</div>
      )}
    </div>
  );
}

НО…

Предположим, что REQUEST_STATUS импортируется из какого-то глобального файла и используется в наших компонентах, например:

import { REQUEST_STATUS } from '../some/other/file'

и кто-то изменил статусы в глобальном файле на что-то, что ломает наш компонент Users.tsx. Бум!

export const REQUEST_STATUS = { idle: "idle", pending: "pending", success: "success", error: "error", };

Введите проверку useState

Хотя добавление типа в setNetworkState является окончательным решением этой проблемы, как мы можем сделать это без особых изменений в коде?

Во-первых, давайте выведем тип REQUEST_STATUS в нашем пользовательском компоненте,

Здесь тип ключей idle, pending, success, error выводится как string, поэтому использование keyof и typeof все равно приведет к string в качестве общего типа.

Объезд — keyof и typeof

typeof

Если приведенный выше синтаксис сбивает с толку, позвольте мне объяснить его для вас ;) В противном случае перейдите к следующему разделу.

Здесь TypeScript сможет определить для вас тип вашего объекта. Он поднимет созданный вами объект с уровня значения на уровень типа.

так что по сути typeof REQUEST_STATUS будет означать { idle: string, pending: string, success: string, error: string }

Итак, если мы хотим получить тип определенного ключа в объекте, мы можем сделать что-то вроде

type IDLE_TYPE = typeof REQUEST_STATUS["idle"]; // string
type PENDING_TYPE = typeof REQUEST_STATUS["pending"]; // string
type SUCCESS_TYPE = typeof REQUEST_STATUS["success"]; // string
type ERROR_TYPE = typeof REQUEST_STATUS["error"]; // stringtype

keyof

Оператор keyof можно использовать для извлечения ключей типа объекта.

keyof можно использовать вместе с typeof для создания типа объединения всех ключей в объекте.

Извлекая все ключи типа объекта, мы можем комбинировать эти ключевые слова для создания типов объединения.

type REQUEST_TYPE = tyepof REQUEST_STATUS[keyof typeof REQUEST_STATUS] // string

// the above expression is equivalent to
type REQUEST_TYPE =
| { idle: string, pending: string, success: string, error: string }["idle"] 
| { idle: string, pending: string, success: string, error: string }["pending"]
| { idle: string, pending: string, success: string, error: string }["success"]
| { idle: string, pending: string, success: string, error: string }["error"]t

Назад к спасению с «как const»

Теперь, когда мы знаем, как typeof и keyof можно использовать вместе для создания типа объединения из объектов, давайте посмотрим, как сюда подходит 'as const'.

Приведенный выше тип export type REQUEST_TYPE = tyepof REQUEST_STATUS[keyof typeof REQUEST_STATUS] возвращает string в качестве типа, поэтому, если ввести setState, это не решит нашу проблему. Это связано с тем, что пока мы устанавливаем «строку», TypeScript принимает ее как допустимое значение.

import { REQUEST_TYPE, REQUEST_STATUS } from '../some/other/file'

const [networkState, setNetworkState] = useState<REQUEST_TYPE>(REQUEST_STATES.idle);

// ...somewhere in the code, we can still do
setNetworkState("idle"); // no error
setNetworkState("invalid_state"); // no error
setNetworkState(12) // Error as type "string" is inferred from initial state

Без дальнейших объяснений, именно здесь в игру вступает утверждение as const, которое делает его строго типизированным. Просто добавив as const к объекту, мы можем сделать его доступным только для чтения и гарантировать, что всем свойствам будет назначен литеральный тип, а не более общая версия, такая как string или number.

В сочетании с нашим ранее изученным приемом мы можем создать тип объединения значений объектов.

Собираем это вместе

// types
export const REQUEST_STATUS = {
  idle: "IDLE",
  pending: "PENDING",
  success: "SUCCESS",
  error: "ERROR",
} as const;

export type REQUEST_TYPES = typeof REQUEST_STATUS[keyof typeof REQUEST_STATUS];

// User.tsx
const [networkState, setNetworkState] = useState<REQUEST_TYPES>(REQUEST_STATUS.idle);

С этим обновленным useState компилятор TypeScript будет выдавать ошибку для недопустимых статусов.

Заключение

В заключение, использование as const в сочетании с typeof и keyof может помочь вам создать строго типизированные типы объединения для вашего хука React useState, предотвращая потенциальные ошибки, вызванные неожиданными изменениями значений в вашем приложении. Реализуя этот подход, вы обеспечите лучшую безопасность типов и удобство сопровождения в своей кодовой базе.

Удачного кодирования!

Первоначально опубликовано на https://blog.asciibi.dev.