Опануйте утилітарні типи TypeScript: інструменти для трансформації типів, що покращують повторне використання коду та підвищують типізаційну безпеку.
Утилітарні типи TypeScript: вбудовані інструменти для маніпуляції типами
TypeScript — це потужна мова, що додає статичну типізацію до JavaScript. Однією з її ключових особливостей є здатність маніпулювати типами, що дозволяє розробникам створювати більш надійний та легкий у підтримці код. TypeScript надає набір вбудованих утилітарних типів, які спрощують поширені трансформації типів. Ці утилітарні типи є неоціненними інструментами для підвищення безпеки типів, покращення повторного використання коду та оптимізації вашого робочого процесу розробки. Цей вичерпний посібник досліджує найважливіші утилітарні типи TypeScript, надаючи практичні приклади та дієві поради, щоб допомогти вам їх освоїти.
Що таке утилітарні типи TypeScript?
Утилітарні типи — це попередньо визначені оператори типів, які перетворюють існуючі типи на нові. Вони вбудовані в мову TypeScript і надають стислий та декларативний спосіб виконання поширених маніпуляцій з типами. Використання утилітарних типів може значно зменшити кількість шаблонного коду та зробити ваші визначення типів більш виразними та легкими для розуміння.
Думайте про них як про функції, що працюють з типами замість значень. Вони приймають тип на вхід і повертають змінений тип на вихід. Це дозволяє створювати складні відношення та трансформації типів з мінімальною кількістю коду.
Навіщо використовувати утилітарні типи?
Існує кілька вагомих причин для включення утилітарних типів у ваші проєкти на TypeScript:
- Підвищена безпека типів: Утилітарні типи допомагають вам застосовувати суворіші обмеження типів, зменшуючи ймовірність помилок під час виконання та покращуючи загальну надійність вашого коду.
- Покращене повторне використання коду: Використовуючи утилітарні типи, ви можете створювати загальні компоненти та функції, які працюють з різними типами, сприяючи повторному використанню коду та зменшуючи надлишковість.
- Зменшення шаблонного коду: Утилітарні типи надають стислий та декларативний спосіб виконання поширених трансформацій типів, зменшуючи кількість шаблонного коду, який вам потрібно писати.
- Покращена читабельність: Утилітарні типи роблять ваші визначення типів більш виразними та легкими для розуміння, покращуючи читабельність та підтримуваність вашого коду.
Основні утилітарні типи TypeScript
Давайте розглянемо деякі з найчастіше використовуваних та корисних утилітарних типів у TypeScript. Ми розглянемо їхнє призначення, синтаксис та надамо практичні приклади для ілюстрації їх використання.
1. Partial<T>
Утилітарний тип Partial<T>
робить усі властивості типу T
необов'язковими. Це корисно, коли ви хочете створити новий тип, який має деякі або всі властивості існуючого типу, але ви не хочете вимагати наявності всіх з них.
Синтаксис:
type Partial<T> = { [P in keyof T]?: T[P]; };
Приклад:
interface User {
id: number;
name: string;
email: string;
}
type OptionalUser = Partial<User>; // Усі властивості тепер необов'язкові
const partialUser: OptionalUser = {
name: "Alice", // Надаємо лише властивість name
};
Приклад використання: Оновлення об'єкта лише з певними властивостями. Наприклад, уявіть форму оновлення профілю користувача. Ви не хочете вимагати від користувачів оновлювати кожне поле одночасно.
2. Required<T>
Утилітарний тип Required<T>
робить усі властивості типу T
обов'язковими. Це протилежність Partial<T>
. Це корисно, коли у вас є тип з необов'язковими властивостями, і ви хочете переконатися, що всі властивості присутні.
Синтаксис:
type Required<T> = { [P in keyof T]-?: T[P]; };
Приклад:
interface Config {
apiKey?: string;
apiUrl?: string;
}
type CompleteConfig = Required<Config>; // Усі властивості тепер обов'язкові
const config: CompleteConfig = {
apiKey: "your-api-key",
apiUrl: "https://example.com/api",
};
Приклад використання: Забезпечення того, що всі налаштування конфігурації надані перед запуском програми. Це може допомогти запобігти помилкам під час виконання, спричиненим відсутніми або невизначеними налаштуваннями.
3. Readonly<T>
Утилітарний тип Readonly<T>
робить усі властивості типу T
доступними лише для читання. Це запобігає випадковій зміні властивостей об'єкта після його створення. Це сприяє незмінності та покращує передбачуваність вашого коду.
Синтаксис:
type Readonly<T> = { readonly [P in keyof T]: T[P]; };
Приклад:
interface Product {
id: number;
name: string;
price: number;
}
type ImmutableProduct = Readonly<Product>; // Усі властивості тепер доступні лише для читання
const product: ImmutableProduct = {
id: 123,
name: "Example Product",
price: 25.99,
};
// product.price = 29.99; // Помилка: Неможливо призначити значення 'price', оскільки це властивість лише для читання.
Приклад використання: Створення незмінних структур даних, таких як об'єкти конфігурації або об'єкти передачі даних (DTO), які не повинні змінюватися після створення. Це особливо корисно в парадигмах функціонального програмування.
4. Pick<T, K extends keyof T>
Утилітарний тип Pick<T, K extends keyof T>
створює новий тип, вибираючи набір властивостей K
з типу T
. Це корисно, коли вам потрібна лише підмножина властивостей існуючого типу.
Синтаксис:
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
Приклад:
interface Employee {
id: number;
name: string;
department: string;
salary: number;
}
type EmployeeNameAndDepartment = Pick<Employee, "name" | "department">; // Вибираємо лише name та department
const employeeInfo: EmployeeNameAndDepartment = {
name: "Bob",
department: "Engineering",
};
Приклад використання: Створення спеціалізованих об'єктів передачі даних (DTO), які містять лише необхідні дані для певної операції. Це може покращити продуктивність та зменшити кількість даних, що передаються по мережі. Уявіть, що ви надсилаєте деталі користувача клієнту, але виключаєте конфіденційну інформацію, таку як зарплата. Ви можете використати Pick, щоб надіслати лише `id` та `name`.
5. Omit<T, K extends keyof any>
Утилітарний тип Omit<T, K extends keyof any>
створює новий тип, виключаючи набір властивостей K
з типу T
. Це протилежність Pick<T, K extends keyof T>
і корисно, коли ви хочете виключити певні властивості з існуючого типу.
Синтаксис:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Приклад:
interface Event {
id: number;
title: string;
description: string;
date: Date;
location: string;
}
type EventSummary = Omit<Event, "description" | "location">; // Виключаємо description та location
const eventPreview: EventSummary = {
id: 1,
title: "Conference",
date: new Date(),
};
Приклад використання: Створення спрощених версій моделей даних для конкретних цілей, наприклад, для відображення короткого опису події без повного опису та місця проведення. Це також можна використовувати для видалення конфіденційних полів перед надсиланням даних клієнту.
6. Exclude<T, U>
Утилітарний тип Exclude<T, U>
створює новий тип, виключаючи з T
усі типи, які можна призначити U
. Це корисно, коли ви хочете видалити певні типи з типу-об'єднання.
Синтаксис:
type Exclude<T, U> = T extends U ? never : T;
Приклад:
type AllowedFileTypes = "image" | "video" | "audio" | "document";
type MediaFileTypes = "image" | "video" | "audio";
type DocumentFileTypes = Exclude<AllowedFileTypes, MediaFileTypes>; // "document"
const fileType: DocumentFileTypes = "document";
Приклад використання: Фільтрація типу-об'єднання для видалення конкретних типів, які не є релевантними в певному контексті. Наприклад, ви можете захотіти виключити певні типи файлів зі списку дозволених типів файлів.
7. Extract<T, U>
Утилітарний тип Extract<T, U>
створює новий тип, витягуючи з T
усі типи, які можна призначити U
. Це протилежність Exclude<T, U>
і корисно, коли ви хочете вибрати конкретні типи з типу-об'єднання.
Синтаксис:
type Extract<T, U> = T extends U ? T : never;
Приклад:
type InputTypes = string | number | boolean | null | undefined;
type PrimitiveTypes = string | number | boolean;
type NonNullablePrimitives = Extract<InputTypes, PrimitiveTypes>; // string | number | boolean
const value: NonNullablePrimitives = "hello";
Приклад використання: Вибір конкретних типів з типу-об'єднання на основі певних критеріїв. Наприклад, ви можете захотіти витягти всі примітивні типи з типу-об'єднання, яке включає як примітивні, так і об'єктні типи.
8. NonNullable<T>
Утилітарний тип NonNullable<T>
створює новий тип, виключаючи null
та undefined
з типу T
. Це корисно, коли ви хочете переконатися, що тип не може бути null
або undefined
.
Синтаксис:
type NonNullable<T> = T extends null | undefined ? never : T;
Приклад:
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>; // string
const message: DefinitelyString = "Hello, world!";
Приклад використання: Забезпечення того, що значення не є null
або undefined
перед виконанням операції над ним. Це може допомогти запобігти помилкам під час виконання, спричиненим несподіваними значеннями null або undefined. Розглянемо сценарій, коли вам потрібно обробити адресу користувача, і вкрай важливо, щоб адреса не була null перед будь-якою операцією.
9. ReturnType<T extends (...args: any) => any>
Утилітарний тип ReturnType<T extends (...args: any) => any>
витягує тип значення, що повертається функцією типу T
. Це корисно, коли ви хочете знати тип значення, яке повертає функція.
Синтаксис:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
Приклад:
function fetchData(url: string): Promise<{ data: any }> {
return fetch(url).then(response => response.json());
}
type FetchDataReturnType = ReturnType<typeof fetchData>; // Promise<{ data: any }>
async function processData(data: FetchDataReturnType) {
// ...
}
Приклад використання: Визначення типу значення, що повертається функцією, особливо при роботі з асинхронними операціями або складними сигнатурами функцій. Це дозволяє переконатися, що ви правильно обробляєте повернуте значення.
10. Parameters<T extends (...args: any) => any>
Утилітарний тип Parameters<T extends (...args: any) => any>
витягує типи параметрів функції типу T
у вигляді кортежу. Це корисно, коли ви хочете знати типи аргументів, які приймає функція.
Синтаксис:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
Приклад:
function createUser(name: string, age: number, email: string): void {
// ...
}
type CreateUserParams = Parameters<typeof createUser>; // [string, number, string]
function logUser(...args: CreateUserParams) {
console.log("Creating user with:", args);
}
Приклад використання: Визначення типів аргументів, які приймає функція, що може бути корисним для створення загальних функцій або декораторів, які повинні працювати з функціями різних сигнатур. Це допомагає забезпечити безпеку типів при динамічній передачі аргументів у функцію.
11. ConstructorParameters<T extends abstract new (...args: any) => any>
Утилітарний тип ConstructorParameters<T extends abstract new (...args: any) => any>
витягує типи параметрів функції-конструктора типу T
у вигляді кортежу. Це корисно, коли ви хочете знати типи аргументів, які приймає конструктор.
Синтаксис:
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
Приклад:
class Logger {
constructor(public prefix: string, public enabled: boolean) {}
log(message: string) {
if (this.enabled) {
console.log(`${this.prefix}: ${message}`);
}
}
}
type LoggerConstructorParams = ConstructorParameters<typeof Logger>; // [string, boolean]
function createLogger(...args: LoggerConstructorParams) {
return new Logger(...args);
}
Приклад використання: Аналогічно до Parameters
, але спеціально для функцій-конструкторів. Це допомагає при створенні фабрик або систем впровадження залежностей, де вам потрібно динамічно створювати екземпляри класів з різними сигнатурами конструкторів.
12. InstanceType<T extends abstract new (...args: any) => any>
Утилітарний тип InstanceType<T extends abstract new (...args: any) => any>
витягує тип екземпляра функції-конструктора типу T
. Це корисно, коли ви хочете знати тип об'єкта, який створює конструктор.
Синтаксис:
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
Приклад:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
type GreeterInstance = InstanceType<typeof Greeter>; // Greeter
const myGreeter: GreeterInstance = new Greeter("World");
console.log(myGreeter.greet());
Приклад використання: Визначення типу об'єкта, створеного конструктором, що є корисним при роботі з успадкуванням або поліморфізмом. Це надає типобезпечний спосіб посилання на екземпляр класу.
13. Record<K extends keyof any, T>
Утилітарний тип Record<K extends keyof any, T>
створює тип об'єкта, ключами властивостей якого є K
, а значеннями властивостей — T
. Це корисно для створення словникоподібних типів, де ви знаєте ключі заздалегідь.
Синтаксис:
type Record<K extends keyof any, T> = { [P in K]: T; };
Приклад:
type CountryCode = "US" | "CA" | "GB" | "DE";
type CurrencyMap = Record<CountryCode, string>; // { US: string; CA: string; GB: string; DE: string; }
const currencies: CurrencyMap = {
US: "USD",
CA: "CAD",
GB: "GBP",
DE: "EUR",
};
Приклад використання: Створення словникоподібних об'єктів, де у вас є фіксований набір ключів і ви хочете переконатися, що всі ключі мають значення певного типу. Це часто зустрічається при роботі з конфігураційними файлами, мапуванням даних або таблицями пошуку.
Власні утилітарні типи
Хоча вбудовані утилітарні типи TypeScript є потужними, ви також можете створювати власні утилітарні типи для вирішення конкретних потреб у ваших проєктах. Це дозволяє інкапсулювати складні трансформації типів та повторно використовувати їх у вашій кодовій базі.
Приклад:
// Утилітарний тип для отримання ключів об'єкта, які мають певний тип
type KeysOfType<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
interface Person {
name: string;
age: number;
address: string;
phoneNumber: number;
}
type StringKeys = KeysOfType<Person, string>; // "name" | "address"
Найкращі практики використання утилітарних типів
- Використовуйте описові імена: Давайте вашим утилітарним типам значущі імена, які чітко вказують на їхнє призначення. Це покращує читабельність та підтримуваність вашого коду.
- Документуйте ваші утилітарні типи: Додавайте коментарі, щоб пояснити, що роблять ваші утилітарні типи та як їх слід використовувати. Це допомагає іншим розробникам зрозуміти ваш код і правильно його використовувати.
- Будьте простішими: Уникайте створення надто складних утилітарних типів, які важко зрозуміти. Розбивайте складні трансформації на менші, більш керовані утилітарні типи.
- Тестуйте ваші утилітарні типи: Пишіть юніт-тести, щоб переконатися, що ваші утилітарні типи працюють коректно. Це допомагає запобігти несподіваним помилкам і гарантує, що ваші типи поводяться так, як очікувалося.
- Враховуйте продуктивність: Хоча утилітарні типи зазвичай не мають значного впливу на продуктивність, пам'ятайте про складність ваших трансформацій типів, особливо у великих проєктах.
Висновок
Утилітарні типи TypeScript — це потужні інструменти, які можуть значно покращити безпеку типів, повторне використання та підтримуваність вашого коду. Освоївши ці утилітарні типи, ви зможете писати більш надійні та виразні додатки на TypeScript. Цей посібник охопив найважливіші утилітарні типи TypeScript, надавши практичні приклади та дієві поради, щоб допомогти вам включити їх у ваші проєкти.
Не забувайте експериментувати з цими утилітарними типами та досліджувати, як їх можна використовувати для вирішення конкретних проблем у вашому власному коді. Коли ви станете більш знайомими з ними, ви будете використовувати їх все частіше для створення чистіших, легших у підтримці та більш типобезпечних додатків на TypeScript. Незалежно від того, чи ви створюєте вебдодатки, серверні додатки чи щось інше, утилітарні типи надають цінний набір інструментів для покращення вашого робочого процесу розробки та якості вашого коду. Використовуючи ці вбудовані інструменти для маніпуляції типами, ви можете розкрити весь потенціал TypeScript і писати код, який є одночасно виразним і надійним.