Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Типи шаблонних літералів у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Типи шаблонних літералів** (template literal types) у TypeScript генерують нові рядкові типи через шаблонний синтаксис. `${Size}-${Color}` з union-типами `"small" | "large"` і `"red" | "blue"` дає рівно чотири конкретні літерали: `"small-red" | "small-blue" | "large-red" | "large-blue"`. **Головне:** розширення відбувається лише в компіляторі, без жодних змін у runtime.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Типи шаблонних літералів** (template literal types) дозволяють TypeScript генерувати нові рядкові типи через шаблонний синтаксис у момент компіляції, автоматично виробляючи всі комбінації з вхідних union-типів. ## Теорія ### TL;DR - Схоже на Mad Libs: ти визначаєш варіанти для «пропусків» (union-типи), TypeScript генерує кожне можливе заповнення як окремий тип. - Поведінка дистрибутивна: `` `${A | B}-${C}` `` розкривається до `"A-C" | "B-C"`, декартовий добуток. - Нульова вартість у runtime. Типи стираються до `string`; розширення відбувається лише в перевірці типів. - Використовуй, коли треба вичерпний набір рядкових ключів або шляхів. Для простих рядків або генерації під час виконання - звичайний `string`. - З'явилось у TypeScript 4.1, березень 2021. ### Швидкий приклад ```typescript type Size = "small" | "large"; type Color = "red" | "blue"; type TShirt = `${Size}-${Color}`; // "small-red" | "small-blue" | "large-red" | "large-blue" const shirt: TShirt = "small-red"; // ✅ const invalid: TShirt = "small"; // ❌ Type '"small"' is not assignable to type 'TShirt' ``` TypeScript бере обидва union-типи, застосовує шаблон до кожної комбінації членів і видає чотири точних рядкових літерали. Перераховувати їх вручну не потрібно. ### Головна відмінність від звичайних union-типів Звичайний union рядків статичний: кожен член пишеш руками. Типи шаблонних літералів генерують повний набір із частин. Три розміри помножити на три кольори дає дев'ять рядків, але оголошувати треба лише шість значень. У цьому декартовому розширенні і полягає вся ідея. Крім того, template literal types перевіряють *формат*, а не лише значення. `` `user/${number}/profile` `` приймає `"user/123/profile"`, але відхиляє `"user/abc/profile"`. Звичайні union-типи так не можуть. ### Коли використовувати - **Генеровані набори ключів** (назви подій, CSS-класи, пари getter/setter): template literal types, щоб не перелічувати вручну. - **Шаблони маршрутів API** з динамічними сегментами: типи шаблонних літералів перевірять формат під час компіляції. - **Type-safe event emitter**, де імена методів слідують шаблону на зразок `onEventName`. - **Прості статичні рядки** з менш ніж 5 варіантами без шаблону: звичайні рядкові літерали. - **Генерація рядків у runtime**: звичайні template strings. Template literal types існують лише під час компіляції. ### Як це обробляє компілятор Перевірка типів у TypeScript 4.1+ парсить шаблонний синтаксис, рекурсивно підставляє кожен член union у placeholder і зводить результат до union рядкових літералів. Кожна гілка обробляється незалежно. Код під час виконання не змінюється. У скомпільованому JavaScript тип стирається до `string`. Якщо placeholder отримує `string` замість кінцевого union, результат вироджується до патернового типу на зразок `` `${string}-suffix` `` замість конкретних варіантів. Це очікувана поведінка. ### Вбудовані утиліти для роботи з рядками TypeScript постачається з чотирма вбудованими типами для трансформації рядкових літералів: ```typescript type Upper = Uppercase<"hello">; // "HELLO" type Lower = Lowercase<"HELLO">; // "hello" type Cap = Capitalize<"hello">; // "Hello" type Uncap = Uncapitalize<"Hello">; // "hello" type Event = "click" | "focus" | "blur"; type Handler = `on${Capitalize<Event>}`; // "onClick" | "onFocus" | "onBlur" ``` Вони застосовуються зліва направо і добре поєднуються з template literal types. ### Типові помилки **1. Очікування роботи в runtime** ```typescript // Runtime-функція - тип повернення `string`, не template literal type const makeKey = (prefix: string) => `key-${prefix}`; type Key = ReturnType<typeof makeKey>; // string // Якщо потрібен тип, оголоси його окремо: type Key = `key-${"a" | "b"}`; ``` Типи шаблонних літералів існують у системі типів. Функції, що будують рядки в runtime, повертають `string`. **2. Домішування `string` у union** ```typescript type Prefix = "a" | "b"; type T = `${Prefix | string}-suffix`; // `${string}-suffix`, а не "a-suffix" | "b-suffix" ``` `string` поглинає кінцевий union і конкретні літерали зникають. Тримай placeholder як кінцевий union. **3. Використання `number` як нескінченного placeholder** ```typescript type Id = `user-${number}`; // Коректно для перевірки присвоєння, але конкретний union не створюється ``` `number` розкривається в усі числові рядки, які TypeScript не може перелічити. Тип приймає `"user-123"`, але автодоповнення і перевірка вичерпності не працюватимуть. Використовуй кінцевий union чисел або branded type. **4. Глибока рекурсія і ліміт компілятора** Рекурсивні template literal types можуть дати «Type instantiation is excessively deep and possibly infinite» приблизно після 10-20 рівнів. Вирівняй рекурсію або використовуй mapped types з фіксованою глибиною. ### Де це використовується - **React Router v6**: `` `user/${number}/${"view" | "edit" | "delete"}` `` для type-safe навігації. - **TanStack Query**: типи ключів на зразок `` `user-${number}-profile` `` щоб уникнути колізій. - **tRPC**: типи шляхів процедур із форми роутера. - **GraphQL Codegen**: типи резолверів як `` `Query.${Keys}` ``. - **Tailwind-подібні утиліти**: `` `p${Direction}-${Spacing}` `` для CSS-класів. - **Mapped types з перейменуванням ключів**: `` `get${Capitalize<string & K>}` `` для генерації getter-методів з інтерфейсу. ### Питання на співбесіді **Q:** Що означає «дистрибутивна» поведінка? Наведи приклад. **A:** TypeScript застосовує шаблон незалежно до кожного члена union. `` `${"a" | "b"}-${"x" | "y"}` `` стає `"a-x" | "a-y" | "b-x" | "b-y"`. Це декартовий добуток, а не zip. **Q:** Як `Uppercase`, `Capitalize` та схожі утиліти взаємодіють з template literal types? **A:** Поєднуються без проблем. `` `on${Capitalize<"click" | "focus">}` `` дає `"onClick" | "onFocus"`. Утиліти застосовуються зліва направо всередині шаблону. **Q:** Яка різниця між template literal types і `as const`? **A:** `as const` звужує існуюче значення до його літерального типу. Template literal types *генерують нові* рядкові типи з частин. Вони вирішують різні задачі і можуть поєднуватись. **Q:** Наскільки глибокою може бути рекурсія? **A:** Приблизно 10-20 рівнів, після чого TypeScript видає «Type instantiation is excessively deep and possibly infinite». Для складних типів шляхів використовуй mapped types або явно обмежуй глибину. **Q:** (Senior) Напиши тип, що витягує prefix з патерну `` `${Prefix}-${Suffix}` ``. **A:** ```typescript type ExtractPrefix<T extends `${string}-${string}`> = T extends `${infer P}-${string}` ? P : never; type Result = ExtractPrefix<"small-red">; // "small" ``` `infer` всередині template literal type захоплює відповідний сегмент як нову типову змінну. ## Приклади ### Базовий: CSS-класи для відступів ```typescript type Spacing = 0 | 1 | 2 | 4 | 8; type Direction = "t" | "b" | "l" | "r"; type PaddingClass = `p${Direction}-${Spacing}`; // "pt-0" | "pt-1" | "pt-2" | "pt-4" | "pt-8" | "pb-0" | ... 20 комбінацій const cls: PaddingClass = "pt-4"; // ✅ const bad: PaddingClass = "pt-3"; // ❌ 3 не входить до Spacing ``` TypeScript генерує всі 20 назв класів із 4 напрямків і 5 значень відступу. Додай нове значення до union і всі типи класів оновляться автоматично. ### Середній: Type-safe event emitter з mapped types ```typescript type Events = { userCreated: { id: string; name: string }; userDeleted: { id: string }; orderPlaced: { orderId: string; total: number }; }; type EventEmitter = { [K in keyof Events as `on${Capitalize<string & K>}`]: (data: Events[K]) => void; }; // Результат: // { // onUserCreated: (data: { id: string; name: string }) => void; // onUserDeleted: (data: { id: string }) => void; // onOrderPlaced: (data: { orderId: string; total: number }) => void; // } ``` Перейменування ключів через `as` у mapped type разом з template literal і `Capitalize` - один із найпоширеніших патернів у продакшен-коді на TypeScript. Цю конструкцію зручно використовувати в event-driven сервісах, щоб тримати контракт emitter-а в синхроні з картою подій. ### Senior: Витягування параметрів маршруту через рекурсивний infer ```typescript type ExtractRouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Param | ExtractRouteParams<Rest> : T extends `${string}:${infer Param}` ? Param : never; type Params = ExtractRouteParams<"/users/:userId/posts/:postId">; // "userId" | "postId" function navigate<T extends string>( path: T, params: Record<ExtractRouteParams<T>, string> ) { // params типізовано як { userId: string; postId: string } // для path "/users/:userId/posts/:postId" } navigate("/users/:userId/posts/:postId", { userId: "1", postId: "42" }); // ✅ navigate("/users/:userId/posts/:postId", { userId: "1" }); // ❌ Відсутній postId ``` Саме цей патерн лежить в основі type-safe роутерів на зразок React Router v6. Рекурсія відщипує один `:param` за раз і об'єднує результати через union. Для типових форм маршрутів ліміт глибини не досягається.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.