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

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

Итак, как мы можем быть уверены, что наши типы работают, как мы можем проверить наши типы?

Оказывается, это и просто, и сложно, но сначала мы сосредоточимся на простой части.

давайте возьмем подсчет подстроки типа строкового литерала в качестве нашего объекта тестирования

type GetCountOfSubString<
    String_ extends string,
    SubString extends string,
    Count extends unknown[] = []
> = String_ extends `${string}${SubString}${infer Tail}`
    ? GetCountOfSubString<Tail, SubString, [1, ...Count]>
    : Count['length']

type NumberOfA = GetCountOfSubString<"a--a--aa--a","a"> // 5

Мы хотим, чтобы GetCountOfSubString<"a--a--aa--a","a"> всегда приводило к 5

в основном оба должны расширять друг друга

далее создаем чекер, чекер состоит из 2-х частей

во-первых, это Expect, мы хотим проверить, расширяют ли оба типа друг друга

type Expect<T, U>= T extends U ? U extends T ? true : false : false 
type r1 = Expect<GetCountOfSubString<"a--a--aa--a","a">,5> // true, success check
type r2 = Expect<GetCountOfSubString<"a--a--aa--a","a">,1> // false, fail check

"детская площадка"

пока все хорошо, вы получаете желаемый результат, тип true, если результат правильный, и false, если результат неверен

но чего-то не хватает, когда вы запускаете проверку типов с помощью tsc, ничего не происходит, потому что он просто возвращает тип как true и false, что является допустимым типом, поэтому typescript не видит в этом ничего плохого.

поэтому нам нужна 2-я часть, утверждение

type Assert<T extends true> = T // be anything after '=', doesn't matter

применяя их

type Expect<T, U>= T extends U ? U extends T ? true : false : false 
type Assert<T extends true> = T // be anything after '=', doesn't matter
type r1 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,5>> // true, pass test
type r2 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,1>> // false, fail test

теперь мы видим, что фейл-тест провалился, и когда мы запускаем tsc, мы можем увидеть ошибку в консоли.

но подождите, что-то все еще не так, что это?

ну, фейл-тест должен провалиться, что ожидается, и не должен вызывать ошибку

так мы вернулись к исходной точке?

нет, мы ближе, вот как мы это решаем, используя комментарий @ts-expect-error

type r1 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,5>> // true, pass test
// @ts-expect-error
type r2 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,1>> // false, fail test

"детская площадка"

там больше нет ошибки проверки типа

@ts-expect-error подавлять ошибку только в том случае, если в строке есть ошибка, иначе, если вы используете его в совершенно исправной строке, вместо этого TS выдаст нам ошибку, и это поведение, которое мы хотим

Итак, давайте посмотрим, есть ли ошибка в GetCountOfSubString, будет ли это работать так, как ожидалось?

давайте попробуем провалить наш проходной тест:

type GetCountOfSubString<
    String_ extends string,
    SubString extends string,
    Count extends unknown[] = []
> = "BUG!!"
type Expect<T, U>= T extends U ? U extends T ? true : false : false 
type Assert<T extends true> = T // be anything after '=', doesn't matter
type r1 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,5>> // true, pass test
// @ts-expect-error
type r2 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,1>> // false, fail test

"детская площадка"

давайте попробуем провалить наш фейл-тест:

type GetCountOfSubString<
    String extends string,
    SubString extends string,
    Count extends unknown[] = []
> = 1
type Expect<T, U>= T extends U ? U extends T ? true : false : false 
type Assert<T extends true> = T // be anything after '=', doesn't matter
type r1 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,5>> // true, pass test
// @ts-expect-error
type r2 = Assert<Expect<GetCountOfSubString<"a--a--aa--a","a">,1>> // false, fail test

"детская площадка"

да, это работает!

но мы еще не закончили, если вы используете линтер, такой как eslint, он будет жаловаться, что тип объявлен, но никогда не используется

есть 2 пути решения:

сначала мы можем экспортировать их

или вместо этого мы превращаем Assert в функцию

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const assert = <T extends true>() => {
    //
}
assert<Expect<GetCountOfSubString<'a--a--aa--a', 'a'>, 5>>() // true, pass test
// @ts-expect-error
assert<Expect<GetCountOfSubString<'a--a--aa--a', 'a'>, 1>>() // false, fail test

"детская площадка"

рекомендуется второй метод, он короче, потому что не требует создания нового типа для каждого утверждения

это все для части 1, во части 2 мы позаботимся о некоторых крайних случаях, которые являются сложной частью