Как стать автором
Обновить

Возможности TypeScript, которых нужно избегать

Время на прочтение8 мин
Количество просмотров17K
Автор оригинала: Execute Program team

Добрый день, меня зовут Павел Поляков, я Principal Engineer в каршеринг компании SHARE NOW, в Гамбурге в ?? Германии. А еще я автор телеграм канала Хороший разработчик знает, где рассказываю обо всем, что обычно знает хороший разработчик.

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

Возможности TypeScript, которых нужно избегать

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

TypeScript это сложный язык, который развивался в течение долгого времени. Вначале, команда добавляла свойства, которые были не совместимы с JavaScript. Недавнее развитие все же более консервативно, т.е. более совместимо с возможностями JavaScript.

Как и с любым зрелым языком, нам предстоит принимать сложные решения о том, какие возможности TypeScript применять, а каких избегать. Мы тоже столкнулись с этим вопросом, когда разрабатывали платформу Execute Program (бэк-энд и фронт-энд написан на TypeScript) и когда мы создавали наши TypeScript курсы. Мы сделали выводы. Вот четыре рекомендации о том, какие возможности следует избегать.

Избегайте enum

С помощью enum мы можем дать имена набору констант. В примере ниже HttpMethod.Get это имя для строки GET. Тип HttpMethod полностью идентичен объединению (union) двух литеральных типов, т.е. 'GET' | 'POST'.

enum HttpMethod {
  Get = 'GET',
  Post = 'POST',
}
const method: HttpMethod = HttpMethod.Post;
method; // Evaluates to 'POST'

Есть аргументы за использование enum:

Представьте, что нам внезапно нужно заменить строку POST, в примере выше, на post. Нам нужно лишь заменить значение enum на post и все! Остальной код в приложении только ссылается на элемент enum через HttpMethod.Post, и этот элемент продолжает существовать.

Теперь представьте то же изменение в типе, который представлен объединением, а не enum. Мы определяем объединение как 'GET' | 'POST', а позже решаем изменить его на 'get' | 'post'. Любой код, который пытается использовать 'GET' или 'POST' как HttpMethod теперь возвращает ошибку. Мы должны вручную обновить весь этот код. Определенно это дополнительные действия, если сравнивать с enum.

Аргумент о стоимости поддержки кода в случае с enum очень сильный. Когда мы добавляем новый элемент в enum или union, то он редко изменяется после создания. Если мы используем union, то действительно, нам придется потратить какое-то время на обновление, но это происходит крайне редко. А даже когда это происходит, ошибки из-за несоответствия типов покажут нам, где мы должны обновить.

Недостаток enum заключается в том, как они встроены в TypeScript. TypeScript, по идее, это JavaScript, но с дополнительным слоем статических типов. Если мы уберем все типы из TypeScript кода, тогда то, что останется, должно быть рабочим JavaScript. Формальное определение, которое используется в TypeScript документации, звучит как “расширение слоем типов” (type-level extension). Большинство TypeScript возможностей это действительно расширение в слое типов для JavaScript и они не влияют на то как код работает, когда выполняется как JavaScript.

Вот пример расширения типами. У нас есть такой TypeScript код:

function add(x: number, y: number): number {
  return x + y;
}
add(1, 2); // Evaluates to 3

Компилятор проверяет типы данных в коде. Потом он должен сгенерировать JavaScript код. К счастью, этот этап прост: компилятор просто убирает все аннотации типов. В этом случае это значит убрать упоминания :number. То что осталось — это полностью рабочий JavaScript код:

function add(x, y) {
  return x + y;
}
add(1, 2); // Evaluates to 3

Большинство свойств TypeScript работают так же. Они соответствуют правилу “расширение на уровне типов”. Чтобы получить JavaScript код компилятор просто убирает аннотации типов.

К сожалению ,enum нарушают это правило. HttpMethod и HttpMethod.Post были частью типа, так что они будут удалены, когда TypeScript сгенерирует JavaScript код. Но, если компилятор просто уберет enum типы из нашего примера, то у нас все равно останется JavaScript код, который ссылается на HttpMethod.Post. Это вызовет ошибку во время исполнения. Мы не можем ссылаться на HttpMethod.Post, если компилятор его удалил!

/* This is compiled JavaScript code referencing a TypeScript enum. But if the
 * TypeScript compiler simply removes the enum, then there's nothing to
 * reference!
 *
 * This code fails at runtime:
 *   Uncaught ReferenceError: HttpMethod is not defined */
const method = HttpMethod.Post;

TypeScript решает эту проблему нарушая свое же правило. Когда компилируется enum, компилятор вызывает дополнительный JavaScript код, который никогда не присутствовал в оригинальном TypeScript коде. Только несколько возможностей, которые предоставляет TypeScript, работают таким образом. Реализация каждой из них добавляет сложностей при компиляции, хотя в целом модель TypeScript достаточно проста. Именно по этой причине мы рекомендуем не использовать enum, а вместо этого использовать объединения.

Почему вообще важно правило расширения на уровне типов?

Давайте посмотрим, как это правило взаимодействует с экосистемой JavaScript и TypeScript инструментов. TypeScript проекты по сути, также являются JavaScript проектами. Поэтому часто они используют средства сборки JavaScript, такие как Babel и webpack. Эти инструменты были разработаны для JavaScript и до сих пор это их основной фокус. Каждый инструмент, к тому же, сам по себе является экосистемой. Существует примерно бесконечность плагинов для Babel или webpack, которые обрабатывают код.

Как может Babel, webpack и множество их плагинов и все другие инструменты в экосистеме полностью поддерживать TypeScript? Для большей части TypeScript правило расширения на уровне типов делает их работу относительно простой. Эти инструменты просто удаляют аннотации типов и оставляют работающий JavaScript.

А когда речь идет о enum (а еще namespace, о них тоже скоро поговорим), то ситуация усложняется. Теперь недостаточно просто удалить enum. Инструменты должны превратить enum HttpMethod {...} в рабочий JavaScript код, несмотря на то, что в JavaScript нет enum.

Это приводит нас к конкретной проблеме с TypeScript, который нарушает свое же правило расширения на уровне типов. Для инструментов типа Babel и webpack работа с TypeScript это всего лишь опция, одна из многочисленных возможностей. В результате, иногда поддержка TypeScript не получает столько внимания, как поддержка JavaScript, а это ведет к багам.

Большинство инструментов хорошо справляются, когда в TypeScript определяются переменные, функции и т.п. Все эти конструкции достаточно просты, чтобы их обработать. Но иногда ошибки проявляются при обработке enum и namespace, потому что эти свойства требуют чего-то более, чем просто удаления аннотаций типов. Вы можете доверять компилятору TypeScript, что он правильно обработает эти случаи, но иногда определенные инструменты в экосистеме будут выдавать ошибки.

Когда ваш компилятор, бандлер, минификатор, линтер, форматировщик кода и т.п. без явных внешних признаков просто неправильно компилируют часть вашей системы, то вам будет сложно с этим разобраться. Баги компилятора очень сложно отдебажить. Прочтите внимательно: “в течение недели, с помощью моих коллег, нам удалось лучше понять где именно происходит баг”. Хотите так же? Нет.

Избегайте namespace

Определения namespace — они как модули, кроме того, что несколько namespace могут быть расположены в одном файле. Например, мы можем создать файл, который определяет отдельный namespace для кода, который экспортируется и код для тестов. (Мы не рекомендуем так делать, но так проще показать что делают namespace)

namespace Util {
  export function wordCount(s: string) {
    return s.split(/\b\w+\b/g).length - 1;
  }
}

namespace Tests {
  export function testWordCount() {
    if (Util.wordCount('hello there') !== 2) {
      throw new Error("Expected word count for 'hello there' to be 2");
    }
  }
}

Tests.testWordCount();

На практике namespace вызывают проблемы. В секции про enum выше, мы говорили о правиле расширения на уровне типов в TypeScript. Обычно, компилятор удаляет все аннотации типов и остается рабочий JavaScript.

namespace нарушают это правило так же как и enum. В namespace Util { export function wordCount ...} мы не можем убрать определения типов. Потому что весь namespace является определением типа в TypeScript! Что случится с остальным кодом вне namespace, когда будет вызван Util.wordCount(...)? Если мы удалим namespace Util до того, как сгенерируем JavaScript код, то Util больше не будет существовать. Так что функция Util.wordCount(...) даже в теории не может работать.

Как и с enum, компилятор TypeScript не может просто удалить определения namespace. Вместо этого он должен сгенерировать JavaScript код, которого не существует в оригинальном TypeScript коде.

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

Избегайте декораторов (пока что)

Декораторы это функции, которые изменяют или заменяют другие функции (или классы). Вот пример декоратора, который мы взяли из официальной документации:

// This is the decorator.
@sealed
class BugReport {
  type = "report";
  title: string;

  constructor(t: string) {
    this.title = t;
  }
}

Декоратор @sealed намекает на sealed модификатор в C#, который запрещает другим классам наследование “запечатанного” класса. Мы можем реализовать его с помощью sealed функции, которая берет класс и изменяет его, чтобы предотвратить возможное наследование.

Декораторы были сперва добавлены в TypeScript, до того, как начали процесс своей стандартизации в JavaScript (ECMAScript). К январю 2022 декораторы все еще находятся в стадии 2 предложения ECMAScript. Стадия 2 — это “черновик”. Предложение о стандартизации декораторов как будто застряло в чистилище ECMAScript комитета — оно находится в стадии 2 с февраля 2019.

Мы рекомендуем избегать декораторов, пока предложение хотя бы не доберется до стадии 3 (кандидат). Или стадии 4 (завершено), для более консервативных команд.

В принципе, есть шанс, что ECMAScript декораторы никогда не будут приняты комитетом. В таком случае, их реализация в TypeScript окажется рядом с enum и namespace. Они продолжают нарушать правило TypeScript про расширение на уровне типов. Также существует риск, что что-то пойдет не так, когда вы будете использовать инструменты, отличающиеся от официального компилятора TypeScript. Мы не знаем произойдет это или нет, но преимущества декораторов минимальны, лучше просто подождать.

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

Исключите использование слова private

В TypeScript есть два варианта как сделать свойства класса приватными. Есть старое ключевое слово private, которое свойственно только TypeScript. И есть новый синтаксис #somePrivateField, который заимствован из JavaScript. Вот пример, где показываются два варианта:

class MyClass {
  private field1: string;
  #field2: string;
  ...
}

Мы рекомендуем использовать новый #somePrivateField синтаксис по простой причине: оба варианта одинаковы. Мы хотим поддерживать паритет характеристик с JavaScript, если у нас нет серьезных причин его нарушать.

Резюмируем

Теперь резюмируем наши рекомендации:

  1. Избегайте enum

  2. Избегайте namespace

  3. Предпочитайте #somePrivateField а не private somePrivateField

  4. Повремените с использованием декораторов, пока они не стандартизированы. Если вы действительно хотите использовать библиотеку, где они используются, то принимайте информированное решение.

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

А еще...

Здесь говорю опять я, Павел. В конце еще раз приглашу вас в свой Telegram-канал. На канале Хороший разработчик знает я минимум три раза в неделю простым языком рассказываю про свой опыт, хард скиллы и софт скиллы. Я 15+ лет в IT, мне есть чем поделиться. Все это нужно разработчику, чтобы делать свою работу хорошо, работать в удовольствие, быть востребованным на рынке и получать высокую компенсацию.

А для любителей картинок и историй есть ? Instagram.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 27: ↑5 и ↓22-16
Комментарии29

Публикации