Русский

Освойте утилитарные типы 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. Это полезно, когда вы хотите удалить определённые типы из объединения типов (union type).

Синтаксис:

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 в виде кортежа (tuple). Это полезно, когда вы хотите знать типы аргументов, которые принимает функция.

Синтаксис:

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 и писать код, который является одновременно выразительным и надёжным.