Розкрийте можливості службових типів TypeScript для написання чистішого, надійнішого та типобезпечного коду. Розгляньте практичні застосування на реальних прикладах для розробників у всьому світі.
Опановуємо службові типи TypeScript: практичний посібник для розробників з усього світу
TypeScript пропонує потужний набір вбудованих службових типів, які можуть значно покращити типобезпечність, читабельність та підтримку вашого коду. Ці службові типи є, по суті, попередньо визначеними перетвореннями типів, які ви можете застосовувати до існуючих типів, заощаджуючи час на написання повторюваного та схильного до помилок коду. У цьому посібнику ми розглянемо різноманітні службові типи з практичними прикладами, що будуть корисними для розробників у всьому світі.
Навіщо використовувати службові типи?
Службові типи вирішують поширені сценарії маніпуляції типами. Використовуючи їх, ви можете:
- Зменшити кількість шаблонного коду: Уникайте написання повторюваних визначень типів.
- Покращити типобезпечність: Переконайтеся, що ваш код відповідає обмеженням типів.
- Поліпшити читабельність коду: Зробіть ваші визначення типів більш стислими та легкими для розуміння.
- Підвищити зручність підтримки: Спрощуйте модифікації та зменшуйте ризик виникнення помилок.
Основні службові типи
Partial<T>
Partial<T>
створює тип, у якому всі властивості T
стають необов'язковими. Це особливо корисно, коли ви хочете створити тип для часткових оновлень або об'єктів конфігурації.
Приклад:
Уявіть, що ви створюєте платформу електронної комерції з клієнтами з різних регіонів. У вас є тип Customer
:
interface Customer {
id: string;
firstName: string;
lastName: string;
email: string;
phoneNumber: string;
address: {
street: string;
city: string;
country: string;
postalCode: string;
};
preferences?: {
language: string;
currency: string;
}
}
При оновленні інформації про клієнта ви, можливо, не захочете вимагати заповнення всіх полів. Partial<Customer>
дозволяє визначити тип, у якому всі властивості Customer
є необов'язковими:
type PartialCustomer = Partial<Customer>;
function updateCustomer(id: string, updates: PartialCustomer): void {
// ... реалізація для оновлення клієнта з вказаним ID
}
updateCustomer("123", { firstName: "John", lastName: "Doe" }); // Допустимо
updateCustomer("456", { address: { city: "London" } }); // Допустимо
Readonly<T>
Readonly<T>
створює тип, у якому всі властивості T
стають readonly
(тільки для читання), що запобігає їх модифікації після ініціалізації. Це цінно для забезпечення незмінності (immutability).
Приклад:
Розглянемо об'єкт конфігурації для вашого глобального застосунку:
interface AppConfig {
apiUrl: string;
theme: string;
supportedLanguages: string[];
version: string; // Додано версію
}
const config: AppConfig = {
apiUrl: "https://api.example.com",
theme: "dark",
supportedLanguages: ["en", "fr", "de", "es", "zh"],
version: "1.0.0"
};
Щоб запобігти випадковій зміні конфігурації після ініціалізації, ви можете використовувати Readonly<AppConfig>
:
type ReadonlyAppConfig = Readonly<AppConfig>;
const readonlyConfig: ReadonlyAppConfig = {
apiUrl: "https://api.example.com",
theme: "dark",
supportedLanguages: ["en", "fr", "de", "es", "zh"],
version: "1.0.0"
};
// readonlyConfig.apiUrl = "https://newapi.example.com"; // Помилка: Неможливо присвоїти значення 'apiUrl', оскільки це властивість тільки для читання.
Pick<T, K>
Pick<T, K>
створює тип, вибираючи набір властивостей K
з типу T
, де K
— це об'єднання рядкових літеральних типів, що представляють імена властивостей, які ви хочете включити.
Приклад:
Припустимо, у вас є інтерфейс Event
з різними властивостями:
interface Event {
id: string;
title: string;
description: string;
location: string;
startTime: Date;
endTime: Date;
organizer: string;
attendees: string[];
}
Якщо для певного компонента відображення вам потрібні лише title
, location
та startTime
, ви можете використати Pick
:
type EventSummary = Pick<Event, "title" | "location" | "startTime">;
function displayEventSummary(event: EventSummary): void {
console.log(`Event: ${event.title} at ${event.location} on ${event.startTime}`);
}
Omit<T, K>
Omit<T, K>
створює тип, виключаючи набір властивостей K
з типу T
, де K
— це об'єднання рядкових літеральних типів, що представляють імена властивостей, які ви хочете виключити. Це протилежність Pick
.
Приклад:
Використовуючи той самий інтерфейс Event
, якщо ви хочете створити тип для створення нових подій, ви можете виключити властивість id
, яка зазвичай генерується на бекенді:
type NewEvent = Omit<Event, "id">;
function createEvent(event: NewEvent): void {
// ... реалізація для створення нової події
}
Record<K, T>
Record<K, T>
створює тип об'єкта, ключами властивостей якого є K
, а значеннями властивостей — T
. K
може бути об'єднанням рядкових літеральних типів, числових літеральних типів або символом. Це ідеально підходить для створення словників або карт (maps).
Приклад:
Уявіть, що вам потрібно зберігати переклади для інтерфейсу користувача вашого застосунку. Ви можете використовувати Record
для визначення типу для ваших перекладів:
type Translations = Record<string, string>;
const enTranslations: Translations = {
"hello": "Hello",
"goodbye": "Goodbye",
"welcome": "Welcome to our platform!"
};
const frTranslations: Translations = {
"hello": "Bonjour",
"goodbye": "Au revoir",
"welcome": "Bienvenue sur notre plateforme !"
};
function translate(key: string, language: string): string {
const translations = language === "en" ? enTranslations : frTranslations; //Спрощено
return translations[key] || key; // Повертаємо ключ, якщо переклад не знайдено
}
console.log(translate("hello", "en")); // Output: Hello
console.log(translate("hello", "fr")); // Output: Bonjour
console.log(translate("nonexistent", "en")); // Output: nonexistent
Exclude<T, U>
Exclude<T, U>
створює тип, виключаючи з T
усі члени об'єднання, які можна присвоїти U
. Це корисно для фільтрації певних типів з об'єднання.
Приклад:
У вас може бути тип, що представляє різні типи подій:
type EventType = "concert" | "conference" | "workshop" | "webinar";
Якщо ви хочете створити тип, що виключає події типу "webinar", ви можете використати Exclude
:
type PhysicalEvent = Exclude<EventType, "webinar">;
// Тепер PhysicalEvent - це "concert" | "conference" | "workshop"
function attendPhysicalEvent(event: PhysicalEvent): void {
console.log(`Attending a ${event}`);
}
// attendPhysicalEvent("webinar"); // Помилка: Аргумент типу '"webinar"' не може бути присвоєний параметру типу '"concert" | "conference" | "workshop"'.
attendPhysicalEvent("concert"); // Допустимо
Extract<T, U>
Extract<T, U>
створює тип, витягуючи з T
усі члени об'єднання, які можна присвоїти U
. Це протилежність Exclude
.
Приклад:
Використовуючи той самий EventType
, ви можете витягти тип події "webinar":
type OnlineEvent = Extract<EventType, "webinar">;
// Тепер OnlineEvent - це "webinar"
function attendOnlineEvent(event: OnlineEvent): void {
console.log(`Attending a ${event} online`);
}
attendOnlineEvent("webinar"); // Допустимо
// attendOnlineEvent("concert"); // Помилка: Аргумент типу '"concert"' не може бути присвоєний параметру типу '"webinar"'.
NonNullable<T>
NonNullable<T>
створює тип, виключаючи null
та undefined
з T
.
Приклад:
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// Тепер DefinitelyString - це string
function processString(str: DefinitelyString): void {
console.log(str.toUpperCase());
}
// processString(null); // Помилка: Аргумент типу 'null' не може бути присвоєний параметру типу 'string'.
// processString(undefined); // Помилка: Аргумент типу 'undefined' не може бути присвоєний параметру типу 'string'.
processString("hello"); // Допустимо
ReturnType<T>
ReturnType<T>
створює тип, що складається з типу повернення функції T
.
Приклад:
function greet(name: string): string {
return `Hello, ${name}!`;
}
type Greeting = ReturnType<typeof greet>;
// Тепер Greeting - це string
const message: Greeting = greet("World");
console.log(message);
Parameters<T>
Parameters<T>
створює тип кортежу з типів параметрів функціонального типу T
.
Приклад:
function logEvent(eventName: string, eventData: object): void {
console.log(`Event: ${eventName}`, eventData);
}
type LogEventParams = Parameters<typeof logEvent>;
// Тепер LogEventParams - це [eventName: string, eventData: object]
const params: LogEventParams = ["user_login", { userId: "123", timestamp: Date.now() }];
logEvent(...params);
ConstructorParameters<T>
ConstructorParameters<T>
створює тип кортежу або масиву з типів параметрів конструктора типу T
. Він виводить типи аргументів, які необхідно передати в конструктор класу.
Приклад:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
type GreeterParams = ConstructorParameters<typeof Greeter>;
// Тепер GreeterParams - це [message: string]
const paramsGreeter: GreeterParams = ["World"];
const greeterInstance = new Greeter(...paramsGreeter);
console.log(greeterInstance.greet()); // Outputs: Hello, World
Required<T>
Required<T>
створює тип, що складається з усіх властивостей T
, встановлених як обов'язкові. Він робить усі необов'язкові властивості обов'язковими.
Приклад:
interface UserProfile {
name: string;
age?: number;
email?: string;
}
type RequiredUserProfile = Required<UserProfile>;
// Тепер RequiredUserProfile - це { name: string; age: number; email: string; }
const completeProfile: RequiredUserProfile = {
name: "Alice",
age: 30,
email: "alice@example.com"
};
// const incompleteProfile: RequiredUserProfile = { name: "Bob" }; // Помилка: Властивість 'age' відсутня в типі '{ name: string; }', але є обов'язковою в типі 'Required'.
Просунуті службові типи
Шаблонні літеральні типи
Шаблонні літеральні типи дозволяють створювати нові рядкові літеральні типи шляхом об'єднання існуючих рядкових літеральних типів, числових літеральних типів та інших. Це уможливлює потужні маніпуляції з типами на основі рядків.
Приклад:
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/users` | `/api/products`;
type RequestURL = `${HTTPMethod} ${APIEndpoint}`;
// Тепер RequestURL - це "GET /api/users" | "POST /api/users" | "PUT /api/users" | "DELETE /api/users" | "GET /api/products" | "POST /api/products" | "PUT /api/products" | "DELETE /api/products"
function makeRequest(url: RequestURL): void {
console.log(`Making request to ${url}`);
}
makeRequest("GET /api/users"); // Допустимо
// makeRequest("INVALID /api/users"); // Помилка
Умовні типи
Умовні типи дозволяють визначати типи, що залежать від умови, вираженої як відношення типів. Вони використовують ключове слово infer
для вилучення інформації про тип.
Приклад:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// Якщо T є Promise, то тип - U; інакше, тип - T.
async function fetchData(): Promise<number> {
return 42;
}
type Data = UnwrapPromise<ReturnType<typeof fetchData>>;
// Тепер Data - це number
function processData(data: Data): void {
console.log(data * 2);
}
processData(await fetchData());
Практичні застосування та реальні сценарії
Давайте розглянемо складніші реальні сценарії, де службові типи проявляють себе найкраще.
1. Обробка форм
При роботі з формами часто виникають сценарії, коли потрібно представити початкові значення форми, оновлені значення та остаточні надіслані значення. Службові типи можуть допомогти вам ефективно керувати цими різними станами.
interface FormData {
firstName: string;
lastName: string;
email: string;
country: string; // Обов'язково
city?: string; // Необов'язково
postalCode?: string;
newsletterSubscription?: boolean;
}
// Початкові значення форми (необов'язкові поля)
type InitialFormValues = Partial<FormData>;
// Оновлені значення форми (деякі поля можуть бути відсутні)
type UpdatedFormValues = Partial<FormData>;
// Обов'язкові поля для відправки
type RequiredForSubmission = Required<Pick<FormData, 'firstName' | 'lastName' | 'email' | 'country'>>;
// Використовуйте ці типи у ваших компонентах форм
function initializeForm(initialValues: InitialFormValues): void { }
function updateForm(updates: UpdatedFormValues): void {}
function submitForm(data: RequiredForSubmission): void {}
const initialForm: InitialFormValues = { newsletterSubscription: true };
const updateFormValues: UpdatedFormValues = {
firstName: "John",
lastName: "Doe"
};
// const submissionData: RequiredForSubmission = { firstName: "test", lastName: "test", email: "test" }; // ПОМИЛКА: Відсутнє 'country'
const submissionData: RequiredForSubmission = { firstName: "test", lastName: "test", email: "test", country: "USA" }; //OK
2. Трансформація даних API
При отриманні даних з API вам може знадобитися перетворити дані в інший формат для вашого застосунку. Службові типи можуть допомогти вам визначити структуру перетворених даних.
interface APIResponse {
user_id: string;
first_name: string;
last_name: string;
email_address: string;
profile_picture_url: string;
is_active: boolean;
}
// Перетворюємо відповідь API у більш читабельний формат
type UserData = {
id: string;
fullName: string;
email: string;
avatar: string;
active: boolean;
};
function transformApiResponse(response: APIResponse): UserData {
return {
id: response.user_id,
fullName: `${response.first_name} ${response.last_name}`,
email: response.email_address,
avatar: response.profile_picture_url,
active: response.is_active
};
}
function fetchAndTransformData(url: string): Promise<UserData> {
return fetch(url)
.then(response => response.json())
.then(data => transformApiResponse(data));
}
// Ви навіть можете примусово встановити тип:
function saferTransformApiResponse(response: APIResponse): UserData {
const {user_id, first_name, last_name, email_address, profile_picture_url, is_active} = response;
const transformed: UserData = {
id: user_id,
fullName: `${first_name} ${last_name}`,
email: email_address,
avatar: profile_picture_url,
active: is_active
};
return transformed;
}
3. Робота з об'єктами конфігурації
Об'єкти конфігурації поширені в багатьох застосунках. Службові типи можуть допомогти вам визначити структуру об'єкта конфігурації та забезпечити його правильне використання.
interface AppSettings {
theme: "light" | "dark";
language: string;
notificationsEnabled: boolean;
apiUrl?: string; // Необов'язкова URL-адреса API для різних середовищ
timeout?: number; //Необов'язково
}
// Налаштування за замовчуванням
const defaultSettings: AppSettings = {
theme: "light",
language: "en",
notificationsEnabled: true
};
// Функція для об'єднання налаштувань користувача з налаштуваннями за замовчуванням
function mergeSettings(userSettings: Partial<AppSettings>): AppSettings {
return { ...defaultSettings, ...userSettings };
}
// Використовуйте об'єднані налаштування у вашому застосунку
const mergedSettings = mergeSettings({ theme: "dark", apiUrl: "https://customapi.example.com" });
console.log(mergedSettings);
Поради для ефективного використання службових типів
- Починайте з простого: Почніть з базових службових типів, таких як
Partial
таReadonly
, перш ніж переходити до складніших. - Використовуйте описові імена: Надавайте псевдонімам типів змістовні імена для покращення читабельності.
- Комбінуйте службові типи: Ви можете комбінувати кілька службових типів для досягнення складних перетворень типів.
- Використовуйте підтримку редактора: Скористайтеся чудовою підтримкою редактора для TypeScript, щоб дослідити ефекти службових типів.
- Розумійте основні концепції: Глибоке розуміння системи типів TypeScript є важливим для ефективного використання службових типів.
Висновок
Службові типи TypeScript — це потужні інструменти, які можуть значно покращити якість та підтримку вашого коду. Розуміючи та ефективно застосовуючи ці службові типи, ви можете писати чистіші, більш типобезпечні та надійніші застосунки, що відповідають вимогам глобального ландшафту розробки. Цей посібник надав вичерпний огляд поширених службових типів та практичних прикладів. Експериментуйте з ними та досліджуйте їхній потенціал для покращення ваших проєктів на TypeScript. Не забувайте надавати пріоритет читабельності та ясності при використанні службових типів, і завжди прагніть писати код, який легко зрозуміти та підтримувати, незалежно від того, де знаходяться ваші колеги-розробники.