Quartet 9: Allegro | TypeScript

Когда создавалась библиотека для валидации данных quartet были поставленны следующие цели-ориентиры:

В этой статье я хотел бы рассмотреть ориентированность 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,
  }
})

image

Кол-во дополнительных символов: 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())
});

image

Тут сайт не очень хорошо подсветил дополнительные символы, но если точно подсчитать, до будет 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"
      }
    }
  }
});

image

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

Сравним результаты:

image

Конечно эти библиотеки не стремились создать схемы похожие на TypeScript. Но, на мой взгляд, это не значит, что такая схожесть не является плюсом.

Итого

Наличие встроенной поддержки TypeGuard механизма — одна из мелочей, но приятных.

Схожесть схем с TypeScript типами была сделана нарочно минимальной. На мой взгляд — это позволяет сделать схемы более легкими для чтения и модификации.

Only registered users can participate in poll. Log in, please.

Схожесть схем с TypeScript типами — єто плюс?

  • 0.0%Да0

  • 0.0%Скорее, да0

  • 100.0%Безразлично1

  • 0.0%Скорее, нет0

  • 0.0%Нет0

  • 0.0%Зависит0

Специально для сайта ITWORLD.UZ. Новость взята с сайта Хабр