Пошук уроків, статей та іншого контенту
Перетворюватимете властивості типів через mapped types, модифікатори readonly та optional і фільтрацію ключів.
Mapped type — це тип, який створюється на основі ключів іншого типу. Він дає змогу пройтися по всіх властивостях і змінити їхні характеристики або типи.
Базовий синтаксис:
type NewType<Type> = {
[Key in keyof Type]: Type[Key];
};keyof Type отримує об’єднання ключів типу Type;
Key in keyof Type перебирає кожен ключ;
Type[Key] отримує тип відповідної властивості.
Наприклад, цей mapped type копіює всі властивості без змін:
type User = {
id: number;
name: string;
email: string;
};
type Copy<Type> = {
[Key in keyof Type]: Type[Key];
};
type CopiedUser = Copy<User>;Тип CopiedUser буде еквівалентним User:
type CopiedUser = {
id: number;
name: string;
email: string;
};На практиці mapped types найчастіше використовують для створення похідних типів: типів для часткового оновлення, незмінних об’єктів, станів форм або вибору окремих властивостей.
readonlyMapped type може додавати модифікатор readonly до кожної властивості:
type ReadonlyType<Type> = {
readonly [Key in keyof Type]: Type[Key];
};
type User = {
id: number;
name: string;
};
type ReadonlyUser = ReadonlyType<User>;
const user: ReadonlyUser = {
id: 1,
name: "Olena",
};
// Помилка: властивість доступна лише для читання
// user.name = "Kateryna";У TypeScript вже існує вбудований utility type Readonly<Type>, який працює за таким самим принципом:
type ReadonlyUser = Readonly<User>;readonlyЗнак - перед модифікатором видаляє його:
type Mutable<Type> = {
-readonly [Key in keyof Type]: Type[Key];
};
type ReadonlyUser = {
readonly id: number;
readonly name: string;
};
type MutableUser = Mutable<ReadonlyUser>;
const user: MutableUser = {
id: 1,
name: "Olena",
};
user.name = "Kateryna";Отже:
readonly додає обмеження лише для читання;
-readonly прибирає це обмеження.
?Модифікатор ? робить кожну властивість необов’язковою:
type Optional<Type> = {
[Key in keyof Type]?: Type[Key];
};
type User = {
id: number;
name: string;
email: string;
};
type OptionalUser = Optional<User>;
const user: OptionalUser = {
name: "Olena",
};Тепер об’єкт може містити будь-яку кількість властивостей User, включно з нульовою.
Для цього також існує вбудований utility type Partial<Type>:
type OptionalUser = Partial<User>;?Знак - перед ? робить властивість обов’язковою:
type RequiredType<Type> = {
[Key in keyof Type]-?: Type[Key];
};
type PartialUser = {
id?: number;
name?: string;
};
type CompleteUser = RequiredType<PartialUser>;
const user: CompleteUser = {
id: 1,
name: "Olena",
};Вбудований utility type для цього випадку — Required<Type>:
type CompleteUser = Required<PartialUser>;Модифікатори можна комбінувати. Наприклад, можна зробити всі властивості обов’язковими та доступними лише для читання:
type ReadonlyRequired<Type> = {
+readonly [Key in keyof Type]-?: Type[Key];
};
type Settings = {
theme?: string;
language?: string;
};
type FixedSettings = ReadonlyRequired<Settings>;
const settings: FixedSettings = {
theme: "dark",
language: "uk",
};
// Помилка: властивість доступна лише для читання
// settings.theme = "light";Знак + зазвичай можна не писати, оскільки додавання модифікатора є поведінкою за замовчуванням:
type ReadonlyRequired<Type> = {
readonly [Key in keyof Type]-?: Type[Key];
};Mapped type може не лише змінювати модифікатори, а й перетворювати тип кожної властивості.
Наприклад, усі властивості можна перетворити на boolean:
type Flags<Type> = {
[Key in keyof Type]: boolean;
};
type Features = {
darkMode: string;
notifications: number;
autoSave: boolean;
};
type FeatureFlags = Flags<Features>;
const flags: FeatureFlags = {
darkMode: true,
notifications: false,
autoSave: true,
};У FeatureFlags усі ключі збереглися, але значення кожної властивості тепер мають тип boolean:
type FeatureFlags = {
darkMode: boolean;
notifications: boolean;
autoSave: boolean;
};Можна також використовувати початковий тип властивості у виразі:
type Nullable<Type> = {
[Key in keyof Type]: Type[Key] | null;
};
type Product = {
id: number;
title: string;
};
type NullableProduct = Nullable<Product>;
const product: NullableProduct = {
id: null,
title: "Keyboard",
};asУ mapped type можна перейменовувати або виключати ключі за допомогою as.
Для виключення ключа потрібно перетворити його на never.
type OmitByKey<Type, KeyToRemove extends keyof Type> = {
[Key in keyof Type as Key extends KeyToRemove ? never : Key]: Type[Key];
};
type User = {
id: number;
name: string;
email: string;
};
type UserWithoutId = OmitByKey<User, "id">;
const user: UserWithoutId = {
name: "Olena",
email: "olena@example.com",
};У цьому прикладі:
Key extends KeyToRemove перевіряє поточний ключ;
якщо умова істинна, результатом є never;
властивості з ключем never не потрапляють до нового типу.
Вбудований utility type Omit<Type, Keys> вирішує таке завдання готовим способом:
type UserWithoutId = Omit<User, "id">;Параметр ключів може бути об’єднанням:
type PublicUser = Omit<User, "id" | "email">;
const user: PublicUser = {
name: "Olena",
};Тип PublicUser містить лише властивість name.
Фільтрацію можна використовувати не тільки для виключення, а й для вибору ключів.
type PickByKey<Type, SelectedKey extends keyof Type> = {
[Key in keyof Type as Key extends SelectedKey ? Key : never]: Type[Key];
};
type User = {
id: number;
name: string;
email: string;
isAdmin: boolean;
};
type UserCredentials = PickByKey<User, "email" | "isAdmin">;
const credentials: UserCredentials = {
email: "olena@example.com",
isAdmin: false,
};Вбудований utility type Pick<Type, Keys> має таке саме призначення:
type UserCredentials = Pick<User, "email" | "isAdmin">;За допомогою as ключ можна перетворити на інше ім’я. Для роботи з рядковими ключами використовують template literal types.
type Getters<Type> = {
[Key in keyof Type as `get${Capitalize<string & Key>}`]: () => Type[Key];
};
type User = {
name: string;
age: number;
};
type UserGetters = Getters<User>;
const getters: UserGetters = {
getName: () => "Olena",
getAge: () => 28,
};
console.log(getters.getName());
console.log(getters.getAge());Тип UserGetters матиме такий вигляд:
type UserGetters = {
getName: () => string;
getAge: () => number;
};string & Key потрібен тому, що keyof Type не обов’язково складається лише з рядків. Це звужує ключ до типу, який можна використати в template literal type.
Розглянемо тип даних користувача та створимо кілька похідних типів:
type User = {
id: number;
name: string;
email: string;
isActive: boolean;
};
type UpdateData<Type> = {
[Key in keyof Type]?: Type[Key];
};
type ReadonlyData<Type> = {
readonly [Key in keyof Type]: Type[Key];
};
type StringFields<Type> = {
[Key in keyof Type as Type[Key] extends string ? Key : never]: Type[Key];
};
type UserUpdate = UpdateData<User>;
type ReadonlyUser = ReadonlyData<User>;
type UserStringFields = StringFields<User>;
function updateUser(user: User, changes: UserUpdate): User {
return {
...user,
...changes,
};
}
const user: User = {
id: 1,
name: "Olena",
email: "olena@example.com",
isActive: true,
};
const updatedUser = updateUser(user, {
name: "Kateryna",
});
const stringFields: UserStringFields = {
name: "Olena",
email: "olena@example.com",
};
const readonlyUser: ReadonlyUser = updatedUser;
console.log(updatedUser);
console.log(stringFields);
console.log(readonlyUser);У цьому прикладі:
UpdateData<User> робить усі властивості необов’язковими;
ReadonlyData<User> робить усі властивості доступними лише для читання;
StringFields<User> залишає тільки властивості, значення яких мають тип string;
updateUser приймає лише допустимі властивості для оновлення.
keyofНеправильно:
// Помилка: Type не можна використовувати як набір ключів
// type Broken<Type> = {
// [Key in Type]: Type[Key];
// };Правильно:
type Correct<Type> = {
[Key in keyof Type]: Type[Key];
};Mapped type перебирає ключі, тому зазвичай потрібно використовувати keyof Type.
Якщо у mapped type вказати конкретний тип, усі властивості отримають саме його:
type AllStrings<Type> = {
[Key in keyof Type]: string;
};
type Product = {
id: number;
title: string;
};
type StringProduct = AllStrings<Product>;
// id тепер має тип string, а не numberЯкщо потрібно зберегти тип кожної конкретної властивості, використовуйте Type[Key]:
type SameTypes<Type> = {
[Key in keyof Type]: Type[Key];
};readonlyreadonly захищає властивість від присвоєння через цей тип, але не робить значення глибоко незмінним.
type State = {
items: string[];
};
type ReadonlyState = Readonly<State>;
const state: ReadonlyState = {
items: ["TypeScript"],
};
// Помилка: не можна замінити властивість items
// state.items = [];
// Дозволено: масив сам по собі не став readonly
state.items.push("Mapped Types");
console.log(state.items);Mapped type Readonly<Type> додає readonly властивостям верхнього рівня. Для вкладених об’єктів або масивів потрібне окреме рекурсивне перетворення, якщо це необхідно.
neverЯкщо ключ потрібно виключити, результат умови має бути never:
type OnlyStrings<Type> = {
[Key in keyof Type as Type[Key] extends string ? Key : never]: Type[Key];
};Якщо замість never повернути інший ключ або початковий Key, властивість не буде виключена.
Mapped types створюють нові типи на основі ключів існуючого типу.
keyof Type отримує ключі типу, а Type[Key] — тип поточної властивості.
readonly додає властивостям режим лише для читання.
-readonly видаляє цей модифікатор.
? робить властивості необов’язковими.
-? робить властивості обов’язковими.
Через as можна перейменовувати ключі або виключати їх, повертаючи never.
Для готових поширених перетворень існують Partial, Required, Readonly, Pick та Omit.
Mapped types дають змогу створювати похідні типи без дублювання структури вручну.