Пошук уроків, статей та іншого контенту
Створюватимете нові назви ключів за допомогою as у mapped types і комбінуватимете їх із шаблонними літералами.
Mapped types дають змогу пройтися по ключах іншого типу та створити новий тип. За замовчуванням назва ключа зберігається:
type Optional<T> = {
[K in keyof T]?: T[K];
};Іноді потрібно не лише змінити модифікатори ключів, а й створити для них нові назви. Для цього в mapped type використовується as:
type Renamed<T> = {
[K in keyof T as NewKey]: T[K];
};Вираз після as визначає назву ключа в новому типі.
type User = {
name: string;
age: number;
};
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type UserGetters = Getters<User>;Тип UserGetters буде еквівалентним такому:
type UserGetters = {
getName: () => string;
getAge: () => number;
};Тут відбуваються такі кроки:
keyof T отримує ключі типу.
K in keyof T перебирає ці ключі.
K & string обмежує ключ значенням, сумісним із рядковим шаблонним літералом.
Capitalize<K & string> робить першу літеру великою.
`get${...}` додає префікс get.
T[K] зберігає тип значення відповідної властивості.
as і шаблонних літералівШаблонні літеральні типи дають змогу будувати нові ключі з частин:
type ChangedHandlers<T> = {
[K in keyof T as K extends string
? `on${Capitalize<K>}Changed`
: never]: (value: T[K]) => void;
};Для такого типу:
type User = {
name: string;
age: number;
};результат буде таким:
type UserChangedHandlers = {
onNameChanged: (value: string) => void;
onAgeChanged: (value: number) => void;
};Умову K extends string використано тому, що шаблонні літеральні типи працюють із рядками. keyof T може містити не лише рядкові ключі, а й number або symbol.
Створимо тип, який перетворює властивості моделі на обробники змін.
type User = {
name: string;
age: number;
active: boolean;
};
type ChangeHandlers<T> = {
[K in keyof T as K extends string
? `on${Capitalize<K>}Changed`
: never]: (value: T[K]) => void;
};
const handlers: ChangeHandlers<User> = {
onNameChanged: (value) => {
console.log(`Нове ім'я: ${value}`);
},
onAgeChanged: (value) => {
console.log(`Новий вік: ${value}`);
},
onActiveChanged: (value) => {
console.log(`Активний користувач: ${value}`);
},
};
handlers.onNameChanged("Olena");
handlers.onAgeChanged(30);
handlers.onActiveChanged(true);TypeScript автоматично пов'язує нову назву ключа з початковим ключем:
onNameChanged приймає string;
onAgeChanged приймає number;
onActiveChanged приймає boolean.
Якщо передати неправильний тип, TypeScript повідомить про помилку:
handlers.onAgeChanged("30");
// Помилка: аргумент типу string не можна передати параметру типу number.TypeScript має кілька вбудованих типів для роботи з рядками:
Uppercase<S> — переводить усі літери у верхній регістр;
Lowercase<S> — переводить усі літери в нижній регістр;
Capitalize<S> — робить першою велику літеру;
Uncapitalize<S> — робить першою малу літеру.
Їх можна комбінувати з шаблонними літералами:
type ApiMethods<T> = {
[K in keyof T as K extends string
? `fetch${Capitalize<K>}`
: never]: () => Promise<T[K]>;
};
type Product = {
title: string;
price: number;
};
type ProductApi = ApiMethods<Product>;ProductApi матиме такий вигляд:
type ProductApi = {
fetchTitle: () => Promise<string>;
fetchPrice: () => Promise<number>;
};Перейменування ключа не змінює тип його значення. У наступному прикладі всі властивості отримують префікс data:
type DataFields<T> = {
[K in keyof T as K extends string ? `data_${K}` : never]: T[K];
};
type Settings = {
theme: string;
timeout: number;
};
type DataSettings = DataFields<Settings>;Результат:
type DataSettings = {
data_theme: string;
data_timeout: number;
};Зв'язок із початковим ключем зберігається через T[K].
neverЯкщо результат виразу після as — never, відповідний ключ не потрапляє до нового типу.
type OnlyStringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type Profile = {
name: string;
age: number;
email: string;
};
type StringProfile = OnlyStringProperties<Profile>;StringProfile міститиме лише рядкові властивості:
type StringProfile = {
name: string;
email: string;
};Ту саму техніку можна поєднати з перейменуванням:
type StringGetters<T> = {
[K in keyof T as T[K] extends string
? `get${Capitalize<K & string>}`
: never]: () => T[K];
};
type ProfileGetters = StringGetters<Profile>;Результат:
type ProfileGetters = {
getName: () => string;
getEmail: () => string;
};Властивість age не потрапила до типу, оскільки її значення має тип number.
Тип keyof може містити числові або символьні ключі. Якщо нова назва будується за допомогою шаблонного літерала, такі ключі потрібно обробити окремо.
type StringKeyGetters<T> = {
[K in keyof T as K extends string
? `get${Capitalize<K>}`
: never]: () => T[K];
};
type Mixed = {
name: string;
42: boolean;
};
type MixedGetters = StringKeyGetters<Mixed>;У MixedGetters буде лише ключ getName. Числовий ключ 42 буде виключено через never.
Якщо потрібно зберегти ключі, які не є рядками, їх можна залишити без перейменування:
type GettersAndOriginalKeys<T> = {
[K in keyof T as K extends string
? `get${Capitalize<K>}`
: K]: () => T[K];
};Тоді:
рядкові ключі отримають нові назви;
числові та символьні ключі залишаться з початковими назвами.
Один початковий ключ може породити кілька ключів. Для цього в as використовують об'єднання літеральних типів:
type Accessors<T> = {
[K in keyof T as K extends string
? K | `get${Capitalize<K>}`
: K]: T[K];
};
type Account = {
login: string;
points: number;
};
type AccountAccessors = Accessors<Account>;Результат міститиме:
type AccountAccessors = {
login: string;
getLogin: string;
points: number;
getPoints: number;
};Тип значення для кожного створеного ключа залишається T[K].
as у mapped type та приведенням типуУ TypeScript ключове слово as може використовуватися в різних контекстах.
У mapped type:
type Renamed<T> = {
[K in keyof T as `field_${K & string}`]: T[K];
};as перейменовує ключі під час створення нового типу.
У виразі:
const value = input as string;as використовується для приведення типу. Це інша можливість TypeScript, яка не перейменовує властивості.
K у шаблонному літералі без перевіркиТакий запис може спричинити помилку:
type Getters<T> = {
[K in keyof T as `get${Capitalize<K>}`]: () => T[K];
};K не обов'язково є рядком. Безпечні варіанти:
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};або:
type Getters<T> = {
[K in keyof T as K extends string
? `get${Capitalize<K>}`
: never]: () => T[K];
};Після перейменування потрібно використовувати початковий ключ K, щоб отримати відповідне значення:
type Correct<T> = {
[K in keyof T as `field_${K & string}`]: T[K];
};Якщо замість T[K] вказати один спільний тип, усі властивості отримають саме його:
type Incorrect<T> = {
[K in keyof T as `field_${K & string}`]: string;
};У Incorrect<T> тип кожного значення буде string, навіть якщо в початковому типі значення мало інший тип.
Якщо умовний вираз після as повертає never, ключ видаляється:
type OnlyNumbers<T> = {
[K in keyof T as T[K] extends number ? K : never]: T[K];
};Це корисно для фільтрування, але може бути помилкою, якщо умова надто обмежувальна.
Mapped type змінює лише типову інформацію. Він не перейменовує властивості реального JavaScript-об'єкта:
type Prefixed<T> = {
[K in keyof T as `data_${K & string}`]: T[K];
};Цей тип описує об'єкт із ключами data_name, data_age тощо, але сам об'єкт потрібно створити або перетворити окремо під час виконання.
as у mapped type дає змогу створювати нові назви ключів.
Новий ключ записується у формі [K in keyof T as NewKey].
Шаблонні літерали дають змогу додавати префікси, суфікси та будувати складені назви.
Capitalize, Uppercase, Lowercase і Uncapitalize допомагають перетворювати частини ключів.
Вираз as ... ? NewKey : never дає змогу фільтрувати ключі.
T[K] зберігає тип значення, пов'язаний із початковим ключем.
Для number і symbol ключів потрібно враховувати, що шаблонні літеральні типи працюють із рядками.