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

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

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

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

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

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

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

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

Введение

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

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

Примечание к терминологии

Данное руководство ссылается на терминологию стандарта RFC 2119 при использовании фраз ДОЛЖЕН, НЕ ДОЛЖЕН, РЕКОМЕНДУЕТСЯ, НЕ РЕКОМЕНДУЕТСЯ и ВОЗМОЖНО [1]. Термины ПРЕДПОЧИТАТЬ и ИЗБЕГАТЬ соответствуют терминам РЕКОМЕНДУЕТСЯ и НЕ РЕКОМЕНДУЕТСЯ соответственно. Императивные и декларативные высказывания носят предписывающий характер и соответствуют термину ДОЛЖЕН.

Примечание к руководству

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

Основы исходных файлов

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

Исходные файлы кодируются в UTF-8.

Специальные символы

Пробельные символы

Помимо последовательности символов перевода строки, ASCII-символ горизонтального пробела (0x20) является единственным допустимым пробельным символом, который может появляться где-либо в исходном коде. Это также подразумевает, что в строковых литералах все прочие пробельные символы экранируются.

Специальные экранирующие последовательности

Для каждого символа, для которого существует специальная экранирующая последовательность (\', \", \\, \b, \f, \n, \r, \t, \v) предпочтительнее использовать именно ее вместо соответствующего числового экранирования (например, \x0a, \u000a или \u{a}). Устаревшие восьмеричные символы экранирования не используются.

Символы не из таблицы ASCII

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

// ✅ ХОРОШО ↴

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

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

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

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

Структура исходного файла

Исходный файл состоит из следующих по порядку частей:

Для разделения перечисленных частей друг от друга используется только одна пустая строка.

Информация об авторских правах

Если необходимо указать в файле информацию о лицензии или авторских правах, добавьте ее в JSDoc в верхней части файла[2].

JSDoc тег @fileoverview

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

Пример[3]:

// ✅ ХОРОШО ↴

/**
 * @fileoverview Описание файла. Lorem ipsum dolor sit amet, consectetur
 * adipiscing elit, sed do eiusmod tempor incididunt.
 */

Импорты

В 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';

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

В 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';

Импорты пространств имен в сравнении с именованными импортами

Могут использоваться как импорты пространств имен[4], так и именованные импорты[5].

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

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

// ❌ ПЛОХО ↴

// Плохо: слишком длинный оператор импорта с излишними пространствами имен.
import {Item as TableviewItem, Header as TableviewHeader, Row as TableviewRow,
  Model as TableviewModel, Renderer as TableviewRenderer} from './tableview';

let item: TableviewItem|undefined;
// ✅ ХОРОШО ↴

// Лучше: используйте модуль для пространства имен. 
import * as tableview from './tableview';

let item: tableview.Item|undefined;
// ❌ ПЛОХО ↴

import * as testing from './testing';

// Плохо: Имя модуля не способствует удобочитаемости.
testing.describe('foo', () => {
  testing.it('bar', () => {
    testing.expect(null).toBeNull();
    testing.expect(undefined).toBeUndefined();
  });
});
// ✅ ХОРОШО ↴

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

describe('foo', () => {
  it('bar', () => {
    expect(null).toBeNull();
    expect(undefined).toBeUndefined();
  });
});
Особый случай: Приложения использующие JSPB proto-файлы

Приложения использующие JSPB proto-файлы должны использовать для них именованный импорт, даже если это приводит к возникновению длинных строк импорта[6].

Это правило существует для повышения производительности сборки и избавления от мертвого кода, поскольку часто файлы .proto содержат множество "сообщений" (message), которые не всегда нужны все вместе. Используя деструктурированный импорт, система сборки может создавать более точную структуру зависимостей от "сообщений" JSPB в приложении их использующем, сохраняя при этом удобство импорта на основе путей.

// ✅ ХОРОШО ↴

// ХОРОШО: Импортируйте из proto-файла именно тот набор элементов, который вам нужен.
import {Foo, Bar} from './foo.proto';

function copyFooBar(foo: Foo, bar: Bar) {...}

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

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

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

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

Экспорты

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

// ✅ ХОРОШО ↴

// Использование именованного экспорта:
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; }

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

Импорт типа

Вы можете использовать import type {...} если вы используете импортируемый элемент только в качестве типа. Для значений используйте обычный импорт:

// ✅ ХОРОШО ↴

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

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

Почему?

Компилятор TypeScript автоматически определяет различия и не внедряет динамическую (runtime) загрузку для обращений к типам. Так зачем же тогда аннотировать импорт типов?

Компилятор TypeScript может работать в двух режимах:

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

Экспорт типа

Используйте export type при реэкспорте типа, например:

// ✅ ХОРОШО ↴

export type {AnInterface} from './foo';

Почему?

export type полезен тем, что позволяет использовать реэкспорт типов при пофайловой транспиляции. См. документацию по isolatedModules.

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

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

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 импорты.

Языковые особенности

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

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

Объявление локальных переменных

Используйте const и let

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

// ✅ ХОРОШО ↴

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

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

// ❌ ПЛОХО ↴

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

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

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

При каждом объявлении локальных переменных задается только одна переменная: т.е. такие объявления, как let a = 1, b = 2, не используются.

Литералы массива

Не используйте конструктор Array

В коде не должен использоваться конструктор 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);

Не определяйте свойства в массивах

Не определяйте и не используйте нечисловые свойства массива (кроме length). Вместо этого используйте Map (или Object).

Использование синтаксиса spread-оператора

Использование синтаксиса spread-оператора [...foo]; {...bar} является удобным сокращением для неглубокого копирования или конкатенации итерируемых структур.

// ✅ ХОРОШО ↴

const foo = [
  1,
];

const foo2 = [
  ...foo,
  6,
  7,
];

const foo3 = [
  5,
  ...foo,
];

foo2[1] === 6;
foo3[1] === 1;

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

// ❌ ПЛОХО ↴

const foo = [7];
const bar = [5, ...(shouldUseFoo && foo)]; // может быть undefined

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

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

Деструктуризация массива

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

// ✅ ХОРОШО ↴

const [a, b, c, ...rest] = generateResults();
let [, b,, d] = someArray;

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

// ✅ ХОРОШО ↴

function destructured([a = 4, b = 2] = []) { … }

Запрещено:

// ❌ ПЛОХО ↴

function badDestructuring([a, b] = [4, 2]) { … }

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

Объектные литералы

Не используйте конструктор Object

Конструктор Object запрещен. Вместо этого используйте объектные литералы ({} или {a: 0, b: 1, c: 2}).

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

Итерация по объектам с помощью 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
}

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

// ❌ ПЛОХО ↴

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

Вычисленные имена свойств

Вычисляемые имена свойств (например, {['key' + foo()]: 42}) разрешены, заключаются в кавычки и рассматриваются в стиле ключей словаря (т.е. не должны смешиваться с ключами без кавычек), если только вычисляемое свойство не принадлежит типу symbol (например, [Symbol.iterator]).

Деструктуризация объекта

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

Деструктурированные объекты также могут использоваться в качестве параметров функции, но при этом их следует делать как можно более простыми, с одним уровнем вложенности состоящем из коротких свойств без кавычек. Более глубокие уровни вложенности и вычисляемые свойства не следует использовать при деструктуризации параметров. Любые значения по умолчанию указываются в левой части деструктурируемого параметра ({str = 'some default'} = {}, а не {str} = {str: 'some default'}), а если деструктурируемый объект сам по себе необязателен, то по умолчанию он должен иметь значение {}.

Например:

// ✅ ХОРОШО ↴

interface Options {
  /** Сколько раз выполнять то или иное действие. */
  num?: number;

  /** Строка для обработки. */
  str?: string;
}

function destructured({num, str = 'default'}: Options = {}) {}

Запрещено:

// ❌ ПЛОХО ↴

function nestedTooDeeply({x: {num, str}}: {x: Options}) {}
function nontrivialDefault({num, str}: Options = {num: 42, str: 'default'}) {}

Классы

Объявления классов

Объявления классов не должны завершаться точкой с запятой:

// ✅ ХОРОШО ↴

class Foo {
}
// ❌ ПЛОХО ↴

class Foo {
}; // Ненужная точка с запятой

В отличие от этого, инструкции, содержащие выражения классов, должны завершаться точкой с запятой:

// ✅ ХОРОШО ↴

export const Baz = class extends Bar {
  method(): number {
    return this.x;
  }
}; // Здесь точка с запятой, поскольку это инструкция, а не объявление класса
// ❌ ПЛОХО ↴

exports const Baz = class extends Bar {
  method(): number {
    return this.x;
  }
}

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

// ✅ ХОРОШО ↴

// Никаких пустых строк возле скобок — хорошо.
class Baz {
  method(): number {
    return this.x;
  }
}

// Одна пустая строка возле обоих скобок - тоже хорошо
class Foo {

  method(): number {
    return this.x;
  }

}

Объявления методов класса

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

// ✅ ХОРОШО ↴

class Foo {
  doThing() {
    console.log("A");
  }
}
// ❌ ПЛОХО ↴

class Foo {
  doThing() {
    console.log("A");
  }; // <-- ненужно
}

Объявления методов рекомендуется отделять от окружающего кода одной пустой строкой:

// ✅ ХОРОШО ↴

class Foo {
  doThing() {
    console.log("A");
  }

  getOtherThing(): number {
    return 4;
  }
}
// ❌ ПЛОХО ↴

class Foo {
  doThing() {
    console.log("A");
  }
  getOtherThing(): number {
    return 4;
  }
}
Переопределение метода toString

Метод toString может быть переопределен, но всегда должен срабатывать успешно и не иметь ощутимых побочных эффектов.

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

Статические методы

Избегайте приватных статических методов

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

Не полагайтесь на динамическую диспетчеризацию

В коде не рекомендуется полагаться на динамическую диспетчеризацию статических методов. Статические методы рекомендуется вызывать только непосредственно на базовом классе (в котором они определены). Статические методы не рекомендуется вызывать на переменных, содержащих динамический экземпляр, который может быть конструктором как самого класса, так и конструктором его подкласса (и должен быть определен с помощью @nocollapse[7], если он существует), а также не должны вызываться непосредственно на подклассе, который не определяет данный метод.

Запрещено:

// ❌ ПЛОХО ↴

// Контекст для примеров ниже (этот класс вполне приемлем сам по себе)
class Base {
  /** @nocollapse */ static foo() {}
}
class Sub extends Base {}

// Предостережение: не вызывайте статические методы динамически
function callFoo(cls: typeof Base) {
  cls.foo();
}

// Запрещено: не вызывайте статические методы в подклассах, которые сами их не определяют
Sub.foo();

// Запрещено: не обращайтесь к `this` в статических методах
class MyClass {
  static foo() {
    return this.staticField;
  }
}
MyClass.staticField = 1;
Избегайте 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 и могут быть удивлены, обнаружив, что они могут быть переопределены — подобная функциональность используется не часто.

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

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

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

// ❌ ПЛОХО ↴

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

const x = new Foo();

Отсутствие скобок может привести к трудноуловимым ошибкам. Эти две строки не эквивалентны:

// ✅ ХОРОШО ↴

new Foo().Bar();
new Foo.Bar();

Нет необходимости предоставлять пустой конструктор или конструктор, который просто делегирует в родительский класс, поскольку 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() {}
}

Конструктор рекомендуется отделять от окружающего кода как сверху, так и снизу одной пустой строкой:

// ✅ ХОРОШО ↴

class Foo {
  myField = 10;

  constructor(private readonly ctorParam) {}

  doThing() {
    console.log(ctorParam.getThing() + myField);
  }
}
// ❌ ПЛОХО ↴

class Foo {
  myField = 10;
  constructor(private readonly ctorParam) {}
  doThing() {
    console.log(ctorParam.getThing() + myField);
  }
}

Члены класса

Не используйте приватные поля вида #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[] = [];
}

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

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

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

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

В 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;
  }
}
// ❌ ПЛОХО ↴

class Foo {
  nextId = 0;
  get next() {
    return this.nextId++; // Плохо: геттер изменяет наблюдаемое состояние]
  }
}

Если аксессор используется для сокрытия свойства класса, то для скрытого свойства возможно указать префикс или суффикс с любым целым словом, например 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;
  }
}

Геттеры и сеттеры не должны задаваться с помощью Object.defineProperty, так как это препятствует переименованию свойств.

Вычисляемые свойства

Вычисляемые свойства могут использоваться в классах только в том случае, если свойство имеет тип symbol. Свойства в словарном стиле (т.е. заключенные в кавычки или не являющиеся типом symbol в качестве ключа) не допускаются. Метод [Symbol.iterator] рекомендуется определять для любых классов которые логически итерируемы. В остальном, Symbol рекомендуется использовать умеренно.

Совет: Будьте осторожны с использованием других встроенных свойств типа symbol (например Symbol.isConcatSpreadable), поскольку они не полифиллятся компилятором и следовательно не будут работать в старых браузерах.

Видимость

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

// ❌ ПЛОХО ↴

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

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

Недопустимые паттерны при работе с классами

Не взаимодействуйте напрямую с прототипами (prototype)

Ключевое слово class позволяет создавать более понятные и читаемые определения классов, чем определение свойств в прототипах (prototype). В обычном прикладном коде нет необходимости манипулировать этими объектами. Миксины и модификация прототипов встроенных объектов однозначно запрещены.

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

Функции

Терминология

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

Методы и классы/конструкторы в этом разделе не рассматриваются.

Для создания именованных функций предпочтительно использовать объявления функций (Function Declaration)

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

// ✅ ХОРОШО ↴

function foo() {
  return 42;
}
// ❌ ПЛОХО ↴

const foo = () => 42;

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

// ✅ ХОРОШО ↴

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

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

Вложенные функции

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

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

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

// ✅ ХОРОШО ↴

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

bar(function() { ... })

Исключение: Функциональные выражения возможно использовать только в тех случаях, когда код должен динамически перепривязывать 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;
}

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

// ❌ ПЛОХО ↴

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

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

Совет: Оператор void можно использовать для обеспечения гарантии того, что стрелочная функция c коротким телом функции всегда будет возвращать undefined для тех случаев, когда результат не планируется использовать.

Перепривязывание 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, такими как f.bind(this), goog.bind(f, this) или const self = this.

В качестве функции обратного вызова (callback) предпочтите передавать стрелочную функцию

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

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

// ❌ ПЛОХО ↴

// ПЛОХО: Аргументы не передаются явно, что приводит к нежелательному поведению, 
// поскольку необязательный аргумент функции parseInt, задающий основание счисления,
// получает индексы массивов 0, 1 и 2.
const numbers = ['11', '5', '10'].map(parseInt);
// > [11, NaN, 2];

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

// ✅ ХОРОШО ↴

// ХОРОШО: Аргументы явно передаются в функцию обратного вызова
const numbers = ['11', '5', '3'].map((n) => parseInt(n));
// > [11, 5, 3]

// ХОРОШО: Функция определена локально и предназначена для использования в качестве функции обратного вызова
function dayFilter(element: string|null|undefined) {
  return element != null && element.endsWith('day');
}

const days = ['tuesday', undefined, 'juice', 'wednesday'].filter(dayFilter);

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

В классах обычно не рекомендуется содержать свойства, которые проинициализированы как стрелочные функции. Использование стрелочных функций как свойств требует чтобы вызывающая их функция корректно понимала, что у вызываемой функции уже есть привязанный 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('Вы хотите покинуть страницу?');
  }
}

Инициализаторы параметров (значения параметров по умолчанию)

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

// ✅ ХОРОШО ↴

function process(name: string, extraContext: string[] = []) {}
function activate(index = 0) {}
// ❌ ПЛОХО ↴

// ПЛОХО: побочный эффект инкрементирования счетчика
let globalCounter = 0;
function newId(index = globalCounter++) {}

// ПЛОХО: раскрывает общее мутабельное состояние, что может привести к непреднамеренной связанности между вызовами функций
class Foo {
  private readonly defaultPaths: string[];
  frobnicate(paths = defaultPaths) {}
}

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

Предпочтительно использовать rest-оператор и spread-оператор в случае если это вполне уместно

Используйте rest-параметр вместо доступа к переменной arguments. Никогда не называйте локальную переменную или параметр как arguments, поскольку это вносит путаницу, перекрывая встроенное имя.

// ✅ ХОРОШО ↴

function variadic(array: string[], ...numbers: number[]) {}

Вместо Function.prototype.apply используйте функционал spread-оператора.

Форматирование функций

Пустые строки в начале или конце тела функции не допускаются.

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

В генераторах рекомендуется добавлять символ * к ключевым словам function и yield, т.е. function* foo() и yield* iter, а не function *foo() или yield *iter.

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

Не ставьте пробел после ... в синтаксисе rest-оператора или spread-оператора.

// ✅ ХОРОШО ↴

function myFunction(...elements: number[]) {}
myFunction(...array, ...iterable, ...generator());

Использование this

Используйте this только в конструкторах и методах классов, в функциях, в которых явно объявлен тип this (например, function func(this: ThisType, ...)), или в стрелочных функциях, определенных в той области видимости, в которой может использоваться this.

Никогда не используйте this в качестве ссылки на глобальный объект, контекст eval, цель события, или (если нет в этом явной необходимости) на вызываемые через методы call() и apply() функции.

// ❌ ПЛОХО ↴

this.alert('Hello');

Примитивные литералы

Строковые литералы

Используйте одинарные кавычки

Обычные строковые литералы заключаются в одинарные кавычки ('), а не в двойные (").

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

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

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

Запрещено:

// ❌ ПЛОХО ↴

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

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

// ✅ ХОРОШО ↴

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

const SINGLE_STRING =
    'http://тут.также/допустимо_использовать_единую_длинную_строку_если_разрыв_этой_строки_затруднит_ее_обнаружение_при_поиске';
Шаблонные литералы

Используйте шаблонные литералы (разделенные `) вместо сложной конкатенации строк, особенно если речь идет о многострочных литералах. Шаблонные литералы могут занимать несколько строк.

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

Пример:

// ✅ ХОРОШО ↴

function arithmetic(a: number, b: number) {
  return `Это таблица с арифметическими операторами:
${a} + ${b} = ${a + b}
${a} - ${b} = ${a - b}
${a} * ${b} = ${a * b}
${a} / ${b} = ${a / b}`;
}

Числовые литералы

Числа могут быть указаны в десятичной, шестнадцатеричной, восьмеричной или двоичной форме. Используйте соответствующие префиксы 0x, 0o и 0b со строчными буквами для шестнадцатеричных, восьмеричных и двоичных форм соответственно. Никогда не включайте ведущий ноль, если за ним не следуют x, o или b.

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

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

Управляющие структуры

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

Операторы управления потоком (if, else, for, do, while и т.д.) всегда используют блоки со скобками для размещения содержащегося в них кода, даже если тело состоит из одного выражения.

// ✅ ХОРОШО ↴

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())) {
  // Двойная скобка указывает на то, что присваивание сделано намеренно
  // ...
}
Итерация по массивам

Для итерации по массивам предпочтительно использовать for (... of someArr). Также приемлемо использовать 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()) {
  // Альтернативная версия предыдущего.
}

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

// ❌ ПЛОХО ↴

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

В циклах for-in рекомендуется использовать Object.prototype.hasOwnProperty для исключения нежелательных свойств прототипа. По возможности, вместо for-in предпочтительно использовать for-of с Object.keys, Object.values или Object.entries.

// ✅ ХОРОШО ↴

for (const key in obj) {
  if (!obj.hasOwnProperty(key)) continue;
  doWork(key, obj[key]);
}
for (const key of Object.keys(obj)) {
  doWork(key, obj[key]);
}
for (const value of Object.values(obj)) {
  doWorkValOnly(value);
}
for (const [key, value] of Object.entries(obj)) {
  doWork(key, value);
}

Группирующие круглые скобки

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

Не используйте лишние круглые скобки вокруг всего выражения, следующего за delete, typeof, void, return, throw, case, in, of или yield.

Обработка исключений

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

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

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

Использование 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) позволяет бросать исключения или отклонять промисы (Promise) с произвольными значениями. Однако если выброшенное значение не является экземпляром класса Error, то оно не получит записи трассировки стека, что затруднит отладку. Это правило распространяется и на отклоняемые значения Promise, поскольку Promise.reject(obj) эквивалентен throw obj; в асинхронных функциях.

// ❌ ПЛОХО ↴

// плохо: не позволяет получить трассировку стека.
throw 'ой, ошибка!';
// Для промисов
new Promise((resolve, reject) => void reject('ой, ошибка!'));
Promise.reject();
Promise.reject('ой, ошибка!');

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

// ✅ ХОРОШО ↴

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

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

// ✅ ХОРОШО ↴

function assertIsError(e: unknown): asserts e is Error {
  if (!(e instanceof Error)) throw new Error('"e" не принадлежит классу Error');
}

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

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

// ✅ ХОРОШО ↴

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

Почему?

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

Пустой блок catch

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

// ✅ ХОРОШО ↴

try {
  return handleNumericResponse(response);
} catch (e: unknown) {
  // Ответ не является числовым. Продолжаем обрабатывать как текст.
}
return handleTextResponse(response);

Запрещено:

// ❌ ПЛОХО ↴

try {
  shouldFail();
  fail('ожидалась ошибка');
} catch (expected: unknown) {
}

Оператор switch

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

// ✅ ХОРОШО ↴

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

В блоке switch каждая группа операторов завершается либо оператором break, либо оператором return, либо выбросом исключения. Непустые группы операторов (case ...) не должны проваливаться (обеспечивается настройками компилятора[8]):

// ❌ ПЛОХО ↴

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

Утверждения типа (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;
Двойные утверждения типа

Из руководства TypeScript следует, что в TypeScript допускаются только утверждения типа, которые преобразуют тип в его более специфичную или менее специфичную версию. Добавление утверждения типа (x as Foo), которое не соответствует этому критерию, приведет к ошибке: "Conversion of type 'X' to type 'Y' may be a mistake because neither type sufficiently overlaps with the other." (что в переводе: "Преобразование типа 'X' в тип 'Y' может быть ошибкой, поскольку ни один из типов в достаточной степени не пересекается с другим.").

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

// ✅ ХОРОШО ↴

// Здесь "x" это "Foo", потому что ...
(x as unknown as Foo).fooMethod();

Соответственно, используйте unknown (вместо any или {}) как промежуточный тип.

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

Используйте аннотации типа (: 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.
  };
}

Сохраняйте блоки 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 для охвата всего цикла — это нормально.

Декораторы

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

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

Почему?

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

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

// ✅ ХОРОШО ↴

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

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

Запрещенные возможности

Классы-обертки для примитивных типов

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

// ❌ ПЛОХО ↴

const s = new String('hello');
const b = new Boolean(false);
const n = new Number(5);

Обертки могут быть вызваны как функции для приведения типа (что предпочтительнее, чем использование + или конкатенации пустой строки) или создания символов (Symbol). См. раздел «Преобразование типов» для дополнительной информации.

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

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

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

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

Почему?

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

Оператор debugger

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

// ❌ ПЛОХО ↴

function debugMe() {
  debugger;
}

Оператор with

Не используйте ключевое слово with. Это делает ваш код более трудным для понимания и к тому же он запрещен в строгом режиме (strict mode) начиная с ES5.

Динамическое выполнение кода

Не используйте eval или конструктор Function(...string) (за исключением загрузчиков кода). Эти функции потенциально опасны и просто не работают в окружениях, использующих строгие политики CSP (Content Security Policy).

Нестандартные возможности

Не используйте нестандартные возможности ECMAScript или Web-платформы.

Сюда входят:

Проекты, ориентированные на конкретные среды исполнения JavaScript, такие как только последние версии Chrome, расширения Chrome, Node.JS, Electron, очевидно могут использовать эти API. Будьте осторожны при рассмотрении к использованию тех API, которые являются проприетарными и реализованы только в определенных браузерах; изучите, есть ли какая-то общая библиотека, которая может абстрагировать такой API для вас.

Модификация встроенных объектов

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

Не добавляйте символы (symbol) в глобальный объект, если это не является абсолютно необходимым (например, по требованию стороннего API).

Именование

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

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

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

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

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

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

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

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

// ✅ ХОРОШО ↴

// Хорошие идентификаторы:
errorCount          // Без сокращений.
dnsConnectionIndex  // Большинство людей знают, что означает "DNS".
referrerUrl         // То же самое касается "URL".
customerId          // "Id" распространено повсеместно и вряд ли будет понято неправильно.
// ❌ ПЛОХО ↴

// Запрещенные идентификаторы:
n                   // Бессмысленный.
nErr                // Неясная аббревиатура.
nCompConns          // Неясная аббревиатура.
wgcConnections      // Только ваша команда разработчиков знает, что это означает.
pcReader            // Многие вещи можно назвать как "pc".
cstmrId             // Удалены внутренние символы.
kSecondsPerDay      // Не используйте Венгерскую нотацию.
customerID          // Регистр букв в слове «ID» не в camelcase стиле

Верблюжий стиль (camelcase)

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

Знак доллара

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

Правила в соответствии с типом идентификатора

Большинство имен идентификаторов должны соответствовать регистру, указанному в таблице ниже, в зависимости от типа идентификатора.

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

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

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

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

Название тестовых методов в 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 {BrewStateEnum} = SomeType;
const CAPACITY = 5;

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

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

Вывод типа

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

// ✅ ХОРОШО ↴

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

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

// ❌ ПЛОХО ↴

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

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

Явное указание типов может потребоваться для предотвращения вычисления для дженерика параметров типа как unknown. Например, при инициализации для дженерика типов без заданных значений (например, пустые массивы, объекты, коллекции Map и Set).

// ✅ ХОРОШО ↴

const x = new Set<string>();

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

// ❌ ПЛОХО ↴

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

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

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

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

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

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

Undefined & Null

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

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

// ❌ ПЛОХО ↴

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

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

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

class CoffeeService {
  getLatte(): CoffeeResponse|undefined { ... };
}

Опциональные параметры предпочтительнее |undefined

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

// ✅ ХОРОШО ↴

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

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

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

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

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

// ✅ ХОРОШО ↴

class MyClass {
  field = '';
}

Используйте структурную типизацию

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

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

// ✅ ХОРОШО ↴

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

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

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

// ✅ ХОРОШО ↴

interface Foo {
  a: number;
  b: string;
}

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

class Foo {
  readonly a: number;
  readonly b: number;
}

const foo: Foo = {
  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[] или readonly T[], а не более длинную форму Array<T> или ReadonlyArray<T>.

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

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

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

// ✅ ХОРОШО ↴

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

let a: Array<string>;  // Синтаксический сахар короче
let b: ReadonlyArray<string>;
let c: Array<ns.MyObj>;
let d: Array<string[]>;
let e: {n: number, s: string}[];  // Фигурные скобки ухудшают читабельность
let f: (string|number)[];         // Аналогично и с круглыми скобками
let g: readonly (string | number)[];
let h: InjectionToken<Array<string>>;
let i: readonly string[][];
let j: (readonly string[])[];

Индексируемые типы / индексные сигнатуры ({[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);

Тип {}

Тип {}, также известный как пустой тип интерфейса, представляет собой интерфейс без свойств. Пустой тип интерфейса не имеет заданных свойств, поэтому ему можно присвоить любое значение, не являющееся null или undefined.

// ❌ ПЛОХО ↴

let player: {};

player = {
  health: 50,
}; // Allowed.

console.log(player.health) // Property 'health' does not exist on type '{}'.
// ❌ ПЛОХО ↴

function takeAnything(obj:{}) {

}

takeAnything({});
takeAnything({ a: 1, b: 2 });

В большинстве случаев в коде Google3[13] не рекомендуется использовать {}. {} представляет собой любой кроме null и undefined примитив или объектный тип, что редко когда бывает уместно. Предпочтите один из следующих более описательных типов:

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

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

// ❌ ПЛОХО ↴

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 у которых возвращаемый тип представлен только дженериком, всегда явно указывайте дженерик [14].

Требования к цепочке инструментов

Стиль Google требует использования ряда инструментов определенными способами, описанными здесь.

Компилятор TypeScript

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

@ts-ignore

Не используйте @ts-ignore, а также такие варианты, как @ts-expect-error или @ts-nocheck.

Почему?

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

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

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

Соответствия

Google TypeScript включает в себя несколько фреймворков соответствия[15]: tsetse и tsec.

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

Код TypeScript в стиле Google должен соответствовать всем применимым глобальным или локальным для фреймворков правилам соответствия.

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

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

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

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

Многострочные комментарии

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

// ✅ ХОРОШО ↴

// Это хороший
// комментарий
// ❌ ПЛОХО ↴

/*
 * Тут рекомендуется
 * использовать многочисленные
 * однострочные комментарии
 */

/* Тут рекомендуется использовать // */

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

Общая форма JSDoc

Основное форматирование комментариев JSDoc выглядит так, как показано в этом примере:

// ✅ ХОРОШО ↴

/**
 * Множество строк текста JSDoc записанных здесь,
 * переносятся нормально
 * @param arg Число, с которым нужно что-то делать.
 */
function doSomething(arg: number) { … }

или в этом однострочном примере:

// ✅ ХОРОШО ↴

/** Этот краткий jsdoc описывает функцию. */
function doSomething(arg: number) { … }

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

Многие инструменты извлекают метаданные из комментариев JSDoc для выполнения валидации и оптимизации кода. Поэтому эти комментарии должны быть хорошо сформированы.

Использование Markdown

JSDoc написан в формате Markdown, хотя при необходимости возможно добавление HTML.

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

// ❌ ПЛОХО ↴

/**
 * Вычисляет вес на основе трех факторов:
 *   элементов отправлено
 *   элементов получено
 *   последняя временная метка
 */

Это будет преобразовано подобным образом:

Вычисляет вес на основе трех факторов: элементов отправлено элементов получено последняя временная метка

Вместо этого напишите список в формате Markdown:

// ✅ ХОРОШО ↴

/**
 * Вычисляет вес на основе трех факторов:
 *  - элементов отправлено
 *  - элементов получено
 *  - последняя временная метка
 */

JSDoc теги

Стиль Google позволяет использовать подмножество тегов JSDoc. Большинство тегов должны занимать отдельную строку, причем тег должен находиться в начале строки.

// ✅ ХОРОШО ↴

/**
 * Тэг "param" должен занимать свою собственную строку и не может комбинироваться.
 * @param left Описание левого параметра.
 * @param right Описание правого параметра.
 */
function add(left: number, right: number) { ... }
// ❌ ПЛОХО ↴

/**
 * Тэг "param" должен занимать свою собственную строку и не может комбинироваться.
 * @param left @param right
 */
function add(left: number, right: number) { ... }

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

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

// ❌ ПЛОХО ↴

/**
 * Иллюстрирует перенос строк для длинных описаний параметров и возвращаемых значений.
 * @param foo Это параметр с особенно длинным описанием, которое просто 
 *     не помещается в одну строку.
 * @return Возвращает нечно с длинным описанием, которое не помещается
 *     в одну строку.
 */
exports.method = function(foo) {
  return 5;
};

Не делайте отступ при переносе строк в описании @desc или @fileoverview тегов.

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

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

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

Комментарии класса

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

Комментарии методов и функций

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

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

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

Параметризованное свойство — это параметр конструктора, которому предшествует один из модификаторов 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;
  }
}

Аннотации типа в JSDoc

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

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

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

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

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

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

Комментарии "Имя параметра" располагаются перед значением параметра и включают в себя имя параметра и суффикс =:

// ✅ ХОРОШО ↴

someFunction(obviousParam, /* shouldRender= */ true, /* name= */ 'hello');

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

// ✅ ХОРОШО ↴

someFunction(obviousParam, true /* shouldRender */, 'hello' /* name */);

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

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

Политики

Единообразие

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

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

Переформатирование существующего кода

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

При обновлении стиля существующего кода следуйте этим рекомендациям:

Устаревание

Пометьте устаревшие методы, классы или интерфейсы JSDoc аннотацией @deprecated. Комментарий об устаревании должен содержать простые и ясные для людей указания по исправлению их мест вызова.

Сгенерированный код: в основном без правил

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

Цели руководства по стилю

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

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

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

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

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

    Примеры:

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

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

    Примеры:

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

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

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


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

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

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

  2. Прим. пер.: В JSDoc для указания информации о лицензии используется тег @license, а для информации об авторских правах тег @copyright. ↩︎

  3. Прим. пер.: В примере комментария представлен классический условный текст-заполнитель (текст-"заглушка", текст-"рыба") "Lorem ipsum dolor sit amet, consectetur...", который не несет в себе никакой смысловой нагрузки и часто используется для простой имитации какого-либо текстового содержимого. ↩︎

  4. Импорт пространства имен часто называют "импортом модуля". ↩︎

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

  6. Прим. пер.: Данный тема связана с использованием технологии Protocol Buffers (https://github.com/protocolbuffers/protobuf) ↩︎

  7. Прим. пер.: Аннотация @nocollapse используется в Google Closure Compiler. Подробнее с этим вы можете ознакомиться тут: https://github.com/google/closure-compiler/wiki/Annotating-JavaScript-for-the-Closure-Compiler#nocollapse ↩︎

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

  9. Прим. пер.: В TypeScript 5.0 появился функционал декораторов, который доступен по умолчанию и имеет несколько отличий от ранее существовавших экспериментальных декораторов. С отличиями вы можете ознакомиться тут: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#differences-with-experimental-legacy-decorators. ↩︎

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

  11. Прим. пер.: Такое соглашение было популяризовано Cycle.js и также применяется в Angular. ↩︎

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

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

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

  15. Прим. пер.: В данном контексте в понятие "Соответствие" (Conformance) разработчиками из Google заложен особый смысл. См. подробнее на странице https://developer.chrome.com/blog/conformance. ↩︎