MinusOne
Проблема
Вы получаете на входе число (всегда положительное). Ваш тип должен вернуть то же число, только на единицу меньше. Например:
type Zero = MinusOne<1>; // 0
type FiftyFour = MinusOne<55>; // 54
Решение
Эта задача и вправду довольно сложная. TypeScript не может ничего предложить для работы с числами - ничего!
Поэтому, мы должны как-то найти обходной путь для реализации такой математической операции. И, поймите, что это вряд ли будет отличное решение и использовать его как пример для подражания вряд ли получится.
Я начал с того, что спросил себя. Были ли у нас случаи, когда мы работали с числовыми литералами, но без использования самих литералов. И, как оказалось, да. Мы использовали кортежи.
У нас уже были задачи, в которых мы работали с кортежами и узнавали длину этих
кортежей. Помните синтаксис для этого? Это было очень просто - мы обращались к
свойству length
, которое и возвращало нам числовой тип литерал.
Поэтому я подумал, а что если мы создадим кортеж нужной нам длины и выведем его часть без последнего элемента. А после, возьмем длину этого выведенного кортежа.
Давайте начнём с вспомогательного типа, который будет нам создавать кортеж
нужной длины. Назовём его Tuple
:
type Tuple<L extends number, T extends unknown[] = []> = never;
Он принимает в качестве аргументов длину кортежа и временный аккумулятор. Этот
аккумулятор будет накапливать кортеж, пока мы не получим кортеж нужной длины.
Чтобы эту проверку реализовать, обратимся к свойству length
и сравним его с
требуемым:
type Tuple<L extends number, T extends unknown[] = []> = T["length"] extends L
? never
: never;
Как только мы получили кортеж нужной длины - возвращаем его:
type Tuple<L extends number, T extends unknown[] = []> = T["length"] extends L
? T
: never;
Но, если же мы не получаем кортеж нужной длины, нам нужно добавить к нему ещё один элемент. И продолжать так стоит до тех пор, пока не получим ожидаемую длину кортежа:
type Tuple<L extends number, T extends unknown[] = []> = T["length"] extends L
? T
: Tuple<L, [...T, unknown]>;
Теперь, если мы вызовем наш тип с параметром 5
, например, то мы получим кортеж
длиной в 5 элементов и типа unknown
. Если же мы обратимся к свойству length
на этом кортеже, то мы получим числовой литерал - 5
. То что и нужно было.
Как же достать литерал 4
из такого кортежа? Вывести кортеж, длина которого 5,
но без последнего элемента. Другими словами, кортеж на один элемент будет
короче.
type MinusOne<T extends number> = Tuple<T> extends [...infer L, unknown]
? never
: never;
Используя такую конструкцию, мы получим в тип параметре L
кортеж без
последнего его элемента. Всё что остается сделать - это вернуть длину
выведенного кортежа.
type MinusOne<T extends number> = Tuple<T> extends [...infer L, unknown]
? L["length"]
: never;
Таким образом, мы реализовали некое подобие математической операции в системе
типов. Например, вызывая наш тип с параметром 5
, мы получим числовой литерал
4
.
type Tuple<L extends number, T extends unknown[] = []> = T["length"] extends L
? T
: Tuple<L, [...T, unknown]>;
type MinusOne<T extends number> = Tuple<T> extends [...infer L, unknown]
? L["length"]
: never;
Но! Большое “но”! В последних версиях TypeScript, они добавили проверку на количество рекурсивных вызовов. Поэтому, если честно, мы не пройдём тесты, в которых используются числа больше 50. Так что это решение сложно назвать решением.
Если у вас есть идеи получше, не стесняйтесь, пишите их с объяснениями в комментариях ниже. Спасибо!
Комментарии