Когда создавалась библиотека для валидации данных quartet были поставленны следующие цели-ориентиры:
- TypeScript
- Краткость
- Простота
- Производительность
В этой статье я хотел бы рассмотреть ориентированность quartet на TypeScript.
Мотивация
Мы работаем на своих проектах с использованием TypeScript. Поэтому, когда я создавал эту библиотеку, мне хотелось, чтобы человек, который знает TypeScript, не изучал quartet, как нечто ему совсем новое, а узнавал в этой библиотеке то, что он уже знает.
User-Defined Type Guards
Рассмотрим пример. Мы запрашиваем данные про пользователя с API. Предполгаем, что они имеют следующий тип:
interface User { id: string; name: string; gender: "male" | "female"; age: number; phoneBook: { [name: string]: string; }; }
Что я хочу получить от функции валидации:
const probablyUser: unkown = { ... } if (checkUser(probablyUser)) { // probablyUser has type User console.log(probablyUser.name) } else { // probablyUser has type unkown throw new Error('Probably User has not type User') }
Для достижения такой цели используются User-Defined Type Guards.
То есть объявление функции должно иметь следующий вид:
function checkUser(probablyUser: any): probablyUser is User { // ... }
Давайте используем quartet для создания такой функции:
import { v } from "quartet"; const checkUser = v({ id: v.string, name: v.string, gender: ["male", "female"], age: v.number phoneBook: { [v.rest]: v.string, } });
Написав такой код мы получим на выходе функцию, которая не является TypeGuard’ом:
chechUser: (value: any) => boolean;
Чтобы сделать её TypeGuard’ом необходимо декларативно указать какой именно тип будет валидироватся этой функцией. Это делается так:
const checkUser = v<User>({ // ... });
В итоге:
chechUser: (value: any) => value is User
Есть два момента касательно этого пункта:
Гарантии
Сам факт того, что разработчик может указать какой тип валидируется схемой может насторожить, потому что вполне реально написать так:
const checkNumber = v<number>({ name: v.string }); // checkNumber: (value: any) => value is number
Видим несоответствие — схема написана для типа { name: string }, а разработчик указал, что результат должен иметь тип number.
Данная возможность была допущена намерено. Потому что средства недопущения подобного(например описанные в статье) приводят к тому, что схема валидации всё меньше похожа на описание типа — и становится более "технической" и менее легкой для написания и чтения.
Поэтому было принято решение, допустить такую возможность не потеряв при этом читаемости.
Некоторые сложности возникли, когда я описывал тип функции v. Изначально я писал примерно так:
const v: <T>(schema: Schema) => (value: any) => value is T;
Но это делает параметр типа обязательным.
Я не хотел, чтобы эта возможность с TypeGuard была обязательной.
Поэтому написал так:
const v: <T = any>(schema: Schema) => (value: any) => value is T;
Но это привело к тому, что когда валидация не проходила — она присваивала переменной тип never:
const checkNumber = v(v.number); const value: any = "123"; if (!checkNumber(value)) { // value has type never }
Ну это логично потому что, если переменная не любого типа, то она никакого типа.
Мне нужно было иметь тип, который смог бы определить является ли T типом any и поставил бы один тип результата, а если T === any, то другой.
Я хотел написать как-то так:
const v: <T = any>( schema: Schema ) => IfAny<T, (value: any) => boolean, (value: any) => value is T>;
type IfAny<T,A,B> = // ...
И тут я немного завис — писал разные варианты, но сработала в конечном итоге такая идея:
Если множество типов являются подтипом типа T — то скорее всего он является типом any
В итоге я написал вот это:
type IfAny<T, A, B> = true extends T ? "1" extends T ? 1 extends T ? {} extends T ? (() => void) extends T ? null extends T ? A : B : B : B : B : B : B;
Я предположил, что никто в здравом уме не будет писать код, в результате которого в переменной может быть тип: boolean | number | string | object | function | null и чтобы он не подразумевался быть еквивалентом any.
Схожесть Схемы и TypeScript типов
Я хотел добиться того, чтобы написать схему, имея тип TypeScript’a занимало как омжно меньше времени.
Давайте рассмотрим создание схем с использованием @hapi/joi и ajv, для нашего типа User.
Будем пользоваться сайтом Text Compare чтобы определить схожесть.
quartet
const checkUser = v({ id: v.string, name: v.string, gender: ["male", "female"], age: v.number phoneBook: { [v.rest]: v.string, } })

Кол-во дополнительных символов: 24
hapi/joi
const schema = j.object({ id: j.string().required(), name: j.string().required(), gender: j .string() .valid("male", "female") .required(), age: j.number().required(), phoneBook: j.object().pattern(/.*/, j.string()) });

Тут сайт не очень хорошо подсветил дополнительные символы, но если точно подсчитать, до будет 118 дополнительных символов.
ajv
const checkUser = a.compile({ type: "object", required: ["id", "name", "gender", "age", "phoneBook"], properties: { id: { type: "string" }, name: { type: "string" }, gender: { type: "string", enum: ["male", "female"] }, phoneBook: { type: "object", additionalProperties: { type: "string" } } } });

Они и не подразумевались быть похожими, но разница 146 символов.
Сравним результаты:

Конечно эти библиотеки не стремились создать схемы похожие на TypeScript. Но, на мой взгляд, это не значит, что такая схожесть не является плюсом.
Итого
Наличие встроенной поддержки TypeGuard механизма — одна из мелочей, но приятных.
Схожесть схем с TypeScript типами была сделана нарочно минимальной. На мой взгляд — это позволяет сделать схемы более легкими для чтения и модификации.
ссылка на оригинал статьи https://habr.com/ru/post/495332/
Добавить комментарий