Template Literal Types
Template literal types — строковые шаблоны на уровне типов, позволяющие конструировать и трансформировать строковые типы.
Зачем нужно
- Типобезопасное конструирование строковых ключей: event handlers, CSS-свойства, API-пути
- Встроенные string manipulation types: Uppercase, Lowercase, Capitalize, Uncapitalize
- Pattern matching на строках на уровне типов
- Автоматическая генерация типов из строковых шаблонов
Где используется
- Event systems:
"onClick","onMouseMove"из"click","mousemove" - CSS-in-JS: типизация CSS-свойств и значений
- API клиенты: парсинг URL-паттернов
- Конфигурации: генерация getter/setter имён
Предпосылки
- Литеральные типы — строковые литералы
- Mapped types — key remapping с as
- Conditional types — infer для извлечения подстрок
Базовый синтаксис
// Template literal type — шаблон строки с подстановкой типов
type Greeting = `Hello, ${string}`;
const a: Greeting = "Hello, World"; // OK
const b: Greeting = "Hello, Alice"; // OK
const c: Greeting = "Hi, World"; // Ошибка!
// С числами
type PixelValue = `${number}px`;
const width: PixelValue = "100px"; // OK
const bad: PixelValue = "widepx"; // Ошибка!
// Комбинация литералов — создаёт все комбинации
type Variant = "primary" | "secondary" | "danger";
type Size = "sm" | "md" | "lg";
type ButtonClass = `btn-${Variant}-${Size}`;
// "btn-primary-sm" | "btn-primary-md" | "btn-primary-lg" |
// "btn-secondary-sm" | "btn-secondary-md" | "btn-secondary-lg" |
// "btn-danger-sm" | "btn-danger-md" | "btn-danger-lg"
String Manipulation Types
Четыре встроенных типа для преобразования строк:
// Uppercase — всё в верхний регистр
type A = Uppercase<"hello world">; // "HELLO WORLD"
// Lowercase — всё в нижний регистр
type B = Lowercase<"HELLO WORLD">; // "hello world"
// Capitalize — первая буква заглавная
type C = Capitalize<"hello">; // "Hello"
// Uncapitalize — первая буква строчная
type D = Uncapitalize<"Hello">; // "hello"
// Комбинирование
type Event = "click" | "focus" | "blur";
type OnEvent = `on${Capitalize<Event>}`;
// "onClick" | "onFocus" | "onBlur"
type PropName = "backgroundColor" | "fontSize";
type CssProp = `--${Uncapitalize<PropName>}`;
// "--backgroundColor" | "--fontSize"
Паттерны для Event Handlers
// Из DOM-событий → React-стиль
type DomEvent = "click" | "mouseenter" | "mouseleave" | "keydown" | "keyup" | "scroll";
type ReactHandler = `on${Capitalize<DomEvent>}`;
// "onClick" | "onMouseenter" | "onMouseleave" | "onKeydown" | "onKeyup" | "onScroll"
// Типизированный EventEmitter
type EventCallback<T> = (data: T) => void;
type EventHandlers<Events extends Record<string, unknown>> = {
[K in keyof Events as `on${Capitalize<string & K>}`]: EventCallback<Events[K]>;
};
interface AppEvents {
login: { userId: string };
logout: {};
error: { message: string; code: number };
}
type Handlers = EventHandlers<AppEvents>;
// {
// onLogin: (data: { userId: string }) => void;
// onLogout: (data: {}) => void;
// onError: (data: { message: string; code: number }) => void;
// }
Pattern Matching с infer
// Извлечение частей строки
type ExtractRoute<S extends string> = S extends `/${infer Path}` ? Path : never;
type A = ExtractRoute<"/users">; // "users"
type B = ExtractRoute<"/posts">; // "posts"
type C = ExtractRoute<"users">; // never (нет ведущего /)
// Парсинг URL-параметров
type ExtractParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: Path extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"
// Объект параметров
type RouteParams<Path extends string> = {
[K in ExtractParams<Path>]: string;
};
type UserPostParams = RouteParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }
Разбор строк
// Split строки
type Split<
S extends string,
D extends string
> = S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S];
type A = Split<"a.b.c", ".">; // ["a", "b", "c"]
type B = Split<"hello", "">; // ["h", "e", "l", "l", "o"]
// Join (обратная операция)
type Join<T extends string, D extends string> = T extends [
infer First extends string,
...infer Rest extends string
]
? Rest["length"] extends 0
? First
: `${First}${D}${Join<Rest, D>}`
: "";
type C = Join<["a", "b", "c"], ".">; // "a.b.c"
// Replace
type Replace<
S extends string,
From extends string,
To extends string
> = S extends `${infer Before}${From}${infer After}`
? `${Before}${To}${After}`
: S;
type D = Replace<"Hello World", "World", "TypeScript">;
// "Hello TypeScript"
// ReplaceAll
type ReplaceAll<
S extends string,
From extends string,
To extends string
> = S extends `${infer Before}${From}${infer After}`
? ReplaceAll<`${Before}${To}${After}`, From, To>
: S;
type E = ReplaceAll<"a-b-c-d", "-", ".">;
// "a.b.c.d"
CSS-типизация
// CSS единицы
type CSSUnit = "px" | "em" | "rem" | "%" | "vh" | "vw";
type CSSLength = `${number}${CSSUnit}` | "auto" | "0";
let width: CSSLength = "100px"; // OK
let height: CSSLength = "50vh"; // OK
let margin: CSSLength = "auto"; // OK
// CSS цвета
type HexDigit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "a" | "b" | "c" | "d" | "e" | "f";
type HexColor = `#${string}`; // Упрощённый вариант
// CSS custom properties
type CSSVariable = `--${string}`;
type CSSVarUsage = `var(${CSSVariable})`;
const varName: CSSVariable = "--primary-color"; // OK
const varUse: CSSVarUsage = "var(--primary-color)"; // OK
Типизация API путей
// REST API endpoint builder
type ApiVersion = "v1" | "v2";
type Resource = "users" | "posts" | "comments";
type ApiEndpoint = `/api/${ApiVersion}/${Resource}`;
// "/api/v1/users" | "/api/v1/posts" | "/api/v1/comments" |
// "/api/v2/users" | "/api/v2/posts" | "/api/v2/comments"
type DetailEndpoint = `${ApiEndpoint}/${number}`;
// "/api/v1/users/${number}" | ...
// Типизированный fetch
type ApiRoutes = {
"/api/users": User;
"/api/users/:id": User;
"/api/posts": Post;
};
type TypedFetch = <Path extends keyof ApiRoutes>(
path: Path
) => Promise<ApiRoutes[Path]>;
camelCase / snake_case конвертация
// snake_case → camelCase
type CamelCase<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${CamelCase<Capitalize<Tail>>}`
: S;
type A = CamelCase<"user_first_name">; // "userFirstName"
type B = CamelCase<"created_at">; // "createdAt"
// Применение: конвертация ключей объекта
type CamelCaseKeys<T> = {
[K in keyof T as K extends string ? CamelCase<K> : K]: T[K];
};
interface SnakeCaseUser {
user_id: number;
first_name: string;
last_name: string;
created_at: Date;
}
type CamelCaseUser = CamelCaseKeys<SnakeCaseUser>;
// {
// userId: number;
// firstName: string;
// lastName: string;
// createdAt: Date;
// }
Частые ошибки
- Слишком большой union — комбинирование множества литералов создаёт exponential blow-up
// 10 * 10 * 10 = 1000 вариантов — замедляет компилятор!
type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
type ThreeDigit = `${Digit}${Digit}${Digit}`; // 1000 вариантов!
- Забыть
string &—keyof Tможет включатьsymbol, а Capitalize ожидает string - Глубокая рекурсия — TypeScript ограничивает глубину рекурсии для типов
- Использовать template literals для runtime — это только типы, в рантайме они стираются
Практика
- Создайте тип
EventHandler<E>— из"click"получите"onClick" - Напишите
ExtractParams<"/users/:id/posts/:postId">→"id" | "postId" - Реализуйте
CamelCase<"snake_case_string">→"snakeCaseString" - Создайте CSS-типы для
CSSLengthиCSSColor - Напишите
Replace<"hello world", "world", "TS">через template literal + infer
Связанные темы
- Литеральные типы — основа template literals
- Mapped types — key remapping с template literals
- Conditional types — infer для pattern matching
- typeof и keyof — keyof для итерации по ключам