Урок 11

Работа с API и внешними данными

Данные из API приходят как unknown. Нужно проверить их и привести к типу.

Проблема

async function getUser(id: number) {
    const res = await fetch(`/api/users/${id}`);
    return res.json(); // Promise<any>
}

any теряет всю пользу типизации.

Приведение через as

Быстро, но небезопасно:

async function getUser(id: number): Promise<User> {
    const res = await fetch(`/api/users/${id}`);
    return res.json() as Promise<User>;
}

Если сервер вернёт другую структуру — ошибка всплывёт в рантайме.

Type guard

Правильный путь — проверить данные:

type User = {
    id: number;
    name: string;
    email: string;
};

function isUser(value: unknown): value is User {
    return (
        typeof value === 'object' &&
        value !== null &&
        'id' in value &&
        typeof (value as User).id === 'number' &&
        'name' in value &&
        typeof (value as User).name === 'string'
    );
}

async function getUser(id: number): Promise<User> {
    const res = await fetch(`/api/users/${id}`);
    const data: unknown = await res.json();

    if (!isUser(data)) {
        throw new Error('Invalid user data');
    }

    return data;
}

Zod

Библиотека для валидации с выводом типов:

import { z } from 'zod';

const UserSchema = z.object({
    id: z.number(),
    name: z.string(),
    email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

async function getUser(id: number): Promise<User> {
    const res = await fetch(`/api/users/${id}`);
    return UserSchema.parse(await res.json());
}

z.infer автоматически выводит тип из схемы. Одно определение — и валидация, и тип.

Zod: примеры

const PostSchema = z.object({
    id: z.number(),
    title: z.string().min(1),
    tags: z.array(z.string()),
    published: z.boolean().optional(),
    createdAt: z.string().datetime(),
});

type Post = z.infer<typeof PostSchema>;

Вложенные схемы

const AddressSchema = z.object({
    city: z.string(),
    zip: z.string(),
});

const UserSchema = z.object({
    name: z.string(),
    address: AddressSchema,
});

Union в Zod

const ShapeSchema = z.discriminatedUnion('kind', [
    z.object({ kind: z.literal('circle'), radius: z.number() }),
    z.object({ kind: z.literal('square'), side: z.number() }),
]);

Transform

const DateSchema = z.string().transform((s) => new Date(s));

Safe parse

const result = UserSchema.safeParse(data);

if (result.success) {
    console.log(result.data); // User
} else {
    console.log(result.error.issues);
}

Когда что использовать

  • Одно поле — typeof
  • Простой объект — type guard
  • Сложные данные — Zod или аналог
  • Формы — Zod + React Hook Form

Другие библиотеки

  • Yup — старая, но популярная
  • Valibot — лёгкая альтернатива
  • ArkType — быстрая, типы как синтаксис
  • io-ts — функциональный подход

Итоги

  • Данные из API — unknown
  • Type guards для проверки
  • Zod для сложных схем
  • z.infer даёт тип из схемы
  • Валидация и тип — из одного источника