Убедитесь, что ваш код TypeScript выдержит испытание временем

Независимо от того, создаете ли вы библиотеки, интерфейсные или серверные приложения с использованием JavaScript, небольшое усилие по переходу на TypeScript уже является большим шагом на пути к защите вашего кода в будущем.

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

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

Кроме того, это значительно упростит рефакторинг вашего кода, поможет с будущими разработками и уменьшит технический долг - поскольку теперь IDE (например, VS Code) знает, какие типы использует ваш код, она может помочь с переименованием, разделением, перемещением вокруг кода, а также показать неиспользуемые функции.

Однако есть несколько недорогих приемов, которые помогут вам в большей степени защитить ваш код от будущего, особенно если вы планируете работать над своим кодом в команде или если ваш код больше, чем просто прототип. .

1. Включите строгие проверки

По умолчанию TypeScript не использует свой strict режим. Это может быть замечательно, если вы еще не хотите полностью использовать TypeScript (например, переносить в него только части файлов JavaScript), но это также означает, что многие распространенные ошибки JavaScript не будут обнаружены TypeScript.

Это происходит потому, что TypeScript не ожидает, что вы добавите определение типа ко всему (вместо этого помечаете параметры и переменные как any), и позволит вам назначить null или undefined любому типу, среди нескольких других правил (см. Docs ).

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

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

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

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

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

2. Используйте Babel вместо tsc

Начиная с версии 7, Babel добавила поддержку TypeScript, что означает, что вам больше не нужно использовать компилятор TypeScript tsc для сборки вашего проекта, вместо этого вы можете просто использовать Babel, который просто удаляет ваши типы из всех файлов TypeScript, а затем выдает результат в виде JavaScript. .

Это не только намного быстрее, чем tsc, особенно в крупных проектах, но также позволяет вам использовать всю экосистему Babel в вашем проекте.

Для серверных проектов это означает, что вы можете упростить свои неуклюжие сценарии просмотра файлов и просто использовать babel-node для отслеживания изменений:

nodemon — inspect=0.0.0.0:9229 — exec babel-node — extensions ‘.ts,.tsx’ src/index.ts

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

{
  "plugins": ["import-graphql"]
}

И, конечно же, для проектов, использующих инструменты командной строки, созданные с помощью TypeScript, вам больше не понадобится ts-node или шаги для предварительной сборки перед запуском, а запускайте свой интерфейс командной строки напрямую через babel-node.

Минусы минимальны. Реализация TypeScript в Babel не поддерживает перечисления const в TypeScript (что звучит как нечто большее, чем есть на самом деле), и вам все равно понадобится tsc в вашем конвейере фиксации / CI, если вы действительно хотите запускать проверки всех ваших файлов, как это не делает Babel Не интерпретирую TypeScript.

3. Закрепите свои версии

Если вы еще этого не сделали, начните закреплять свои версии в своем package.json. Даже если у вас есть файл блокировки, считайте свою package.json понятной истиной, от которой зависит ваш проект.

Это особенно актуально для проектов TypeScript. В то время как некоторые проекты JavaScript обновляют версию патча своих зависимостей, только для того, чтобы вы заметили, что сопровождающий пакета на самом деле не следил за SemVer или не ввел ошибку, все разработчики TypeScript в какой-то момент пострадали от обновления версий патчей. их @types определения типов, чтобы затем заметить множество новых ошибок TypeScript.

Часто это происходит из-за того, что многие сторонние типы для сторонних зависимостей развиваются с течением времени, а иногда и становятся более строгими непредвиденными способами.

Например, определение Koa, которое только позволяло параметрический контекст или определение Express, которое только позволяло использовать пользовательский объект в контексте, поскольку эти ключи были установлены на any на их интерфейсах, внезапно терпит неудачу на десятках маршрутов, потому что добавлены правильные типы.

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

Тем более с TypeScript, и так же, как вы, надеюсь, уже делаете с другими обновлениями зависимостей, рассмотрите возможность использования таких инструментов, как Greenkeeper.io (или Renovate, или что-то еще, что предлагает ваш CI) для автоматического обновления одной зависимости в отдельной ветке. чтобы упростить обновление вашего проекта.

И никогда не думайте об обновлении только версии патча этого маленького @types/ пакета в пятницу вечером. Именно так начинаются ужасные выходные.

4. Непрозрачные типы FTW

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

const trackLogin = (currentDate: string, sessionId: string) => { someCode(); };
const sessionUuid: string = currentSession.getUuid();
const currentDate: string = (new Date()).toISOString();
trackLogin(sessionUuid, currentDate);

Вы можете легко предотвратить такие случаи, а также значительно упростить рефакторинг кода в будущем. Просто определите и используйте так называемые непрозрачные типы, которые могут быть настолько детализированными, насколько вам нужно для вашего проекта:

type Opaque<K, T> = T & { __TYPE__: K };
type Uuid = Opaque<"Uuid", string>;
type DateISOString = Opaque<"DateISOString", string>;

Функция полезности Opaque<K, T> просто определяет новый тип, который, помимо значения переменной, также хранит (уникальный) ключ, например Uuid или DateISOString.

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

Это означает, что теперь вы можете добавить столько строковых типов с уникальными именами, они по-прежнему будут передаваться как строковые значения, но с добавленной проверкой типа сверху:

const trackLogin = (currentDate: DateISOString, sessionId: Uuid) => { someCode(); }
const sessionUuid = currentSession.getUuid() as Uuid;
const currentDate = new Date().toISOString() as DateISOString;
trackLogin(sessionUuid, currentDate);
// Your IDE / TypeScript will now understand this function call and show an error

5. Используйте типы утилит

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

TypeScript уже имеет несколько встроенных служебных типов, таких как Partial<T>, что делает все свойства T необязательными, или Readonly<T>, что делает T доступным только для чтения. (Также доступны пакеты npm дополнительных сторонних типов утилит.)

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

Например, если вы разработчик React, для редуктора Redux вы можете просто определить состояние ввода редуктора как Readonly<T>, чтобы легче было увидеть, не перезаписываете ли вы его случайно, без добавления другого типа состояния, доступного только для чтения.

Однако они не только помогают уменьшить количество повторяющихся типов; они также могут помочь вам не экспортировать слишком много интерфейсов и сохранить ваш код понятным.

Чтобы придерживаться примера React, вы можете определить экспортируемые интерфейсы для свойств компонентов React или возвращаемых значений создателя действия.

Однако, если вы думаете, что интерфейсы являются шаблонными, а ваш код действительно связан с компонентом React, вы можете просто использовать React.ComponentProps<T>, чтобы получить свойства компонента простым способом (что применимо как к компонентам на основе классов, так и к компонентам на основе функций), или ReturnType<T> чтобы получить тип создателя действия.

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

Иногда гораздо проще объединить интерфейс и ссылаться на него через квадратные скобки, например IUser["parents"] может быть простым способом ссылки на объект parents значения, который хранит еще два Users, вместо того, чтобы вводить одноразовый IParents, который через неделю может стать слишком неоднозначным.

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

6. Используйте Prettier

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

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

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

Вы можете просто использовать Prettier для форматирования кода. Он очень самоуверенный и (к счастью!) Не предлагает много вариантов конфигурации, на которые можно было бы тратить время, и он просто безупречно работает со многими форматами файлов, например HTML, а также JSON, CSS и TypeScript.

Вы можете настроить свою IDE (например, VS Code) для автоматического использования при каждом сохранении файла, что на самом деле является самым большим преимуществом Prettier - вы перестанете тратить драгоценное время кодирования на отступы, длину строки и т. Д., А вместо этого можете просто ввести снова и снова, и Prettier сотворит чудеса.

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

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

* Единственное изменение конфигурации, которое я позволил бы - использовать завершающие запятые в стиле ES5:

{
  "trailingComma": "es5"
}

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

7. Используйте объекты как полезные данные для функций

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

Традиционно функции, к которым применялось это правило, были создателями действий Redux:

const sendMail = (type: string, subject: string, body: string, to: string, attachment?: File) => {
  return {
    payload: {
      attachment, body, subject, to
    },
    type,
  };
};

Против:

const sendMail = (type: string, payload: { subject: string, body: string, to: string, attachment?: File }) => {
  return {
    payload,
    type,
  };
};

Когда теперь вам нужно добавить новый параметр, например userId получателя электронного письма в нашем примере, при первом способе выражения функции вам нужно будет изменить подпись функции, а вместе с ней и все компоненты React / функции отправки, где это вызывается.

Если вы просто передаете объект, не только легче выразить возвращаемый объект в создателе действий, но вам также не придется беспокоиться о порядке ввода параметров и о том, что есть необязательный параметр.

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

8. Используйте ESLint и SonarJS.

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

(TSLint заменяется на typescript-eslint; плагин TSLint SonarTS был принят и теперь является частью SonarJS).

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

Даже для опытных программистов полезно напомнить, что геттер не имеет возвращаемого значения или переменная не объявлена ​​в текущей области; все мы делаем ошибки.

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

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