TypeScript Patterns. Строковые шаблоны
Продолжаем цикл статей про лучшие практики типизации в TypeScript. Сегодня разберем примеры использования Template Literal Types, не самой популярной и, на мой взгляд, сильно недооцененной фичи языка.
Пример. Уровень - easy
Короткий пример, аналогичный примерам из документации:
type PositionY = 'top' | 'middle' | 'bottom'
type PositionX = 'left' | 'center' | 'right'
type Position = `${PositionY}_${PositionX}` // type Position = "top_left" | "top_center" | "top_right" | "middle_left" | "middle_center" | "middle_right" | "bottom_left" | "bottom_center" | "bottom_right"
const positionY = 'top'
const positionX = 'left'
const position1: Position = `${positionY}${positionX}` // - Type Error. Компилятор строго следит за тем, как мы конкатенируем строку в рантайме.
const position2: Position = `${positionY}_${positionX}` // - Valid
Суть очень простая - литерал `` в типах работает так же как в JS с возможностью шаблонизации и использования переменных внутри ${}. Юнион типы внутри шаблона перемножаются и создают один общий строковый юнион со всеми вариациями переменных. Это позволяет навести дополнительные мосты между runtime-кодом и compile-time типизацией в тех местах, где мы обычно идем в обход компилятора и используем кастинг as.
Пример. Уровень - medium
Напишем type-safe функцию для парсинга URL:
function parseUrl<
Protocol extends 'http' | 'https',
Host extends `${string}.${string}`,
Path extends string,
Result extends { protocol: Protocol; host: Host; pathname: Path }
>(url: `${Protocol}://${Host}/${Path}`): Result {
const { protocol, host, pathname } = new URL(url)
return { protocol, host, pathname } as Result
}
// TS теперь в compile-time проверяет корректность шаблона URL при вызове функции
parseUrl('http_://example.com/home') // Type Error: Аргумент типа ""http_://example.com/home"" нельзя назначить параметру типа ""http://example.com/home" | "https://example.com/home""
parseUrl('https://example@com/home') // Type Error: Аргумент типа ""https://example@com/home"" нельзя назначить параметру типа "`https://${string}.${string}/home`".
parseUrl('example.com/home') // Type Error: Аргумент типа ""example.com/home"" нельзя назначить параметру типа "`http://${string}.${string}/${string}` | `https://${string}.${string}/${string}`"
const url1 = parseUrl('https://example.com/home/page') // Валидный вариант
// Так же TS за нас выводит возвращаемый тип для url1 как:
// {
// protocol: "https";
// host: "example.com";
// pathname: "home/page";
// }
Впечатляет, правда? Я не встречал ничего подобного в других строго-типизированных языках. Если знаете какой язык умеет так же - напишите в комментариях.
Пример. Уровень - hard
Типизируем стандартный метод String.split()
export declare type SplitString<
S extends string,
D extends string //
> = S extends ''
? []
: S extends `${D}${infer Tail}` // infer позволяет создать переменную внутри Conditional Types
? [...SplitString<Tail, D>] // с помощью рекурсии склеиваем куски в один массив
: S extends `${infer Head}${D}${infer Tail}`
? [Head, ...SplitString<Tail, D>]
: [S]
// Переопределяем глобальный тип метода String.split()
// из стандартной библиотеки языка
declare global {
interface String {
split<S extends string, D extends string>(this: S, delimiter: D): SplitString<S, D> // this: S - линкуем строковый литерал в переменную
}
}
// Используем .split() как обычно в коде
const parts1 = '/category/section/page'.split('/') // тип const parts1: ["category", "section", "page"]
const parts2 = 'my_name@example.com'.split('@') // тип const parts2: ["my_name", "example.com"]
По-моему - бомба! TS невероятен, но это еще не все. Добавим еще немного магии и сделаем type-safe функцию для создания http-хендлеров для роутинга:
export declare type ExtractParam<Part extends string> = Part extends `{${infer Param}}` ? Param : never
export declare type PathParams<PathPattern extends string> = {
[Key in ExtractParam<SplitString<PathPattern, '/'>[number]>]: string
}
function createRouteHandler<Path extends string, Params extends PathParams<Path>>(
path: Path,
handler: (params: { [P in keyof Params]: Params[P] }) => {}
) {
// Implementation....
}
createRouteHandler('/users/{id}', (params) => {
// тип params автоматически выводится как
// {
// id: string;
// }
})
createRouteHandler('/users/{userID}/orders/{orderID}', (params) => {
// тип params автоматически выводится как
// {
// userID: string;
// orderID: string;
// }
})
Буум! Очередной слой логики покрыт compile-time проверками с минимальными усилиями со стороны разработчика.
Заключение
На мой взгляд, TypeScript задизайнен просто шедеврально. Он позволяет делать невероятные вещи, в сравнении с классическими строго-типизированными языками, предоставляя очень мощный и выразительный синтаксис. Кому интересно узнать больше advanced-приемов в TS - подписывайтесь на мой канал в Telegram.


