Пошук уроків, статей та іншого контенту
Створіть власні Utility Types за допомогою mapped types, conditional types, keyof, typeof та infer.
У цьому уроці ви навчитеся створювати власні Utility Types за допомогою:
keyof для отримання ключів типу;
typeof для отримання типу зі значення;
mapped types для перебору та перетворення властивостей;
conditional types для умовної логіки на рівні типів;
infer для вилучення частин складного типу.
Власні Utility Types корисні, коли стандартних Partial, Pick, Omit або ReturnType недостатньо для моделювання типів конкретної програми.
keyof: отримання ключів типуОператор keyof створює union із ключів об'єктного типу:
type User = {
id: number;
name: string;
email: string;
};
type UserKey = keyof User;
// "id" | "name" | "email"Отриманий тип можна використовувати для обмеження параметрів функції:
function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
const user = {
id: 1,
name: "Olena",
email: "olena@example.com",
};
const userName = getProperty(user, "name");
// string
const userId = getProperty(user, "id");
// number
// Помилка: "age" не є ключем об'єкта user
// const userAge = getProperty(user, "age");Конструкція T[K] називається indexed access type. Вона означає тип властивості K у типі T.
Наприклад:
type UserName = User["name"];
// string
type UserProperty = User[keyof User];
// string | numbertypeof: отримання типу зі значенняУ TypeScript typeof має два різні контексти:
у JavaScript-коді — перевірка типу під час виконання;
у позиції типу — отримання типу змінної або властивості.
const defaultConfig = {
retries: 3,
timeout: 5000,
logLevel: "info" as const,
};
type Config = typeof defaultConfig;
// {
// retries: number;
// timeout: number;
// logLevel: "info";
// }Це зручно, коли джерелом правди є константа, а не окремо оголошений тип:
const statuses = {
draft: "DRAFT",
published: "PUBLISHED",
archived: "ARCHIVED",
} as const;
type Status = typeof statuses[keyof typeof statuses];
// "DRAFT" | "PUBLISHED" | "ARCHIVED"Тут:
typeof statuses отримує тип об'єкта;
keyof typeof statuses отримує його ключі;
typeof statuses[keyof typeof statuses] отримує union усіх значень.
Mapped type перебирає ключі існуючого типу та створює новий тип.
Базовий синтаксис:
type NewType<T> = {
[Key in keyof T]: /* новий тип властивості */;
};Наприклад, створимо тип, у якому всі властивості мають тип boolean:
type Flags<T> = {
[Key in keyof T]: boolean;
};
type FeatureFlags = Flags<{
darkMode: boolean;
notifications: boolean;
betaFeatures: boolean;
}>;
// {
// darkMode: boolean;
// notifications: boolean;
// betaFeatures: boolean;
// }На практиці mapped type зазвичай не просто змінює тип властивості, а зберігає або змінює її модифікатори.
PartialСтандартний Partial<T> робить усі властивості необов'язковими. Його спрощена реалізація виглядає так:
type MyPartial<T> = {
[Key in keyof T]?: T[Key];
};
type User = {
id: number;
name: string;
email: string;
};
type PartialUser = MyPartial<User>;
// {
// id?: number;
// name?: string;
// email?: string;
// }Модифікатор ? додає властивостям ознаку optional.
RequiredЩоб зробити всі властивості обов'язковими, використовується модифікатор -?:
type MyRequired<T> = {
[Key in keyof T]-?: T[Key];
};
type Draft = {
title?: string;
description?: string;
};
type CompleteDraft = MyRequired<Draft>;
// {
// title: string;
// description: string;
// }Знак - перед модифікатором видаляє цей модифікатор.
ReadonlyМодифікатор readonly забороняє змінювати властивості:
type MyReadonly<T> = {
readonly [Key in keyof T]: T[Key];
};
type Settings = MyReadonly<{
theme: string;
language: string;
}>;
const settings: Settings = {
theme: "dark",
language: "uk",
};
// Помилка: властивість доступна лише для читання
// settings.theme = "light";Щоб видалити readonly, використовується -readonly:
type Mutable<T> = {
-readonly [Key in keyof T]: T[Key];
};
type MutableSettings = Mutable<{
readonly theme: string;
readonly language: string;
}>;
const mutableSettings: MutableSettings = {
theme: "dark",
language: "uk",
};
mutableSettings.theme = "light";Mapped types можна поєднувати з conditional types, щоб залишати лише властивості, які відповідають певній умові.
Створимо тип, який знаходить ключі властивостей заданого типу:
type KeysOfType<T, ValueType> = {
[Key in keyof T]-?: T[Key] extends ValueType ? Key : never;
}[keyof T];Розглянемо його частинами:
type Product = {
id: number;
title: string;
price: number;
isAvailable: boolean;
};
type NumberKeys = KeysOfType<Product, number>;
// "id" | "price"
type StringKeys = KeysOfType<Product, string>;
// "title"Внутрішній mapped type створює проміжний об'єкт:
type ProductKeyMap = {
[Key in keyof Product]-?:
Product[Key] extends number ? Key : never;
};
// {
// id: "id";
// title: never;
// price: "price";
// isAvailable: never;
// }Додавання [keyof T] в кінці отримує union значень цього об'єкта:
type NumberKeys = ProductKeyMap[keyof Product];
// "id" | "price"PickByValueТепер можна створити Utility Type, який залишає в об'єкті тільки властивості певного типу:
type PickByValue<T, ValueType> = {
[Key in keyof T as T[Key] extends ValueType ? Key : never]: T[Key];
};
type Account = {
id: number;
username: string;
isAdmin: boolean;
createdAt: Date;
};
type AccountTextFields = PickByValue<Account, string>;
// {
// username: string;
// }Конструкція as дозволяє перетворити або відфільтрувати ключ. Якщо результатом буде never, ключ не потрапить до нового типу.
type NumberFields = PickByValue<Account, number>;
// {
// id: number;
// }Conditional type має форму:
type Result<T> = T extends SomeType
? TypeIfTrue
: TypeIfFalse;Приклад:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;
// true
type B = IsString<number>;
// falseConditional types часто використовують усередині власних Utility Types.
NullableСтворимо тип, який додає null до кожного типу:
type Nullable<T> = T | null;
type NullableString = Nullable<string>;
// string | null
type NullableUser = Nullable<{
id: number;
name: string;
}>;
// { id: number; name: string } | nullЯкщо потрібно додати null до кожної властивості об'єкта, потрібен mapped type:
type NullableProperties<T> = {
[Key in keyof T]: T[Key] | null;
};
type NullableUserProperties = NullableProperties<{
id: number;
name: string;
}>;
// {
// id: number | null;
// name: string | null;
// }NonNullableМожна видалити null і undefined за допомогою conditional type:
type MyNonNullable<T> = T extends null | undefined ? never : T;
type DefinedValue = MyNonNullable<string | null | undefined>;
// stringУ цьому прикладі conditional type розподіляється по кожному елементу union:
type MyNonNullable<string> = string;
type MyNonNullable<null> = never;
type MyNonNullable<undefined> = never;Union із never спрощується:
type Result = string | never;
// stringЯкщо перевіряється безпосередньо параметр типу T, conditional type є distributive:
type ToArray<T> = T extends unknown ? T[] : never;
type Values = ToArray<string | number>;
// string[] | number[]TypeScript фактично обробляє типи окремо:
type Values =
| ToArray<string>
| ToArray<number>;
// string[] | number[]Іноді потрібно заборонити такий розподіл і перевірити весь union як одне значення. Для цього тип обгортають у tuple:
type ToArrayWithoutDistribution<T> =
[T] extends [unknown] ? T[] : never;
type Values = ToArrayWithoutDistribution<string | number>;
// (string | number)[]Ця відмінність важлива під час створення складних conditional types.
infer: вилучення типівinfer дозволяє оголосити змінну типу всередині conditional type та вивести її значення.
Найпростіший приклад — отримання типу елементів масиву:
type ElementType<T> = T extends Array<infer Element>
? Element
: never;
type StringElement = ElementType<string[]>;
// string
type MixedElement = ElementType<Array<string | number>>;
// string | number
type NotAnArray = ElementType<boolean>;
// neverУмова:
T extends Array<infer Element>означає: якщо T є масивом, вивести тип його елементів у Element.
ReturnTypeЗа допомогою infer можна отримати тип, який повертає функція:
type MyReturnType<T> = T extends (...args: never[]) => infer Result
? Result
: never;
function createUser(name: string) {
return {
id: 1,
name,
active: true,
};
}
type CreatedUser = MyReturnType<typeof createUser>;
// {
// id: number;
// name: string;
// active: boolean;
// }typeof createUser отримує тип функції, а infer Result вилучає її тип результату.
Функцію можна передати як generic-параметр:
type UserResult = MyReturnType<() => {
id: number;
name: string;
}>;
// {
// id: number;
// name: string;
// }Аналогічно можна вилучити tuple параметрів функції:
type MyParameters<T> = T extends (
...args: infer Parameters
) => unknown
? Parameters
: never;
type SaveUser = MyParameters<
(name: string, age: number) => boolean
>;
// [name: string, age: number]infer Parameters знаходить tuple усіх параметрів.
PromiseСтворимо Utility Type для вилучення типу, який знаходиться всередині Promise:
type AwaitedValue<T> = T extends Promise<infer Value>
? Value
: T;
type UserResponse = AwaitedValue<Promise<{
id: number;
name: string;
}>>;
// {
// id: number;
// name: string;
// }
type SynchronousValue = AwaitedValue<string>;
// stringМожна підтримати вкладені Promise рекурсивно:
type DeepAwaited<T> = T extends Promise<infer Value>
? DeepAwaited<Value>
: T;
type Result = DeepAwaited<Promise<Promise<number>>>;
// numberСкладні типи часто створюють із кількох простіших кроків.
Наприклад, потрібно описати параметри оновлення користувача:
ідентифікатор має бути обов'язковим;
решта властивостей можуть бути відсутніми;
id не можна змінювати в об'єкті оновлення.
type User = {
id: number;
name: string;
email: string;
isActive: boolean;
};
type UpdateUser = {
id: User["id"];
} & Partial<Omit<User, "id">>;
const update: UpdateUser = {
id: 42,
email: "new-email@example.com",
};Цей приклад використовує стандартні типи, але ті самі принципи можна реалізувати власними Utility Types:
type MyOmit<T, Keys extends keyof T> = {
[Key in keyof T as Key extends Keys ? never : Key]: T[Key];
};
type MyPartial<T> = {
[Key in keyof T]?: T[Key];
};
type CustomUpdateUser = {
id: User["id"];
} & MyPartial<MyOmit<User, "id">>;
const customUpdate: CustomUpdateUser = {
id: 42,
name: "Ihor",
};Обмеження Keys extends keyof T гарантує, що до MyOmit можна передати лише існуючі ключі.
Наведений код можна зберегти у файл utility-types.ts і перевірити компілятором TypeScript:
type MyPartial<T> = {
[Key in keyof T]?: T[Key];
};
type MyRequired<T> = {
[Key in keyof T]-?: T[Key];
};
type MyReadonly<T> = {
readonly [Key in keyof T]: T[Key];
};
type KeysOfType<T, ValueType> = {
[Key in keyof T]-?: T[Key] extends ValueType ? Key : never;
}[keyof T];
type PickByValue<T, ValueType> = {
[Key in keyof T as T[Key] extends ValueType ? Key : never]: T[Key];
};
type ElementType<T> = T extends Array<infer Element>
? Element
: never;
type MyReturnType<T> = T extends (...args: never[]) => infer Result
? Result
: never;
type MyParameters<T> = T extends (
...args: infer Parameters
) => unknown
? Parameters
: never;
type DeepAwaited<T> = T extends Promise<infer Value>
? DeepAwaited<Value>
: T;
const roles = {
admin: "ADMIN",
editor: "EDITOR",
viewer: "VIEWER",
} as const;
type Role = typeof roles[keyof typeof roles];
type User = {
id: number;
name: string;
email: string;
role: Role;
active: boolean;
};
type UserUpdate = MyPartial<User>;
const userUpdate: UserUpdate = {
name: "Oksana",
active: true,
};
type UserNumberKeys = KeysOfType<User, number>;
// "id"
type UserTextFields = PickByValue<User, string>;
const textFields: UserTextFields = {
name: "Oksana",
email: "oksana@example.com",
};
function getUser(): Promise<{
id: number;
name: string;
}> {
return Promise.resolve({
id: 1,
name: "Oksana",
});
}
type UserPromiseResult = MyReturnType<typeof getUser>;
// Promise<{ id: number; name: string }>
type UserResult = DeepAwaited<UserPromiseResult>;
// { id: number; name: string }
type CreateUserParameters = MyParameters<
(name: string, role: Role) => User
>;
// [name: string, role: Role]
const createUserParameters: CreateUserParameters = [
"Taras",
"EDITOR",
];
console.log(userUpdate);
console.log(textFields);
console.log(createUserParameters);Для перевірки типів важливо запускати TypeScript із суворим режимом:
npx tsc utility-types.ts --strict --noEmitПрапорець --noEmit перевіряє код, але не створює JavaScript-файл.
T[K] без обмеження ключаНеправильно:
function getProperty<T, K>(object: T, key: K) {
return object[key];
}TypeScript не може гарантувати, що K є ключем T.
Правильно:
function getProperty<T, K extends keyof T>(
object: T,
key: K,
): T[K] {
return object[key];
}keyof T і значеннями властивостейtype User = {
id: number;
name: string;
};
type Keys = keyof User;
// "id" | "name"
type Values = User[keyof User];
// number | stringkeyof User повертає ключі, а User[keyof User] — типи значень.
Під час фільтрації ключів optional-властивості можуть впливати на результат перевірки. Якщо потрібні гарантовано обов'язкові властивості на проміжному етапі, використовуйте -?:
type KeysOfType<T, ValueType> = {
[Key in keyof T]-?: T[Key] extends ValueType ? Key : never;
}[keyof T];inferinfer працює лише всередині conditional type:
type Result<T> = T extends Promise<infer Value>
? Value
: never;Не можна оголосити infer як окремий універсальний тип поза умовою.
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[]Якщо потрібен один масив із union-типом, використовуйте tuple-обгортку:
type ToSingleArray<T> =
[T] extends [unknown] ? T[] : never;
type Result = ToSingleArray<string | number>;
// (string | number)[]Власний Utility Type має вирішувати конкретну проблему. Якщо тип стає важким для читання, розділіть його на кілька менших:
type NumberKeys<T> = KeysOfType<T, number>;
type NumberFields<T> = PickByValue<T, number>;Такі проміжні назви полегшують читання та діагностику помилок.
keyof T отримує union ключів типу T.
T[K] отримує тип властивості K.
typeof value у позиції типу отримує тип конкретного значення.
Mapped types перебирають ключі та створюють нову структуру.
?, -?, readonly і -readonly змінюють модифікатори властивостей.
Conditional types мають форму T extends U ? A : B.
Conditional types можуть розподілятися по union-типах.
Обгортка [T] вимикає такий розподіл.
infer дозволяє вилучати типи елементів масивів, параметрів функцій і результатів функцій.
Комбінування keyof, typeof, mapped types, conditional types та infer дає змогу створювати спеціалізовані Utility Types для доменної моделі застосунку.