Проблема

Реализовать тип PercentageParser<T extends string>. Этот тип должен разбить строку на три элемента, согласно /^(\+|\-)?(\d*)?(\%)?$/.

Структура этих элементов выглядит следующим образом: [+ или -, число, %]. В случае, если совпадения нет, нужно вернуть пустую строку. Например:

type PString1 = "";
type PString2 = "+85%";
type PString3 = "-85%";
type PString4 = "85%";
type PString5 = "85";

type R1 = PercentageParser<PString1>; // expected ['', '', '']
type R2 = PercentageParser<PString2>; // expected ["+", "85", "%"]
type R3 = PercentageParser<PString3>; // expected ["-", "85", "%"]
type R4 = PercentageParser<PString4>; // expected ["", "85", "%"]
type R5 = PercentageParser<PString5>; // expected ["", "85", ""]

Решение

Синтаксический разбор строк это очень интересная задача (как для меня). Жаль только, что мы не сможем добиться хорошего решения в этом случае. Так как всё что у нас есть это только система типов TypeScript.

Нам нужно разбить строку на три компонента: знак числа, число, знак процента. Чтобы упростить решение, давайте реализуем их как отдельные типы. Первый тип будет возвращать знак числа. Второй будет возвращать само число, а третий - знак процента.

Начнём с первого типа. Нам нужно проверить, что первый символ в строке это знак плюса или минуса. Чтобы этого достичь, нам нужно сначала вывести этот первый символ.

type ParseSign<T extends string> = T extends `${infer S}${any}` ? never : never;

Имея первый символ в тип параметре S, мы можем проверить плюс это или минус. Если это плюс или минус, то возвращаем тип параметр S, возвращаем знак, что мы вывели. Во всех остальных случаях возвращаем пустую строку, согласно постановке задачи.

type ParseSign<T extends string> = T extends `${infer S}${any}`
  ? S extends "+" | "-"
    ? S
    : ""
  : "";

Таким образом, мы реализовали тип, который может распознать и вернуть знак числа. Теперь, сделаем то же самое со знаком процента.

Для начала, проверим, а есть ли символ процента в конце строки.

type ParsePercent<T extends string> = T extends `${any}%` ? never : never;

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

type ParsePercent<T extends string> = T extends `${any}%` ? "%" : "";

Имея два типа, два анализатора, которые возвращают знаки, мы можем начать думать о числе. Нам нужно вывести число, которое стоит между этими знаками. Проблема заключается в том, что эти знаки опциональные.

Чтобы реализовать поддержку опциональных знаков, нам нужно проверять их наличие. То есть нам нужно проверять, если присутствует знак числа, то пропустить его и взять число. И так далее. А проблема в том, что если мы этого не сделаем, то в наше число попадут ненужные нам знаки.

Но у нас же эта логика уже реализована в наших других типах. Всё что нам нужно это правильно их совместить.

type ParseNumber<T extends string> =
  T extends `${ParseSign<T>}${infer N}${ParsePercent<T>}` ? never : never;

Видите что происходит? Сначала, мы анализируем случай со знаком числа, используя ранее написанный тип. Если знак числа присутствует, тип его возвращает и делает частью тип литерала. А значит, он не попадет в часть, которая выводит число.

То же самое мы проделываем и со знаком процента. Если процент присутствует, тип его нам возвращает и делает частью строчного тип литерала. Это не дает ему попасть в часть с выведением числа.

В результате мы остаемся только с самим числом, которое мы и получаем через выведение типов. Нам остается его только вернуть из условного типа.

type ParseNumber<T extends string> =
  T extends `${ParseSign<T>}${infer N}${ParsePercent<T>}` ? N : "";

Вы, наверняка, уже догадываетесь, как мы можем это использовать для решения задачи. Как сказано в условии, нам нужно вернуть кортеж с тремя элементами. А у нас как раз есть три типа!

type PercentageParser<A extends string> = [
  ParseSign<A>,
  ParseNumber<A>,
  ParsePercent<A>,
];

Мои поздравления! Мы получили простейший “синтаксический анализатор” с использованием системы типов.

Что почитать