Добрый день, меня зовут Павел Поляков, я 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
, если у нас нет серьезных причин его нарушать.
Резюмируем
Теперь резюмируем наши рекомендации:
Избегайте
enum
Избегайте
namespace
Предпочитайте
#somePrivateField
а неprivate somePrivateField
Повремените с использованием декораторов, пока они не стандартизированы. Если вы действительно хотите использовать библиотеку, где они используются, то принимайте информированное решение.
Даже если вы избегаете этих возможностей TypeScript
, хорошо иметь представление о том, как они работают. Вы часто можете встретить их в старом коде или даже в свежих проектах. Не все согласны, что их следует избегать. Осведомлен значит вооружен.
А еще...
Здесь говорю опять я, Павел. В конце еще раз приглашу вас в свой Telegram-канал. На канале Хороший разработчик знает я минимум три раза в неделю простым языком рассказываю про свой опыт, хард скиллы и софт скиллы. Я 15+ лет в IT, мне есть чем поделиться. Все это нужно разработчику, чтобы делать свою работу хорошо, работать в удовольствие, быть востребованным на рынке и получать высокую компенсацию.
А для любителей картинок и историй есть ? Instagram.