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