TypeScript размером с байт #1 — Тип фильтра

Привет! Добро пожаловать в первый пост моей новой серии статей о TypeScript.

В этой серии я буду демонстрировать и деконструировать байтовые фрагменты TypeScript, чтобы помочь вам понять, как писать код на уровне типов.


Давайте начнем.

Фильтр типа

В TypeScript есть понятие кортежных типов, которые, по сути, напрямую сопоставимы с массивами в js. Выглядит это примерно так:

type MyTuple = [1, 2, string, boolean, null];
Войти в полноэкранный режим Выйти из полноэкранного режима

Допустим, мы хотим реализовать метод Filter, похожий на родной метод JavaScript Array.Filter в Type Level, как нам это сделать?

Вот полная реализация:
По мере продвижения вперед я буду объяснять этот тип построчно.

type Filter<Arr extends unknown[], FilterBy> = Arr extends [
  infer FirstElement,
  ...infer Rest
]
  ? FirstElement extends FilterBy
    ? [FirstElement, ...Filter<Rest, FilterBy>]
    : Filter<Rest, FilterBy>
  : Arr;

type Demo = Filter<[1, 2, string, boolean], number>
// -> [1, 2]
Вход в полноэкранный режим Выйти из полноэкранного режима

Открыть на игровой площадке

Строка 1:

В строке 1 у нас есть Arr extends [ infer FirstElement, ...infer Rest].

Здесь мы говорим:
Если Arr равен [infer FirstElement, ...infer Rest], то переходим в истинную ветвь условного типа. Эта строка также гарантирует, что переданный Arr действительно является кортежем. Если нет, то будет выбрана ложная ветвь, которая возвращает Arr в последней строке.

Важной частью является ключевое слово infer, ключевое слово infer — это смотровое стекло TypeScript, оно может извлечь значение типа в определенном месте.

По сути, мы спрашиваем компилятор TypeScript: «Эй! Что бы ни находилось в этом месте, присвойте это переменной FirstElement«.

Затем мы можем получить доступ к значению этого типа FirstElement в условной ветви.

Рассмотрим пример:

type GetFirstElementOfArray<Arr extends any[]> = 
  Arr extends [infer FirstElement, ...infer Rest] 
    ? FirstElement 
    : never

type Demo = GetFirstElementOfArray<[1, 2, 3]> // 1
Вход в полноэкранный режим Выход из полноэкранного режима

Вы также заметите часть ...infer Rest, Она похожа на синтаксис rest в JavaScript. Эта часть выведет остальные элементы массива и сохранит их в переменной Rest, к которой вы также можете обратиться в условной ветви, в данном случае она будет иметь значения [2, 3].

Линии 5, 6, 7:

Теперь мы находимся внутри истинной ветви условного типа.

В FirstElement extends FilterBy мы проверяем, равен ли FirstElement FilterBy.

Если условие выполняется, мы возвращаем новый кортеж типа [FirstElement, ...Filter<Rest, FilterBy>], здесь мы включаем FirstElement &, затем рекурсивно вызываем Filter generic снова с Rest элементами.

Заметьте, что мы также используем синтаксис распространения ...Filter<Rest, FilterBy>, поскольку дженерик Filter возвращает кортеж, мы можем распространить его значение на этот новый кортеж, который мы возвращаем.

И если FirstElement extends FilterBy не сработает, мы снова рекурсивно вызовем Filter, но пропустим FirstElement на этот раз, Filter<Rest, FilterBy>.

Теперь давайте пройдемся по коду каждого рекурсивного вызова, чтобы лучше понять, что происходит:

// Initial state
type D = Filter<[1, 'hello', 'world'], number>
Вход в полноэкранный режим Выход из полноэкранного режима

Рекурсия #1:

// input <[1, 'hello', 'world'], number>
type Filter<Arr extends unknown[], FilterBy> = Arr extends [
  infer FirstElement, // 1
  ...infer Rest // ['hello', 'world']
]
  ? FirstElement extends FilterBy // 1 == number -> true
    ? [FirstElement, ...Filter<Rest, FilterBy>] // [1, ...Filter<['hello', 'world'], FilterBy>]
    : Filter<Rest, FilterBy>
  : Arr;

Вход в полноэкранный режим Выход из полноэкранного режима

Рекурсия #2:

// input <['hello', 'world'], number>
type Filter<Arr extends unknown[], FilterBy> = Arr extends [
  infer FirstElement, // 'hello'
  ...infer Rest // ['world']
]
  ? FirstElement extends FilterBy // 'hello' == number -> false
    ? [FirstElement, ...Filter<Rest, FilterBy>]
    : Filter<Rest, FilterBy>  // Filter<['world'], FilterBy>
  : Arr;
Войти в полноэкранный режим Выход из полноэкранного режима

Рекурсия #3:

// input <['world'], number>
type Filter<Arr extends unknown[], FilterBy> = Arr extends [
  infer FirstElement, // 'world'
  ...infer Rest // []
]
  ? FirstElement extends FilterBy // 'world' == number -> false
    ? [FirstElement, ...Filter<Rest, FilterBy>]
    : Filter<Rest, FilterBy>  // Filter<[], FilterBy>
  : Arr;
Войти в полноэкранный режим Выход из полноэкранного режима

Рекурсия #4:

// input <[], number>
type Filter<Arr extends unknown[], FilterBy> = Arr extends [
  infer FirstElement, // unknown
  ...infer Rest // unknown
]
  ? FirstElement extends FilterBy
    ? [FirstElement, ...Filter<Rest, FilterBy>]
    : Filter<Rest, FilterBy>
  : Arr;
  // ^ false branch is taken since `Arr extends [infer FirstElement, ...infer Rest]` failed,
 //   thus empty Arr is returned.
Войти в полноэкранный режим Выход из полноэкранного режима

Вот и все, надеюсь, теперь вы поняли, как работает тип Filter. По своей сути тип достаточно прост, но понимание того, как работает каждая его часть, является важной частью.

Если вы все еще не можете понять код на уровне типа, как насчет того, чтобы переписать код на уровне типа в код javascript во время выполнения?

Вы будете удивлены, насколько похоже это выглядит:

function Filter(Arr, FilterBy) {
    if (Arr.length < 1) return Arr;
    const [FirstElement, ...Rest] = Arr;
    if (typeof FirstElement === FilterBy) {
        return [FirstElement, ...Filter(Rest, FilterBy)];
    } else {
        return Filter(Rest, FilterBy)
    }
}

Filter([1, 2, 3, 'hello', true], 'number')
// [1, 2, 3]
Вход в полноэкранный режим Выход из полноэкранного режима

Круто, не правда ли?

В общем, я думаю, что это все, надеюсь, вы узнали что-то новое и вам понравился этот пост.

Я буду продолжать эту серию статей, так что не забудьте следить за мной в twitter или на dev.to, чтобы получать свежие новости.

Оцените статью
Procodings.ru
Добавить комментарий