Проблема

Вы получаете на входе число (всегда положительное). Ваш тип должен вернуть то же число, только на единицу меньше. Например:

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. Так что это решение сложно назвать решением.

Если у вас есть идеи получше, не стесняйтесь, пишите их с объяснениями в комментариях ниже. Спасибо!

Что почитать