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

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

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

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

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

Перевод основан на следующей версии оригинального руководства: https://github.com/google/styleguide/blob/1faa779a126c3564e74d6254d596da8dd2b4bf56/tsguide.html.

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

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

Введение

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

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

Синтаксис

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

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

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

Псевдонимы

При создании локального псевдонима существующего элемента, используйте формат уже существующего его обозначения. Локальный псевдоним должен совпадать с существующим именем и форматом источника. Для переменных при создании локальных псевдонимов используйте 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;

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

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

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

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

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

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

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

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

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

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

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

Не используйте @override

Не используйте @override в исходном коде TypeScript[5].

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

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

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

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

Параметризованное свойство — это когда класс объявляет поле и параметр конструктора в одном объявлении путем пометки параметра как свойства в конструкторе. Например constructor(private readonly 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 написан перед декоратором.

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

Видимость

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

// ❌ ПЛОХО ↴

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[] = [];
}

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

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

Для этих свойств предпочтительна публичная (public) видимость, однако при необходимости можно использовать и защищенную (protected) видимость. Например, для свойств используемых в шаблонах Angular и Polymer следует использовать public, а в AngularJS - protected.

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

Почему?

Когда свойство является приватным (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;
  }
}

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

Код 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}`;

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

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

Примечание: Number(''), Number(' '), и Number('\t') могут вернуть 0 вместо NaN. Number('Infinity') и Number('-Infinity') могут вернуть Infinity и -Infinity соответственно. Эти случаи могут потребовать особого обращения.

// ✅ ХОРОШО ↴

const aNumber = Number('123');
if (isNaN(aNumber)) throw new Error(...);  // Обрабатываем NaN, если в строке может не быть чисел
assertFinite(aNumber, ...);                // Необязательно: если NaN не может возникнуть, потому что он был проверен ранее.

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

// ❌ ПЛОХО ↴

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) {...}

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

// ✅ ХОРОШО ↴

// Явное сравнение > 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() при создании исключений вместо простого вызова Error(). В обоих случаях создается новый экземпляр Error, но использование new более согласуется с тем, как создаются экземпляры других объектов.

// ✅ ХОРОШО ↴

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

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

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

Итерация по объектам с помощью 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) или обычные циклы 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()) {
  // Альтернативная версия предыдущего.
}

Не используйте Array.prototype.forEach, Set.prototype.forEach, и Map.prototype.forEach. Они усложняют отладку кода и препятствуют некоторым полезным проверкам компилятора (например, проверку достижимости).

// ❌ ПЛОХО ↴

someArr.forEach((item, index) => {
  someFn(item, index);
});

Почему?

Рассмотрим следующий код:

// ❌ ПЛОХО ↴

let x: string|null = 'abc';
myArray.forEach(() => { x.charAt(0); });

Вы можете видеть, что этот код вполне в порядке: x не является null и не изменяется до обращения к нему. Но компилятор не может знать, что этот вызов .forEach() не привязан к переданному замыканию и не вызовет его позже, возможно, после того, как x будет установлен в null, поэтому он помечает этот код как ошибку. Эквивалентный цикл for-of работает нормально.

Посмотреть в песочнице ошибочный и безошибочный варианты

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

Применение 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);
  andSomeMore();
}
if (x) {
  doSomethingWithALongMethodName(x);
}
// ❌ ПЛОХО ↴

if (x)
  x.doFoo();
for (let i = 0; i < x; i++)
  doSomethingWithALongMethodName(i);

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

// ✅ ХОРОШО ↴

if (x) x.doFoo();

Switch оператор

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

// ✅ ХОРОШО ↴

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

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

// ❌ ПЛОХО ↴

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. 
}

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

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

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

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

// ✅ ХОРОШО ↴

function foo() { ... }
// ❌ ПЛОХО ↴

// Учитывая приведенное выше объявление, это не будет компилироваться:
foo = () => 3;  // ОШИБКА: Недопустимая левая часть выражения присваивания.

// Так что такие объявления излишни.
const foo = function() { ... }

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

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

// ✅ ХОРОШО ↴

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

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

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

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

Всегда используйте стрелочные функции вместо функциональных выражений которые были до 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` в них. 
  }
}

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

// ❌ ПЛОХО ↴

// ПЛОХО: используйте блочное тело функции ({ ... }) если возвращаемое значение функции не используется.
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[7]). Явно завершайте все операторы с помощью точки с запятой. Это предотвращает ошибки, возникающие из-за неправильной вставки точки с запятой, а также обеспечивает совместимость с инструментами, которые имеют ограниченную поддержку ASI (например, clang-format).

@ts-ignore

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

Например, если вы используете @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;
// ✅ ХОРОШО ↴

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);

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

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

// ✅ ХОРОШО ↴

// Хорошо: объявление интерфейса
declare interface ServerInfoJson {
  appVersion: string;
  user: UserJson;
}
const data = JSON.parse(serverResponse) as ServerInfoJson;
console.log(data.appVersion); // Тип защищен и переименование безопасно!

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

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

// ✅ ХОРОШО ↴

import {method1, method2} from 'utils';
class A {
  readonly utils = {method1, method2};
}
// ❌ ПЛОХО ↴

import * as utils from 'utils';
class A {
  readonly utils = utils;
}

Исключение

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

Перечисления (Enums)

Всегда используйте enum, а не const 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 'google3/path/from/root';
import {Symbol2} from '../parent/file';
import {Symbol3} from './sibling';

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

TypeScript поддерживает два метода организации кода: пространства имен (namespaces) и модули, но использование пространств имен необходимо избегать. В google3[8] в коде должны использоваться TypeScript модули (которые являются модулями ECMAScript 6). Т.е. ваш код должен ссылаться на код в других файлах с помощью импорта и экспорта вида 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 ... from или export type ... from.

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

// ❌ ПЛОХО ↴

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

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

// ✅ ХОРОШО ↴

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

Инструментарий 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 для всех типов выражений (переменных, полей класса, возвращаемых типов и т.д.). Флаги компилятора в google3 позволяют не допускать код, который не имеет аннотации типа и тип при этом не может быть выведен, поэтому весь код гарантированно является типизированным (но может явно использовать тип any).

// ✅ ХОРОШО ↴

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>();

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

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

Вопрос о том, следует ли включать аннотации типа возвращаемого значения для функций и методов, зависит от автора кода. Рецензенты могут запросить аннотации для уточнения сложных типов возвращаемых данных, которые трудно понять. В проектах может существовать локальная политика, согласно которой всегда требуется указывать возвращаемые типы, но это не является общим требованием стиля 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>.

Это также относится к 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: Array<string>;            // синтаксический сахар короче 
const g: ReadonlyArray<string>;
const h: {n: number, s: string}[]; // фигурные/круглые скобки ухудшают читабельность
const i: (string|number)[];
const j: readonly (string|number)[];

Индексируемый ({[key: string]: number}) тип

В JavaScript принято использовать объект в качестве ассоциативного массива (он же карта (map), хеш-таблица, или словарь):

// ✅ ХОРОШО ↴

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 - строгие зависимости), чтобы рефакторинг в какой-либо зависимости не изменил зависимости его пользователей.
    • Мы просим пользователей писать тесты. Без тестов мы не можем быть уверены, что изменения, которые вносятся в язык, или изменения в библиотеках google3, не нарушат работу пользователей.
  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. Прим. пер.: В TS 4.3 появилась полноценная поддержка модификатора override, поэтому нет необходимости в JSDoc @override (См. исключите те комментарии, которые излишни в TypeScript). Подробнее с нативным override вы можете ознакомиться тут: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-3.html#override-and-the---noimplicitoverride-flag. ↩︎

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

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

  8. google3 - название основного внутреннего монорепозитория Google. Подробнее: https://opensource.google/documentation/reference/glossary#google3 ↩︎

  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, что может сбить с толку и привести к нежелательным последствиям. ↩︎