Зачем типизировать данные токена и интерфейсы пользователя?
Вот представьте: вы в вашем приложении получаете с сервера токен, и там внутри — JSON с полезными данными, например, идентификатор пользователя, его роли или имя. Если вы забыли про TypeScript, то эти данные превращаются в неструктурированную массу информации. С вами остаётся только одно — молиться, чтобы сервер отправил всё, что нужно, и в нужном формате (спойлер: не всегда так бывает).
Типизация данных токена и пользователя помогает:
- Убедиться, что данные внутри токена имеют ожидаемый формат.
- Упростить работу с токеном в коде: IDE будет подсказывать вам поля токена.
- Избежать ошибок при работе с данными пользователя.
А теперь перейдём к практике!
Структура JWT
Напомним, что JWT разделён на три части:
- Header (заголовок) — метаданные токена.
- Payload (полезная нагрузка) — сюда обычно входят данные о пользователе.
- Signature (подпись) — проверяет подлинность токена.
В основном мы будем работать с полезной нагрузкой payload, так как именно там хранится информация о пользователе. Вот пример содержимого payload в JWT:
{
"sub": "1234567890",
"name": "John Doe",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
Какие данные могут быть в токене?
sub(subject) — идентификатор пользователя.name— имя пользователя.role— роль пользователя в системе (например, администратор или обычный пользователь).iat(issued at) — время создания токена.exp(expiration) — время истечения токена.
Давайте опишем это с TypeScript!
Типизация токена
Создадим интерфейс JwtPayload для данных токена:
// Интерфейс для полезной нагрузки токена
export interface JwtPayload {
sub: string; // Идентификатор пользователя (обычно строка)
name: string; // Имя пользователя
role: 'admin' | 'user'; // Роль пользователя (можем задать конкретные значения)
iat?: number; // Время создания токена (опционально)
exp?: number; // Время истечения токена (опционально)
}
Обратите внимание на два момента:
- Поля
iatиexpсделали опциональными?, потому что сервер может их не вернуть, но это не должно ломать ваш код. - Тип для
roleограничили строковыми литералами'admin' | 'user'— это защищает нас от случайных ошибок и гарантирует, что роль всегда имеет одно из указанных значений.
Теперь, когда вы получите JWT из запроса, вы можете его декодировать (например, с помощью библиотеки jsonwebtoken) и точно знать, какие данные вас ожидают.
Типизация пользователя
Данные пользователя в вашем приложении чаще всего идут отдельно от токена. Например, после успешного входа в систему вы можете получать полный объект пользователя с дополнительной информацией, такой как email, аватар или статус.
Создадим интерфейс User для пользователя:
export interface User {
id: string; // Уникальный идентификатор пользователя
name: string; // Имя пользователя
email: string; // Email пользователя
role: 'admin' | 'user'; // Роль пользователя (совпадает с данными в токене)
avatarUrl?: string; // Ссылка на аватар (опционально)
}
Связь токена и пользователя
Теперь представьте, что мы получаем токен с минимальным набором данных, а при необходимости делаем запрос на сервер за более полными данными о пользователе. Например:
- В токене содержатся только
sub,nameиrole, чтобы снизить его размер и не передавать лишние данные. - Полный объект пользователя мы храним в состоянии приложения (например, в Redux или Context API).
Пример работы с токеном
Окей, давайте напишем функцию для декодирования токена и преобразования его в данные пользователя:
import jwtDecode from 'jwt-decode'; // Библиотека для декодирования JWT
import { JwtPayload, User } from './types'; // Наши интерфейсы
// Функция для получения данных пользователя из токена
export function getUserFromToken(token: string): User {
// Декодируем токен с помощью jwt-decode
const payload = jwtDecode<JwtPayload>(token);
// Преобразуем данные из токена в объект User
const user: User = {
id: payload.sub,
name: payload.name,
email: '', // Email может быть недоступен в токене
role: payload.role,
avatarUrl: undefined, // Например, аватар может быть загружен позже
};
return user;
}
Что здесь происходит:
- Мы декодируем токен с помощью
jwt-decode(не забудьте установить библиотеку:npm install jwt-decode). - Сопоставляем данные из токена с интерфейсом
User. - Возвращаем готовый объект.
Типизация состояния пользователя
В React-приложениях аутентификация часто связывается с состоянием. Обычно мы храним объект текущего пользователя в глобальном состоянии или контексте.
Создадим интерфейс для состояния аутентификации:
export interface AuthState {
user: User | null; // Текущий пользователь (если вошёл в систему)
token: string | null; // JWT-токен
isAuthenticated: boolean; // Флаг аутентификации
}
Глобальное состояние или контекст
Допустим, мы используем Context API для управления этим состоянием:
import React, { createContext, useState, useContext } from 'react';
import { AuthState } from './types';
// Начальное состояние аутентификации
const initialAuthState: AuthState = {
user: null,
token: null,
isAuthenticated: false,
};
// Создаём контекст
const AuthContext = createContext<{
authState: AuthState;
setAuthState: React.Dispatch<React.SetStateAction<AuthState>>;
}>({
authState: initialAuthState,
setAuthState: () => {},
});
// Хук для доступа к состоянию аутентификации
export const useAuth = () => useContext(AuthContext);
// Провайдер для AuthContext
export const AuthProvider: React.FC = ({ children }) => {
const [authState, setAuthState] = useState(initialAuthState);
return (
<AuthContext.Provider value={{ authState, setAuthState }}>
{children}
</AuthContext.Provider>
);
};
Теперь мы можем использовать хук useAuth для получения информации о текущем пользователе в любом компоненте.
Обработка данных пользователя в реальном приложении
На практике вы можете:
- После успешного входа в систему сохранить токен в
localStorageилиsessionStorage. - С помощью токена получить дополнительные данные пользователя с сервера.
- Сохранять всё это в глобальном состоянии и использовать во всём приложении.
Например, в компоненте входа:
import { useAuth } from './AuthProvider';
const Login = () => {
const { setAuthState } = useAuth();
const handleLogin = async () => {
const token = await loginUserOnServer(); // Ваш API-запрос
const user = getUserFromToken(token);
setAuthState({
user,
token,
isAuthenticated: true,
});
localStorage.setItem('token', token); // Сохраняем токен
};
return <button onClick={handleLogin}>Войти</button>;
};
Типичные ошибки
Одной из частых ошибок является отсутствие проверки структуры токена. Всегда проверяйте, что поле действительно существует в объекте, перед использованием его значения. Также не забывайте, что токен может быть просрочен!
if (!payload.sub) {
throw new Error('Недействительный токен!');
}
Теперь вы знаете, как типизировать данные токена и пользователя, а также как это всё связать воедино. Надеюсь, вам понравилось!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ