Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. TypeScript: данные из API без any
modern_js_ts

TypeScript: данные из API без any

Проверка неизвестных данных: `unknown`, сужение типов, проверка массива и полей, неизменяемый результат и атомарный отказ.

Открыть лабораториюv0.2.0Запускается локально из публичного репозитория

TypeScript: данные из API без any

Ответ сервера не становится безопасным только потому, что мы написали рядом имя типа.

#Что сделаем

Представьте: вы написали const data: Product[] = await response.json(). Компилятор молчит. Затем API отдаёт null или { error: "rate limit" } — приложение падает без объяснений. unknown заставляет проверить данные до того, как они попадут в компонент.

Напишем функцию normalizeProducts(value: unknown). Она примет неизвестное значение, проверит массив и каждую запись, вернёт новые объекты товаров или выбросит TypeError, не оставляя частичного результата.

Этот контракт совпадает с лабораторной работой. В предыдущем уроке мы копировали объекты через spread, чтобы не портить исходные данные — здесь мы продолжим эту практику, но на границе с внешним миром.

#1. На внешней границе используем unknown

TypeScript проверяет программу до запуска, но не управляет ответом сервера. Даже документированный API может вернуть пустое тело, страницу ошибки или данные старой версии.

// src/api.ts async function loadProducts(signal: AbortSignal): Promise<unknown> { const response = await fetch('/api/products', { signal }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); }

unknown означает: «значение есть, но его форму ещё нужно доказать». В отличие от any, этот тип не разрешит сразу обратиться к полю или вызвать метод.

const value: unknown = await loadProducts(signal); // Ошибка TypeScript: форма value пока неизвестна. console.log(value[0].title);

Это полезная ошибка. Она не даёт случайно распространить недоверенные данные по приложению.

#2. Сначала проверяем контейнер

Лаборатория ожидает массив товаров. Первым делом отсекаем всё остальное:

// src/normalizeProducts.ts function normalizeProducts(value: unknown): readonly Product[] { if (!Array.isArray(value)) { throw new TypeError('products must be an array'); } // Здесь value уже имеет тип unknown[]. return value.map(parseProduct); }

После Array.isArray компилятор сужает тип value до массива неизвестных элементов. Это и есть сужение типа: проверка в коде даёт TypeScript новую информацию для текущей ветви.

Проверьте себя. Передайте null, объект и строку. Во всех трёх случаях функция должна выбросить TypeError до обхода элементов.

#3. Затем проверяем одну запись

У unknown нельзя читать свойства. Сначала убеждаемся, что перед нами объект, и отдельно исключаем null, потому что typeof null возвращает строку "object".

После проверки typeof value === 'object' TypeScript знает, что перед нами объект, но не какие поля в нём есть. Приведение к Record<string, unknown> даёт читать произвольные ключи — каждый всё ещё unknown.

// src/normalizeProducts.ts type Product = { id: string; title: string; price: number; }; function parseProduct(value: unknown): Product { if (typeof value !== 'object' || value === null) { throw new TypeError('product must be an object'); } const row = value as Record<string, unknown>; if ( typeof row.id !== 'string' || typeof row.title !== 'string' || typeof row.price !== 'number' || !Number.isFinite(row.price) || row.price < 0 ) { throw new TypeError('invalid product'); } return { id: row.id, title: row.title, price: row.price }; }

Обратите внимание на цену: сначала проверяется typeof row.price === 'number', и только после этого вызывается Number.isFinite. Это решает две задачи одновременно: Number.isFinite принимает только число, поэтому TypeScript требует сначала убедиться в типе; а во время выполнения проверка отсекает строки, null и undefined до вызова функции, которая ожидает число.

#4. Ошибочная запись отклоняет весь результат

Если одна из десяти записей повреждена, функция выбрасывает исключение и не возвращает девять хороших. Это называется атомарный результат: или всё прошло проверку, или ничего. Иначе вызывающий код получит смесь доверенных и недоверенных данных.

map(parseProduct) сначала создаёт новый объект для каждой корректной записи. Если одна запись ошибочна, parseProduct выбрасывает исключение, и normalizeProducts не возвращает частичный массив.

const input = [ { id: 'p1', title: 'Клавиатура', price: 99 }, { id: 2, title: 'Мышь', price: 40 }, ]; normalizeProducts(input); // TypeError: invalid product

Функция не меняет input. Даже успешно проверенные записи копируются в новые объекты. После возвращения результата код приложения больше не зависит от того, кто ещё хранит ссылку на исходный JSON.

Весь поток проверки выглядит так:

unknown
  ↓ Array.isArray?
  ↓ нет → TypeError
unknown[]
  ↓ typeof === 'object' && !== null?
  ↓ нет → TypeError
Record<string, unknown>
  ↓ typeof id/title === 'string'?
  ↓ typeof price === 'number' && isFinite && >= 0?
  ↓ нет → TypeError
Product

Проверьте себя. Сохраните ссылку на первый входной объект и убедитесь, что соответствующий товар результата равен ему по содержимому, но не по ссылке.

#5. Что даёт Readonly

Представьте: normalizeProducts вернула проверенный массив. Коллега пишет компонент и случайно меняет цену товара через products[0].price = 0. Компилятор молчит — объект не заморожен, TypeScript не знает, что менять нельзя.

TypeScript предлагает два уровня защиты:

Уровень 1: readonly перед массивом — запрещает менять сам массив. Это уже стоит в сигнатуре normalizeProducts:

const products = normalizeProducts(payload); products.push({ id: 'p2', title: 'Мышь', price: 40 }); // ошибка типов products[0].price = 0; // пока работает!

push запрещён, но products[0].price = 0 — нет: readonly Product[] защищает массив, но не его элементы.

Уровень 2: Readonly вокруг типа — запрещает менять поля каждого объекта. Это встроенный утилитарный тип TypeScript, который делает все свойства доступными только для чтения:

type Product = Readonly<{ id: string; title: string; price: number; }>; // теперь и products[0].price = 0 — ошибка типов

Это защита при компиляции, а не заморозка JavaScript-объекта. Object.freeze делает то же самое, но поверхностно и без рекурсивного обхода. Для типизации readonly надёжнее и проще.

В лабораторной работе readonly в возвращаемом типе — это контракт: функция обещает, что результат нельзя изменить через её ссылку.

#Лаборатория

Реализуйте экспорт:

export function normalizeProducts(value: unknown): readonly Product[];

Требования:

  • значение верхнего уровня должно быть массивом;
  • каждая запись содержит строковые id и title;
  • price — конечное неотрицательное число;
  • любая ошибка приводит к TypeError;
  • входной массив и его элементы не изменяются;
  • результат содержит новые объекты.

Запустите проверку:

LAB_MODE=starter npm test -- modern_js_ts

После прохождения открытых тестов добавьте свой случай: null, NaN, отрицательную цену, отсутствующее поле или смешанный массив с ошибкой в последней записи.

#Источники для сверки

  • TypeScript: тип unknown
  • TypeScript: сужение типов
  • MDN: Array.isArray
  • MDN: Number.isFinite

Устранение неисправностей

Далее: Инструментарий React-проекта