Пошук уроків, статей та іншого контенту
Формуватимете типи рядкових ключів і подій через об’єднання, інтерполяцію та перевірки шаблонних літералів.
Шаблонні літеральні типи дають змогу створювати нові типи рядків на основі інших типів. Вони нагадують шаблонні рядки JavaScript, але працюють на рівні типів.
Синтаксис:
type Greeting = `Hello, ${string}`;Тип Greeting описує будь-який рядок, що починається з Hello, :
const first: Greeting = "Hello, TypeScript";
const second: Greeting = "Hello, developer";
// Помилка: рядок не відповідає шаблону
const invalid: Greeting = "Hi, TypeScript";Шаблонні літеральні типи підтримують:
конкретні рядкові літерали;
об’єднання типів;
інтерполяцію інших типів;
умовні типи та infer.
Усередині шаблону можна використовувати типи в конструкції ${...}:
type EventName = `on${string}`;
const clickEvent: EventName = "onclick";
const submitEvent: EventName = "onsubmit";У цьому прикладі ${string} означає будь-який рядок.
Можна інтерполювати й інші примітивні типи:
type Id = `user-${number}`;
const firstUser: Id = "user-42";
const secondUser: Id = "user-100";Тип Id приймає рядки, у яких після user- розташоване значення, сумісне з типом number.
Однак шаблонний тип не перевіряє формат числа так само детально, як регулярний вираз. Його завдання — описати загальну структуру рядка на рівні TypeScript.
Якщо інтерполювати об’єднання, TypeScript створює всі можливі комбінації його значень:
type Size = "small" | "large";
type Color = "red" | "blue";
type ProductClass = `${Size}-${Color}`;Тип ProductClass еквівалентний такому об’єднанню:
type ProductClass =
| "small-red"
| "small-blue"
| "large-red"
| "large-blue";Це зручно для опису обмежених наборів рядкових ключів:
const firstClass: ProductClass = "small-red";
const secondClass: ProductClass = "large-blue";
// Помилка: такої комбінації немає в типі
const invalidClass: ProductClass = "medium-red";Коли кожна частина шаблону має кілька варіантів, кількість результатів зростає як декартів добуток усіх об’єднань. Тому для дуже великих об’єднань варто уникати надмірно складних шаблонів.
Шаблонні літеральні типи часто використовують разом із keyof. Так можна автоматично сформувати пов’язані ключі на основі властивостей об’єкта.
type User = {
name: string;
age: number;
active: boolean;
};
type UserGetterName = `get${Capitalize<keyof User & string>}`;UserGetterName містить:
type UserGetterName = "getName" | "getAge" | "getActive";Тут застосовано вбудований допоміжний тип Capitalize, який робить перший символ рядка великим.
Аналогічно можна створити ключі для обробників подій:
type UserEventName = `${keyof User & string}Changed`;Результат:
type UserEventName = "nameChanged" | "ageChanged" | "activeChanged";Перетворення keyof User & string потрібне тому, що keyof загалом може містити не лише рядкові ключі. Шаблон ${...} у цьому прикладі очікує саме рядкову частину.
Шаблонні літеральні типи можна перевіряти за допомогою extends.
type IsGetterName<Name extends string> =
Name extends `get${string}` ? true : false;
type First = IsGetterName<"getName">; // true
type Second = IsGetterName<"setName">; // falseУмовний тип перевіряє, чи відповідає рядок певному шаблону.
Це корисно для вибору різних типів залежно від форми ключа:
type AccessResult<Name extends string> =
Name extends `get${string}`
? string
: Name extends `set${string}`
? void
: never;
type GetterResult = AccessResult<"getName">; // string
type SetterResult = AccessResult<"setName">; // void
type InvalidResult = AccessResult<"removeName">; // neverinferУсередині шаблонного літерального типу можна використовувати infer, щоб витягнути частину рядка.
type EventProperty<EventName extends string> =
EventName extends `${infer Property}Changed`
? Property
: never;
type NameProperty = EventProperty<"nameChanged">; // "name"
type AgeProperty = EventProperty<"ageChanged">; // "age"
type InvalidProperty = EventProperty<"nameUpdated">; // neverУ шаблоні ${infer Property}Changed TypeScript:
перевіряє, чи закінчується рядок на Changed;
зберігає частину перед Changed у тип Property;
повертає цю частину.
Можна витягувати кілька частин:
type ParseRoute<Route extends string> =
Route extends `/users/${infer UserId}/posts/${infer PostId}`
? {
userId: UserId;
postId: PostId;
}
: never;
type RouteData = ParseRoute<"/users/42/posts/7">;
// {
// userId: "42";
// postId: "7";
// }Для літерального типу рядка RouteData міститиме саме "42" і "7" як рядкові літерали.
Шаблонні літеральні типи особливо корисні для зв’язування назви властивості та назви події.
Розглянемо об’єкт, який повідомляє про зміни своїх властивостей:
type Model = {
name: string;
age: number;
};Назва події має складатися з назви властивості та суфікса Changed:
type ModelEventName = `${keyof Model & string}Changed`;
// "nameChanged" | "ageChanged"За назвою події можна визначити тип нового значення:
type EventValue<
T,
EventName extends string
> = EventName extends `${infer Property}Changed`
? Property extends keyof T
? T[Property]
: never
: never;
type NameEventValue = EventValue<Model, "nameChanged">; // string
type AgeEventValue = EventValue<Model, "ageChanged">; // numberТепер можна описати метод on, який перевіряє і назву події, і тип її значення:
type Model = {
name: string;
age: number;
};
type EventName<T> = `${keyof T & string}Changed`;
type EventValue<T, Event extends string> =
Event extends `${infer Property}Changed`
? Property extends keyof T
? T[Property]
: never
: never;
type ModelListener<Event extends EventName<Model>> =
(value: EventValue<Model, Event>) => void;
function onModelEvent<Event extends EventName<Model>>(
event: Event,
listener: ModelListener<Event>
): void {
console.log(`Зареєстровано обробник для події ${event}`);
// У реальному застосунку тут зберігався б обробник події.
void listener;
}
onModelEvent("nameChanged", (value) => {
console.log(value.toUpperCase());
});
onModelEvent("ageChanged", (value) => {
console.log(value.toFixed(0));
});
// Помилка: для nameChanged очікується string
onModelEvent("nameChanged", (value) => {
console.log(value.toFixed(0));
});
// Помилка: подія не відповідає властивостям Model
onModelEvent("emailChanged", (value) => {
console.log(value);
});У цьому прикладі:
EventName<Model> створює "nameChanged" | "ageChanged";
EventValue витягує назву властивості з назви події;
Property extends keyof T перевіряє, що така властивість справді існує;
T[Property] повертає тип значення властивості;
TypeScript автоматично виводить тип параметра value у функції-обробнику.
TypeScript має вбудовані типи для зміни регістру:
Uppercase<StringType>;
Lowercase<StringType>;
Capitalize<StringType>;
Uncapitalize<StringType>.
Приклад:
type Property = "name" | "email";
type Getter = `get${Capitalize<Property>}`;
// "getName" | "getEmail"
type Environment = "DEV" | "PROD";
type LowerEnvironment = Lowercase<Environment>;
// "dev" | "prod"Їх можна комбінувати з шаблонними типами:
type Action = "create" | "update" | "delete";
type ActionEvent = `${Uppercase<Action>}_STARTED`;
// "CREATE_STARTED" | "UPDATE_STARTED" | "DELETE_STARTED"stringЛітеральний тип і тип string мають різну точність:
const exactEvent = "nameChanged";
// Тип: "nameChanged"
let generalEvent = "nameChanged";
// Тип: stringЗмінна з типом string уже не зберігає конкретний літерал, тому її не завжди можна передати туди, де очікується шаблонний тип:
type EventName = "nameChanged" | "ageChanged";
function handleEvent(event: EventName): void {
console.log(event);
}
const eventName = "nameChanged";
// Тип eventName — string, тому такий виклик може бути відхилено
handleEvent(eventName);
const exactName = "nameChanged" as const;
handleEvent(exactName);as const зберігає максимально вузький літеральний тип. Для об’єктів і масивів ця конструкція також робить властивості та елементи readonly, тому застосовувати її потрібно свідомо.
Якщо потрібна перевірка саме форми рядка, можна описати її умовним типом:
type ValidApiPath<Path extends string> =
Path extends `/api/${string}` ? Path : never;
function request<Path extends string>(
path: Path & ValidApiPath<Path>
): void {
console.log(`Запит до ${path}`);
}
request("/api/users");
request("/api/products");
// Помилка: шлях не починається з /api/
request("/admin/users");У цьому прикладі ValidApiPath повертає початковий рядковий тип, якщо він відповідає шаблону, або never, якщо не відповідає.
Важливо, що така перевірка працює під час компіляції лише тоді, коли TypeScript знає достатньо точний тип значення. Для довільної змінної типу string компілятор не знає її фактичного вмісту.
stringtype EventName = `${string}Changed`;Такий тип перевіряє лише суфікс Changed. Він не гарантує, що частина перед суфіксом є назвою властивості конкретного об’єкта.
Якщо події мають бути пов’язані з об’єктом, використовуйте keyof:
type User = {
name: string;
age: number;
};
type UserEvent = `${keyof User & string}Changed`;Шаблонні літеральні типи існують лише на рівні типів:
type Prefix = "user";
type UserKey = `${Prefix}-${number}`;Це не створює рядки під час виконання. Для формування реального значення все одно потрібен звичайний шаблонний рядок JavaScript:
const id = 42;
const key: UserKey = `user-${id}`;Тип ${number} описує рядок, сумісний із числовим шаблоном TypeScript, але не замінює повноцінну перевірку вхідних даних під час виконання. Значення, отримані від користувача або з мережі, за потреби потрібно перевіряти окремо під час виконання.
Якщо значення розширилося до string, TypeScript уже не може перевірити конкретний шаблон:
type Command = `user:${"create" | "delete"}`;
function run(command: Command): void {
console.log(command);
}
const command: string = "user:create";
// Значення має тип string, а не конкретний літерал
run(command);Якщо значення справді відоме й відповідає обмеженому набору, його можна оголосити з точнішим типом:
const knownCommand: Command = "user:create";
run(knownCommand);Шаблонні літеральні типи створюють типи рядків за допомогою синтаксису `${...}`.
Об’єднання всередині шаблону утворюють усі можливі комбінації значень.
Разом із keyof вони дають змогу формувати типізовані ключі та імена подій.
Умовні типи extends перевіряють, чи відповідає рядок заданому шаблону.
infer дає змогу витягувати частини рядка з шаблонного літерального типу.
Capitalize, Uppercase та інші вбудовані типи допомагають змінювати регістр частин ключів.
Шаблонні літеральні типи працюють під час компіляції й не виконують перевірку рядків під час роботи програми.