13. Template Literal Types
TypeScript: Броня. Урок 12: Template Literal Types
Заголовок раздела «TypeScript: Броня. Урок 12: Template Literal Types»Template Literal Types - это мощная фича TypeScript 4.1+, которая позволяет создавать новые строковые типы путём комбинирования строковых литералов. Они работают как JavaScript template literals, но на уровне системы типов. Это открывает невероятные возможности для создания type-safe API, роутинга, событий и многого другого.
Базовый синтаксис
Заголовок раздела «Базовый синтаксис»Синтаксис template literal types идентичен template literals в JavaScript, но используется для типов:
type Greeting = `Hello, ${string}!`;
const greet1: Greeting = "Hello, World!"; // ✓const greet2: Greeting = "Hello, TypeScript!"; // ✓// const greet3: Greeting = "Hi, World!"; // ✗ не начинается с "Hello,"
// С конкретными литераламиtype Color = "red" | "green" | "blue";type HexColor = `#${string}`;
type AllColors = Color | HexColor;
const color1: AllColors = "red"; // ✓const color2: AllColors = "#ff0000"; // ✓const color3: AllColors = "yellow"; // ✗ не в спискеКомбинирование union типов
Заголовок раздела «Комбинирование union типов»TypeScript автоматически создаёт все возможные комбинации:
type Size = "small" | "medium" | "large";type Color = "red" | "blue" | "green";
type SizeColor = `${Size}-${Color}`;// Результат:// "small-red" | "small-blue" | "small-green" |// "medium-red" | "medium-blue" | "medium-green" |// "large-red" | "large-blue" | "large-green"
const product: SizeColor = "medium-blue"; // ✓// const wrong: SizeColor = "medium-yellow"; // ✗
// HTTP методы с путямиtype HttpMethod = "GET" | "POST" | "PUT" | "DELETE";type Endpoint = "/users" | "/posts" | "/comments";
type ApiRoute = `${HttpMethod} ${Endpoint}`;// "GET /users" | "GET /posts" | ... | "DELETE /comments"Встроенные утилиты для строк
Заголовок раздела «Встроенные утилиты для строк»TypeScript предоставляет встроенные типы для преобразования строк:
type Uppercase<S extends string> = intrinsic;type Lowercase<S extends string> = intrinsic;type Capitalize<S extends string> = intrinsic;type Uncapitalize<S extends string> = intrinsic;
// Примеры использованияtype Loud = Uppercase<"hello">; // "HELLO"type Quiet = Lowercase<"WORLD">; // "world"type Title = Capitalize<"typescript">; // "Typescript"type Lower = Uncapitalize<"TypeScript">; // "typeScript"
// Практическое применениеtype EventName = "click" | "focus" | "blur";type EventHandler = `on${Capitalize<EventName>}`;// "onClick" | "onFocus" | "onBlur"
type HttpMethod = "get" | "post" | "put" | "delete";type FunctionName = `${HttpMethod}Request`;// "getRequest" | "postRequest" | "putRequest" | "deleteRequest"С mapped types
Заголовок раздела «С mapped types»Комбинирование template literals с mapped types даёт огромные возможности:
type Getters<T> = { [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];};
type Setters<T> = { [K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void;};
interface User { name: string; age: number; email: string;}
type UserGetters = Getters<User>;// {// getName: () => string;// getAge: () => number;// getEmail: () => string;// }
type UserSetters = Setters<User>;// {// setName: (value: string) => void;// setAge: (value: number) => void;// setEmail: (value: string) => void;// }
// Комбинированиеtype Accessors<T> = Getters<T> & Setters<T>;Практические примеры
Заголовок раздела «Практические примеры»// Создание type-safe CSS классовtype Spacing = 0 | 1 | 2 | 3 | 4 | 5;type Direction = "t" | "r" | "b" | "l" | "x" | "y";
type MarginClass = `m${Direction}-${Spacing}`;type PaddingClass = `p${Direction}-${Spacing}`;
type SpacingClass = MarginClass | PaddingClass;
const margin: SpacingClass = "mt-3"; // ✓ margin-top: 3const padding: SpacingClass = "px-2"; // ✓ padding-x: 2// const invalid: SpacingClass = "mt-10"; // ✗ неверное значение
// REST API endpointstype Resource = "users" | "posts" | "comments";type Action = "create" | "update" | "delete" | "list";
type Endpoint = `/${Resource}/${Action}`;// "/users/create" | "/users/update" | ... | "/comments/list"
type EndpointWithId = `/${Resource}/${number}/${Action}`;// "/users/123/update", "/posts/456/delete", etc.
// SQL query builder typestype Table = "users" | "posts" | "comments";type Operation = "SELECT" | "INSERT" | "UPDATE" | "DELETE";
type Query = `${Operation} FROM ${Table}`;// "SELECT FROM users" | "INSERT FROM users" | ...Жизненный пример: Type-safe Event System
Заголовок раздела «Жизненный пример: Type-safe Event System»// Система событий с автоматической генерацией типовtype DomEvents = { click: MouseEvent; focus: FocusEvent; input: InputEvent; submit: SubmitEvent;};
type EventName = keyof DomEvents;
type EventHandlerName<E extends EventName> = `on${Capitalize<E>}`;type EventHandlerType<E extends EventName> = (event: DomEvents[E]) => void;
type EventHandlers = { [E in EventName as EventHandlerName<E>]: EventHandlerType<E>;};
// Результат:// {// onClick: (event: MouseEvent) => void;// onFocus: (event: FocusEvent) => void;// onInput: (event: InputEvent) => void;// onSubmit: (event: SubmitEvent) => void;// }
// Использованиеconst handlers: Partial<EventHandlers> = { onClick: (e) => { console.log(e.clientX, e.clientY); // TypeScript знает, что это MouseEvent }, onInput: (e) => { console.log(e.data); // TypeScript знает, что это InputEvent },};
// Type-safe route definitionstype RouteParams = { "/": {}; "/users": {}; "/users/:id": { id: string }; "/users/:id/posts": { id: string }; "/users/:id/posts/:postId": { id: string; postId: string };};
type RoutePath = keyof RouteParams;
function navigate<T extends RoutePath>( path: T, params: RouteParams[T]): void { // implementation}
navigate("/users/:id", { id: "123" }); // ✓navigate("/users/:id/posts/:postId", { id: "123", postId: "456" }); // ✓// navigate("/users/:id", { userId: "123" }); // ✗ неверное имя параметра// navigate("/users/:id", {}); // ✗ отсутствует idParsing строковых литералов
Заголовок раздела «Parsing строковых литералов»// Извлечение параметров из routetype ExtractRouteParams<T extends string> = T extends `${infer _Start}:${infer Param}/${infer Rest}` ? { [K in Param | keyof ExtractRouteParams<Rest>]: string } : T extends `${infer _Start}:${infer Param}` ? { [K in Param]: string } : {};
type UserRoute = ExtractRouteParams<"/users/:id">;// { id: string }
type PostRoute = ExtractRouteParams<"/users/:userId/posts/:postId">;// { userId: string; postId: string }
// Разбор CSS классовtype ParseClass<T extends string> = T extends `${infer Prefix}-${infer Value}` ? { prefix: Prefix; value: Value } : { prefix: T; value: never };
type MarginTop = ParseClass<"mt-3">;// { prefix: "mt"; value: "3" }Комбинирование с рекурсивными типами
Заголовок раздела «Комбинирование с рекурсивными типами»// Создание dotted path типа для nested objectstype DottedPath<T, Prefix extends string = ""> = { [K in keyof T]: T[K] extends object ? K extends string ? DottedPath<T[K], `${Prefix}${K}.`> | `${Prefix}${K}` : never : K extends string ? `${Prefix}${K}` : never;}[keyof T];
interface Config { server: { host: string; port: number; ssl: { enabled: boolean; cert: string; }; }; database: { url: string; };}
type ConfigPath = DottedPath<Config>;// "server" | "server.host" | "server.port" |// "server.ssl" | "server.ssl.enabled" | "server.ssl.cert" |// "database" | "database.url"
function getConfig<T extends ConfigPath>(path: T): any { // implementation}
getConfig("server.ssl.enabled"); // ✓// getConfig("server.invalid"); // ✗ путь не существуетКлючевые моменты
Заголовок раздела «Ключевые моменты»- Template literal types позволяют создавать строковые типы через интерполяцию
- Автоматически генерируют все комбинации при использовании union типов
- Встроенные утилиты:
Uppercase,Lowercase,Capitalize,Uncapitalize - Мощны в комбинации с mapped types для автогенерации типов
- Используются для type-safe роутинга, событий, CSS классов, API endpoints
- Можно извлекать части строк с помощью
inferв условных типах - Критически важны для создания DSL (Domain-Specific Languages) на уровне типов
- Позволяют избежать ручного дублирования типов для похожих паттернов
- Существенно улучшают developer experience и предотвращают ошибки в runtime