В данной заметке рассмотрю неочевидные приемы организации циклов при написании типов. И применю предложенный подход для утилиты типа, создающей шаблон строкового литерала заданной длины.
Вместо введения
Формат заметок для статей я выбрал потому что веду их для себя. А делюсь ими, так как это может кому-нибудь показаться интересным.
Выбранные задачи, техники и решения стоит в первую очередь рассматривать как информацию к размышлению.
Подготовка
Для тестов типов я выбрал vitest. И использую assertType
, которая позволяет работать с данными как если бы мы их использовали.
Комментарии к типам добавляю, что бы IDE выводила красивую подсказку с описанием параметров.
Встроенного способа организовать цикл при создании типа на TS нет, поэтому нужно использовать специальные приемы.
Задача: Создать тип, который принимает параметры T и N, где T - любой тип, а N - число и возвращает кортеж длины N с элементами T
const tuple3length: [number, number, number] = [1, 2, 3]
assertType<BuildTuple<number, 3>>(tuple3length)
Решение: Для решения задачи нам потребуется промежуточный тип, который мы как раз и будем рекурсивно вызываться для накопления результата
/**
* Создает кортеж заданной длины
* @template T типа элементов кортежа
* @template N длина кортежа
*/
type BuildTuple<T, N extends number> = _buildTuple<T, N>
type _buildTuple<T, N extends number, Result extends T[] = []> = Result['length'] extends N
? Result
: _buildTuple<T, N, [T, ...Result]>
И теперь можно легко догадаться как реализовывать циклы и счетчики при написании типов.
Циклы при работе с типами
Задача: написать тип-утилиту для строки длины N представляющей шестнадцатеричное число
type Digit =
'0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' |
'A' | 'B' | 'C' | 'D' | 'E' | 'F'
const hexValue1: `${Digit}` = '7'
const hexValue2: `${Digit}${Digit}` = '1A'
const hexValue3: `${Digit}${Digit}${Digit}` = '9FF'
assertType<BuildHexString<1>>(hexValue1)
assertType<BuildHexString<2>>(hexValue2)
assertType<BuildHexString<3>>(hexValue3)
Решение
В предыдущей задаче длина результирующего кортежа использовалась как условие для остановки цикла.
Это свойство кортежей и будет использоваться для решения поставленной задачи.
/**
* Создает тип для строки длины N представляющей шестнадцатеричное число
* @template N длина строки
*/
type BuildHexString<N extends number> = N extends 0
? ''
: _BuildHexString<N, Digit, [1]>
/**
* Промежуточный тип-утилита
* @template N длина строки
* @template Result накапливает результат
* @template Count счетчик цикла, когда Count['length'] === N цикл завершится
*/
type _BuildHexString<N extends number, Result extends string, Count extends unknown[]> = Count['length'] extends N
? Result
: _BuildHexString<N, Result | Join<BuildTuple<Digit, [1, ...Count]['length']>>, [1, ...Count]>
/**
* Преобразует кортеж в шаблонную строку
* @template T входной кортеж
*/
type Join<T extends string[]> =
T extends [string, ...infer Rest extends string[]]
? `${T[0]}${Join<Rest>}`
: ''
Само решение - это объединение типов переменных hexValue1, hexValue2, hexValue3
.
BuildHexString
: тип с коротким ясным интерфейсом.Инициализирует счетчик и задает алфавит для служебного типа
_BuildHexString
_BuildHexString
: служебный тип, который создает объединение строк и работает со счетчикомCount
.Сам счетчик представляет собой кортеж вида
[1, 1, ...., 1]
Цикл представляет собой рекурсию, которая останавливается, когда "значение" счетчика становится равным N
Join
: преобразует кортеж строк в шаблонный строковый литерал.Необходимость связки создание кортежа и преобразование в строку обусловлено тем, что TS не поддерживает "нормальную" обработку длины для шаблона строкового литерала. Подробности можно прочитать в топике проблемы 34692
T extends [string, ...infer Rest extends string[]]
: вместоstring
можно использоватьinfer Head extends string
, которое затем использовать вместоT[0]
infer Rest extends string[]
: вывод типа использует ограничение, которое и позволяет рекурсивно использовать Join
Вместо заключения
Сама возможность организовывать счетчики на TS мне показалась любопытной. Разумеется необязательно создавать сложные утилиты для создания небольших типов.
Но учитывать такую возможность, на мой взгляд, определенно стоит.