Лучшие практики TypeScript: Строгая типизация, гибкость и производительность

от автора

TypeScript давно стал неотъемлемой частью современного фронтенда, но чтобы действительно раскрыть его возможности и избежать подводных камней, важен опыт и осознанное применение его возможностей. В этой статье мы рассмотрим углубленные практики работы с TypeScript, которые могут улучшить производительность и читаемость кода в крупных проектах.

Использование строгих типов для межсервисных взаимодействий

При работе с большими приложениями, которые включают в себя несколько микросервисов, важно обеспечивать строгую типизацию для данных, передаваемых между сервисами.

  • Рекомендация: Используйте типы не только в клиентском коде, но и в API-контрактах, таких как REST и GraphQL.

  • Кейс: В крупном проекте для онлайн-торговли каждый микросервис (например, для обработки заказов и управления товаром) взаимодействует через API. Использование строгих типов помогает убедиться, что данные, передаваемые между сервисами, соответствуют заранее установленным контрактам. Например, если один сервис отправляет данные о заказах, другой может сразу понять, что его поля будут точно такими, как ожидалось, предотвращая ошибки и улучшая стабильность системы.

Пример:

// Тип данных, который передает API export interface UserData {   id: string;   name: string;   email: string; }  // Тип для ответа с сервера export interface ApiResponse<T> {   data: T;   error: string | null; }

Повышение производительности с помощью const assertions

С помощью const assertions можно добиться того, чтобы TypeScript трактовал некоторые типы как неизменяемые, что позволяет избежать лишних проверок и повышает производительность.

  • Рекомендация: Используйте as const для работы с массивами и объектами, если значения в них не изменяются. Это помогает TypeScript правильно типизировать такие данные и улучшает производительность.

  • Кейс: В крупном проекте по управлению правами пользователей используется массив разрешений. Благодаря as const, который явно указывает, что значения в массиве фиксированы, мы можем точно определить типы этих разрешений, избежать ошибок при присвоении и повысить производительность, так как TypeScript будет уверен в неизменности этих значений.

Пример:

const roles = ['admin', 'user', 'guest'] as const;  type Role = typeof roles[number]; // "admin" | "user" | "guest"

Использование типов в тестах и моках для улучшения покрытия

Тестирование в TypeScript можно значительно улучшить, если чётко определять типы данных для моков и заглушек. Это не только улучшит покрытие, но и повысит точность тестов.

  • Рекомендация: Типизируйте мок-объекты и ответы на запросы, чтобы тесты были не только актуальными, но и стабильными.

  • Кейс: В проекте для управления заказами моки используются для имитации ответов сервера в юнит-тестах. Благодаря типизации этих моков тесты становятся более надёжными, так как проверяется не только структура возвращаемых данных, но и соответствие типам, что помогает предотвратить ошибки на ранних этапах разработки.

Пример:

// Мок для API-запроса const mockApiResponse: ApiResponse<UserData> = {   data: { id: "123", name: "John Doe", email: "john.doe@example.com" },   error: null };

Использование шаблонных строк и литеральных типов для улучшения читаемости

Типы, построенные на основе шаблонных строк и литеральных типов, могут значительно улучшить читаемость кода и предоставить дополнительные гарантии на этапе компиляции.

  • Рекомендация: Используйте литеральные типы и шаблонные строки для создания более предсказуемых типов данных в местах, где требуется точная валидация строк.

  • Кейс: В системе обработки заказов литеральные типы могут быть использованы для указания статуса заказа (например, «pending», «shipped», «delivered»). Это гарантирует, что статус будет всегда валиден, и предотвращает ошибочные строки вроде «shippeded» или «shippd».

Пример:

type QueryMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';  function sendRequest(method: QueryMethod, url: string) {   console.log(`${method} request to ${url}`); }

Декораторы и метаданные для продвинутых паттернов проектирования

Декораторы и метаданные позволяют создавать более элегантные и модульные решения, особенно если речь идет о фреймворках и паттернах, таких как DI (dependency injection) и AOP (aspect-oriented programming).

  • Рекомендация: Используйте декораторы для реализации дополнительных функциональностей в объектах или классах без нарушения принципа SOLID.

  • Кейс: В системе обработки заказов литеральные типы могут быть использованы для указания статуса заказа (например, «pending», «shipped», «delivered»). Это гарантирует, что статус будет всегда валиден, и предотвращает ошибочные строки вроде «shippeded» или «shippd».

Пример:

function logMethod(target: any, propertyName: string, descriptor: PropertyDescriptor) {   const originalMethod = descriptor.value;   descriptor.value = function(...args: any[]) {     console.log(`Method ${propertyName} called with args: ${args}`);     return originalMethod.apply(this, args);   }; }  class UserService {   @logMethod   fetchUserData(id: number) {     return `User data for ${id}`;   } }

Использование дженериков для повышения гибкости и типобезопасности

Дженерики позволяют создавать универсальные функции и классы, которые могут работать с различными типами данных, при этом сохраняя типобезопасность.

  • Рекомендация: Используйте дженерики для работы с разными типами данных в одной функции или классе, не теряя точности типизации.

  • Кейс: В проекте для работы с различными типами пользователей (например, для обычных пользователей и администраторов) используется дженерик для универсализации функции получения данных. Это позволяет обрабатывать как одного пользователя, так и всех, при этом сохраняя типобезопасность и гибкость функции.

Пример:

function identity<T>(value: T): T {   return value; }  const stringValue = identity("Hello, world!"); // string const numberValue = identity(42); // number 

Дженерики полезны, например, при работе с коллекциями или утилитами, где необходимо работать с различными типами данных, но при этом обеспечивать консистентность типов.

Использование утилитарных типов для сокращения шаблонного кода

TypeScript включает множество утилитарных типов, которые помогают делать код более чистым и компактным, автоматически типизируя часто используемые структуры.

  • Рекомендация: Используйте утилитарные типы, такие как Partial, Readonly, Record, Pick, Exclude и другие, чтобы сокращать повторяющийся код и повысить читаемость.

  • Кейс: В проекте по управлению пользователями утилитарный тип Partial используется для обновления только отдельных полей пользователя, не меняя всю структуру данных. Это позволяет работать с объектами, где обновляются только нужные свойства, что уменьшает вероятность ошибок и упрощает логику обновлений.

Пример:

interface User {   id: number;   name: string;   email: string; }  type UserWithPartialName = Partial<User>; // Все свойства могут быть неопределёнными  const user: UserWithPartialName = { id: 1 }; // Валидно, т.к. name и email не обязательны  type UserWithoutEmail = Omit<User, 'email'>; // Исключаем email const user2: UserWithoutEmail = { id: 2, name: 'John Doe' }; 

Типизация замыкания и функций высшего порядка

Когда вы работаете с функциями, которые принимают другие функции в качестве аргументов или возвращают их, правильная типизация помогает избежать неочевидных ошибок.

  • Рекомендация: Явно типизируйте функции и их параметры в таких случаях, чтобы TypeScript мог правильно проверять корректность их использования.

  • Кейс: В проекте для кэширования данных используется функция высшего порядка, которая оборачивает исходные функции, чтобы запоминать результаты выполнения. Явная типизация помогает удостовериться, что типы аргументов и возвращаемых значений сохраняются правильными при каждой функции, которая проходит через кэширование.

Пример:

function memoize<T>(fn: (...args: any[]) => T): (...args: any[]) => T {   const cache = new Map<string, T>();   return (...args: any[]) => {     const key = JSON.stringify(args);     if (cache.has(key)) {       return cache.get(key) as T;     }     const result = fn(...args);     cache.set(key, result);     return result;   }; }

Использование шаблонных типов и conditional types

Гибкие и мощные возможности TypeScript, такие как условные типы и шаблонные типы, позволяют создавать очень точные типы для сложных кейсов.

  • Рекомендация: Используйте условные типы и шаблонные типы для реализации гибкой и мощной типизации в приложении.

  • Кейс: В проекте для управления заказами используется тип, который зависит от состояния заказа. Например, если заказ был оплачен, его тип может включать дополнительную информацию о платеже, а если заказ находится в обработке, тип будет содержать информацию о логистике. С помощью conditional types мы можем динамически изменять тип в зависимости от состояния. Шаблонные типы также используются для создания универсальных структур данных, таких как типы для обработки различных видов платежей в системе, где каждый вид оплаты требует свой уникальный набор параметров.

Пример:

type OrderStatus = 'pending' | 'paid' | 'shipped';  type Order<T extends OrderStatus> = T extends 'paid'   ? { amount: number; paymentDate: Date }   : T extends 'shipped'   ? { shippingDate: Date }   : {};  const paidOrder: Order<'paid'> = { amount: 100, paymentDate: new Date() }; const shippedOrder: Order<'shipped'> = { shippingDate: new Date() };

Использование readonly и неизменяемых структур

Постоянная мутация объектов и массивов в крупных проектах может привести к непредсказуемым ошибкам. Используйте типы readonly и неизменяемые структуры данных для защиты от случайных изменений.

  • Рекомендация: Массивы и объекты, которые не требуют изменений, должны быть объявлены как readonly.

  • Кейс: В проекте для обработки данных заказов, где заказ уже отправлен и не может быть изменён, используется тип readonly. Это предотвращает любые изменения в данных заказа после того, как они были отправлены на обработку, что обеспечивает целостность данных и предотвращает возможные ошибки при изменении заказа на разных этапах.

Пример:

const user: Readonly<UserData> = {   id: "123",   name: "John",   email: "john.doe@example.com" };  // Ошибка: нельзя изменить свойство в readonly объекте user.name = "Doe";

Заключение

Важно помнить, что эффективность TypeScript зависит не только от его возможностей, но и от того, как вы подходите к решению задач в вашем проекте. Строгая типизация и продуманная структура типов — это основа качественного кода, но они не заменят хорошую архитектуру и подход к проектированию.


ссылка на оригинал статьи https://habr.com/ru/articles/859016/