← Курс/Template Literal Types#151 из 257+30 XP

Template Literal Types

Что такое Template Literal Types

Template Literal Types — это типы, созданные с помощью синтаксиса шаблонных строк. Они позволяют строить новые строковые типы путём комбинирования литеральных строковых типов.

type Greeting = `Hello, ${string}!`
// Любая строка вида "Hello, ...!"

type EventName = `on${string}`
// Любая строка начинающаяся с "on": 'onClick', 'onChange', ...

Комбинирование с union типами

Мощь template literal types раскрывается при комбинировании с union:

type Direction = 'top' | 'right' | 'bottom' | 'left'
type Property = 'margin' | 'padding'

type CSSProperty = `${Property}-${Direction}`
// type CSSProperty =
//   | 'margin-top'    | 'margin-right'    | 'margin-bottom'    | 'margin-left'
//   | 'padding-top'   | 'padding-right'   | 'padding-bottom'   | 'padding-left'

// TypeScript автоматически создаёт все комбинации!

Реальный пример: события объекта

type ObjectKeys = 'name' | 'age' | 'email'

// Автоматически создаём обработчики событий:
type EventHandlers = {
  [K in ObjectKeys as `on${Capitalize<K>}Change`]: (value: string) => void
}
// type EventHandlers = {
//   onNameChange: (value: string) => void
//   onAgeChange: (value: string) => void
//   onEmailChange: (value: string) => void
// }

Встроенные строковые утилиты

TypeScript предоставляет 4 встроенных утилиты для работы со строковыми типами:

type S = 'hello world'

type Upper = Uppercase<S>    // 'HELLO WORLD'
type Lower = Lowercase<'HELLO'>  // 'hello'
type Cap = Capitalize<S>    // 'Hello world' — первая буква в верхнем регистре
type Uncap = Uncapitalize<'Hello'>  // 'hello' — первая буква в нижнем регистре

Они особенно полезны в сочетании с template literal types:

type Getter<T extends string> = `get${Capitalize<T>}`

type NameGetter = Getter<'name'>   // 'getName'
type AgeGetter = Getter<'age'>     // 'getAge'
type EmailGetter = Getter<'email'> // 'getEmail'

Паттерн: типизированные CSS классы

type Size = 'sm' | 'md' | 'lg' | 'xl'
type Color = 'primary' | 'secondary' | 'danger'

type ButtonClass = `btn-${Color}-${Size}`
// 'btn-primary-sm' | 'btn-primary-md' | ... | 'btn-danger-xl'
// 12 комбинаций автоматически!

function getButtonClass(color: Color, size: Size): ButtonClass {
  return `btn-${color}-${size}`
}

Паттерн: строгие пути к API

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Resource = 'users' | 'posts' | 'comments'

type ApiEndpoint = `/api/${Resource}`
// '/api/users' | '/api/posts' | '/api/comments'

type ApiRoute = `${HttpMethod} /api/${Resource}`
// 'GET /api/users' | 'POST /api/users' | ... | 'DELETE /api/comments'
// 12 комбинаций

Инференция в template literal types

С помощью infer (в conditional types) можно извлекать части строки:

type ExtractRoute<T extends string> =
  T extends `GET /api/${infer R}` ? R : never

type Route = ExtractRoute<'GET /api/users'>
// type Route = 'users'

Примеры

Template literal типы в действии: генерация строк из комбинаций и утилиты строк

// В TS: Template Literal Types создают новые строковые типы
// В JS: показываем runtime-аналог — генерацию всех комбинаций

// === Генерация всех комбинаций (как TS template literal + union) ===
function generateCombinations(...arrays) {
  // В TS: `${A}-${B}` где A и B — union types создаёт все комбинации
  return arrays.reduce((acc, curr) =>
    acc.flatMap(a => curr.map(b => `${a}-${b}`))
  )
}

const properties = ['margin', 'padding']
const directions = ['top', 'right', 'bottom', 'left']

const cssProperties = generateCombinations(properties, directions)
console.log('=== CSS свойства (margin/padding + направления) ===')
console.log(cssProperties)
// ['margin-top', 'margin-right', ..., 'padding-left']
console.log(`Всего: ${cssProperties.length} свойств`)  // 8

// === Capitalize утилита (как TS Capitalize<string>) ===
function capitalize(str) {
  // В TS: Capitalize<'hello'> = 'Hello'
  return str.charAt(0).toUpperCase() + str.slice(1)
}

// === Паттерн: генераторы getter/setter имён ===
console.log('\n=== Getters/Setters (Capitalize + template) ===')

const fields = ['name', 'age', 'email', 'status']

const methods = fields.flatMap(field => [
  // В TS: `get${Capitalize<K>}` и `set${Capitalize<K>}`
  `get${capitalize(field)}`,
  `set${capitalize(field)}`,
])
console.log(methods)
// ['getName', 'setName', 'getAge', 'setAge', 'getEmail', 'setEmail', ...]

// === Паттерн: типизированные event handlers ===
console.log('\n=== Event handlers (on + Capitalize + Change) ===')

function createEventHandlers(fields) {
  // В TS: { [K in Fields as `on${Capitalize<K>}Change`]: handler }
  const handlers = {}
  fields.forEach(field => {
    const eventName = `on${capitalize(field)}Change`
    handlers[eventName] = (value) => {
      console.log(`${field} изменён на: ${value}`)
    }
  })
  return handlers
}

const handlers = createEventHandlers(['name', 'age', 'email'])
console.log('Созданные обработчики:', Object.keys(handlers))
// ['onNameChange', 'onAgeChange', 'onEmailChange']

handlers.onNameChange('Алексей')  // 'name изменён на: Алексей'
handlers.onAgeChange(28)           // 'age изменён на: 28'

// === Паттерн: строгие API маршруты ===
console.log('\n=== API маршруты ===')

const methods2 = ['GET', 'POST', 'PUT', 'DELETE']
const resources = ['users', 'posts', 'comments']

// В TS: type ApiRoute = `${HttpMethod} /api/${Resource}`
const routes = generateCombinations(methods2, resources.map(r => `/api/${r}`))
console.log('Все API маршруты:', routes)
// ['GET-/api/users', 'GET-/api/posts', ..., 'DELETE-/api/comments']
// (12 комбинаций)