Немного введения
Вы уже все знаете, какая выразительная система типов в нашем синем друге, ведь об этом было написано немало статей. Думаю, втирать рассказывать об этом уже нет смысла, поэтому сразу перейдём к делу.
Про себя я рассказывать не буду. Ничем таким не прославился. Просто сижу пишу код 24/7, потому что нравится.
Вывод типов
Да-да, начнём именно с таких основ. Позже будем углубляться.
Давайте рассмотрим следующий код:
let str = '123'; let anotherStr: '123' = '123';
Для первой переменной мы не указывали тип, поэтому язык нам выведет тип string для неё. Для другой же мы явно указали тип, который, в принципе предполагает, что мы в эту переменную никакую другую строку поместить не сможем. К слову, эта возможность нам сегодня и пригодится.
Думаю, шаблонные строки должны быть знакомы всем — это довольно мощный инструмент в языке программирования, и он не обошёл стороной даже нашего сине-белого товарища:
let numbers: '1234567890'; let letters: 'abcdefghijklmnopqrstuvwxyz'; let possibleSymbols: `${typeof numbers}${typeof letters}`;
Тут довольно простая операция — мы конкатенируем типы numbers и letters, получая при этом новый тип, который будет состоять из цифр и букв английского алфавита в нижнем регистре. Впрочем, это всё можно сократить до следующего кода (если это будет необходимо):
let possibleSymbols: '1234567890abcdefghijklmnopqrstuvwxyz';
В принципе по большей части мы сегодня будем работать в основном со строками (и ещё парочкой преимуществ).
Типы-утилиты
По мимо мощной системы типов микромягкие добавили утилиты для более простой работы с типами. Например, у нас есть интерфейс пользователя:
interface IUser { username: string; password: string; address: string; ip: string; } // Я долго думал, это у меня код неверный или подсветка не так работает на хабре // Сообщите, если всё же код неверный, а то вдруг
И, например, нам нужно описать функцию авторизации для пользователя. Давайте согласимся, что для авторизации нам не нужен адрес проживания и IP-адрес пользователя. Поэтому воспользуемся типом Pick следующим образом:
declare function authenticate( credential: Pick<IUser, 'username' | 'password'> ): boolean;
В теле функции нам IDE будет подсказывать, что мы можем использовать только поля username и password.
А вам тоже надоело проверять постоянно переменную/свойство на существование? Пожалуйста, даже для этой проблемы есть решение — приведение типов!
declare function maybeString(): string | undefined; maybeString().split(''); // Тут будет ошибка "Object is possibly 'undefined'"
Мы можем понять его — он старается заботиться о нас и уберечь нас от опасности. Но мы безбашенные, поэтому нам всё равно на какие-то там опасности. Перепишем, но функцию оставим такой же!
let onlyString: string = <string>maybeString();
Если вы приверженец Чехова, то для есть другой синтаксис:
let onlyString: string = maybeString()!;
Предупреждение: не используйте это никогда. Все действия выполнены непрофессионалом на свой страх и риск в чисто ознакомительных целях. Типы хороши, но если логика хромает, то никакие типы не спасут (имею ввиду типы в TypeScript)
От простого к сложному — infer
Для меня данная функция языка была очень долго загадкой, но со временем я стал её понимать лучше, благодаря строкам (похоже на всеми ненавистную рекламу).
Давайте возьмём пример высосанный из пальца:
type Str<T> = T extends `${infer R}` ? R : never;
Что собственно этот тип делает? Мы передаём в generic некий тип Т, а далее проверяем, наследуется ли он от некой строки с infer R… А что такое infer? Представьте, что это что-то в духе переменной, только вот значение определяет сам TypeScript, мы можем только указать конкретное место, а TypeScript подставит из типа Т сам.
Итого получаем следующие вариации:
let notString: Str<123>; // вернёт never let daEtoStroka: Str<'123'>; // вернёт '123'
Ограничения, безусловно, есть:
-
inferможет использоваться только с применениемextends; -
Время жизни ограничивается блоком для выполнения истинного условия тернарного оператора. То есть, код:
type S<T> = T extends `${infer R}` ? T : R;будет выдавать ошибку при транспиляции.
Рекурсия
На этом этапе уже можно назвать систему типов отдельным языком программирования — что у нас тут только нет: и типы, и работа со строками, и переменные, и даже рекурсия.
Собственно, как и во всех других языках программирования, тут рекурсия тоже не бесконечная. Так например следующий код выдаст ошибку.
type Recursive<T> = Recursive<T>;
Если быть точнее, то код выдаст две ошибки. Сначала подсветит определение типа и скажет, что у него там есть неразрешимая рекурсия. Потом подсветит значение типа и скажет, что Recursive — это не generic-тип.
Поэтому, если захочется использовать рекурсию, то она должна быть:
-
неглубокой;
-
ограниченной;
Вот тут-то мы начинаем использовать мощность синего друга.
Практика
А давайте затипизируем несколько функций со строками? А то что это такое сейчас есть: передал строку — получил строку. Что делает функция — неизвестно. Продолжаем высасывать проблемы из пальца.
Начнём с простенького — конкатенация. Что нам нужно? Generic-функция и всё
function concat<T extends string, T1 extends string>( a: T, b: T1, ): `${T}${T1}` { return <`${T}${T1}`>a + b; }
Тут нам пригодится приведение типов, потому что при обычной конкатенации нам бы вывелся тип string, а нам нужен немного ограниченный string.
Зато в таком случае подсказки будут решать проблемы уже за вас

Но что это? Кажется, мы ограничены в количестве параметров. Давайте исправим этот момент. Для этого создадим отдельный тип, который будет превращать массив строк в одну строку:
type Join<T, _O extends string = ''> = T extends [ infer D extends string, ...infer R ] ? Join<R, `${_O}${D}`> : _O;
Что за ещё один параметр _O? Мы туда будем класть каждый строковый элемент просто потому, что можем. Собственно вот и практическое применение рекурсии. Если на каком-то из этапов мы получим пустой массив, то сразу выдадим значение параметра _O.
А теперь определим новую функцию, которая может принимать уже сколько угодно строк и выводит верный тип.
Далее в статье будет опускаться логика функций/методов.
declare function concat<T extends string[]>(...strs: T): Join<T>;
В таком случае, если мы попробуем использовать данную функцию, то получим следующую подсказку:

Что же, с конкатенацией тут всё. Перейдём к следующей такой же важной функции, как разбивание строки на массив по разделителю. Добро пожаловать, split.
Данный метод принимает разделитель и лимит. Опустим последний параметр, потому что TypeScript ещё не научился выполнять операции над числами.
Давайте попробуем описать эту операцию с помощью типов:
type Split< T, S extends string, _O extends string[] = [] > = T extends `${infer R}${S}${infer D}` ? D extends '' ? S extends '' ? [..._O, R] : [..._O, R, ''] : Split<D, S, [..._O, R]> : T extends `${infer R}` ? [..._O, R] : _O;
Что, собственно тут происходит? В нашем случае T — это строка, которую передаём, а S — это разделитель. Собственно, _О — результат выполнения. Мы забираем из строки первую подстроку, за которой идёт разделитель, за которой идёт остальная часть строки.
Здесь очень хорошо видна природа infer. Последний объявленный infer в строковом типе будет жадным, то есть, он заберёт всё, в то время как идущие перед ним будут забирать первое попавшееся совпадение. Отсюда делаем вывод, что R — конкретная подстрока, а не объединение подстрок.
Далее проверяем, а пусто ли после разделителя, если да, то возвращаем массив с добавлением пустой строки (так поступает стандартный split). В противном случае продолжаем выполнять рекурсию. Если на каком-то этапе у нас останется строка без разделителя, то добавляем её к нашему массиву и возвращаем результат, в противном случае возвращаем просто массив, ничего к нему не добавляя.
Опишем функцию:
declare function split< T extends string, S extends string >(str: T, sep: S): Split<T, S>;
Попробуем воспользоваться и получим такую подсказку:

На вкусненькое
Раз уж мы продвинулись настолько вперёд, то предлагаю вам посмотреть парсер JSON на типах Typescript Парсер здесь (тыкни на меня).
Код парсера, чтобы далеко не ходить
type json = '{ "a": [1, 2] }' type Trim<T> = T extends ` ${infer R}` ? Trim<R> : T extends `${infer R} ` ? Trim<R> : T type StringToArray<T extends string, _O extends any[] = []> = T extends `[${infer R}]` ? Trim<R> extends `${infer E},${infer Other}` ? StringToArray<`[${Trim<Other>}]`, [..._O, JSONParse<E>]> : Trim<R> extends `${infer E}` ? [..._O, JSONParse<E>] : never : never; type JSONParse<T extends string> = T extends Trim<T> ? T extends `{${string}}` ? StringToObjectV2<T> : T extends `[${string}]` ? StringToArray<T> : T extends `${infer R extends number}` ? R : T extends `${infer R extends boolean}` ? R : T extends `${infer R extends null}` ? R : T extends `"${infer R extends string}"` ? R : never : never; type StringToObjectV2<T extends string, _O extends Record<string | number, any> = {}> = T extends `{${infer R}}` ? Trim<R> extends `"${infer Key}":${infer Other}` ? Trim<Other> extends `[${infer Arr}],${infer NewOther}` ? StringToObjectV2<`{${Trim<NewOther>}}`, _O & { [L in Key]: StringToArray<`[${Trim<Arr>}]`> }> : Trim<Other> extends `[${infer Arr}]` ? _O & { [L in Key]: StringToArray<`[${Trim<Arr>}]`> } : Trim<Other> extends `${infer Value},${infer LOther}` ? StringToObjectV2<`{${Trim<LOther>}}`, _O & { [L in Key]: JSONParse<Trim<Value>> }> : Trim<Other> extends `${infer Value}` ? _O & { [L in Key]: JSONParse<Trim<Value>> } : never : never : never; let l: JSONParse<json>
Также можете глянуть задачу чуть посложнее — инкремент числа. Так как в типах мы не можем использовать математические операторы, то приходится изворачиваться. По такой же аналогии можно реализовать и декремент. Пока что такой инкремент работает корректно только с положительными числами. Суть работы его следующая: мы берём число, превращаем его в строку, разбиваем строку посимвольно, каждый символ превращаем в цифру от 0 до 9. Далее выполняем инкремент последнего элемента массива, если в результате инкремента мы получаем 0, то это означает, что произошло переполнение, а значит нам нужно выполнить инкремент ещё раз, только уже на другом разряде (пользуемся рекурсией). Далее правильно соединяем все части массива и за одно проверяем, не передан ли нам на одном из этапов пустой массив — это помогает тоже обнаружить переполнение — в этом случае мы просто добавляем единицу в начала массива.
Здесь инкремент (тыкни на меня)
Код инкремента
type ToString<T extends string | number | bigint | boolean | null | undefined> = `${T}`; type Split< T, S extends string, _O extends string[] = [] > = T extends `${infer R}${S}${infer D}` ? D extends '' ? S extends '' ? [..._O, R] : [..._O, R, ''] : Split<D, S, [..._O, R]> : T extends `${infer R}` ? [..._O, R] : _O; type JoinToNumber<T extends number[], _O extends string = ''> = T extends [infer Digit extends number, ...infer Other extends number[]] ? JoinToNumber<Other, `${_O}${Digit}`> : T extends [infer Digit extends number] ? `${_O}${Digit}` extends `${infer N extends number}` ? N : never : `${_O}` extends `${infer N extends number}` ? N : never type PossibleDigitChars = { "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, } type DigitWithoutZero = [1, 2, 3, 4, 5, 6, 7, 8, 9] type IncrementDigit = [...DigitWithoutZero, 0]; type DecrementDigit = [0, ...DigitWithoutZero]; type MapCharToDigit<T extends keyof PossibleDigitChars> = PossibleDigitChars[T]; type MapStringArrayToNumber< T extends string[], _O extends number[] = [] > = T extends [infer Digit extends keyof PossibleDigitChars, ...infer Others extends string[]] ? MapStringArrayToNumber<Others, [..._O, MapCharToDigit<Digit>]> : T extends [infer Digit extends keyof PossibleDigitChars] ? [..._O, MapCharToDigit<Digit>] : _O type SplitNumber<T extends number> = MapStringArrayToNumber<Split<ToString<T>, ''>>; type IncrementArray< T extends number[], _O extends number[] = [], _OverflowFlag extends boolean = false > = T extends [...infer Others extends number[], infer LastDigit extends number] ? IncrementDigit[LastDigit] extends 0 ? IncrementArray<Others, [IncrementDigit[LastDigit], ..._O], true> : _OverflowFlag extends true ? IncrementDigit[LastDigit] extends 0 ? IncrementArray<Others, [IncrementDigit[LastDigit], ..._O], true> : T extends [...infer Others extends number[], infer _ extends number] ? [...Others, IncrementDigit[LastDigit], ..._O] : never : T extends [...infer Others extends number[], infer _ extends number] ? [...Others, IncrementDigit[LastDigit], ..._O] : never : T extends [] ? [1, ..._O] : [..._O] type Increment<T extends number> = IncrementArray<SplitNumber<T>>; type Eq<T, E extends T> = T extends E ? true : false; type test = [ Eq<Increment<1>, [2]>, Eq<Increment<666>, [6, 6, 7]>, Eq<Increment<999>, [1, 0, 0, 0]>, Eq<Increment<2999>, [3, 0, 0, 0]>, ]
Вывод
-
Система типов TS очень выразительная. Позволяет затипизировать почти всё, что угодно.
-
Не надейтесь на систему типов, потому что-то с плохой логикой типы не помогут справиться.
-
Данный подход опасен, но при осторожном использовании может быть очень мощным средством (например, есть возможность затипизировать роутер так, чтобы на каждый / в строке была подсказка, а собственно потом можно и разрешить API метод с выводом типа возвращаемого значения при запросе).
Обычно там в конце статьи люди что-то оставляют, но у меня ничего нет, чтобы оставить вам, простите.
ссылка на оригинал статьи https://habr.com/ru/post/679454/
Добавить комментарий