Skip to main content

Command Palette

Search for a command to run...

TypeScript Patterns. Строковые шаблоны

Updated
3 min read

Продолжаем цикл статей про лучшие практики типизации в 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.

More from this blog

Найм в IT. Как правильно проводить технические собеседования?

Ранее я описывал свое видение организации процесса найма и построения команды в общих чертах. В данной статье хочу рассказать подробнее про этап технического интервью. Тема горячая и всегда бурно обсуждаемая в комьюнити. Споры про "Зачем спрашивать а...

Jul 2, 20239 min read
Найм в IT. Как правильно проводить технические собеседования?

Релокация в Дубай. Фриланс виза

Данную статью можно рассматривать как полный гайд по релокации в Дубай для тех, кто работает удаленно или на себя как фрилансер. Мой процесс легализации завершился в марте 2023, но я планирую в будущем поддерживать актуальность гайда, обновляя информ...

Apr 1, 20236 min read
Релокация в Дубай. Фриланс виза

TypeScript Patterns. Номинальные типы

В этом посте я хочу начать цикл практических статей про advanced-level практики в TypeScript (далее TS). Далеко не все осознают насколько мощный инструмент достался frontend-сообществу и какие он открывает возможности для того, чтобы писать удобный, ...

Feb 3, 20236 min read
TypeScript Patterns. Номинальные типы

Найм в IT: Построение процесса

На мой взгляд, найм является ключевым фактором в построении успешной команды разработки. В то же время, во многих IT-компаниях не уделяют этому процессу должного внимания и считают его фоновым, стараясь отвлекаться на него лишь по мере необходимости....

Jan 29, 202310 min read
U

Untitled Publication

6 posts