Руководство Google по стилю написания кода на языке TypeScript (перевод)
Содержание

Руководство Google по стилю написания кода на языке TypeScript (перевод руководства "Google TypeScript Style Guide")

Дополнительная информация по переводу

Репозиторий текущего перевода расположен по адресу: https://github.com/olegbarabanov/google-typescript-style-guide-ru.

С оригинальным руководством по стилю вы можете ознакомиться по адресу: https://google.github.io/styleguide/tsguide.html.

Перевод основан на версии оригинального руководства от 10.11.2022.

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

Если Вы нашли несоответствие, ошибку или неточность в переводе, вы можете оформить это в виде issue или предложить собственное исправление в виде pull request в репозиторий проекта, либо написать переводчику по адресу mail@olegbarabanov.ru.

Введение

Данное руководство основано на внутреннем руководстве Google по стилю написания кода на языке TypeScript, но при этом оно было незначительно скорректировано с целью удаления разделов предназначенных для внутреннего пользования Google. Внутренняя среда Google предусматривает иные ограничения на TypeScript, чем те, что вы могли бы встретить за пределами Google. Приведенные здесь советы особенно полезны для людей, создающих код, который они намерены импортировать в Google, однако в других случаях они могут и не применяться в вашей внешней по отношению к Google среде.

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

Данное руководство ссылается на терминологию стандарта RFC 2119 при использовании фраз ДОЛЖНЫ, НЕ ДОЛЖНЫ, РЕКОМЕНДУЕТСЯ, НЕ РЕКОМЕНДУЕТСЯ и ВОЗМОЖНО [1]. Все приведенные примеры не носят нормативного характера и служат лишь для иллюстрации стандартных формулировок из данного руководства по стилю.

Синтаксис

Идентификаторы

Идентификаторы должны использовать только ASCII символы, цифры, символы подчеркивания (для констант и названий методов структурных тестов) и знак $. Таким образом, каждое допустимое имя идентификатора соответствует регулярному выражению [$\w]+[2].

СтильКатегория
UpperCamelCaseкласс / интерфейс / тип / перечисление / декоратор / параметр типа
lowerCamelCaseпеременная / параметр / функция / метод / свойство / псевдонимы модулей
CONSTANT_CASEглобальные константы, включая имена элементов перечислений (enum). См. ниже раздел Константы
#identподобные приватные идентификаторы не применяются

Аббревиатуры

Рассматривайте используемые в именах аббревиатуры типа акронимов как целые слова, т.е. используйте loadHttpUrl, а не loadHTTPURL, если только это не обусловлено названием конкретной платформы (например XMLHttpRequest).

Знак доллара

В идентификаторах, как правило, не рекомендуется использовать символ $, за исключением случаев, когда это соответствуют соглашениям об именовании для сторонних фреймворков. Подробнее об использовании суффикса $ для наблюдаемых (Observable) значений см. ниже.

Параметры типа

Для обозначения параметров типа, как например в Array<T>, возможно использовать один символ верхнего регистра (T) или UpperCamelCase.

Названия тестов

Название тестовых методов в Closure testSuite и подобных тестовых фреймворках в стиле xUnit возможно представлять с разделителями _, например testX_whenY_doesZ().

_ префикс/суффикс

Идентификаторы не должны использовать _ в качестве префикса или суффикса.
Это также означает что символ _ сам по себе не должен быть использован в качестве идентификатора (например, чтобы указать, что параметр не используется).

Совет: Если вам нужны только некоторые элементы из массива (или TypeScript кортежа), вы можете вставить дополнительные запятые в выражение деструктуризации, чтобы игнорировать промежуточные элементы:

  // ✅ ХОРОШО ↴

  const [a, , b] = [1, 5, 10];  // a <- 1, b <- 10

Импорты

Импорты пространств имен модулей имеют верблюжий регистр (lowerCamelCase) в то время как файлы имеют змеиный регистр (snake_case), что означает, что корректные импорты не будут совпадать по стилю написания. Например:

// ✅ ХОРОШО ↴

import * as fooBar from './foo_bar';

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

Константы

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

// ✅ ХОРОШО ↴

const UNIT_SUFFIXES = {
  'milliseconds': 'ms',
  'seconds': 's',
};
// Несмотря на то, что в соответствии с правилами JavaScript UNIT_SUFFIXES является изменяемым,
// верхний регистр символов обозначает для пользователей, что они не должны изменять значения.

Константой также может быть статическое свойство класса, которое предназначенно только для чтения (static readonly).

// ✅ ХОРОШО ↴

class Foo {
  private static readonly MY_SPECIAL_NUMBER = 5;

  bar() {
    return 2 * Foo.MY_SPECIAL_NUMBER;
  }
}

Глобальность: Только для элементов, объявленных на уровне модуля, статических полей классов уровня модуля и значений перечислений уровня модуля возможно использовать CONST_CASE стиль. Если во время работы программы значение создается более одного раза (например, локальная переменная, объявленная в функции или статическое поле в классе, вложенном в функцию), тогда должен использоваться lowerCamelCase стиль.

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

Псевдонимы

При создании локального псевдонима существующего элемента, используйте формат уже существующего его обозначения. Локальный псевдоним должен совпадать с существующим именем и форматом источника. Для переменных при создании локальных псевдонимов используйте const, а для полей класса - атрибут readonly.

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

// ✅ ХОРОШО ↴

const {Foo} = SomeType;
const CAPACITY = 5;

class Teapot {
  readonly BrewStateEnum = BrewStateEnum;
  readonly CAPACITY = CAPACITY;
}

Стиль именования

TypeScript отражает информацию в типах, поэтому имена не рекомендуется дополнять информацией, которая включена в тип (см. также Блог о тестировании (Testing Blog) для получения дополнительной информации о том, что не следует включать).

Несколько конкретных примеров для этого правила:

Описательные названия

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

Кодировка файлов: UTF-8

Для символов, отличных от ASCII, используйте фактический символ Юникода (например ). Для непечатаемых символов можно использовать эквивалентный шестнадцатеричный код или экранирование Unicode-символов (например \u221e) вместе с пояснительным комментарием.

// ✅ ХОРОШО ↴

// Совершенно ясно даже без комментария
const units = 'μs';

// Используйте Unicode-экранирование для непечатаемых символов
const output = '\ufeff' + content; // это маркер последовательности байтов (Unicode BOM)
// ❌ ПЛОХО ↴

// Даже с комментарием, это сложно для чтения и подвержено потенциальным ошибкам.
const units = '\u03bcs'; // Греческая буква mu, 's'

// Читающий код не поймет, что это такое
const output = '\ufeff' + content;

Не используйте продолжения строк

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

Запрещено:

// ❌ ПЛОХО ↴

const LONG_STRING = 'Это очень длинная строка, которая превышает лимит в \
    80 символов. К сожалению, она содержит длинные отрезки пустого пространства, так \
    как в продолженных строках имеются отступы для поддержания форматирования.';

Вместо этого напишите:

// ✅ ХОРОШО ↴

const LONG_STRING = 'Это очень длинная строка, которая превышает лимит в ' +
    '80 символов. Она не содержит длинные отрезки пустого пространства, поскольку ' +
    'конкатенируемые строки не имеют в себе лишних отступов.';

Комментарии & Документация

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

Существует два типа комментариев, JSDoc (/** ... */) и не относящиеся к JSDoc обычные комментарии (// ... или /* ... */).

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

Правила JSDoc соответствуют стилю языка JavaScript

В общих чертах, следуйте правилам для JSDoc из руководства по стилю написания JavaScript[4], разделы 7.1 - 7.5. В остальной части этого раздела описываются исключения из этих правил.

Документирование всех экспортов верхнего уровня в составе модулей

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

Исключение: элементы, которые экспортируются только для использования инструментальными программами, например классы @NgModule, не требуют комментариев.

Исключите те комментарии, которые излишни в TypeScript

Для примера, не указывайте типы в @param или @return блоках, не пишите @implements, @enum, @private, @override в коде, который использует implements, enum, private, override и пр. ключевые слова.

Делайте комментарии, которые действительно добавляют информацию

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

Комментарии к параметризованным свойствам

Параметризованное свойство — это параметр конструктора, которому предшествует один из модификаторов private, protected, public или readonly. Параметризованное свойство объявляет одновременно и параметр и свойство экземпляра, а также неявно присваивает им значения. Для примера, выражение constructor(private readonly foo: Foo) объявляет то, что конструктор принимает параметр foo, а также объявляет приватное и доступное только для чтения свойство foo и присваивает значение параметра этому свойству перед выполнением остальной части конструктора.

Чтобы задокументировать эти поля, используйте JSDoc @param аннотацию. Редакторы отображают описание при вызовах конструктора и доступе к свойствам.

// ✅ ХОРОШО ↴

/** Этот класс демонстрирует, как документируются параметризованные свойства. */
class ParamProps {
  /**
   * @param percolator Кофеварка, используемая для варки.
   * @param beans Зерна для варки.
   */
  constructor(
    private readonly percolator: Percolator,
    private readonly beans: CoffeeBean[]) {}
}
// ✅ ХОРОШО ↴

/** Этот класс демонстрирует, как документируются обычные поля. */
class OrdinaryClass {
  /** Кофейные зерна, которые будут использоваться в следующем вызове brew(). */
  nextBean: CoffeeBean;

  constructor(initialBean: CoffeeBean) {
    this.nextBean = initialBean;
  }
}

Комментарии при вызове функции

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

// ✅ ХОРОШО ↴

// Встраивание блочных комментариев для параметров, которые трудны для понимания:
new Percolator().brew(/* amountLitres= */ 5);
// Также рассмотрите возможность использования именованных аргументов и деструктуризации параметров (в объявлении метода "brew"):
new Percolator().brew({amountLitres: 5});
// ✅ ХОРОШО ↴

/** Перколятор, как старый вариант кофеварки {@link CoffeeBrewer} */
export class Percolator implements CoffeeBrewer {
  /**
   * Сварить кофе.
   * @param amountLitres Количество, которое надо сварить. Должно соответствовать объему кофейника!
   */
  brew(amountLitres: number) {
    // Так или иначе, эта реализация создает ужасный кофе.
    // TODO(b/12345): Улучшить процесс варки кофе в кофеварке.
  }
}

Размещайте документацию перед декораторами

Когда класс, метод или свойство имеют и декораторы вида @Component и JSDoc, убедитесь, что JSDoc написан перед декоратором.

Языковые правила

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

Видимость

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

// ❌ ПЛОХО ↴

class Foo {
  public bar = new Bar();  // ПЛОХО: нет необходимости в модификаторе "public"

  constructor(public readonly baz: Baz) {}  // ПЛОХО: модификатор "readonly" подразумевает, что это свойство имеет по умолчанию модификатор "public"
}
// ✅ ХОРОШО ↴

class Foo {
  bar = new Bar();  // ХОРОШО: нет необходимости в модификаторе "public"

  constructor(public baz: Baz) {}  // допускается модификатор "public"
}

См. также Область видимости экспортируемых элементов ниже.

Конструкторы

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

// ❌ ПЛОХО ↴

const x = new Foo;
// ✅ ХОРОШО ↴

const x = new Foo();

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

// ❌ ПЛОХО ↴

class UnnecessaryConstructor {
  constructor() {}
}
// ❌ ПЛОХО ↴

class UnnecessaryConstructorOverride extends Base {
    constructor(value: number) {
      super(value);
    }
}
// ✅ ХОРОШО ↴

class DefaultConstructor {
}

class ParameterProperties {
  constructor(private myService) {}
}

class ParameterDecorators {
  constructor(@SideEffectDecorator myService) {}
}

class NoInstantiation {
  private constructor() {}
}

Члены класса

Не используйте приватные поля вида #private

Не используйте приватные поля (также известные как приватные идентификаторы):

// ❌ ПЛОХО ↴

class Clazz {
  #ident = 1;
}

Вместо этого используйте поддерживаемые TypeScript аннотации видимости:

// ✅ ХОРОШО ↴

class Clazz {
  private ident = 1;
}

Почему?

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

Используйте модификатор readonly

Пометьте модификатором readonly те свойства, которые никогда не переназначаются вне конструктора (они не обязательно должны быть глубоко неизменяемыми).

Параметризованные свойства

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

// ❌ ПЛОХО ↴

class Foo {
  private readonly barService: BarService;

  constructor(barService: BarService) {
    this.barService = barService;
  }
}
// ✅ ХОРОШО ↴

class Foo {
  constructor(private readonly barService: BarService) {}
}

Если параметризованное свойство нуждается в документировании, то используйте JSDoc тег @param.

Инициализаторы полей

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

// ❌ ПЛОХО ↴

class Foo {
  private readonly userList: string[];
  constructor() {
    this.userList = [];
  }
}
// ✅ ХОРОШО ↴

class Foo {
  private readonly userList: string[] = [];
}

Свойства, используемые за пределами лексической области класса

Для свойств, так или иначе задействованных вне лексической области видимости содержащего их класса, например, для свойств контроллера Angular используемых из шаблона, не должна использоваться приватная (private) область видимости, т.к. к этим свойствам потребуется доступ за пределами лексической области видимости их класса.

Для этих свойств используйте либо protected, либо public, в зависимости от того, что подходит. Для свойств используемых в шаблонах Angular и AngularJS следует использовать protected, а в Polymer - public.

В TypeScript коде не должны использоваться obj['foo'] для обхода ограничения видимости свойства [5].

Почему?

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

Хотя это и кажется, что obj['foo'] может обойти область видимости в компиляторе TypeScript, эта схема может быть нарушена путем изменения правил сборки, а также нарушается согласованность с оптимизациями.

Геттеры и Сеттеры (Аксессоры)

Для членов класса возможно иcпользовать геттеры и сеттеры. Методы-геттеры должны быть чистыми функциями (т.е. не иметь побочных эффектов и каждый раз возвращать одинаковый результат при одних и тех же параметрах). Они также полезны как средство ограничения видимости внутренних или подробных деталей реализации (показано ниже).

// ✅ ХОРОШО ↴

class Foo {
  constructor(private readonly someService: SomeService) {}

  get someMember(): string {
    return this.someService.someVariable;
  }

  set someMember(newValue: string) {
    this.someService.someVariable = newValue;
  }
}

Если аксессор используется для сокрытия свойства класса, то для скрытого свойства возможно указать префикс или суффикс с любым целым словом, например internal или wrapped. При использовании этих приватных свойств по возможности обращайтесь к их значению через аксессор. По крайней мере один аксессор к свойству должен быть нетривиальным: не определяйте сквозные аксессоры только для того, чтобы скрыть свойство. Вместо этого сделайте свойство публичным (или подумайте о том, чтобы сделать его доступным только для чтения (readonly), чем просто определять геттер без сеттера).

// ✅ ХОРОШО ↴

class Foo {
  private wrappedBar = '';
  get bar() {
    return this.wrappedBar || 'bar';
  }

  set bar(wrapped: string) {
    this.wrappedBar = wrapped.trim();
  }
}
// ❌ ПЛОХО ↴

class Bar {
  private barInternal = '';
  // Ни один из этих аксессоров не имеет логики, поэтому просто сделайте bar публичным
  get bar() {
    return this.barInternal;
  }

  set bar(value: string) {
    this.barInternal = value;
  }
}

this в статическом контексте

В коде не должен использоваться this в статическом контексте.

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

// ❌ ПЛОХО ↴

class ShoeStore {
  static storage: Storage = ...;

  static isAvailable(s: Shoe) {
    // Плохо: не используйте `this` в статическом методе.
    return this.storage.has(s.id);
  }
}

class EmptyShoeStore extends ShoeStore {
  static storage: Storage = EMPTY_STORE;  // переопределяет storage из ShoeStore
}

Почему?

Этот код может привести к неожиданностям: авторы могут не ожидать, что к статическим полям можно обращаться через указатель this и могут быть удивлены, обнаружив, что они могут быть переопределены — подобная функциональность используется не часто.

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

Примитивные типы & Классы-обертки

Код TypeScript не должен создавать экземпляры классов-оберток для примитивных типов String, Boolean и Number. Классы-обертки имеют неожиданное поведение, такое как new Boolean(false) равное true.

// ❌ ПЛОХО ↴

const s = new String('hello');
const b = new Boolean(false);
const n = new Number(5);
// ✅ ХОРОШО ↴

const s = 'hello';
const b = false;
const n = 5;

Конструктор массива

В коде на Typescript не должен использоваться Array() конструктор, с или без new. Его применение неоднозначно и сбивает с толку:

// ❌ ПЛОХО ↴

const a = new Array(2); // [undefined, undefined]
const b = new Array(2, 3); // [2, 3];

Вместо этого всегда используйте скобки для инициализации массивов или from для инициализации Array с определенным размером:

// ✅ ХОРОШО ↴

const a = [2];
const b = [2, 3];

// Эквивалент для Array(2):
const c = [];
c.length = 2;

// [0, 0, 0, 0, 0]
Array.from<number>({length: 5}).fill(0);

Преобразование типов

В TypeScript коде возможно использовать String() и Boolean() (примечание: без new!) функции, строковые шаблонные литералы или !! для преобразования типов.

// ✅ ХОРОШО ↴

const bool = Boolean(false);
const str = String(aNumber);
const bool2 = !!str;
const str2 = `result: ${bool2}`;

Значения перечислений (enum) (включая объединения перечислений и других типов) не должны преобразовываться в булевы значения с помощью Boolean() или !!, а должны вместо этого сравниваться явным образом с помощью операторов сравнения.

// ❌ ПЛОХО ↴

enum SupportLevel {
  NONE,
  BASIC,
  ADVANCED,
}

const level: SupportLevel = ...;
let enabled = Boolean(level);

const maybeLevel: SupportLevel|undefined = ...;
enabled = !!maybeLevel;
// ✅ ХОРОШО ↴

enum SupportLevel {
  NONE,
  BASIC,
  ADVANCED,
}

const level: SupportLevel = ...;
let enabled = level !== SupportLevel.NONE;

const maybeLevel: SupportLevel|undefined = ...;
enabled = level !== undefined && level !== SupportLevel.NONE;

Почему?

Для большинства задач не имеет значения, числовое или строковое значение сопоставлено с именем перечисления во время выполнения программы, поскольку значения перечислений указываются в исходном коде по имени. Следовательно, инженеры привыкли не задумываться об этом, а потому нежелательны ситуации, когда это действительно важно, так как они будут приводить к неожиданностям. Так происходит и в случае преобразования перечислений в булевы значения; в частности, вероятно может быть неожиданным, что по умолчанию первое объявленное значение перечисления является ложным (потому что оно равно 0), в то время как остальные значения являются истинными. Пользователи, читающие код, в котором используется значение перечисления, могут даже не знать, является ли оно первым объявленным значением или нет.

Не приветствуется для приведения к строке использовать конкатенацию строк, так как при проверке кода мы отслеживаем, чтобы операнды оператора «плюс» имели совпадающие типы.

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

Примечание: Number(''), Number(' '), и Number('\t') могут вернуть 0 вместо NaN. Number('Infinity') и Number('-Infinity') могут вернуть Infinity и -Infinity соответственно. Кроме того, экспоненциальная запись, такая как Number('1e+309') и Number('-1e+309'), может привести к переполнению и преобразованию в Infinity. Подобные случаи могут потребовать особого обращения.

// ✅ ХОРОШО ↴

const aNumber = Number('123');
if (!isFinite(aNumber)) throw new Error(...);

В коде не должен использоваться унарный плюс (+) для преобразования строки в число. Парсинг чисел может привести к неудаче, иметь неожиданные исключительные ситуации и может быть признаком дурно пахнущего кода (парсинг чисел не на том уровне). Учитывая это, унарный плюс слишком легко пропустить при проверке кода.

// ❌ ПЛОХО ↴

const x = +y;

В коде также не должны использоваться parseInt или parseFloat для парсинга чисел, за исключением случаев парсинга в строках недесятичных числовых значений (см. ниже). Обе эти функции игнорируют конечные символы в строке, что может привести к возникновению ошибочного состояния (например, парсинг 12 гномов как 12).

// ❌ ПЛОХО ↴

const n = parseInt(someString, 10);  // Подвержено ошибкам,
const f = parseFloat(someString);    // независимо от передачи основания системы счисления.

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

// ✅ ХОРОШО ↴

if (!/^[a-fA-F0-9]+$/.test(someString)) throw new Error(...);
  // Требуется для парсинга восьмеричного числа.
// tslint:disable-next-line:ban
const n = parseInt(someString, 16);  // Допустимо только для основания числа != 10

Используйте Number(), а затем Math.floor или Math.trunc (там, где это возможно) для парсинга целых чисел:

// ✅ ХОРОШО ↴

let f = Number(someString);
if (isNaN(f)) handleError();
f = Math.floor(f);

Неявное преобразование типов

Не используйте явное булево преобразование в условиях, в которых уже имеется неявное булево преобразование. Это условия в операторах if, for и while.

// ❌ ПЛОХО ↴

const foo: MyInterface|null = ...;
if (!!foo) {...}
while (!!foo) {...}
// ✅ ХОРОШО ↴

const foo: MyInterface|null = ...;
if (foo) {...}
while (foo) {...}

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

// ❌ ПЛОХО ↴

enum SupportLevel {
  NONE,
  BASIC,
  ADVANCED,
}

const level: SupportLevel = ...;
if (level) {...}

const maybeLevel: SupportLevel|undefined = ...;
if (level) {...}
// ✅ ХОРОШО ↴

enum SupportLevel {
  NONE,
  BASIC,
  ADVANCED,
}

const level: SupportLevel = ...;
if (level !== SupportLevel.NONE) {...}

const maybeLevel: SupportLevel|undefined = ...;
if (level !== undefined && level !== SupportLevel.NONE) {...}

Другие типы значений могут быть либо неявно преобразованы в булевы значения, либо явно сравнены с помощью операторов сравнения:

// ✅ ХОРОШО ↴

// Явное сравнение > 0 это хорошо:
if (arr.length > 0) {...}
// так же как и полагаться на неявное булево преобразование:
if (arr.length) {...}

Переменные

Всегда используйте const или let для объявления переменных. По умолчанию используйте const, если не требуется переназначение переменной. Никогда не используйте var.

// ✅ ХОРОШО ↴

const foo = otherValue;  // Используйте, если "foo" никогда не меняется.
let bar = someValue;     // Используйте, если для "bar" когда-либо позднее будет присвоено значение 

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

// ❌ ПЛОХО ↴

var foo = someValue;     // Не используйте - область видимости var сложна и подвержена ошибкам.

Переменные не должны использоваться до их объявления.

Исключения (Exceptions)

Использование new при создании экземпляров класса Error

Всегда используйте new Error() при создании исключений вместо простого вызова Error(). В обоих случаях создается новый экземпляр Error, но использование new более согласуется с тем, как создаются экземпляры других объектов.

// ✅ ХОРОШО ↴

throw new Error('Foo is not a valid bar.');
// ❌ ПЛОХО ↴

throw Error('Foo is not a valid bar.');

При выбрасывании исключений используйте только экземпляры класса Error

JavaScript (и, следовательно, TypeScript) позволяет при выбрасывании исключений использовать произвольные значения. Однако если выброшенное значение не является экземпляром класса Error, то оно не получит записи трассировки стека, что затруднит отладку.

// ❌ ПЛОХО ↴

// плохо: не позволяет получить трассировку стека.
throw 'ой, ошибка!';

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

// ✅ ХОРОШО ↴

// При выбрасывании исключений используйте только экземпляры класса Error
throw new Error('ой, ошибка!');
// ... или подтипы класса Error
class MyError extends Error {}
throw new MyError('моя "ой, ошибка!"');

Перехват и проброс исключений

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

// ✅ ХОРОШО ↴

try {
  doSomething();
} catch (e: unknown) {
  // Все выбрасываемые исключения должны быть подтипами класса Error. Не обрабатывайте другие
  // возможные значения, кроме случаев, когда вы точно знаете, что именно они будут выброшены.
  assert(e, isInstanceOf(Error));
  displayError(e.message);
  // или проброс
  throw e;
}

Обработчики исключений не должны защитно обрабатывать типы, отличные от Error, за исключением случаев, когда достоверно известно, что вызываемый API выбрасывает исключения, не соответствующие типу Error, в нарушение вышеуказанного правила. В таком случае рекомендуется добавить комментарий, в котором специально указывается источник возникновения исключения, не соответствующего типу Error.

// ✅ ХОРОШО ↴

try {
  badApiThrowingStrings();
} catch (e: unknown) {
  // Примечание: это плохое API при выбрасывании исключения передает строку, вместо экземпляра класса Error
  if (typeof e === 'string') { ... }
}

Почему?

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

Итерация по объектам

Итерация по объектам с помощью for (... in ...) подвержена вероятным ошибкам, т.к. это включает в себя все перечисляемые свойства из цепочки прототипов.

Не используйте не фильтрованные for (... in ...) выражения:

// ❌ ПЛОХО ↴

for (const x in someObj) {
  // x может происходить от некоторого родительского прототипа!
}

Либо явно отфильтруйте значения с помощью оператора if, либо используйте for (... of Object.keys(...)).

// ✅ ХОРОШО ↴

for (const x in someObj) {
  if (!someObj.hasOwnProperty(x)) continue;
  // сейчас x был точно определен в принадлежности someObj
}
for (const x of Object.keys(someObj)) { // примечание: for _of_!
  // сейчас x был точно определен в принадлежности someObj
}
for (const [key, value] of Object.entries(someObj)) { // примечание: for _of_!
  // сейчас key был точно определен в принадлежности someObj
}

Итерация по массивам

Не используйте for (... in ...) для итерации по массивам. Это будет контринтуитивно давать индексы массива (в виде строк!), а не значения:

// ❌ ПЛОХО ↴

for (const x in someArray) {
  // x - это индекс!
}

Для итерации по массивам предпочтительно использовать for (... of someArr)[6]. Также приемлемо использовать Array.prototype.forEach или обычные циклы for:

// ✅ ХОРОШО ↴

for (const x of someArr) {
  // x - ссылается на значение из someArr
}

for (let i = 0; i < someArr.length; i++) {
  // Если необходим индекс, то используйте явный пересчет, а иначе используйте форму for/of.
  const x = someArr[i];
  // ...
}
for (const [i, x] of someArr.entries()) {
  // Альтернативная версия предыдущего.
}

Применение spread-оператора

Использование spread-оператора [...foo]; {...bar} является удобным сокращением для копирования массивов и объектов. При использовании spread-оператора для объектов, более поздние значения заменяют более ранние с тем же ключом.

// ✅ ХОРОШО ↴

const foo = {
  num: 1,
};

const foo2 = {
  ...foo,
  num: 5,
};

const foo3 = {
  num: 5,
  ...foo,
}

foo2.num === 5;
foo3.num === 1;

При использовании spread-оператора раскладываемое значение должно соответствовать создаваемому. Т.е. при создании объекта с spread-оператором можно использовать только объекты, а при создании массива раскладывайте только итерируемые объекты. Примитивы, включая null и undefined, не должны раскладываться.

// ❌ ПЛОХО ↴

const foo = {num: 7};
const bar = {num: 5, ...(shouldUseFoo && foo)}; // может быть undefined

// Создает {0: 'a', 1: 'b', 2: 'c'} но при этом не содержит длины (length)
const fooStrings = ['a', 'b', 'c'];
const ids = {...fooStrings};
// ✅ ХОРОШО ↴

const foo = shouldUseFoo ? {num: 7} : {};
const bar = {num: 5, ...foo};
const fooStrings = ['a', 'b', 'c'];
const ids = [...fooStrings, 'd', 'e'];

Операторы управления потоком & блоки

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

// ✅ ХОРОШО ↴

for (let i = 0; i < x; i++) {
  doSomethingWith(i);
}

if (x) {
  doSomethingWithALongMethodNameThatForcesANewLine(x);
}
// ❌ ПЛОХО ↴

if (x)
  doSomethingWithALongMethodNameThatForcesANewLine(x);

for (let i = 0; i < x; i++) doSomethingWith(i);

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

// ✅ ХОРОШО ↴

if (x) x.doFoo();

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

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

// ❌ ПЛОХО ↴

if (x = someFunction()) {
  // Присваивание легко перепутать с проверкой на равенство
  // ...
}
// ✅ ХОРОШО ↴

x = someFunction();
if (x) {
  // ...
}

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

// ✅ ХОРОШО ↴

while ((x = someFunction())) {
  // Двойная скобка указывает на то, что присваивание сделано намеренно
  // ...
}

Switch оператор

Каждый switch оператор должен включать в себя блок по умолчанию (default), даже если там не содержится кода.

// ✅ ХОРОШО ↴

switch (x) {
  case Y:
    doSomethingElse();
    break;
  default:
    // ничего не делать.
}

Непустые группы операторов (case ...) не должны проваливаться (обеспечивается настройками компилятора[7]):

// ❌ ПЛОХО ↴

switch (x) {
  case X:
    doSomething();
    // дальнейший пропуск - не разрешен!
  case Y:
    // ...
}

Допускается пропуск пустых групп операторов:

// ✅ ХОРОШО ↴

switch (x) {
  case X:
  case Y:
    doSomething();
    break;
  default: // ничего не делать.
}

Проверка равенства

Всегда используйте тройное равенство (===) и неравенство (!==). Операторы двойного равенства вызывают склонные к ошибкам приведения типов, которые трудны для понимания и работают медленнее в реализации виртуальных машин JavaScript. Смотрите также JavaScript таблицу равенства.

// ❌ ПЛОХО ↴

if (foo == 'bar' || baz != bam) {
  // Трудное для понимания поведение из-за преобразования типов.
}
// ✅ ХОРОШО ↴

if (foo === 'bar' || baz !== bam) {
  // Здесь все хорошо и понятно.
}

Исключение: При сравнении с значением null возможно использовать операторы == и != для общего охвата null и undefined значений.

// ✅ ХОРОШО ↴

if (foo == null) {
  // Будет срабатывать, когда foo равен null или undefined. 
}

Сохраняйте блоки try сфокусированными

Ограничьте количество кода внутри блока try, если это можно сделать без ущерба для читабельности.

// ❌ ПЛОХО ↴

try {
  const result = methodThatMayThrow();
  use(result);
} catch (error: unknown) {
  // ...
}
// ✅ ХОРОШО ↴

let result;
try {
  result = methodThatMayThrow();
} catch (error: unknown) {
  // ...
}
use(result);

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

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

Объявление функции (Function Declaration)

Предпочитайте function foo() { ... } для объявления именованных функций верхнего уровня.

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

// ✅ ХОРОШО ↴

interface SearchFunction {
  (source: string, subString: string): boolean;
}

const fooSearch: SearchFunction = (source, subString) => { ... };

Обратите внимание на различия между обсуждаемыми здесь объявлениями функций (function foo() {}) и функциональными выражениями (doSomethingWith(function() {});), которые обсуждаются ниже.

Функциональные выражения

Использование стрелочных функций в выражениях

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

// ✅ ХОРОШО ↴

bar(() => { this.doSomething(); })
// ❌ ПЛОХО ↴

bar(function() { ... })

Функциональные выражения (определенные с помощью ключевого слова function) возможно использовать только в том случае, если код должен динамически перепривязать this, хотя в коде в принципе не рекомендуется перепривязывать this. В коде обычных функций (в отличие от стрелочных функций и методов) не рекомендуется обращаться к this.

Использование выражений или блоков в качестве тела функции

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

// ✅ ХОРОШО ↴

// Для объявления функции верхнего уровня используйте Function Declarations.
function someFunction() {
  // Вполне подходит использование блочных тел стрелочных функций, т.е. у которых тело функции представляет => { } :
  const receipts = books.map((b: Book) => {
    const receipt = payMoney(b.price);
    recordTransaction(receipt);
    return receipt;
  });

  // Использование выражения в качестве тела функции тоже подходит, если возвращаемое значение будет использоваться:
  const longThings = myValues.filter(v => v.length > 1000).map(v => String(v));

  function payMoney(amount: number) {
    // Function Declarations - это хорошо, но не обращайтесь к `this` в них. 
  }

  // Вложенные стрелочные функции могут быть назначены константе
  const computeTax = (amount: number) => amount * 0.12;
}

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

// ❌ ПЛОХО ↴

// ПЛОХО: используйте блочное тело функции ({ ... }) если возвращаемое значение функции не используется.
myPromise.then(v => console.log(v));
// ✅ ХОРОШО ↴

// ХОРОШО: возвращаемое значение не используется, поэтому применяется блочное тело функции.
myPromise.then(v => {
  console.log(v);
});
// ХОРОШО: в коде можно использовать блочное тело функции для повышения удобочитаемости.
const transformed = [1, 2, 3].map(v => {
  const intermediate = someComplicatedExpr(v);
  const more = acrossManyLines(intermediate);
  return worthWrapping(more);
});

Перепривязывание this

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

// ❌ ПЛОХО ↴

function clickHandler() {
  // Плохо: что такое «this» в этом контексте?
  this.textContent = 'Hello';
}
// Плохо: `this` неявно ссылается на document.body .
document.body.onclick = clickHandler;
// ✅ ХОРОШО ↴

// Хорошо: явная ссылка на объект из стрелочной функции.
document.body.onclick = () => { document.body.textContent = 'hello'; };
// Альтернатива: взять явно заданный параметр
const setTextFn = (e: HTMLElement) => { e.textContent = 'hello'; };
document.body.onclick = setTextFn.bind(null, document.body);

Стрелочные функции как свойства

В классах обычно не рекомендуется содержать свойства, которые проинициализированы как стрелочные функции. Использование стрелочных функций как свойств требует чтобы вызывающая их функция корректно понимала, что у вызываемой функции уже есть привязанный this, что увеличивает путаницу в понимании того, что такое this, а сами места вызовов и ссылки использующие эти функции могут смотреться некорректно работающими (т.к. это требует дополнительных знаний об окружении за пределами локальной области вызывающей функции, чтобы определить, что они корректны). В коде рекомендуется всегда использовать стрелочные функции для вызова методов экземпляра (const handler = (x) => { this.listener(x); };) и не рекомендуется получать или передавать ссылки на методы экземпляра (const handler = this.listener; handler(x);).

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

// ❌ ПЛОХО ↴

class DelayHandler {
  constructor() {
    // Проблема: `this` не сохраняется в функции обратного вызова. `this` в обратном вызове
    // не будет экземпляром DelayHandler.
    setTimeout(this.patienceTracker, 5000);
  }
  private patienceTracker() {
    this.waitedPatiently = true;
  }
}
// ❌ ПЛОХО ↴

// Стрелочные функции обычно не рекомендуется задавать свойствам.
class DelayHandler {
  constructor() {
    // Плохо: этот код выглядит так, как будто тут забыли привязать `this`. 
    setTimeout(this.patienceTracker, 5000);
  }
  private patienceTracker = () => {
    this.waitedPatiently = true;
  }
}
// ✅ ХОРОШО ↴

// Явное управление `this` во время вызова.
class DelayHandler {
  constructor() {
    // По возможности используйте анонимные функции.
    setTimeout(() => {
      this.patienceTracker();
    }, 5000);
  }
  private patienceTracker() {
    this.waitedPatiently = true;
  }
}

Обработчики событий

Обработчики событий могут использовать стрелочные функции, когда нет необходимости удалять обработчик (например, если событие генерируется самим классом). Если для обработчика впоследствии требуется удаление, тогда правильным подходом будет использование назначенной свойству стрелочной функции, поскольку они автоматически захватывают this и при этом обеспечивается постоянная ссылка на обработчик для его последующего удаления.

// ✅ ХОРОШО ↴

// Обработчики событий могут быть анонимными функциями или назначенные свойствам стрелочными функциями.
class Component {
  onAttached() {
    // Событие генерируется этим классом, удалять его не нужно.
    this.addEventListener('click', () => {
      this.listener();
    });
    // this.listener это постоянная ссылка на функцию-обработчик, которую мы позже можем удалить.
    window.addEventListener('onbeforeunload', this.listener);
  }
  onDetached() {
    // Событие генерируется окном (window). Если мы не удалим функцию-обработчик (this.listener), то она
    // сохранит ссылку на `this` к которой привязана, что приведет к утечке памяти.
    window.removeEventListener('onbeforeunload', this.listener);
  }
  // Стрелочная функция, хранящаяся в свойстве, автоматически привязывается к `this`.
  private listener = () => {
    confirm('Вы хотите покинуть страницу?');
  }
}

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

// ❌ ПЛОХО ↴

// Привязка слушателей создает временную ссылку, которая недоступна для удаления.
class Component {
  onAttached() {
    // Это создает временную ссылку, которая нам не будет доступна для удаления.
    window.addEventListener('onbeforeunload', this.listener.bind(this));
  }
  onDetached() {
    // метод bind каждый раз создает новую ссылку, поэтому эта строка не делает ничего.
    window.removeEventListener('onbeforeunload', this.listener.bind(this));
  }
  private listener() {
    confirm('Вы хотите покинуть страницу?');
  }
}

Автоматическая вставка точки с запятой

Не следует полагаться на автоматическую вставку точки с запятой (ASI[8]). Явно завершайте все операторы с помощью точки с запятой. Это предотвращает ошибки, возникающие из-за неправильной вставки точки с запятой, а также обеспечивает совместимость с инструментами, которые имеют ограниченную поддержку ASI (например, clang-format).

@ts-ignore

Не используйте @ts-ignore, а также такие варианты, как @ts-expect-error или @ts-nocheck. На первый взгляд кажется, что это простой способ исправить ошибку компилятора, но на практике конкретная ошибка компилятора часто вызывается более серьезной проблемой, которая может быть исправлена более явным путем.

Например, если вы используете @ts-ignore для подавления ошибок типизации, то будет трудно предсказать, какие типы в конечном итоге будет видеть окружающий код. Для многих ошибок типизации, полезны советы в разделе как лучше всего использовать any.

Утверждения типа (Type Assertions) и утверждения ненулевого значения (Non-nullability Assertions)

Утверждения типа (x as SomeType) и утверждения ненулевого значения (y!) не безопасны. Оба только заглушают компилятор TypeScript, но не вставляют никаких проверок во время выполнения, чтобы соответствовать этим утверждениям, поэтому они могут привести к сбою вашей программы во время выполнения.

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

Вместо этого:

// ❌ ПЛОХО ↴

(x as Foo).foo();

y!.bar();

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

// ✅ ХОРОШО ↴

// предположим, что Foo - это класс.
if (x instanceof Foo) {
  x.foo();
}

if (y) {
  y.bar();
}

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

// ✅ ХОРОШО ↴

// x это Foo, потому что ...
(x as Foo).foo();

// y не может быть null, потому что ...
y!.bar();

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

Синтаксис утверждения типа

Утверждения типа должны использовать синтаксис as (в отличие от синтаксиса угловых скобок). Это позволяет заключить утверждение в круглые скобки при обращении к элементу.

// ❌ ПЛОХО ↴

const x = (<Foo>z).length;
const y = <Foo>z.length;
// ✅ ХОРОШО ↴

// z должен быть Foo, потому что ...
const x = (z as Foo).length;

Утверждение типа & объектные литералы

Используйте аннотации типа (: Foo) вместо утверждения типа (as Foo) для указания типа объектного литерала. Это позволяет обнаружить ошибки рефакторинга, когда поля интерфейса меняются со временем.

// ❌ ПЛОХО ↴

interface Foo {
  bar: number;
  baz?: string;  // был "bam", но позднее был переименован в "baz".
}

const foo = {
  bar: 123,
  bam: 'abc',  // нет ошибки!
} as Foo;

function func() {
  return {
    bar: 123,
    bam: 'abc',  // нет ошибки!
  } as Foo;
}
// ✅ ХОРОШО ↴

interface Foo {
  bar: number;
  baz?: string;
}

const foo: Foo = {
  bar: 123,
  bam: 'abc',  // жалуется на то, что "bam" не был объявлен в Foo.
};

function func(): Foo {
  return {
    bar: 123,
    bam: 'abc',  // жалуется на то, что "bam" не был объявлен в Foo.
  };
}

Объявление свойств элементов

В объявлениях интерфейсов и классов, для разделения объявлений отдельных членов, должна использоваться точка с запятой:

// ✅ ХОРОШО ↴

interface Foo {
  memberA: string;
  memberB: number;
}

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

// ❌ ПЛОХО ↴

interface Foo {
  memberA: string,
  memberB: number,
}

Встраиваемое объявление объектного типа в качестве разделителя должно использовать запятую:

// ✅ ХОРОШО ↴

type SomeTypeAlias = {
  memberA: string,
  memberB: number,
};

let someProperty: {memberC: string, memberD: number};

Совместимость с оптимизациями доступа к свойствам

Код не должен смешивать доступ к свойству в кавычках с доступом к свойству через точку:

// ❌ ПЛОХО ↴

// Плохо: код должен использовать либо доступ без кавычек, либо доступ в кавычках для любого свойства
// единообразно для всего приложения:
console.log(x['someField']);
console.log(x.someField);

Свойства, которые используются извне по отношению к приложению, например, свойства объектов JSON или внешних API, должны быть доступны с использованием .dotted нотации, а также должны быть объявлены в качестве так называемых внешних свойств посредством применения модификатора declare.

// ✅ ХОРОШО ↴

// Хорошо: использование "declare" для объявления типов, которые используются извне
// по отношению к приложению, для того, чтобы их свойства не переименовывались.
declare interface ServerInfoJson {
  appVersion: string;
  user: UserJson; // Примечание: UserJson также должен использовать `declare`!
}
// serverResponse должен быть ServerInfoJson в соответствии с контрактом приложения.
const data = JSON.parse(serverResponse) as ServerInfoJson;
console.log(data.appVersion); // Тип защищен и переименование безопасно!

Совместимость с оптимизациями импорта объектов модуля

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

// ❌ ПЛОХО ↴

import * as utils from 'utils';
class A {
  readonly utils = utils;  // <--- ПЛОХО: передача всего объекта модуля
}
// ✅ ХОРОШО ↴

import * as utils from 'utils';
class A {
  readonly utils = {method1: utils.method1, method2: utils.method2};
}

или более кратко:

// ✅ ХОРОШО ↴

import {method1, method2} from 'utils';
class A {
  readonly utils = {method1, method2};
}

Исключение

Это правило согласованности с оптимизациями применимо ко всем веб-приложениям. Оно не применяется к коду, который выполняется только на стороне сервера (например, в NodeJS для выполнения тестов). Но все же для поддержания чистоты кода очень поощряется всегда объявлять все типы и избегать смешивания доступа к свойствам с кавычками и без кавычек.

Константные перечисления

В коде не должны использоваться const enum, вместо этого используйте обычный enum.

Почему?

В TypeScript перечисления и так не могут быть изменены, а const enum — это отдельная особенность языка, связанная с оптимизацией, которая делает перечисление невидимым для пользователей JavaScript модуля.

Команды отладчика

Команды отладчика (наподобие debugger;) не должны включаться в рабочий код.

// ❌ ПЛОХО ↴

function debugMe() {
  debugger;
}

Декораторы

Декораторы обозначаются с помощью префикса @, например @MyDecorator.

Не определяйте новых декораторов. Используйте только те декораторы, которые определены фреймворками:

Почему?

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

При использовании декораторов, декоратор должен непосредственно предшествовать элементу, к которому он применяется, без пустых строк между ними:

// ✅ ХОРОШО ↴

/** Комментарии JSDoc идут перед декораторами */
@Component({...})  // Примечание: после декоратора не должно быть пустой строки. 
class MyComp {
  @Input() myField: string;  // Декораторы полей могут находиться на одной линии... 

  @Input()
  myOtherField: string;  // ...  или переноситься.
}

Организация структуры исходного кода

Модули

Использование путей в импортах

В TypeScript коде обязательно должны указываться пути при импорте другого TypeScript кода. Возможно указывать относительные пути, т.е. начинающиеся с . или .. или с базовой директории, как например root/path/to/file.

В коде рекомендуется использовать относительные импорты (./foo) вместо абсолютных импортов path/to/foo при ссылке на файлы в пределах одного и того же (в логическом смысле) проекта, т.к. это позволяет перемещать весь проект без внесения изменений в эти импорты.

Рассмотрите возможность ограничения количества родительских шагов (../../../), т.к. это может затруднить понимание структуры модулей и путей.

// ✅ ХОРОШО ↴

import {Symbol1} from 'path/from/root';
import {Symbol2} from '../parent/file';
import {Symbol3} from './sibling';

Пространства имен (namespace) & Модули

TypeScript поддерживает два метода организации кода: пространства имен (namespaces) и модули, но использование пространств имен необходимо избегать. Т.е. ваш код должен ссылаться на код в других файлах с помощью импорта и экспорта вида import {foo} from 'bar';

В вашем коде не должны использоваться namespace Foo { ... } конструкции. Пространства имен (namespace) возможно использовать только тогда, когда это необходимо для взаимодействия с внешним сторонним кодом. Чтобы семантически разделить пространство имен вашего кода, используйте отдельные файлы.

В коде не должны использоваться require (как в import x = require('...');) для импортов. Используйте синтаксис модулей ES6.

// ❌ ПЛОХО ↴

// Плохо: не используйте пространства имен:
namespace Rocket {
  function launch() { ... }
}

// Плохо: не используйте <reference>
/// <reference path="..."/>

// Плохо: не используйте require()
import x = require('mydep');

Примечание: В TypeScript пространства имен (namespace) раньше назывались внутренними модулями и использовали ключевое слово module в виде module Foo { ... }. Не используйте такую форму. Всегда используйте ES6 импорты.

Экспорты

По всему коду используйте именованные экспорты:

// ✅ ХОРОШО ↴

// Использование именованного экспорта:
export class Foo { ... }

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

// ❌ ПЛОХО ↴

// Не используйте экспорт по умолчанию:
export default class Foo { ... } // ПЛОХО!

Почему?

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

// ❌ ПЛОХО ↴

import Foo from './bar';  // Валидно.
import Bar from './bar';  // Также валидно.

Преимущество именованного экспорта заключается в том, что оно приводит к ошибкам, когда операторы импорта пытаются импортировать что-то, что не было объявлено. В foo.ts:

// ❌ ПЛОХО ↴

const foo = 'blah';
export default foo;

И в bar.ts:

// ❌ ПЛОХО ↴

import {fizz} from './foo';

В результате возникает ошибка error TS2614: Module '"./foo"' has no exported member 'fizz'. Если указать в bar.ts:

// ❌ ПЛОХО ↴

import fizz from './foo';

В результате получается fizz === foo, что может быть неожиданным и затрудняющим отладку.

Кроме того, экспорт по умолчанию побуждает людей помещать все в один большой объект, чтобы разместить все вместе в пространстве имен:

// ❌ ПЛОХО ↴

export default class Foo {
  static SOME_CONSTANT = ...
  static someHelpfulFunction() { ... }
  ...
}

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

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

// ✅ ХОРОШО ↴

export const SOME_CONSTANT = ...
export function someHelpfulFunction()
export class Foo {
  // тут только элементы класса
}

Область видимости экспортируемых элементов

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

Мутабельность экспортов

Независимо от технической стороны, мутабельные экспорты могут создавать трудно понимаемый и отлаживаемый код, особенно при реэкспорте в различных модулях. Если по другому сформулировать это правило, то export let не допускается.

// ❌ ПЛОХО ↴

export let foo = 3;
// В чистом ES6 foo является мутабельным, и импортеры будут видеть изменение его значения уже через секунду.
// В TS (прим. пер.: в версии TS < 3.9, при использовании модулей CommonJS), если foo реэкспортируется вторым файлом,
// импортеры не увидят изменения значения.
// Прим. пер.: В версии TS >= 3.9, это будет работать по аналогии с ES6.
window.setTimeout(() => {
  foo = 4;
}, 1000 /* миллисекунды */);

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

// ✅ ХОРОШО ↴

let foo = 3;
window.setTimeout(() => {
  foo = 4;
}, 1000 /* миллисекунды */);
// Используйте явно заданный геттер для доступа к мутабельному экспорту.
export function getFoo() { return foo; };

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

// ✅ ХОРОШО ↴

function pickApi() {
  if (useOtherApi()) return OtherApi;
  return RegularApi;
}
export const SomeApi = pickApi();

Классы-контейнеры

Не создавайте классы-контейнеры со статическими методами или свойствами ради пространства имен.

// ❌ ПЛОХО ↴

export class Container {
  static FOO = 1;
  static bar() { return 1; }
}

Вместо этого экспортируйте отдельные константы и функции:

// ✅ ХОРОШО ↴

export const FOO = 1;
export function bar() { return 1; }

Импорты

В ES6 и TypeScript есть четыре варианта операторов импорта:

Вид импортаПримерНазначение
модульныйimport * as foo from '...';Импорты TypeScript
деструктурирующийimport {SomeThing} from '...';Импорты TypeScript
по умолчаниюimport SomeThing from '...';Только для поддержки стороннего кода, который их требует
для использования побочных эффектовimport '...';Только для импорта библиотек ради получения их сторонних эффектов при загрузке (таких как пользовательские элементы)
// ✅ ХОРОШО ↴

// Хорошо: выберите один из двух вариантов в зависимости от ситуации (см. ниже).
import * as ng from '@angular/core';
import {Foo} from './foo';

// Только при необходимости: импорт по умолчанию.
import Button from 'Button';

// Иногда необходимо импортировать библиотеки для получения их вспомогательных эффектов:
import 'jasmine';
import '@polymer/paper-button';

Модульный и деструктурирующий импорты

Как модульный, так и деструктурирующий импорт имеют свои преимущества в зависимости от ситуации.

Несмотря на *, импорт модуля не сопоставим с wildcard импортом , который встречается в других языках. Вместо этого импорт модулей дает имя всему модулю и каждой связанной с упомянутым модулем ссылке на элемент, что может сделать код более читабельным и обеспечивает функцию автоматического определения всех элементов в модуле. Они также требуют меньшего количества операций импорта (все элементы доступны), меньше коллизий имен и позволяют использовать более лаконичные имена в импортируемом модуле. Импорт модулей особенно полезен при использовании множества различных элементов из больших API.

Деструктурирующие импорты дают локальные имена для каждого импортируемого элемента. Они позволяют использовать более краткий и лаконичный код при использовании импортируемого элемента, что особенно полезно для очень часто используемых элементов, как например, describe и it в Jasmine.

// ❌ ПЛОХО ↴

// Плохо: слишком длинный оператор импорта с излишними пространствами имен.
import {TableViewItem, TableViewHeader, TableViewRow, TableViewModel,
  TableViewRenderer} from './tableview';
let item: TableViewItem = ...;
// ✅ ХОРОШО ↴

// Лучше: используйте модуль для пространства имен. 
import * as tableview from './tableview';
let item: tableview.Item = ...;
// ✅ ХОРОШО ↴

import * as testing from './testing';

// Все тесты будут неоднократно использовать одни и те же три функции.
// При импорте только некоторых определенных элементов, которые используются очень часто, также
// рассмотрите возможность импорта элементов напрямую (см. пример ниже).
testing.describe('foo', () => {
  testing.it('bar', () => {
    testing.expect(...);
    testing.expect(...);
  });
});
// ✅ ХОРОШО ↴

// Лучше: дайте локальные имена распространенным функциям.
import {describe, it, expect} from './testing';

describe('foo', () => {
  it('bar', () => {
    expect(...);
    expect(...);
  });
});
...

Переименование импортов

В коде рекомендуется устранить возможные конфликты имен используя импорт модулей и переименовывая сами экспорты. При необходимости в коде можно переименовывать импорты (import {SomeThing as SomeOtherThing}).

Три примера, когда переименование может быть полезным:

  1. Если необходимо избежать коллизий с другими импортируемыми элементами;
  2. Если имя импортированного элемента генерируется;
  3. При импорте элементов, имена которых сами по себе неясны, переименование может улучшить ясность кода. Например, при использовании RxJS функция from может быть более удобочитаемой, если ее переименовать в observableFrom.

Импорты & экспорты типов

Не используйте import type {...} или export type {...}.

// ❌ ПЛОХО ↴

import type {Foo};
export type {Bar};
export type {Bar} from './bar';

Вместо этого просто используйте обычные импорты и экспорты:

// ✅ ХОРОШО ↴

import {Foo} from './foo';
export {Bar} from './bar';

Примечание: это не относится к применению export в отношении определений типов, т.е. export type Foo = ...;.

// ✅ ХОРОШО ↴

export type Foo = string;

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

Почему?

Инструментарий TypeScript автоматически определяет различия и не внедряет динамическую (runtime) загрузку для обращений к типам. Это обеспечивает более удобный UX для разработчиков: переключение туда-сюда между import type и import весьма утомительно. В то же время, import type не дает никаких гарантий: ваш код все равно может иметь жесткую зависимость от какого-либо импорта через различные транзитивные пути.

Если вам необходима обязательная динамическая (runtime) загрузка для получения сторонних эффектов, используйте import '...';. См. импорты.

export type может показаться полезным, чтобы избежать какого-либо экспортирования значения элемента в API. Однако и это не дает гарантий, т.к. последующий код может по-прежнему импортировать API другим путем. Лучший способ для разделения и гарантии использования API по типу и значению - разделить элементы, например, на UserService и AjaxUserService. Это менее подвержено ошибкам и лучше передает смысл.

Формирование по функциональному назначению

Формируйте пакеты по их функциональному назначению, а не по типам. Например, для интернет-магазина рекомендуется иметь пакеты с названиями products, checkout, backend, а не views, models, controllers.

Система типов

Вывод типа

В коде возможно полагаться на вывод типа, реализуемый компилятором TypeScript для всех типов выражений (переменных, полей класса, возвращаемых типов и т.д.).

// ✅ ХОРОШО ↴

const x = 15;  // Тип выведен.

Не указывайте типы для тривиально выводимых типов: переменных или параметров, инициализированных строковыми (string), числовыми (number), логическими (boolean) литералами, литералами регулярных выражений (RegExp) или выражением new.

// ❌ ПЛОХО ↴

const x: boolean = true;  // Плохо: 'boolean' здесь не способствует удобочитаемости
// ❌ ПЛОХО ↴

// Плохо: 'Set' тривиально выводится из инициализации
const x: Set<string> = new Set();
// ✅ ХОРОШО ↴

const x = new Set<string>();

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

// ❌ ПЛОХО ↴

// Трудно предположить тип 'value' без аннотации.
const value = await rpc.getSomeValue().transform();
// ✅ ХОРОШО ↴

// Можно с первого взгляда определить тип 'value'.
const value: string[] = await rpc.getSomeValue().transform();

Необходимость аннотации определяется рецензентом кода.

Возвращаемые типы

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

Явная типизация неявных возвращаемых значений функций и методов имеет два преимущества:

Null & Undefined

TypeScript поддерживает типы null и undefined. Nullable-типы могут быть созданы как union-типы (string|null), что также относится и к undefined. Специального синтаксиса для объединений с null и undefined не существует.

В TypeScript коде для обозначения отсутствия значения можно использовать undefined или null, при этом нет общих рекомендаций для предпочтения одного другому. Множество JavaScript API используют undefined (например, Map.get), в то время как во многих DOM API и Google API используется null (например, Element.getAttribute), поэтому подходящее обозначение отсутствия значения зависит от контекста.

Nullable/undefined псевдонимы типов

Псевдонимы типов не должны включать |null или |undefined в union-тип. Псевдонимы, допускающие значение null, обычно указывают на то, что значения null проходят через слишком много слоев приложения и это затуманивает источник исходной проблемы, которая привела к значению null. Они также делают неясной ситуацию, когда конкретные значения в классе или интерфейсе могут отсутствовать.

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

// ❌ ПЛОХО ↴

// Плохо
type CoffeeResponse = Latte|Americano|undefined;

class CoffeeService {
  getLatte(): CoffeeResponse { ... };
}
// ✅ ХОРОШО ↴

// Лучше
type CoffeeResponse = Latte|Americano;

class CoffeeService {
  getLatte(): CoffeeResponse|undefined { ... };
}
// ✅ ХОРОШО ↴

// Наилучший вариант
type CoffeeResponse = Latte|Americano;

class CoffeeService {
  getLatte(): CoffeeResponse {
    return assert(fetchResponse(), 'Кофеварка сломана, подайте заявку');
  };
}

Опциональные свойства & тип |undefined

Также TypeScript поддерживает специальную конструкцию для опциональных параметров и полей, используя ?:

// ✅ ХОРОШО ↴

interface CoffeeOrder {
  sugarCubes: number;
  milk?: Whole|LowFat|HalfHalf;
}

function pourCoffee(volume?: Milliliter) { ... }

Опциональные параметры неявно включают |undefined в свой тип. Однако они отличаются тем, что их можно не указывать при составлении выражения или вызове метода. Например, {sugarCubes: 1} является валидным CoffeeOrder поскольку milk является опциональным.

Используйте опциональные поля (в интерфейсах или классах) и параметры вместо |undefined типов.

Для классов лучше вообще избегать этого приёма и инициализировать как можно больше полей.

// ✅ ХОРОШО ↴

class MyClass {
  field = '';
}

Структурная & номинальная типизация

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

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

При предоставлении реализации, основанной на структуре, явно указывайте тип в объявлении элемента (это позволяет более точно проверить тип и сообщить об ошибке).

// ✅ ХОРОШО ↴

const foo: Foo = {
  a: 123,
  b: 'abc',
}
// ❌ ПЛОХО ↴

const badFoo = {
  a: 123,
  b: 'abc',
}

Почему?

Приведенный выше объект badFoo полагается на вывод типа. В badFoo могут быть добавлены дополнительные поля, а тип будет выводиться на основе самого объекта.

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

// ✅ ХОРОШО ↴

interface Animal {
  sound: string;
  name: string;
}

function makeSound(animal: Animal) {}

/**
 * 'cat' имеет выводимый тип '{sound: string}'
 */
const cat = {
  sound: 'meow',
};

/**
 * 'cat' не соответствует требуемому для функции типу,
 * поэтому компилятор TypeScript выдает ошибку здесь,
 * что может быть очень далеко от места определения 'cat'.
 */
makeSound(cat);

/**
 * Horse имеет структурный тип, и ошибка типа возникает здесь, а не в вызове функции,
 * поскольку 'horse' не соответствует требованиям типа 'Animal'
 */
const horse: Animal = {
  sound: 'niegh',
};

const dog: Animal = {
  sound: 'bark',
  name: 'MrPickles',
};

makeSound(dog);
makeSound(horse);

Интерфейсы и псевдонимы типов

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

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

// ✅ ХОРОШО ↴

interface User {
  firstName: string;
  lastName: string;
}
// ❌ ПЛОХО ↴

type User = {
  firstName: string,
  lastName: string,
}

Почему?

Эти формы почти эквивалентны, поэтому следуя принципу выбора только одной из двух форм, для предотвращения вариативности, нам стоит выбрать одну из них. Кроме того, существуют также интересные технические причины, по которым предпочтение отдается интерфейсу. На той странице также приводятся слова руководителя команды разработчиков TypeScript: "Честно говоря, я считаю, что на самом деле это должны быть просто интерфейсы для всего, что они могут моделировать. Нет особой выгоды в псевдонимах типов, когда существует так много проблем с их отображением и производительностью".

Тип Array<T>

Для простых типов (содержащих только буквенно-цифровые символы и точку) используйте синтаксический сахар для массивов, T[], а не более длинную форму Array<T>.

Для чего-то более сложного используйте более длинную форму Array<T>.

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

Это также относится к readonly T[] и ReadonlyArray<T>.

// ✅ ХОРОШО ↴

const a: string[];
const b: readonly string[];
const c: ns.MyObj[];
const d: Array<string|number>;
const e: ReadonlyArray<string|number>;
const f: InjectionToken<string[]>;  // Используйте синтаксический сахар для вложенных типов
// ❌ ПЛОХО ↴

const a: Array<string>;            // синтаксический сахар короче 
const b: ReadonlyArray<string>;
const c: {n: number, s: string}[]; // фигурные/круглые скобки ухудшают читабельность
const d: (string|number)[];
const e: readonly (string|number)[];

Индексируемые типы / индексные сигнатуры ({[key: string]: T})

В JavaScript принято использовать объект в качестве ассоциативного массива (он же карта (map), хеш-таблица, или словарь). В TypeScript такие объекты могут быть типизированы с использованием индексной сигнатуры ([k: string]: T):

// ✅ ХОРОШО ↴

const fileSizes: {[fileName: string]: number} = {};
fileSizes['readme.txt'] = 541;

В TypeScript укажите осмысленное обозначение для ключа. (Обозначение существует только для документации; в остальном оно не используется.)

// ❌ ПЛОХО ↴

const users: {[key: string]: number} = ...;
// ✅ ХОРОШО ↴

const users: {[userName: string]: number} = ...;

Вместо использования одного из тех вариантов, рассмотрите возможность использования Map и Set типов ES6. Объекты JavaScript обладают довольно неожиданным нежелательным поведением, а типы ES6 более явно передают ваши намерения. Также, Set могут хранить значения, а Map еще и ключи, отличные от string.

Встроенный в TypeScript тип Record<Keys, ValueType> позволяет создавать типы с определенным набором ключей. Это отличается от ассоциативных массивов тем, что ключи известны статически. См. рекомендации по этому вопросу ниже.

Сопоставленные (Mapped) & Условные (Conditional) Типы

В TypeScript сопоставленные и условные типы позволяют определять новые типы на основе других типов. Стандартная библиотека TypeScript включает в себя ряд основанных на этих операциях типов (Record, Partial, Readonly и др.).

Эти особенности системы типов позволяют лаконично задавать типы и создавать мощные, но в то же время безопасные абстракции типов. Однако они обладают определенным количеством недостатков:

Рекомендация по стилю такова:

Например, встроенный в TypeScript тип Pick<T, Keys> позволяет создать новый тип на основе подмножества другого типа T, но простое расширение интерфейса часто может быть проще для понимания.

// ✅ ХОРОШО ↴

interface User {
  shoeSize: number;
  favoriteIcecream: string;
  favoriteChocolate: string;
}

// В типе FoodPreferences есть favoriteIcecream и favoriteChocolate, но нет shoeSize.
type FoodPreferences = Pick<User, 'favoriteIcecream'|'favoriteChocolate'>;

Это эквивалентно указанию свойств в интерфейсе FoodPreferences:

// ✅ ХОРОШО ↴

interface FoodPreferences {
  favoriteIcecream: string;
  favoriteChocolate: string;
}

Чтобы сократить количество дублирований, User может расширить FoodPreferences или (что, возможно, лучше) вложить отдельное поле для указания предпочтений в еде:

// ✅ ХОРОШО ↴

interface FoodPreferences { /* как описано выше */ }
interface User extends FoodPreferences {
  shoeSize: number;
  // также включает в себя предпочтения.
}

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

Тип any

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

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

Предоставление более специфичного типа

Используйте интерфейсы, встраиваемый объектный тип или псевдоним типа:

// ✅ ХОРОШО ↴

// Используйте декларируемые интерфейсы для представления серверного JSON.
declare interface MyUserJson {
  name: string;
  email: string;
}

// Используйте псевдонимы типов для тех типов, которые приходится писать многократно.
type MyType = number|string;

// Или используйте встраиваемый объектный тип для возврата комплексных значений.
function getTwoThings(): {something: number, other: string} {
  // ...
  return {something, other};
}

// Используйте дженерик там, где в ином случае библиотека указала бы `any`,
// чтобы обозначить, что ей все равно, с каким типом работает пользователь (но обратите 
// внимание на раздел "Возвращаемый тип представлен только дженериком" представленный ниже).
function nicestElement<T>(items: T[]): T {
  // Поиск наиболее подходящего элемента в items.
  // Код может также накладывать ограничения на T, например <T extends HTMLElement>.
}

Использование unknown вместо any

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

// ✅ ХОРОШО ↴

// Можно присваивать любое значение (включая null или undefined), но нельзя 
// использовать его без сужения типа или приведения.
const val: unknown = value;
// ❌ ПЛОХО ↴

const danger: any = value /* результат произвольного выражения */;
danger.whoops();  //  Этот доступ к переменной абсолютно бесконтролен

Чтобы благополучно использовать значения типа unknown, следует сужать тип с помощью защитников типа (type guards).

Подавление предупреждений линтера, связанных с использованием any

Иногда использование any вполне оправдано, например, в тестах для создания Mock-объектов. В таких случаях добавьте комментарий, который подавляет предупреждение линтера, и задокументируйте, почему это решение оправдано.

// ✅ ХОРОШО ↴

// Этому тесту нужна только частичная реализация BookService,
// и если мы что-то упустили, тест очевидно провалится
// Это намеренно небезопасный частичный Mock-объект
// tslint:disable-next-line:no-any
const mockBookService = ({get() { return mockBook; }} as any) as BookService;
// Корзина покупателя (класс ShoppingCart) в этом тесте не используется
// tslint:disable-next-line:no-any
const component = new MyComponent(mockBookService, /* неиспользуемый ShoppingCart */ null as any);

Кортежные типы

Если у вас возникнет соблазн создать парный тип, то используйте вместо него кортежный тип:

// ❌ ПЛОХО ↴

interface Pair {
  first: string;
  second: string;
}
function splitInHalf(input: string): Pair {
  ...
  return {first: x, second: y};
}
// ✅ ХОРОШО ↴

function splitInHalf(input: string): [string, string] {
  ...
  return [x, y];
}

// Используйте это как:
const [leftHalf, rightHalf] = splitInHalf('my string');

Однако часто бывает яснее, если свойствам даются осмысленные имена.

Если объявление интерфейса (interface) слишком обременительно, можно использовать встраиваемый объектным литералом тип:

// ✅ ХОРОШО ↴

function splitHostPort(address: string): {host: string, port: number} {
  ...
}

// Используйте это как:
const address = splitHostPort(userAddress);
use(address.port);

// Вы также можете использовать деструктуризацию, чтобы получить поведение, подобное разложению кортежа на отдельные переменные:
const {host, port} = splitHostPort(userAddress);

Типы-обертки

Есть несколько типов, связанных с JavaScript примитивами, которые не рекомендуется когда-либо использовать:

Кроме того, никогда не вызывайте типы-обертки в качестве конструкторов (с помощью new).

Возвращаемый тип представлен только дженериком

Избегайте создания API у которых возвращаемый тип представлен только дженериком. При работе с существующими API у которых возвращаемый тип представлен только дженериком, всегда явно указывайте дженерик [10].

Согласованность

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

Цели

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

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

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

    Примеры:

    • Типом any легко злоупотребить (действительно ли эта переменная может быть и числом и вызываться как функция?), поэтому у нас есть рекомендации по его использованию.
    • В TypeScript пространство имен (namespace) создает проблемы с оптимизациями Closure.
    • Точки в именах файлов делают их уродливыми/запутанными для импорта из JavaScript.
    • Статические функции в классах оптимизируются довольно запутанно, в то время как функции на уровне файлов достигают тех же целей.
    • Пользователи, не знающие о ключевом слове private, попытаются скрыть имена своих функций с помощью подчеркивания.
  2. Код в различных проектах рекомендуется разрабатывать единообразно, с учетом незначительных отклонений.

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

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

    Примеры:

    • Стиль написания имен с использованием заглавных букв.
    • x as T синтаксис по сравнению с эквивалентным синтаксисом <T>x (запрещено).
    • Array<[number, number]> по сравнению с [number, number][].
  3. Код рекомендуется писать так, чтобы он был поддерживаемым в долгосрочной перспективе.

    Код обычно живет дольше, чем над ним работает его автор и команда специалистов по TypeScript должна обеспечить работоспособность всего кода Google в будущем.

    Примеры:

    • Мы используем программы для автоматизации изменений в коде, поэтому код автоматически форматируется, чтобы программа легко соблюдала правила оформления пробельных символов.
    • Мы предъявляем требования к единому набору флагов компиляции Closure, поэтому конкретная библиотека TS может быть написана с учетом определенного набора флагов, и пользователи всегда могут безопасно использовать разделяемые библиотеки.
    • Код должен импортировать библиотеки, которые он использует (strict deps - строгие зависимости), чтобы рефакторинг в какой-либо зависимости не изменил зависимости его пользователей.
    • Мы просим пользователей писать тесты. Без тестов мы не можем быть уверены, что изменения, которые вносятся в язык, не нарушат работу пользователей.
  4. Рецензенты кода должны быть сосредоточены на улучшении качества кода, а не на соблюдении произвольных правил.

    Часто хорошим знаком считается, если есть возможность реализовать ваше правило в качестве автоматической проверки. Это также способствует принципу №3.

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


  1. Прим. пер.: В оригинале используются термины MUST и SHOULD которые зачастую переводят буквально как должен. При этом MUST носит обязательный характер, а SHOULD - рекомендательный. Т.к. в русском языке такие термины, как: "должен", "обязан", "стоит", "необходимо" многими воспринимаются как имеющими строго обязательный характер, при буквальном переводе это может ввести в заблуждение. Поэтому для большего понимания эти термины были адаптированы как:

    • ДОЛЖНЫ | НЕ ДОЛЖНЫ - носят строго обязательный характер;
    • РЕКОМЕНДУЕТСЯ | НЕ РЕКОМЕНДУЕТСЯ - являются настойчивой рекомендацией, но тем не менее не имеют обязательного характера;
    • ВОЗМОЖНО - обозначают допустимый вариант.

    Такая адаптация вполне совместима с оригинальным стандартом RFC 2119 и не нарушает его. ↩︎

  2. Прим. пер.: В оригинале в этом абзаце присутствует несколько вероятных ошибок:

    • Вместо $ в оригинале был указан знак \(, но такой символ не может быть в имени идентификатора и поэтому в переводе указан более корректный вариант с $;
    • В оригинале упоминается явно ошибочное регулярное выражение [\)\w]+ и поэтому, с учетом прошлого пункта, в переводе было указано более корректное [$\w]+.
    ↩︎
  3. Прим. пер.: Такое соглашение было популяризовано Cycle.js и также применяется в Angular. ↩︎

  4. Прим. пер.: С переводом руководства "Google JavaScript Style Guide" вы можете ознакомиться тут: https://rostislavdugin.github.io/styleguide/jsguide.html ↩︎

  5. Прим. пер.: В оригинале, далее в данном абзаце предлагается ознакомиться со страницей "Тестирование и приватная видимость", если вы хотите получить доступ к защищенным полям из теста. К сожалению, предоставленная сокращенная go-ссылка: go/typescript-testing#export-private-visibility, доступна только из внутреннего окружения Google. ↩︎

  6. Прим. пер.: В оригинале, в данном предложении предоставлена сокращенная go-ссылка: go/tsjs-practices/iteration, которая к сожалению доступна только из внутреннего окружения Google. ↩︎

  7. Прим. пер.: В блоке оператора switch непустые группы операторов case не допускаются к проваливанию компилятором при активной опции noFallthroughCasesInSwitch. Подробнее вы можете ознакомиться тут: https://www.typescriptlang.org/tsconfig#noFallthroughCasesInSwitch. ↩︎

  8. Automatic Semicolon Insertion (ASI) — с англ. переводится как "автоматическая вставка точки с запятой". ↩︎

  9. Google Code Search - проект поисковой системы по исходному коду программ, позволяющий использовать в поисковых запросах регулярные выражения. Репозиторий проекта размещен по адресу: https://github.com/google/codesearch ↩︎

  10. Прим. пер.: Данная проблема под названием "return-only generics" обсуждалась в issue к TypeScript. На странице https://effectivetypescript.com/2020/08/12/generics-golden-rule/ хорошо поясняется суть этой проблемы на примере кода:

    function parseYAML<T>(input: string): T {
      // ...
    }
    
    interface Weight {
      pounds: number;
      ounces: number;
    }
    
    const w: Weight = parseYAML(''); // возвращаемый тип - any!
    

    На примере, функция parseYAML неявно возвращает тип any, но при этом нигде явно не указано ключевое слово any, что может сбить с толку и привести к нежелательным последствиям. ↩︎