
Возьмем типичный компонент 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.