Лучший способ обеспечить безопасность типов во время выполнения в Typescript.
В наши дни разработчикам интерфейсов и бэкендов приходится иметь дело со многими внешними API, обычно REST API. Получить данные легко, но убедиться, что данные действительны и соответствуют ожидаемой схеме, может быть сложно.
Иностранные API могут меняться со временем или просто доставлять чушь, особенно в пятницу днем или в полночь во время лунного затмения.
Поэтому важно быстро обнаруживать проблемы и надежно предотвращать распространение ошибок в более глубокие части нашей программной архитектуры. (Представьте, что ваш торговый бот использует API фондового рынка, например, yahoo-finance. Вы хотите убедиться, что все ответы API тщательно проверяются, прежде чем принимать какие-либо торговые решения.)
Высокие ожидания
Поскольку я избалованный разработчик Typescript, я ожидаю, что мои вызовы API будут типобезопасными, а моя IDE побалует меня автодополнением. Я могу использовать генератор кода здесь и там, если получу правильное описание API. Генератор кода может обернуть выборку данных в хорошо типизированные функции и в некоторой степени реализовать проверку типов. Но это не всегда возможно.
Что делать, если я хочу реализовать свой собственный генератор кода? Что, если я собираю клиент для API, который изначально не предназначался для использования в качестве API? 😉
Начнем с нуля
const response = await fetch("https://jsonplaceholder.typicode.com/users/1")
const result = await resp.json();
console.log(result.email);
console.log((result as User).email);
Здесь, чтобы работало автодополнение, ответ должен быть каким-то образом приведен к User
. К сожалению, каждый раз, когда мы используем явный кастинг, детеныш единорога умирает медленной смертью.
Чего мы на самом деле хотим, так это спасти единорогов убедитесь, что данные соответствуют заданной схеме, прежде чем использовать ее.
const user: User = isUser(data);
console.log(user.email);
Несколько лучше, если не считать уродливого генерирования исключений, но я уверен, что мы можем сделать еще лучше! Особенно в машинописном тексте.
Тип охранников
Typescript дает нам удобную функцию под названием тип гвардии. Мы можем реализовать нашу функцию «isUser» со специальным типом возвращаемого значения: data is User
.
Наша причудливая функция «предикат типа» принимает любые данные и проверяет, являются ли они User
.
const isUser = (data: any): data is User =>
(data as User).email !== undefined;
Теперь идет магия. Компилятор машинописного текста и наша IDE достаточно умны, чтобы обрабатывать user
переменная, отныне, как User
внутри if-ветви:
if(isUser(user)) {
console.log(user.email);
} else {
console.error("This is no User!");
}
Это решение в 3 раза лучше, поскольку мы одновременно (1) добились проверки схемы, (2) получили автозаполнение и (3) не нарушили поток управления исключениями.
Ну, единороги спасены, а как же бедные ленивые программисты? Им теперь приходится писать все эти причудливые охранники типа. Если бы мы только могли это как-то автоматизировать…
Разбирать не проверять
Существует интересное мировоззрение, согласно которому вместо проверки данных мы должны их анализировать.
Мне нравится эта цитата из статьи [Parse don’t validate 1] 1:
Подумайте: что такое парсер? На самом деле синтаксический анализатор — это просто функция, которая получает менее структурированный ввод и выдает более структурированный вывод. Парсеры — невероятно мощный инструмент: они позволяют выполнять проверки ввода заранее, прямо на границе между программой и внешним миром. После того, как эти проверки были выполнены, их больше никогда не нужно проверять! Специальная проверка приводит к явлению, которое теоретико-языковая область безопасности называет дробовик разбор [2] 2.
Для Typescript и Javascript существует несколько пакетов, подходящих для этой задачи. Назвать несколько: надстройка, типы выполнения, io-ts, инструкция по дрочке, Зод.
Мой личный фаворит — это упаковка Зод.
Зод
В zod мы описываем наши типы составным образом и получаем парсер, который проверяет все входные данные во время выполнения и, если они верны, возвращает статически типизированный объект.
Не показывая, как User-parser (здесь ZUser
) фактически определен, наш пример сверху будет выглядеть так:
const result = ZUser.parse(input);
console.log(result.email);
…или без генерации исключений:
const result = ZUser.safeParse(input);
if (!result.success) {
return;
}
console.log(result.email);
Заключение
Я не буду вдаваться в подробности о zod здесь. Давайте сохраним это для другого блога. На данный момент вывод такой:
- не принимайте данные с конечных точек вслепую
- разобрать, не проверять
- изучите какую-нибудь библиотеку, например zod, чтобы сделать это надежно для вас
использованная литература
[1]: Разобрать, не проверятьАлексис Кинг
[2]: Семь башен Вавилона: классификация ошибок LangSec и способы их устраненияФ. Момот, С. Братусь, С. М. Холберг и М. Л. Паттерсон.