Введение
TypeScript изменил подход к разработке API клиентов. Вместо "угадывания" структуры ответов и ручной проверки типов, мы можем получить полную типизацию из OpenAPI спецификации, создать type-safe клиенты и избавиться от множества багов ещё на этапе разработки.
В этом руководстве мы рассмотрим все способы типизации API с TypeScript: от простых интерфейсов до автоматической генерации типов и валидации в runtime.
✅ Что вы узнаете:
- ✅ Автоматическая генерация типов из OpenAPI
- ✅ Создание type-safe API клиентов
- ✅ Типизация fetch и axios запросов
- ✅ Runtime валидация с Zod и Yup
- ✅ Generic types для переиспользуемых API методов
- ✅ Best practices для типизации API
- ✅ Интеграция с популярными фреймворками
💡 Почему типизация API важна:
- Меньше багов: Ошибки типов обнаруживаются до runtime
- Лучший DX: Автодополнение в IDE для всех API методов
- Автоматический рефакторинг: TypeScript найдёт все использования при изменении API
- Самодокументирование: Типы — это документация
- Безопасность: Валидация данных на этапе компиляции и runtime
📋 Содержание
Способ 1: Ручная типизация API 📝
Самый простой способ — создать TypeScript интерфейсы вручную. Подходит для небольших проектов или когда OpenAPI спецификации нет.
1 Создайте интерфейсы для API
Файл: src/types/api.ts
// Модели данных
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
createdAt: string;
updatedAt: string;
}
export interface Todo {
id: string;
title: string;
completed: boolean;
userId: number;
createdAt: string;
}
// DTO для запросов
export interface CreateUserDto {
name: string;
email: string;
role?: 'admin' | 'user' | 'guest';
}
export interface UpdateUserDto {
name?: string;
email?: string;
role?: 'admin' | 'user' | 'guest';
}
// Ответы API
export interface ApiResponse<T> {
data: T;
message?: string;
success: boolean;
}
export interface PaginatedResponse<T> {
data: T[];
meta: {
total: number;
page: number;
limit: number;
pages: number;
};
}
// Ошибки API
export interface ApiError {
message: string;
code: string;
details?: Record<string, string[]>;
}
2 Типизированный API клиент
Файл: src/api/client.ts
import type { User, CreateUserDto, UpdateUserDto, ApiResponse, PaginatedResponse } from '../types/api';
class ApiClient {
private baseURL: string;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
// Типизированные методы
async getUsers(page = 1, limit = 20): Promise<PaginatedResponse<User>> {
const response = await fetch(`${this.baseURL}/users?page=${page}&limit=${limit}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async getUser(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`${this.baseURL}/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async createUser(data: CreateUserDto): Promise<ApiResponse<User>> {
const response = await fetch(`${this.baseURL}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async updateUser(id: number, data: UpdateUserDto): Promise<ApiResponse<User>> {
const response = await fetch(`${this.baseURL}/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async deleteUser(id: number): Promise<void> {
const response = await fetch(`${this.baseURL}/users/${id}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
}
export const apiClient = new ApiClient('https://api.example.com');
⚠️ Проблемы ручной типизации:
- Типы могут устареть, если API изменился
- Нужно вручную обновлять при каждом изменении API
- Высокий риск ошибок при копировании из документации
- Долго для больших API с десятками endpoints
Способ 2: Генерация типов из OpenAPI 🤖
Автоматическая генерация — лучший способ для больших проектов. Типы генерируются из OpenAPI спецификации и всегда актуальны.
1 Установите OpenAPI Generator
# Глобальная установка
npm install -g @openapitools/openapi-generator-cli
# Или локально в проект
npm install --save-dev @openapitools/openapi-generator-cli
2 Генерируйте типы из OpenAPI
# Генерация TypeScript типов и клиента
openapi-generator-cli generate \
-i https://lightboxapi.ru/api/openapi.yaml \
-g typescript-axios \
-o ./src/api/generated
# Или из локального файла
openapi-generator-cli generate \
-i ./openapi.yaml \
-g typescript-axios \
-o ./src/api/generated
3 Использование сгенерированных типов
import { Configuration, UsersApi, User } from './api/generated';
// Настройка клиента
const configuration = new Configuration({
basePath: 'https://api.example.com',
});
// Создание API инстанса
const usersApi = new UsersApi(configuration);
// Использование с полной типизацией
async function fetchUsers() {
try {
// TypeScript знает все параметры и типы ответов
const response = await usersApi.getUsers(1, 20);
const users: User[] = response.data.data; // Полная типизация!
users.forEach(user => {
console.log(user.name); // Автодополнение работает!
console.log(user.email); // TypeScript знает все поля
});
} catch (error) {
console.error('Error:', error);
}
}
4 Автоматизация генерации
Добавьте скрипт в package.json:
{
"scripts": {
"generate:api": "openapi-generator-cli generate -i https://api.example.com/openapi.yaml -g typescript-axios -o ./src/api/generated",
"prebuild": "npm run generate:api"
}
}
Теперь типы будут генерироваться автоматически перед каждой сборкой!
Способ 3: Type-safe API клиенты 🔒
Создайте универсальный type-safe клиент, который гарантирует типобезопасность на всех уровнях.
1 Generic API Client
Файл: src/api/client.ts
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
interface RequestOptions {
headers?: Record<string, string>;
params?: Record<string, string | number>;
}
class TypedApiClient {
constructor(private baseURL: string) {}
async request<TResponse>(
method: HttpMethod,
path: string,
body?: unknown,
options?: RequestOptions
): Promise<TResponse> {
// Добавляем query параметры
const url = new URL(path, this.baseURL);
if (options?.params) {
Object.entries(options.params).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
}
const response = await fetch(url.toString(), {
method,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json() as Promise<TResponse>;
}
// Типизированные методы-хелперы
get<TResponse>(path: string, options?: RequestOptions): Promise<TResponse> {
return this.request<TResponse>('GET', path, undefined, options);
}
post<TResponse, TBody = unknown>(
path: string,
body: TBody,
options?: RequestOptions
): Promise<TResponse> {
return this.request<TResponse>('POST', path, body, options);
}
put<TResponse, TBody = unknown>(
path: string,
body: TBody,
options?: RequestOptions
): Promise<TResponse> {
return this.request<TResponse>('PUT', path, body, options);
}
delete<TResponse>(path: string, options?: RequestOptions): Promise<TResponse> {
return this.request<TResponse>('DELETE', path, undefined, options);
}
}
export const apiClient = new TypedApiClient('https://api.example.com');
2 Использование с полной типизацией
import { apiClient } from './api/client';
import type { User, CreateUserDto, PaginatedResponse } from './types/api';
// Все методы полностью типизированы
const users = await apiClient.get<PaginatedResponse<User>>('/users', {
params: { page: 1, limit: 20 }
});
// TypeScript знает структуру ответа
users.data.forEach(user => {
console.log(user.name); // ✅ Автодополнение
console.log(user.email); // ✅ Типобезопасно
});
// Создание пользователя с типизацией body
const newUser = await apiClient.post<User, CreateUserDto>('/users', {
name: 'John Doe',
email: 'john@example.com'
// TypeScript проверит, что все обязательные поля есть
});
// Обновление с частичными данными
const updated = await apiClient.put<User, Partial<CreateUserDto>>('/users/1', {
name: 'Jane Doe'
// Все поля опциональны благодаря Partial
});
Способ 4: Типизация fetch запросов 🌐
Нативный fetch API можно обернуть для полной типизации без дополнительных библиотек.
1 Типизированная обёртка для fetch
type FetchOptions = RequestInit & {
params?: Record<string, string | number>;
};
async function typedFetch<TResponse>(
url: string,
options?: FetchOptions
): Promise<TResponse> {
// Добавляем query параметры
const urlObj = new URL(url);
if (options?.params) {
Object.entries(options.params).forEach(([key, value]) => {
urlObj.searchParams.append(key, String(value));
});
}
const { params, ...fetchOptions } = options || {};
const response = await fetch(urlObj.toString(), {
...fetchOptions,
headers: {
'Content-Type': 'application/json',
...fetchOptions.headers,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json() as Promise<TResponse>;
}
2 Использование с типами
import type { User, CreateUserDto } from './types/api';
// GET запрос
const user: User = await typedFetch<User>('https://api.example.com/users/1');
// POST запрос
const newUser: User = await typedFetch<User>(
'https://api.example.com/users',
{
method: 'POST',
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
} as CreateUserDto)
}
);
// С параметрами
const users = await typedFetch<User[]>(
'https://api.example.com/users',
{
params: { page: 1, limit: 20 }
}
);
Способ 5: Типизация axios запросов 📦
Axios — популярная библиотека для HTTP запросов. Она поддерживает TypeScript из коробки, но можно улучшить типизацию.
1 Типизированный axios instance
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import type { User, CreateUserDto, ApiResponse } from './types/api';
class TypedAxiosClient {
private client: AxiosInstance;
constructor(baseURL: string) {
this.client = axios.create({
baseURL,
headers: {
'Content-Type': 'application/json',
},
});
}
async get<TResponse>(url: string, config?: AxiosRequestConfig): Promise<TResponse> {
const response: AxiosResponse<TResponse> = await this.client.get(url, config);
return response.data;
}
async post<TResponse, TBody = unknown>(
url: string,
data?: TBody,
config?: AxiosRequestConfig
): Promise<TResponse> {
const response: AxiosResponse<TResponse> = await this.client.post(url, data, config);
return response.data;
}
async put<TResponse, TBody = unknown>(
url: string,
data?: TBody,
config?: AxiosRequestConfig
): Promise<TResponse> {
const response: AxiosResponse<TResponse> = await this.client.put(url, data, config);
return response.data;
}
async delete<TResponse>(url: string, config?: AxiosRequestConfig): Promise<TResponse> {
const response: AxiosResponse<TResponse> = await this.client.delete(url, config);
return response.data;
}
}
export const apiClient = new TypedAxiosClient('https://api.example.com');
2 Использование с полной типизацией
import { apiClient } from './api/client';
import type { User, CreateUserDto } from './types/api';
// Все методы типизированы
const user = await apiClient.get<User>('/users/1');
const users = await apiClient.get<User[]>('/users');
const newUser = await apiClient.post<User, CreateUserDto>('/users', {
name: 'John Doe',
email: 'john@example.com'
});
const updated = await apiClient.put<User, Partial<CreateUserDto>>('/users/1', {
name: 'Jane Doe'
});
await apiClient.delete<void>('/users/1');
Способ 6: Runtime валидация (Zod/Yup) ✅
TypeScript проверяет типы только на этапе компиляции. Для runtime валидации используйте библиотеки Zod или Yup, которые проверяют данные при получении от API.
1 Валидация с Zod
npm install zod
Файл: src/schemas/user.schema.ts
import { z } from 'zod';
// Схема валидации
export const UserSchema = z.object({
id: z.number(),
name: z.string().min(2).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
export const CreateUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']).optional(),
});
// TypeScript типы из схем
export type User = z.infer<typeof UserSchema>;
export type CreateUserDto = z.infer<typeof CreateUserSchema>;
// Валидация данных
export function validateUser(data: unknown): User {
return UserSchema.parse(data); // Выбросит ошибку, если данные невалидны
}
export function safeValidateUser(data: unknown) {
return UserSchema.safeParse(data); // Возвращает { success: boolean, data?: User, error?: ZodError }
}
2 Интеграция с API клиентом
import { validateUser, type User } from './schemas/user.schema';
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`https://api.example.com/users/${id}`);
const data = await response.json();
// Runtime валидация - проверяем, что данные соответствуют схеме
return validateUser(data);
}
// Использование
try {
const user = await fetchUser(1);
// user гарантированно соответствует типу User
console.log(user.name); // TypeScript знает тип
} catch (error) {
// Ошибка валидации - данные не соответствуют схеме
console.error('Validation error:', error);
}
3 Валидация с Yup (альтернатива)
npm install yup
import * as yup from 'yup';
const userSchema = yup.object({
id: yup.number().required(),
name: yup.string().min(2).max(100).required(),
email: yup.string().email().required(),
role: yup.string().oneOf(['admin', 'user', 'guest']).required(),
createdAt: yup.string().required(),
updatedAt: yup.string().required(),
});
// Валидация
async function validateUserYup(data: unknown) {
return await userSchema.validate(data);
}
💡 Zod vs Yup:
- Zod: Более современный, лучшая TypeScript интеграция, легче
- Yup: Более зрелый, больше возможностей для сложной валидации
- Рекомендация: Используйте Zod для новых проектов
Способ 7: Generic types для API 🔄
Generic типы позволяют создавать переиспользуемые API методы, которые работают с любыми типами данных.
1 Generic API методы
// Базовый ресурс
interface Resource {
id: string | number;
}
// Generic CRUD операции
class ApiResource<T extends Resource> {
constructor(
private baseURL: string,
private endpoint: string
) {}
async getAll(): Promise<T[]> {
const response = await fetch(`${this.baseURL}/${this.endpoint}`);
return response.json();
}
async getById(id: T['id']): Promise<T> {
const response = await fetch(`${this.baseURL}/${this.endpoint}/${id}`);
return response.json();
}
async create<TCreate extends Omit<T, 'id'>>(data: TCreate): Promise<T> {
const response = await fetch(`${this.baseURL}/${this.endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
async update<TUpdate extends Partial<Omit<T, 'id'>>>(
id: T['id'],
data: TUpdate
): Promise<T> {
const response = await fetch(`${this.baseURL}/${this.endpoint}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return response.json();
}
async delete(id: T['id']): Promise<void> {
await fetch(`${this.baseURL}/${this.endpoint}/${id}`, {
method: 'DELETE'
});
}
}
2 Использование generic ресурсов
import type { User, Todo } from './types/api';
// Создаём типизированные ресурсы
const usersApi = new ApiResource<User>('https://api.example.com', 'users');
const todosApi = new ApiResource<Todo>('https://api.example.com', 'todos');
// Все методы полностью типизированы
const users = await usersApi.getAll(); // User[]
const user = await usersApi.getById(1); // User
const newUser = await usersApi.create({ // TCreate = Omit<User, 'id'>
name: 'John',
email: 'john@example.com',
role: 'user'
});
const updated = await usersApi.update(1, { name: 'Jane' }); // Partial<Omit<User, 'id'>>
// То же самое для todos
const todos = await todosApi.getAll(); // Todo[]
const todo = await todosApi.getById('123'); // Todo
Best Practices для типизации API 🌟
1️⃣ Используйте OpenAPI Generator
Автоматическая генерация типов из OpenAPI спецификации — самый надёжный способ. Типы всегда актуальны и соответствуют API.
2️⃣ Добавьте runtime валидацию
TypeScript проверяет типы только на этапе компиляции. Добавьте Zod/Yup для проверки данных в runtime, особенно для внешних API.
3️⃣ Используйте Generic types
Создавайте переиспользуемые generic методы для стандартных CRUD операций. Это уменьшает дублирование кода.
4️⃣ Типизируйте ошибки
Создайте типы для ошибок API и используйте их в catch блоках. Это поможет обрабатывать разные типы ошибок по-разному.
5️⃣ Используйте const assertions
Для статических данных используйте as const для более точных типов
и literal types вместо string.
6️⃣ Создайте отдельные типы для запросов/ответов
Не используйте одни и те же типы для запросов и ответов. Ответ может содержать больше полей (id, timestamps), чем запрос.
Пример: Полная типизация с валидацией
import { z } from 'zod';
import axios from 'axios';
// 1. Схема валидации (Zod)
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
// 2. TypeScript тип из схемы
type User = z.infer<typeof UserSchema>;
// 3. Типизированный API метод
async function fetchUser(id: number): Promise<User> {
const response = await axios.get(`/users/${id}`);
// 4. Runtime валидация
const user = UserSchema.parse(response.data);
return user; // Гарантированно соответствует типу User
}
// 5. Использование с полной типизацией
const user = await fetchUser(1);
console.log(user.name); // ✅ TypeScript знает тип
console.log(user.email); // ✅ Автодополнение работает
Сравнение подходов
| Подход | Плюсы | Минусы | Когда использовать |
|---|---|---|---|
| Ручная типизация | Простота, контроль | Может устареть, много ручной работы | Небольшие проекты, прототипы |
| OpenAPI Generator | Автоматизация, актуальность типов | Нужна OpenAPI спецификация | Большие проекты, Contract-First |
| Generic API Client | Переиспользуемость, типобезопасность | Требует настройки | Множество ресурсов, стандартные CRUD |
| Zod/Yup валидация | Runtime проверка, точность | Дополнительная зависимость | Критичные данные, внешние API |
Интеграция с фреймворками 🎨
React + TypeScript API
import { useQuery } from '@tanstack/react-query';
import type { User } from './types/api';
function useUser(id: number) {
return useQuery<User>({
queryKey: ['user', id],
queryFn: async () => {
const response = await fetch(`/api/users/${id}`);
return response.json() as Promise<User>;
}
});
}
Vue.js + TypeScript API
import { ref } from 'vue';
import type { User } from './types/api';
export function useUser(id: number) {
const user = ref<User | null>(null);
async function fetchUser() {
const response = await fetch(`/api/users/${id}`);
user.value = await response.json() as User;
}
return { user, fetchUser };
}
Angular + TypeScript API
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import type { User } from './models/user.model';
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
}
Заключение
Типизация API с TypeScript — это не роскошь, а необходимость для надёжной разработки. Правильный подход к типизации позволяет:
- ✅ Обнаруживать ошибки на этапе компиляции
- ✅ Получать автодополнение в IDE
- ✅ Упрощать рефакторинг при изменении API
- ✅ Создавать самодокументирующийся код
- ✅ Улучшать developer experience
💡 Рекомендации:
- Начните с OpenAPI Generator — автоматическая генерация типов из спецификации
- Добавьте Zod валидацию — для критичных данных и внешних API
- Создайте generic API клиент — для переиспользования
- Типизируйте ошибки — для правильной обработки исключений
- Используйте const assertions — для более точных типов
Создайте типизированный Mock API за 2 минуты
Импортируйте OpenAPI спецификацию в LightBox API, получите готовый Mock API, и генерируйте типы автоматически. Бесплатный старт для всех разработчиков.
Попробовать бесплатно →