Репозиторий текущего перевода расположен по адресу: https://github.com/olegbarabanov/google-typescript-style-guide-ru.
С оригинальным руководством по стилю вы можете ознакомиться по адресу: https://google.github.io/styleguide/tsguide.html.
Перевод основан на версии оригинального руководства от 10.11.2022.
Хотя данный перевод и стремится быть максимально соответствующим оригинальному тексту, в текст перевода были добавлены сноски на комментарии и примечания переводчика, которые дополняют или разъясняют суть конкретного выражения. Также подобные сноски присутствуют в местах исправления явных ошибок, которые присутствовали в оригинале и которые могли бы ввести в заблуждение.
Если Вы нашли несоответствие, ошибку или неточность в переводе, вы можете оформить это в виде issue или предложить собственное исправление в виде pull request в репозиторий проекта, либо написать переводчику по адресу mail@olegbarabanov.ru.
Данное руководство основано на внутреннем руководстве Google по стилю написания кода на языке TypeScript, но при этом оно было незначительно скорректировано с целью удаления разделов предназначенных для внутреннего пользования Google. Внутренняя среда Google предусматривает иные ограничения на TypeScript, чем те, что вы могли бы встретить за пределами Google. Приведенные здесь советы особенно полезны для людей, создающих код, который они намерены импортировать в Google, однако в других случаях они могут и не применяться в вашей внешней по отношению к Google среде.
Для этой версии руководства не существует какого-либо механизма автоматического развертывания, поскольку она предоставляется волонтерами в ответ на запросы пользователей.
Данное руководство ссылается на терминологию стандарта RFC 2119 при использовании фраз ДОЛЖНЫ, НЕ ДОЛЖНЫ, РЕКОМЕНДУЕТСЯ, НЕ РЕКОМЕНДУЕТСЯ и ВОЗМОЖНО [1]. Все приведенные примеры не носят нормативного характера и служат лишь для иллюстрации стандартных формулировок из данного руководства по стилю.
Идентификаторы должны использовать только ASCII символы, цифры, символы подчеркивания (для констант и названий методов структурных тестов) и знак $
. Таким образом, каждое допустимое имя идентификатора соответствует регулярному выражению [$\w]+
[2].
Стиль | Категория |
---|---|
UpperCamelCase | класс / интерфейс / тип / перечисление / декоратор / параметр типа |
lowerCamelCase | переменная / параметр / функция / метод / свойство / псевдонимы модулей |
CONSTANT_CASE | глобальные константы, включая имена элементов перечислений (enum ). См. ниже раздел Константы |
#ident | подобные приватные идентификаторы не применяются |
Рассматривайте используемые в именах аббревиатуры типа акронимов как целые слова, т.е. используйте loadHttpUrl
, а не loadHTTPURL
, если только это не обусловлено названием конкретной платформы (например XMLHttpRequest
).
В идентификаторах, как правило, не рекомендуется использовать символ $
, за исключением случаев, когда это соответствуют соглашениям об именовании для сторонних фреймворков. Подробнее об использовании суффикса $
для наблюдаемых (Observable
) значений см. ниже.
Для обозначения параметров типа, как например в Array<T>
, возможно использовать один символ верхнего регистра (T
) или UpperCamelCase
.
Название тестовых методов в Closure testSuite
и подобных тестовых фреймворках в стиле xUnit возможно представлять с разделителями _
, например testX_whenY_doesZ()
.
_
префикс/суффиксИдентификаторы не должны использовать _
в качестве префикса или суффикса.
Это также означает что символ _
сам по себе не должен быть использован в качестве идентификатора (например, чтобы указать, что параметр не используется).
Совет: Если вам нужны только некоторые элементы из массива (или TypeScript кортежа), вы можете вставить дополнительные запятые в выражение деструктуризации, чтобы игнорировать промежуточные элементы:
// ✅ ХОРОШО ↴ const [a, , b] = [1, 5, 10]; // a <- 1, b <- 10
Импорты пространств имен модулей пишутся в стиле lowerCamelCase
в то время как файлы именуются в стиле snake_case
, что означает, что корректные импорты не будут совпадать по стилю написания с именами файлов. Например:
// ✅ ХОРОШО ↴
import * as fooBar from './foo_bar';
Некоторые библиотеки могут широко использовать префиксы для импорта пространств имен, которые противоречат этой схеме именования, но их обширное использование в решениях с открытым исходным кодом делает этот нарушающий стиль более понятным. Единственными библиотеками, которые в настоящее время подпадают под это исключение, являются:
Иммутабельность: Стиль CONSTANT_CASE
указывает на то, что значение предназначено быть неизменным и при этом такой стиль также возможно использовать для значений, которые могут быть изменены технически (т.е. значений, которые не являются глубоко замороженными), чтобы явно указать пользователям на то, что эти значения нельзя изменять.
// ✅ ХОРОШО ↴
const UNIT_SUFFIXES = {
'milliseconds': 'ms',
'seconds': 's',
};
// Несмотря на то, что в соответствии с правилами JavaScript UNIT_SUFFIXES является изменяемым,
// верхний регистр символов обозначает для пользователей, что они не должны изменять значения.
Константой также может быть статическое свойство класса, которое предназначенно только для чтения (static readonly
).
// ✅ ХОРОШО ↴
class Foo {
private static readonly MY_SPECIAL_NUMBER = 5;
bar() {
return 2 * Foo.MY_SPECIAL_NUMBER;
}
}
Глобальность: Только для элементов, объявленных на уровне модуля, статических полей классов уровня модуля и значений перечислений уровня модуля возможно использовать CONST_CASE
стиль. Если во время работы программы значение создается более одного раза (например, локальная переменная, объявленная в функции или статическое поле в классе, вложенном в функцию), тогда должен использоваться lowerCamelCase
стиль.
Если значение представляет собой стрелочную функцию которая реализует интерфейс, тогда это возможно объявлять в lowerCamelCase
стиле.
При создании локального псевдонима существующего элемента, используйте формат уже существующего его обозначения. Локальный псевдоним должен совпадать с существующим именем и форматом источника. Для переменных при создании локальных псевдонимов используйте const
, а для полей класса - атрибут readonly
.
Примечание: Если вы создаете псевдоним только ради использования его для шаблона в выбранном вами фреймворке, не забудьте также назначить соответствующие модификаторы доступа.
// ✅ ХОРОШО ↴
const {Foo} = SomeType;
const CAPACITY = 5;
class Teapot {
readonly BrewStateEnum = BrewStateEnum;
readonly CAPACITY = CAPACITY;
}
TypeScript отражает информацию в типах, поэтому имена не рекомендуется дополнять информацией, которая включена в тип (см. также Блог о тестировании (Testing Blog) для получения дополнительной информации о том, что не следует включать).
Несколько конкретных примеров для этого правила:
opt_
для необязательных параметров.IMyInterface
MyFooInterface
class TodoItem
и interface TodoItemStorage
если интерфейс выражает формат, используемый для хранения/сериализации в JSON).Observable
) суффикса $
является распространенным внешним соглашением[3] и может помочь устранить путаницу между наблюдаемыми и конкретными значениями. Решение о том, является ли это полезным соглашением, остается на усмотрение отдельных команд, но рекомендуется, чтобы оно было согласованным в рамках проектов.Названия должны быть описательными и ясными для новых читателей. Не используйте аббревиатуры, которые могут быть незнакомыми или двусмысленными для читателей за пределами вашего проекта и не сокращайте, удаляя в словах буквы.
Для символов, отличных от ASCII, используйте фактический символ Юникода (например ∞
). Для непечатаемых символов можно использовать эквивалентный шестнадцатеричный код или экранирование Unicode-символов (например \u221e
) вместе с пояснительным комментарием.
// ✅ ХОРОШО ↴
// Совершенно ясно даже без комментария
const units = 'μs';
// Используйте Unicode-экранирование для непечатаемых символов
const output = '\ufeff' + content; // это маркер последовательности байтов (Unicode BOM)
// ❌ ПЛОХО ↴
// Даже с комментарием, это сложно для чтения и подвержено потенциальным ошибкам.
const units = '\u03bcs'; // Греческая буква mu, 's'
// Читающий код не поймет, что это такое
const output = '\ufeff' + content;
Не используйте продолжения строк (т.е. завершение строки внутри строкового литерала обратным слешем) ни в обычных, ни в шаблонных строковых литералах. Хотя ES5 и позволяет использовать продолжения строк, этот функционал является менее очевидным для читателей, а также может привести к неожиданным ошибкам, если любой пробельный символ стоит после косой черты.
Запрещено:
// ❌ ПЛОХО ↴
const LONG_STRING = 'Это очень длинная строка, которая превышает лимит в \
80 символов. К сожалению, она содержит длинные отрезки пустого пространства, так \
как в продолженных строках имеются отступы для поддержания форматирования.';
Вместо этого напишите:
// ✅ ХОРОШО ↴
const LONG_STRING = 'Это очень длинная строка, которая превышает лимит в ' +
'80 символов. Она не содержит длинные отрезки пустого пространства, поскольку ' +
'конкатенируемые строки не имеют в себе лишних отступов.';
Существует два типа комментариев, JSDoc (/** ... */
) и не относящиеся к JSDoc обычные комментарии (// ...
или /* ... */
).
/** JSDoc */
комментарии для документации. Это те комментарии, с которыми стоит ознакомиться при использовании кода.// строчные комментарии
для комментирования реализации. Эти комментарии которые касаются только реализации самого кода.Комментарии JSDoc могут распознаваться различными инструментальными программами, такими как редакторы кода и генераторы документации, в то время как обычные комментарии могут быть распознаны только другими людьми.
В общих чертах, следуйте правилам для JSDoc из руководства по стилю написания JavaScript[4], разделы 7.1 - 7.5. В остальной части этого раздела описываются исключения из этих правил.
Используйте /** JSDoc */
комментарии для передачи информации пользователям вашего кода. Избегайте простого повторения имени свойства или параметра. Вам рекомендуется документировать все свойства и методы (экспортируемые/публичные или нет), назначение которых, по мнению вашего рецензента, не сразу очевидно из их названия.
Исключение: элементы, которые экспортируются только для использования инструментальными программами, например классы @NgModule, не требуют комментариев.
Для примера, не указывайте типы в @param
или @return
блоках, не пишите @implements
, @enum
, @private
, @override
в коде, который использует implements
, enum
, private
, override
и пр. ключевые слова.
Для неэкспортируемых элементов иногда достаточно имени и типа функции или параметра. Хотя код обычно выигрывает от большего документирования, чем просто имена переменных!
// ❌ ПЛОХО ↴
/** @param fooBarService Сервис "The Bar" для приложения "the Foo". */
@param
и @return
требуются только тогда, когда они добавляют новую информацию, а иначе их возможно исключить.// ✅ ХОРОШО ↴
/**
* Отправляет POST-запрос для начала варки кофе.
* @param amountLitres Количество для заваривания. Должно соответствовать размеру емкости!
*/
brew(amountLitres: number, logger: Logger) {
// ...
}
Параметризованное свойство — это параметр конструктора, которому предшествует один из модификаторов private
, protected
, public
или readonly
. Параметризованное свойство объявляет одновременно и параметр и свойство экземпляра, а также неявно присваивает им значения. Для примера, выражение constructor(private readonly foo: Foo)
объявляет то, что конструктор принимает параметр foo
, а также объявляет приватное и доступное только для чтения свойство foo
и присваивает значение параметра этому свойству перед выполнением остальной части конструктора.
Чтобы задокументировать эти поля, используйте JSDoc @param
аннотацию. Редакторы отображают описание при вызовах конструктора и доступе к свойствам.
// ✅ ХОРОШО ↴
/** Этот класс демонстрирует, как документируются параметризованные свойства. */
class ParamProps {
/**
* @param percolator Кофеварка, используемая для варки.
* @param beans Зерна для варки.
*/
constructor(
private readonly percolator: Percolator,
private readonly beans: CoffeeBean[]) {}
}
// ✅ ХОРОШО ↴
/** Этот класс демонстрирует, как документируются обычные поля. */
class OrdinaryClass {
/** Кофейные зерна, которые будут использоваться в следующем вызове brew(). */
nextBean: CoffeeBean;
constructor(initialBean: CoffeeBean) {
this.nextBean = initialBean;
}
}
При необходимости, документируйте параметры в местах вызова при помощи встраивания блочных комментариев. Также рассмотрите возможность применения именованных параметров с использованием объектного литерала и деструктуризации. При этом нет каких-либо четких правил касательно точного форматирования и размещения комментария.
// ✅ ХОРОШО ↴
// Встраивание блочных комментариев для параметров, которые трудны для понимания:
new Percolator().brew(/* amountLitres= */ 5);
// Также рассмотрите возможность использования именованных аргументов и деструктуризации параметров (в объявлении метода "brew"):
new Percolator().brew({amountLitres: 5});
// ✅ ХОРОШО ↴
/** Перколятор, как старый вариант кофеварки {@link CoffeeBrewer} */
export class Percolator implements CoffeeBrewer {
/**
* Сварить кофе.
* @param amountLitres Количество, которое надо сварить. Должно соответствовать объему кофейника!
*/
brew(amountLitres: number) {
// Так или иначе, эта реализация создает ужасный кофе.
// TODO(b/12345): Улучшить процесс варки кофе в кофеварке.
}
}
Когда класс, метод или свойство имеют и декораторы вида @Component
и JSDoc, убедитесь, что JSDoc написан перед декоратором.
// ❌ ПЛОХО ↴
@Component({
selector: 'foo',
template: 'bar',
})
/** Компонент, который выводит "bar". */
export class FooComponent {}
// ✅ ХОРОШО ↴
/** Компонент, который выводит "bar". */
@Component({
selector: 'foo',
template: 'bar',
})
export class FooComponent {}
Возможности языка TypeScript, которые не рассматриваются в данном руководстве, возможно использовать без каких-либо рекомендаций по их применению.
Ограничение видимости свойств, методов и целых типов помогает сохранить код слабо связанным.
public
, за исключением случаев объявления доступных для чтения и записи (т.е. не readonly
) публичных параметризованных свойств (в конструкторе).// ❌ ПЛОХО ↴
class Foo {
public bar = new Bar(); // ПЛОХО: нет необходимости в модификаторе "public"
constructor(public readonly baz: Baz) {} // ПЛОХО: модификатор "readonly" подразумевает, что это свойство имеет по умолчанию модификатор "public"
}
// ✅ ХОРОШО ↴
class Foo {
bar = new Bar(); // ХОРОШО: нет необходимости в модификаторе "public"
constructor(public baz: Baz) {} // допускается модификатор "public"
}
См. также Область видимости экспортируемых элементов ниже.
При вызове конструктора всегда должны использоваться скобки, даже если никакие аргументы не передаются:
// ❌ ПЛОХО ↴
const x = new Foo;
// ✅ ХОРОШО ↴
const x = new Foo();
Нет необходимости предоставлять пустой конструктор или конструктор, который просто делегирует в родительский класс, поскольку ES2015 предоставляет конструктор класса по умолчанию, если он не указан. Однако не рекомендуется убирать конструкторы с параметризованными свойствами, модификаторами области видимости или декораторами параметров, даже если тело конструктора пустое.
// ❌ ПЛОХО ↴
class UnnecessaryConstructor {
constructor() {}
}
// ❌ ПЛОХО ↴
class UnnecessaryConstructorOverride extends Base {
constructor(value: number) {
super(value);
}
}
// ✅ ХОРОШО ↴
class DefaultConstructor {
}
class ParameterProperties {
constructor(private myService) {}
}
class ParameterDecorators {
constructor(@SideEffectDecorator myService) {}
}
class NoInstantiation {
private constructor() {}
}
#private
Не используйте приватные поля (также известные как приватные идентификаторы):
// ❌ ПЛОХО ↴
class Clazz {
#ident = 1;
}
Вместо этого используйте поддерживаемые TypeScript аннотации видимости:
// ✅ ХОРОШО ↴
class Clazz {
private ident = 1;
}
Почему?
Приватные идентификаторы вызывают существенные проблемы с размером и производительностью при понижении версии стандарта ECMAScript, в которую будет скомпилирован код TypeScript и не поддерживаются до ES2015. Они могут быть понижены только до уровня ES2015, но не ниже. В то же время, они не дают существенных преимуществ, когда для контроля области видимости используется статическая проверка типов.
readonly
Пометьте модификатором readonly
те свойства, которые никогда не переназначаются вне конструктора (они не обязательно должны быть глубоко неизменяемыми).
Вместо того чтобы просто передавать очевидный инициализатор в член класса, используйте параметризованные свойства TypeScript.
// ❌ ПЛОХО ↴
class Foo {
private readonly barService: BarService;
constructor(barService: BarService) {
this.barService = barService;
}
}
// ✅ ХОРОШО ↴
class Foo {
constructor(private readonly barService: BarService) {}
}
Если параметризованное свойство нуждается в документировании, то используйте JSDoc тег @param
.
Если элемент класса не является параметризованным свойством, инициализируйте его там, где он объявлен, что иногда позволяет совсем отбросить конструктор.
// ❌ ПЛОХО ↴
class Foo {
private readonly userList: string[];
constructor() {
this.userList = [];
}
}
// ✅ ХОРОШО ↴
class Foo {
private readonly userList: string[] = [];
}
Для свойств, так или иначе задействованных вне лексической области видимости содержащего их класса, например, для свойств контроллера Angular используемых из шаблона, не должна использоваться приватная (private
) область видимости, т.к. к этим свойствам потребуется доступ за пределами лексической области видимости их класса.
Для этих свойств используйте либо protected
, либо public
, в зависимости от того, что подходит. Для свойств используемых в шаблонах Angular и AngularJS следует использовать protected
, а в Polymer - public
.
В TypeScript коде не должны использоваться obj['foo']
для обхода ограничения видимости свойства [5].
Почему?
Когда свойство является приватным (
private
), вы объявляете автоматизированным системам и людям, что доступ к свойству ограничен методами объявленного класса, и они будут полагаться на это. Например, проверка на неиспользуемый код отметит приватное свойство, которое может посчитаться неиспользуемым, даже если какому-то коду из другого файла удастся обойти ограничение видимости.Хотя это и кажется, что
obj['foo']
может обойти область видимости в компиляторе TypeScript, эта схема может быть нарушена путем изменения правил сборки, а также нарушается согласованность с оптимизациями.
Для членов класса возможно иcпользовать геттеры и сеттеры. Методы-геттеры должны быть чистыми функциями (т.е. не иметь побочных эффектов и каждый раз возвращать одинаковый результат при одних и тех же параметрах). Они также полезны как средство ограничения видимости внутренних или подробных деталей реализации (показано ниже).
// ✅ ХОРОШО ↴
class Foo {
constructor(private readonly someService: SomeService) {}
get someMember(): string {
return this.someService.someVariable;
}
set someMember(newValue: string) {
this.someService.someVariable = newValue;
}
}
Если аксессор используется для сокрытия свойства класса, то для скрытого свойства возможно указать префикс или суффикс с любым целым словом, например internal
или wrapped
. При использовании этих приватных свойств по возможности обращайтесь к их значению через аксессор. По крайней мере один аксессор к свойству должен быть нетривиальным: не определяйте сквозные аксессоры только для того, чтобы скрыть свойство. Вместо этого сделайте свойство публичным (или подумайте о том, чтобы сделать его доступным только для чтения (readonly
), чем просто определять геттер без сеттера).
// ✅ ХОРОШО ↴
class Foo {
private wrappedBar = '';
get bar() {
return this.wrappedBar || 'bar';
}
set bar(wrapped: string) {
this.wrappedBar = wrapped.trim();
}
}
// ❌ ПЛОХО ↴
class Bar {
private barInternal = '';
// Ни один из этих аксессоров не имеет логики, поэтому просто сделайте bar публичным
get bar() {
return this.barInternal;
}
set bar(value: string) {
this.barInternal = value;
}
}
this
в статическом контекстеВ коде не должен использоваться this
в статическом контексте.
JavaScript позволяет обращаться к статическим полям через this
. Кроме того, в отличие от других языков, статические поля являются наследуемыми.
// ❌ ПЛОХО ↴
class ShoeStore {
static storage: Storage = ...;
static isAvailable(s: Shoe) {
// Плохо: не используйте `this` в статическом методе.
return this.storage.has(s.id);
}
}
class EmptyShoeStore extends ShoeStore {
static storage: Storage = EMPTY_STORE; // переопределяет storage из ShoeStore
}
Почему?
Этот код может привести к неожиданностям: авторы могут не ожидать, что к статическим полям можно обращаться через указатель
this
и могут быть удивлены, обнаружив, что они могут быть переопределены — подобная функциональность используется не часто.Этот код также поощряет использование антипаттерна, заключающегося в наличии значительного статического состояния, что вызывает проблемы с тестируемостью.
Код TypeScript не должен создавать экземпляры классов-оберток для примитивных типов String
, Boolean
и Number
. Классы-обертки имеют неожиданное поведение, такое как new Boolean(false)
равное true
.
// ❌ ПЛОХО ↴
const s = new String('hello');
const b = new Boolean(false);
const n = new Number(5);
// ✅ ХОРОШО ↴
const s = 'hello';
const b = false;
const n = 5;
В коде на Typescript не должен использоваться Array()
конструктор, с или без new
. Его применение неоднозначно и сбивает с толку:
// ❌ ПЛОХО ↴
const a = new Array(2); // [undefined, undefined]
const b = new Array(2, 3); // [2, 3];
Вместо этого всегда используйте скобки для инициализации массивов или from
для инициализации Array
с определенным размером:
// ✅ ХОРОШО ↴
const a = [2];
const b = [2, 3];
// Эквивалент для Array(2):
const c = [];
c.length = 2;
// [0, 0, 0, 0, 0]
Array.from<number>({length: 5}).fill(0);
В TypeScript коде возможно использовать String()
и Boolean()
(примечание: без new
!) функции, строковые шаблонные литералы или !!
для преобразования типов.
// ✅ ХОРОШО ↴
const bool = Boolean(false);
const str = String(aNumber);
const bool2 = !!str;
const str2 = `result: ${bool2}`;
Значения перечислений (enum
) (включая объединения перечислений и других типов) не должны преобразовываться в булевы значения с помощью Boolean()
или !!
, а должны вместо этого сравниваться явным образом с помощью операторов сравнения.
// ❌ ПЛОХО ↴
enum SupportLevel {
NONE,
BASIC,
ADVANCED,
}
const level: SupportLevel = ...;
let enabled = Boolean(level);
const maybeLevel: SupportLevel|undefined = ...;
enabled = !!maybeLevel;
// ✅ ХОРОШО ↴
enum SupportLevel {
NONE,
BASIC,
ADVANCED,
}
const level: SupportLevel = ...;
let enabled = level !== SupportLevel.NONE;
const maybeLevel: SupportLevel|undefined = ...;
enabled = level !== undefined && level !== SupportLevel.NONE;
Почему?
Для большинства задач не имеет значения, числовое или строковое значение сопоставлено с именем перечисления во время выполнения программы, поскольку значения перечислений указываются в исходном коде по имени. Следовательно, инженеры привыкли не задумываться об этом, а потому нежелательны ситуации, когда это действительно важно, так как они будут приводить к неожиданностям. Так происходит и в случае преобразования перечислений в булевы значения; в частности, вероятно может быть неожиданным, что по умолчанию первое объявленное значение перечисления является ложным (потому что оно равно 0), в то время как остальные значения являются истинными. Пользователи, читающие код, в котором используется значение перечисления, могут даже не знать, является ли оно первым объявленным значением или нет.
Не приветствуется для приведения к строке использовать конкатенацию строк, так как при проверке кода мы отслеживаем, чтобы операнды оператора «плюс» имели совпадающие типы.
Код должен использовать Number()
для парсинга числовых значений и должен явно проверять его возврат на значения NaN
, за исключением случаев, когда из контекста точно известно, что сбой парсинга невозможен.
Примечание: Number('')
, Number(' ')
, и Number('\t')
могут вернуть 0
вместо NaN
. Number('Infinity')
и Number('-Infinity')
могут вернуть Infinity
и -Infinity
соответственно. Кроме того, экспоненциальная запись, такая как Number('1e+309')
и Number('-1e+309')
, может привести к переполнению и преобразованию в Infinity
. Подобные случаи могут потребовать особого обращения.
// ✅ ХОРОШО ↴
const aNumber = Number('123');
if (!isFinite(aNumber)) throw new Error(...);
В коде не должен использоваться унарный плюс (+
) для преобразования строки в число. Парсинг чисел может привести к неудаче, иметь неожиданные исключительные ситуации и может быть признаком дурно пахнущего кода (парсинг чисел не на том уровне). Учитывая это, унарный плюс слишком легко пропустить при проверке кода.
// ❌ ПЛОХО ↴
const x = +y;
В коде также не должны использоваться parseInt
или parseFloat
для парсинга чисел, за исключением случаев парсинга в строках недесятичных числовых значений (см. ниже). Обе эти функции игнорируют конечные символы в строке, что может привести к возникновению ошибочного состояния (например, парсинг 12 гномов
как 12
).
// ❌ ПЛОХО ↴
const n = parseInt(someString, 10); // Подвержено ошибкам,
const f = parseFloat(someString); // независимо от передачи основания системы счисления.
Код, требующий выполнить парсинг числа с указанием системы счисления, перед вызовом parseInt
должен проверить, что входные данные содержат только подходящие для этой системы счисления цифры;
// ✅ ХОРОШО ↴
if (!/^[a-fA-F0-9]+$/.test(someString)) throw new Error(...);
// Требуется для парсинга восьмеричного числа.
// tslint:disable-next-line:ban
const n = parseInt(someString, 16); // Допустимо только для основания числа != 10
Используйте Number()
, а затем Math.floor
или Math.trunc
(там, где это возможно) для парсинга целых чисел:
// ✅ ХОРОШО ↴
let f = Number(someString);
if (isNaN(f)) handleError();
f = Math.floor(f);
Не используйте явное булево преобразование в условиях, в которых уже имеется неявное булево преобразование. Это условия в операторах if
, for
и while
.
// ❌ ПЛОХО ↴
const foo: MyInterface|null = ...;
if (!!foo) {...}
while (!!foo) {...}
// ✅ ХОРОШО ↴
const foo: MyInterface|null = ...;
if (foo) {...}
while (foo) {...}
Как и в случае явных преобразований, значения перечислений (включая объединения перечислений и других типов) не должны неявно приводиться к булевым значениям, а должны сравниваться явным образом с помощью операторов сравнения.
// ❌ ПЛОХО ↴
enum SupportLevel {
NONE,
BASIC,
ADVANCED,
}
const level: SupportLevel = ...;
if (level) {...}
const maybeLevel: SupportLevel|undefined = ...;
if (level) {...}
// ✅ ХОРОШО ↴
enum SupportLevel {
NONE,
BASIC,
ADVANCED,
}
const level: SupportLevel = ...;
if (level !== SupportLevel.NONE) {...}
const maybeLevel: SupportLevel|undefined = ...;
if (level !== undefined && level !== SupportLevel.NONE) {...}
Другие типы значений могут быть либо неявно преобразованы в булевы значения, либо явно сравнены с помощью операторов сравнения:
// ✅ ХОРОШО ↴
// Явное сравнение > 0 это хорошо:
if (arr.length > 0) {...}
// так же как и полагаться на неявное булево преобразование:
if (arr.length) {...}
Всегда используйте const
или let
для объявления переменных. По умолчанию используйте const
, если не требуется переназначение переменной. Никогда не используйте var
.
// ✅ ХОРОШО ↴
const foo = otherValue; // Используйте, если "foo" никогда не меняется.
let bar = someValue; // Используйте, если для "bar" когда-либо позднее будет присвоено значение
const
и let
имеют блочную область видимости, как и переменные в большинстве других языков. var
в JavaScript ограничен областью действия функции, что может вызвать трудные для понимания ошибки. Не используйте его.
// ❌ ПЛОХО ↴
var foo = someValue; // Не используйте - область видимости var сложна и подвержена ошибкам.
Переменные не должны использоваться до их объявления.
new
при создании экземпляров класса Error
Всегда используйте new Error()
при создании исключений вместо простого вызова Error()
. В обоих случаях создается новый экземпляр Error
, но использование new
более согласуется с тем, как создаются экземпляры других объектов.
// ✅ ХОРОШО ↴
throw new Error('Foo is not a valid bar.');
// ❌ ПЛОХО ↴
throw Error('Foo is not a valid bar.');
Error
JavaScript (и, следовательно, TypeScript) позволяет при выбрасывании исключений использовать произвольные значения. Однако если выброшенное значение не является экземпляром класса Error
, то оно не получит записи трассировки стека, что затруднит отладку.
// ❌ ПЛОХО ↴
// плохо: не позволяет получить трассировку стека.
throw 'ой, ошибка!';
Вместо этого, при выбрасывании исключений используйте только экземпляры класса (или подкласса) Error
:
// ✅ ХОРОШО ↴
// При выбрасывании исключений используйте только экземпляры класса Error
throw new Error('ой, ошибка!');
// ... или подтипы класса Error
class MyError extends Error {}
throw new MyError('моя "ой, ошибка!"');
В коде, при перехвате исключений, рекомендуется рассматривать все бросаемые исключения как экземпляры класса Error
.
// ✅ ХОРОШО ↴
try {
doSomething();
} catch (e: unknown) {
// Все выбрасываемые исключения должны быть подтипами класса Error. Не обрабатывайте другие
// возможные значения, кроме случаев, когда вы точно знаете, что именно они будут выброшены.
assert(e, isInstanceOf(Error));
displayError(e.message);
// или проброс
throw e;
}
Обработчики исключений не должны защитно обрабатывать типы, отличные от Error
, за исключением случаев, когда достоверно известно, что вызываемый API выбрасывает исключения, не соответствующие типу Error
, в нарушение вышеуказанного правила. В таком случае рекомендуется добавить комментарий, в котором специально указывается источник возникновения исключения, не соответствующего типу Error
.
// ✅ ХОРОШО ↴
try {
badApiThrowingStrings();
} catch (e: unknown) {
// Примечание: это плохое API при выбрасывании исключения передает строку, вместо экземпляра класса Error
if (typeof e === 'string') { ... }
}
Почему?
Избегайте чрезмерно защитного программирования. Повторение одних и тех же защитных средств от проблемы, которой не будет существовать в большей части кода, приводит к появлению шаблонного кода, который не является полезным.
Итерация по объектам с помощью for (... in ...)
подвержена вероятным ошибкам, т.к. это включает в себя все перечисляемые свойства из цепочки прототипов.
Не используйте не фильтрованные for (... in ...)
выражения:
// ❌ ПЛОХО ↴
for (const x in someObj) {
// x может происходить от некоторого родительского прототипа!
}
Либо явно отфильтруйте значения с помощью оператора if
, либо используйте for (... of Object.keys(...))
.
// ✅ ХОРОШО ↴
for (const x in someObj) {
if (!someObj.hasOwnProperty(x)) continue;
// сейчас x был точно определен в принадлежности someObj
}
for (const x of Object.keys(someObj)) { // примечание: for _of_!
// сейчас x был точно определен в принадлежности someObj
}
for (const [key, value] of Object.entries(someObj)) { // примечание: for _of_!
// сейчас key был точно определен в принадлежности someObj
}
Не используйте for (... in ...)
для итерации по массивам. Это будет контринтуитивно давать индексы массива (в виде строк!), а не значения:
// ❌ ПЛОХО ↴
for (const x in someArray) {
// x - это индекс!
}
Для итерации по массивам предпочтительно использовать for (... of someArr)
[6]. Также приемлемо использовать Array.prototype.forEach
или обычные циклы for
:
// ✅ ХОРОШО ↴
for (const x of someArr) {
// x - ссылается на значение из someArr
}
for (let i = 0; i < someArr.length; i++) {
// Если необходим индекс, то используйте явный пересчет, а иначе используйте форму for/of.
const x = someArr[i];
// ...
}
for (const [i, x] of someArr.entries()) {
// Альтернативная версия предыдущего.
}
Использование spread-оператора [...foo]; {...bar}
является удобным сокращением для копирования массивов и объектов. При использовании spread-оператора для объектов, более поздние значения заменяют более ранние с тем же ключом.
// ✅ ХОРОШО ↴
const foo = {
num: 1,
};
const foo2 = {
...foo,
num: 5,
};
const foo3 = {
num: 5,
...foo,
}
foo2.num === 5;
foo3.num === 1;
При использовании spread-оператора раскладываемое значение должно соответствовать создаваемому. Т.е. при создании объекта с spread-оператором можно использовать только объекты, а при создании массива раскладывайте только итерируемые объекты. Примитивы, включая null
и undefined
, не должны раскладываться.
// ❌ ПЛОХО ↴
const foo = {num: 7};
const bar = {num: 5, ...(shouldUseFoo && foo)}; // может быть undefined
// Создает {0: 'a', 1: 'b', 2: 'c'} но при этом не содержит длины (length)
const fooStrings = ['a', 'b', 'c'];
const ids = {...fooStrings};
// ✅ ХОРОШО ↴
const foo = shouldUseFoo ? {num: 7} : {};
const bar = {num: 5, ...foo};
const fooStrings = ['a', 'b', 'c'];
const ids = [...fooStrings, 'd', 'e'];
Операторы управления потоком всегда используют блоки для размещения содержащегося в них кода.
// ✅ ХОРОШО ↴
for (let i = 0; i < x; i++) {
doSomethingWith(i);
}
if (x) {
doSomethingWithALongMethodNameThatForcesANewLine(x);
}
// ❌ ПЛОХО ↴
if (x)
doSomethingWithALongMethodNameThatForcesANewLine(x);
for (let i = 0; i < x; i++) doSomethingWith(i);
Исключением, при котором возможно не использовать блоки, являются операторы if
, которые умещаются на одной строке.
// ✅ ХОРОШО ↴
if (x) x.doFoo();
Предпочитайте избегать присваивания значений переменных внутри операторов управления. Присваивание легко спутать с проверкой на равенство внутри этих операторов.
// ❌ ПЛОХО ↴
if (x = someFunction()) {
// Присваивание легко перепутать с проверкой на равенство
// ...
}
// ✅ ХОРОШО ↴
x = someFunction();
if (x) {
// ...
}
В тех случаях, когда присваивание внутри оператора управления более предпочтительно, заключите это присваивание в дополнительные круглые скобки, чтобы указать, что оно сделано намеренно.
// ✅ ХОРОШО ↴
while ((x = someFunction())) {
// Двойная скобка указывает на то, что присваивание сделано намеренно
// ...
}
Каждый switch
оператор должен включать в себя блок по умолчанию (default
), даже если там не содержится кода.
// ✅ ХОРОШО ↴
switch (x) {
case Y:
doSomethingElse();
break;
default:
// ничего не делать.
}
Непустые группы операторов (case ...
) не должны проваливаться (обеспечивается настройками компилятора[7]):
// ❌ ПЛОХО ↴
switch (x) {
case X:
doSomething();
// дальнейший пропуск - не разрешен!
case Y:
// ...
}
Допускается пропуск пустых групп операторов:
// ✅ ХОРОШО ↴
switch (x) {
case X:
case Y:
doSomething();
break;
default: // ничего не делать.
}
Всегда используйте тройное равенство (===
) и неравенство (!==
). Операторы двойного равенства вызывают склонные к ошибкам приведения типов, которые трудны для понимания и работают медленнее в реализации виртуальных машин JavaScript. Смотрите также JavaScript таблицу равенства.
// ❌ ПЛОХО ↴
if (foo == 'bar' || baz != bam) {
// Трудное для понимания поведение из-за преобразования типов.
}
// ✅ ХОРОШО ↴
if (foo === 'bar' || baz !== bam) {
// Здесь все хорошо и понятно.
}
Исключение: При сравнении с значением null
возможно использовать операторы ==
и !=
для общего охвата null
и undefined
значений.
// ✅ ХОРОШО ↴
if (foo == null) {
// Будет срабатывать, когда foo равен null или undefined.
}
try
сфокусированнымиОграничьте количество кода внутри блока try
, если это можно сделать без ущерба для читабельности.
// ❌ ПЛОХО ↴
try {
const result = methodThatMayThrow();
use(result);
} catch (error: unknown) {
// ...
}
// ✅ ХОРОШО ↴
let result;
try {
result = methodThatMayThrow();
} catch (error: unknown) {
// ...
}
use(result);
Вынос не вызывающих исключений строк кода из блока try/catch помогает читающему код понять, какой метод выбрасывает исключения. Некоторые встраиваемые вызовы, которые не выбрасывают исключений, могут оставаться внутри блока, поскольку они могут не стоить дополнительных усложнений кода, связанных с добавлением временной переменной.
Исключение: Могут возникнуть проблемы с производительностью, если блоки try
находятся внутри цикла. Расширение блоков try
для охвата всего цикла — это нормально.
Предпочитайте function foo() { ... }
для объявления именованных функций верхнего уровня.
Стрелочные функции верхнего уровня возможно использовать, например, для обеспечения явной аннотации типа.
// ✅ ХОРОШО ↴
interface SearchFunction {
(source: string, subString: string): boolean;
}
const fooSearch: SearchFunction = (source, subString) => { ... };
Обратите внимание на различия между обсуждаемыми здесь объявлениями функций (
function foo() {}
) и функциональными выражениями (), которые обсуждаются ниже.doSomethingWith(function() {});
Всегда используйте стрелочные функции вместо функциональных выражений которые были до ES6 и задавались с помощью ключевого слова function
.
// ✅ ХОРОШО ↴
bar(() => { this.doSomething(); })
// ❌ ПЛОХО ↴
bar(function() { ... })
Функциональные выражения (определенные с помощью ключевого слова function
) возможно использовать только в том случае, если код должен динамически перепривязать this
, хотя в коде в принципе не рекомендуется перепривязывать this
. В коде обычных функций (в отличие от стрелочных функций и методов) не рекомендуется обращаться к this
.
Используйте стрелочные функции с выражениями или блоками в качестве тела там, где это целесообразно.
// ✅ ХОРОШО ↴
// Для объявления функции верхнего уровня используйте Function Declarations.
function someFunction() {
// Вполне подходит использование блочных тел стрелочных функций, т.е. у которых тело функции представляет => { } :
const receipts = books.map((b: Book) => {
const receipt = payMoney(b.price);
recordTransaction(receipt);
return receipt;
});
// Использование выражения в качестве тела функции тоже подходит, если возвращаемое значение будет использоваться:
const longThings = myValues.filter(v => v.length > 1000).map(v => String(v));
function payMoney(amount: number) {
// Function Declarations - это хорошо, но не обращайтесь к `this` в них.
}
// Вложенные стрелочные функции могут быть назначены константе
const computeTax = (amount: number) => amount * 0.12;
}
Используйте выражения в качестве тела функции только в том случае, если возвращаемое значение функции действительно используется.
// ❌ ПЛОХО ↴
// ПЛОХО: используйте блочное тело функции ({ ... }) если возвращаемое значение функции не используется.
myPromise.then(v => console.log(v));
// ✅ ХОРОШО ↴
// ХОРОШО: возвращаемое значение не используется, поэтому применяется блочное тело функции.
myPromise.then(v => {
console.log(v);
});
// ХОРОШО: в коде можно использовать блочное тело функции для повышения удобочитаемости.
const transformed = [1, 2, 3].map(v => {
const intermediate = someComplicatedExpr(v);
const more = acrossManyLines(intermediate);
return worthWrapping(more);
});
this
Функциональные выражения не должны использовать this
, если только они не существуют специально для перепривязки this
. В большинстве случаев перепривязки this
можно избежать, используя стрелочные функции или явно заданные параметры.
// ❌ ПЛОХО ↴
function clickHandler() {
// Плохо: что такое «this» в этом контексте?
this.textContent = 'Hello';
}
// Плохо: `this` неявно ссылается на document.body .
document.body.onclick = clickHandler;
// ✅ ХОРОШО ↴
// Хорошо: явная ссылка на объект из стрелочной функции.
document.body.onclick = () => { document.body.textContent = 'hello'; };
// Альтернатива: взять явно заданный параметр
const setTextFn = (e: HTMLElement) => { e.textContent = 'hello'; };
document.body.onclick = setTextFn.bind(null, document.body);
В классах обычно не рекомендуется содержать свойства, которые проинициализированы как стрелочные функции. Использование стрелочных функций как свойств требует чтобы вызывающая их функция корректно понимала, что у вызываемой функции уже есть привязанный this
, что увеличивает путаницу в понимании того, что такое this
, а сами места вызовов и ссылки использующие эти функции могут смотреться некорректно работающими (т.к. это требует дополнительных знаний об окружении за пределами локальной области вызывающей функции, чтобы определить, что они корректны). В коде рекомендуется всегда использовать стрелочные функции для вызова методов экземпляра (const handler = (x) => { this.listener(x); };
) и не рекомендуется получать или передавать ссылки на методы экземпляра ().const handler = this.listener; handler(x);
Примечание: в некоторых специфических ситуациях, например, в случае привязки функций к шаблонам, стрелочные функции в качестве свойств полезны и создают гораздо более читабельный код. Руководствуйтесь здравым смыслом при использовании этого правила. Также см. раздел Обработчики событий ниже.
// ❌ ПЛОХО ↴
class DelayHandler {
constructor() {
// Проблема: `this` не сохраняется в функции обратного вызова. `this` в обратном вызове
// не будет экземпляром DelayHandler.
setTimeout(this.patienceTracker, 5000);
}
private patienceTracker() {
this.waitedPatiently = true;
}
}
// ❌ ПЛОХО ↴
// Стрелочные функции обычно не рекомендуется задавать свойствам.
class DelayHandler {
constructor() {
// Плохо: этот код выглядит так, как будто тут забыли привязать `this`.
setTimeout(this.patienceTracker, 5000);
}
private patienceTracker = () => {
this.waitedPatiently = true;
}
}
// ✅ ХОРОШО ↴
// Явное управление `this` во время вызова.
class DelayHandler {
constructor() {
// По возможности используйте анонимные функции.
setTimeout(() => {
this.patienceTracker();
}, 5000);
}
private patienceTracker() {
this.waitedPatiently = true;
}
}
Обработчики событий могут использовать стрелочные функции, когда нет необходимости удалять обработчик (например, если событие генерируется самим классом). Если для обработчика впоследствии требуется удаление, тогда правильным подходом будет использование назначенной свойству стрелочной функции, поскольку они автоматически захватывают this
и при этом обеспечивается постоянная ссылка на обработчик для его последующего удаления.
// ✅ ХОРОШО ↴
// Обработчики событий могут быть анонимными функциями или назначенные свойствам стрелочными функциями.
class Component {
onAttached() {
// Событие генерируется этим классом, удалять его не нужно.
this.addEventListener('click', () => {
this.listener();
});
// this.listener это постоянная ссылка на функцию-обработчик, которую мы позже можем удалить.
window.addEventListener('onbeforeunload', this.listener);
}
onDetached() {
// Событие генерируется окном (window). Если мы не удалим функцию-обработчик (this.listener), то она
// сохранит ссылку на `this` к которой привязана, что приведет к утечке памяти.
window.removeEventListener('onbeforeunload', this.listener);
}
// Стрелочная функция, хранящаяся в свойстве, автоматически привязывается к `this`.
private listener = () => {
confirm('Вы хотите покинуть страницу?');
}
}
Не используйте bind
в выражениях, которые устанавливают обработчики событий, потому что это создает временную ссылку, которую нельзя удалить.
// ❌ ПЛОХО ↴
// Привязка слушателей создает временную ссылку, которая недоступна для удаления.
class Component {
onAttached() {
// Это создает временную ссылку, которая нам не будет доступна для удаления.
window.addEventListener('onbeforeunload', this.listener.bind(this));
}
onDetached() {
// метод bind каждый раз создает новую ссылку, поэтому эта строка не делает ничего.
window.removeEventListener('onbeforeunload', this.listener.bind(this));
}
private listener() {
confirm('Вы хотите покинуть страницу?');
}
}
Не следует полагаться на автоматическую вставку точки с запятой (ASI[8]). Явно завершайте все операторы с помощью точки с запятой. Это предотвращает ошибки, возникающие из-за неправильной вставки точки с запятой, а также обеспечивает совместимость с инструментами, которые имеют ограниченную поддержку ASI (например, clang-format).
Не используйте @ts-ignore
, а также такие варианты, как @ts-expect-error
или @ts-nocheck
. На первый взгляд кажется, что это простой способ исправить ошибку компилятора, но на практике конкретная ошибка компилятора часто вызывается более серьезной проблемой, которая может быть исправлена более явным путем.
Например, если вы используете @ts-ignore
для подавления ошибок типизации, то будет трудно предсказать, какие типы в конечном итоге будет видеть окружающий код. Для многих ошибок типизации, полезны советы в разделе как лучше всего использовать any
.
Утверждения типа (x as SomeType
) и утверждения ненулевого значения (y!
) не безопасны. Оба только заглушают компилятор TypeScript, но не вставляют никаких проверок во время выполнения, чтобы соответствовать этим утверждениям, поэтому они могут привести к сбою вашей программы во время выполнения.
По этой причине, вам не рекомендуется использовать утверждения типа и утверждения ненулевого значения без явной или объяснимой причины.
Вместо этого:
// ❌ ПЛОХО ↴
(x as Foo).foo();
y!.bar();
Когда вы захотите произвести утверждение типа или утверждение ненулевого значения, то лучшим решением будет написать проверку, которая будет работать во время выполнения.
// ✅ ХОРОШО ↴
// предположим, что Foo - это класс.
if (x instanceof Foo) {
x.foo();
}
if (y) {
y.bar();
}
Иногда из-за некоторых внутренних особенностей вашего кода вы можете быть уверены, что форма утверждения безопасна. В таких ситуациях рекомендуется добавить пояснение, объясняющее, почему вы согласны с небезопасным поведением:
// ✅ ХОРОШО ↴
// x это Foo, потому что ...
(x as Foo).foo();
// y не может быть null, потому что ...
y!.bar();
Возможно обойтись без комментариев, если очевидны причины, лежащие в основе применения утверждения типа или утверждения ненулевого значения. Например, сгенерированный код-прототип всегда допускает значение null
, но, возможно, в контексте кода хорошо известно, что определенные поля всегда предоставляются серверной частью. В таком случае, принимайте решение руководствуясь своим профессиональным видением.
Утверждения типа должны использовать синтаксис as
(в отличие от синтаксиса угловых скобок). Это позволяет заключить утверждение в круглые скобки при обращении к элементу.
// ❌ ПЛОХО ↴
const x = (<Foo>z).length;
const y = <Foo>z.length;
// ✅ ХОРОШО ↴
// z должен быть Foo, потому что ...
const x = (z as Foo).length;
Используйте аннотации типа (: Foo
) вместо утверждения типа (as Foo
) для указания типа объектного литерала. Это позволяет обнаружить ошибки рефакторинга, когда поля интерфейса меняются со временем.
// ❌ ПЛОХО ↴
interface Foo {
bar: number;
baz?: string; // был "bam", но позднее был переименован в "baz".
}
const foo = {
bar: 123,
bam: 'abc', // нет ошибки!
} as Foo;
function func() {
return {
bar: 123,
bam: 'abc', // нет ошибки!
} as Foo;
}
// ✅ ХОРОШО ↴
interface Foo {
bar: number;
baz?: string;
}
const foo: Foo = {
bar: 123,
bam: 'abc', // жалуется на то, что "bam" не был объявлен в Foo.
};
function func(): Foo {
return {
bar: 123,
bam: 'abc', // жалуется на то, что "bam" не был объявлен в Foo.
};
}
В объявлениях интерфейсов и классов, для разделения объявлений отдельных членов, должна использоваться точка с запятой:
// ✅ ХОРОШО ↴
interface Foo {
memberA: string;
memberB: number;
}
Интерфейсы специально не должны использовать запятую для разделения полей, поскольку это необходимо для симметричности с объявлениями классов:
// ❌ ПЛОХО ↴
interface Foo {
memberA: string,
memberB: number,
}
Встраиваемое объявление объектного типа в качестве разделителя должно использовать запятую:
// ✅ ХОРОШО ↴
type SomeTypeAlias = {
memberA: string,
memberB: number,
};
let someProperty: {memberC: string, memberD: number};
Код не должен смешивать доступ к свойству в кавычках с доступом к свойству через точку:
// ❌ ПЛОХО ↴
// Плохо: код должен использовать либо доступ без кавычек, либо доступ в кавычках для любого свойства
// единообразно для всего приложения:
console.log(x['someField']);
console.log(x.someField);
Свойства, которые используются извне по отношению к приложению, например, свойства объектов JSON или внешних API, должны быть доступны с использованием .dotted
нотации, а также должны быть объявлены в качестве так называемых внешних свойств посредством применения модификатора declare
.
// ✅ ХОРОШО ↴
// Хорошо: использование "declare" для объявления типов, которые используются извне
// по отношению к приложению, для того, чтобы их свойства не переименовывались.
declare interface ServerInfoJson {
appVersion: string;
user: UserJson; // Примечание: UserJson также должен использовать `declare`!
}
// serverResponse должен быть ServerInfoJson в соответствии с контрактом приложения.
const data = JSON.parse(serverResponse) as ServerInfoJson;
console.log(data.appVersion); // Тип защищен и переименование безопасно!
При импорте объекта модуля напрямую обращайтесь к свойствам объекта модуля, а не передавайте его. Это гарантирует, что модули могут быть проанализированы и оптимизированы. Отношение к импорту модулей как к пространствам имен является нормальным.
// ❌ ПЛОХО ↴
import * as utils from 'utils';
class A {
readonly utils = utils; // <--- ПЛОХО: передача всего объекта модуля
}
// ✅ ХОРОШО ↴
import * as utils from 'utils';
class A {
readonly utils = {method1: utils.method1, method2: utils.method2};
}
или более кратко:
// ✅ ХОРОШО ↴
import {method1, method2} from 'utils';
class A {
readonly utils = {method1, method2};
}
Это правило согласованности с оптимизациями применимо ко всем веб-приложениям. Оно не применяется к коду, который выполняется только на стороне сервера (например, в NodeJS для выполнения тестов). Но все же для поддержания чистоты кода очень поощряется всегда объявлять все типы и избегать смешивания доступа к свойствам с кавычками и без кавычек.
В коде не должны использоваться const enum
, вместо этого используйте обычный enum
.
Почему?
В TypeScript перечисления и так не могут быть изменены, а
const enum
— это отдельная особенность языка, связанная с оптимизацией, которая делает перечисление невидимым для пользователей JavaScript модуля.
Команды отладчика (наподобие debugger;
) не должны включаться в рабочий код.
// ❌ ПЛОХО ↴
function debugMe() {
debugger;
}
Декораторы обозначаются с помощью префикса @
, например @MyDecorator
.
Не определяйте новых декораторов. Используйте только те декораторы, которые определены фреймворками:
@Component
, @NgModule
и т.д.);@property
).Почему?
В основном мы предпочитаем избегать декораторов, поскольку они были экспериментальной функцией, которая с тех пор отклонилась от предложения TC39 и имеет известные ошибки, которые вряд ли будут исправлены [9].
При использовании декораторов, декоратор должен непосредственно предшествовать элементу, к которому он применяется, без пустых строк между ними:
// ✅ ХОРОШО ↴
/** Комментарии JSDoc идут перед декораторами */
@Component({...}) // Примечание: после декоратора не должно быть пустой строки.
class MyComp {
@Input() myField: string; // Декораторы полей могут находиться на одной линии...
@Input()
myOtherField: string; // ... или переноситься.
}
В TypeScript коде обязательно должны указываться пути при импорте другого TypeScript кода. Возможно указывать относительные пути, т.е. начинающиеся с .
или ..
или с базовой директории, как например root/path/to/file
.
В коде рекомендуется использовать относительные импорты (./foo
) вместо абсолютных импортов path/to/foo
при ссылке на файлы в пределах одного и того же (в логическом смысле) проекта, т.к. это позволяет перемещать весь проект без внесения изменений в эти импорты.
Рассмотрите возможность ограничения количества родительских шагов (../../../
), т.к. это может затруднить понимание структуры модулей и путей.
// ✅ ХОРОШО ↴
import {Symbol1} from 'path/from/root';
import {Symbol2} from '../parent/file';
import {Symbol3} from './sibling';
TypeScript поддерживает два метода организации кода: пространства имен (namespaces) и модули, но использование пространств имен необходимо избегать. Т.е. ваш код должен ссылаться на код в других файлах с помощью импорта и экспорта вида import {foo} from 'bar';
В вашем коде не должны использоваться namespace Foo { ... }
конструкции. Пространства имен (namespace
) возможно использовать только тогда, когда это необходимо для взаимодействия с внешним сторонним кодом. Чтобы семантически разделить пространство имен вашего кода, используйте отдельные файлы.
В коде не должны использоваться require
(как в import x = require('...');
) для импортов. Используйте синтаксис модулей ES6.
// ❌ ПЛОХО ↴
// Плохо: не используйте пространства имен:
namespace Rocket {
function launch() { ... }
}
// Плохо: не используйте <reference>
/// <reference path="..."/>
// Плохо: не используйте require()
import x = require('mydep');
Примечание: В TypeScript пространства имен (
namespace
) раньше назывались внутренними модулями и использовали ключевое словоmodule
в видеmodule Foo { ... }
. Не используйте такую форму. Всегда используйте ES6 импорты.
По всему коду используйте именованные экспорты:
// ✅ ХОРОШО ↴
// Использование именованного экспорта:
export class Foo { ... }
Не используйте экспорт по умолчанию. Это гарантирует, что все импорты будут следовать единому шаблону.
// ❌ ПЛОХО ↴
// Не используйте экспорт по умолчанию:
export default class Foo { ... } // ПЛОХО!
Почему?
Экспорт по умолчанию не предоставляет канонического имени, что затрудняет централизованное обслуживание при относительно небольшой пользе для владельцев кода, причем возможно ухудшение читабельности:
// ❌ ПЛОХО ↴ import Foo from './bar'; // Валидно. import Bar from './bar'; // Также валидно.
Преимущество именованного экспорта заключается в том, что оно приводит к ошибкам, когда операторы импорта пытаются импортировать что-то, что не было объявлено. В
foo.ts
:
// ❌ ПЛОХО ↴ const foo = 'blah'; export default foo;
И в
bar.ts
:
// ❌ ПЛОХО ↴ import {fizz} from './foo';
В результате возникает ошибка
error TS2614: Module '"./foo"' has no exported member 'fizz'
. Если указать вbar.ts
:
// ❌ ПЛОХО ↴ import fizz from './foo';
В результате получается
fizz === foo
, что может быть неожиданным и затрудняющим отладку.Кроме того, экспорт по умолчанию побуждает людей помещать все в один большой объект, чтобы разместить все вместе в пространстве имен:
// ❌ ПЛОХО ↴ export default class Foo { static SOME_CONSTANT = ... static someHelpfulFunction() { ... } ... }
В приведенном выше примере у нас есть область видимости файла, которая может использоваться как пространство имен. У нас также есть, возможно, ненужная вторая область видимости (класс
Foo
), которая в других файлах может двусмысленно использоваться и как тип, и как значение.Вместо этого предпочтительно использовать файловую область видимости для пространства имен, а также именованный экспорт:
// ✅ ХОРОШО ↴ export const SOME_CONSTANT = ... export function someHelpfulFunction() export class Foo { // тут только элементы класса }
TypeScript не поддерживает ограничение видимости экспортируемых элементов. Экспортируйте только те элементы, которые используются вне модуля. В целом, минимизируйте экспортируемую часть API модулей.
Независимо от технической стороны, мутабельные экспорты могут создавать трудно понимаемый и отлаживаемый код, особенно при реэкспорте в различных модулях. Если по другому сформулировать это правило, то export let
не допускается.
// ❌ ПЛОХО ↴
export let foo = 3;
// В чистом ES6 foo является мутабельным, и импортеры будут видеть изменение его значения уже через секунду.
// В TS (прим. пер.: в версии TS < 3.9, при использовании модулей CommonJS), если foo реэкспортируется вторым файлом,
// импортеры не увидят изменения значения.
// Прим. пер.: В версии TS >= 3.9, это будет работать по аналогии с ES6.
window.setTimeout(() => {
foo = 4;
}, 1000 /* миллисекунды */);
Если необходимо поддерживать доступные извне мутабельные привязки, то вместо этого рекомендуется явно использовать функции-геттеры.
// ✅ ХОРОШО ↴
let foo = 3;
window.setTimeout(() => {
foo = 4;
}, 1000 /* миллисекунды */);
// Используйте явно заданный геттер для доступа к мутабельному экспорту.
export function getFoo() { return foo; };
При экспорте одного из двух значений в зависимости от условий, в качестве стандартного шаблона, сначала выполняется проверка условий, а затем экспорт. Убедитесь, что все экспортируемые значения являются окончательными после выполнения всего тела модуля.
// ✅ ХОРОШО ↴
function pickApi() {
if (useOtherApi()) return OtherApi;
return RegularApi;
}
export const SomeApi = pickApi();
Не создавайте классы-контейнеры со статическими методами или свойствами ради пространства имен.
// ❌ ПЛОХО ↴
export class Container {
static FOO = 1;
static bar() { return 1; }
}
Вместо этого экспортируйте отдельные константы и функции:
// ✅ ХОРОШО ↴
export const FOO = 1;
export function bar() { return 1; }
В ES6 и TypeScript есть четыре варианта операторов импорта:
Вид импорта | Пример | Назначение |
---|---|---|
модульный | import * as foo from '...'; | Импорты TypeScript |
деструктурирующий | import {SomeThing} from '...'; | Импорты TypeScript |
по умолчанию | import SomeThing from '...'; | Только для поддержки стороннего кода, который их требует |
для использования побочных эффектов | import '...'; | Только для импорта библиотек ради получения их сторонних эффектов при загрузке (таких как пользовательские элементы) |
// ✅ ХОРОШО ↴
// Хорошо: выберите один из двух вариантов в зависимости от ситуации (см. ниже).
import * as ng from '@angular/core';
import {Foo} from './foo';
// Только при необходимости: импорт по умолчанию.
import Button from 'Button';
// Иногда необходимо импортировать библиотеки для получения их вспомогательных эффектов:
import 'jasmine';
import '@polymer/paper-button';
Как модульный, так и деструктурирующий импорт имеют свои преимущества в зависимости от ситуации.
Несмотря на *
, импорт модуля не сопоставим с wildcard импортом , который встречается в других языках. Вместо этого импорт модулей дает имя всему модулю и каждой связанной с упомянутым модулем ссылке на элемент, что может сделать код более читабельным и обеспечивает функцию автоматического определения всех элементов в модуле. Они также требуют меньшего количества операций импорта (все элементы доступны), меньше коллизий имен и позволяют использовать более лаконичные имена в импортируемом модуле. Импорт модулей особенно полезен при использовании множества различных элементов из больших API.
Деструктурирующие импорты дают локальные имена для каждого импортируемого элемента. Они позволяют использовать более краткий и лаконичный код при использовании импортируемого элемента, что особенно полезно для очень часто используемых элементов, как например, describe
и it
в Jasmine.
// ❌ ПЛОХО ↴
// Плохо: слишком длинный оператор импорта с излишними пространствами имен.
import {TableViewItem, TableViewHeader, TableViewRow, TableViewModel,
TableViewRenderer} from './tableview';
let item: TableViewItem = ...;
// ✅ ХОРОШО ↴
// Лучше: используйте модуль для пространства имен.
import * as tableview from './tableview';
let item: tableview.Item = ...;
// ✅ ХОРОШО ↴
import * as testing from './testing';
// Все тесты будут неоднократно использовать одни и те же три функции.
// При импорте только некоторых определенных элементов, которые используются очень часто, также
// рассмотрите возможность импорта элементов напрямую (см. пример ниже).
testing.describe('foo', () => {
testing.it('bar', () => {
testing.expect(...);
testing.expect(...);
});
});
// ✅ ХОРОШО ↴
// Лучше: дайте локальные имена распространенным функциям.
import {describe, it, expect} from './testing';
describe('foo', () => {
it('bar', () => {
expect(...);
expect(...);
});
});
...
В коде рекомендуется устранить возможные конфликты имен используя импорт модулей и переименовывая сами экспорты. При необходимости в коде можно переименовывать импорты (import {SomeThing as SomeOtherThing}
).
Три примера, когда переименование может быть полезным:
from
может быть более удобочитаемой, если ее переименовать в observableFrom
.Не используйте import type {...}
или export type {...}
.
// ❌ ПЛОХО ↴
import type {Foo};
export type {Bar};
export type {Bar} from './bar';
Вместо этого просто используйте обычные импорты и экспорты:
// ✅ ХОРОШО ↴
import {Foo} from './foo';
export {Bar} from './bar';
Примечание: это не относится к применению
export
в отношении определений типов, т.е.export type Foo = ...;
.
// ✅ ХОРОШО ↴
export type Foo = string;
Инструментарий TypeScript автоматически различает элементы, используемые как типы, и элементы, используемые как значения, и только для последних генерируется загружаемый во время выполнения код.
Почему?
Инструментарий TypeScript автоматически определяет различия и не внедряет динамическую (runtime) загрузку для обращений к типам. Это обеспечивает более удобный UX для разработчиков: переключение туда-сюда между
import type
иimport
весьма утомительно. В то же время,import type
не дает никаких гарантий: ваш код все равно может иметь жесткую зависимость от какого-либо импорта через различные транзитивные пути.Если вам необходима обязательная динамическая (runtime) загрузка для получения сторонних эффектов, используйте
import '...';
. См. импорты.
export type
может показаться полезным, чтобы избежать какого-либо экспортирования значения элемента в API. Однако и это не дает гарантий, т.к. последующий код может по-прежнему импортировать API другим путем. Лучший способ для разделения и гарантии использования API по типу и значению - разделить элементы, например, наUserService
иAjaxUserService
. Это менее подвержено ошибкам и лучше передает смысл.
Формируйте пакеты по их функциональному назначению, а не по типам. Например, для интернет-магазина рекомендуется иметь пакеты с названиями products
, checkout
, backend
, а не , views
, models
.controllers
В коде возможно полагаться на вывод типа, реализуемый компилятором TypeScript для всех типов выражений (переменных, полей класса, возвращаемых типов и т.д.).
// ✅ ХОРОШО ↴
const x = 15; // Тип выведен.
Не указывайте типы для тривиально выводимых типов: переменных или параметров, инициализированных строковыми (string
), числовыми (number
), логическими (boolean
) литералами, литералами регулярных выражений (RegExp
) или выражением new
.
// ❌ ПЛОХО ↴
const x: boolean = true; // Плохо: 'boolean' здесь не способствует удобочитаемости
// ❌ ПЛОХО ↴
// Плохо: 'Set' тривиально выводится из инициализации
const x: Set<string> = new Set();
// ✅ ХОРОШО ↴
const x = new Set<string>();
Для более сложных выражений, аннотации типов могут улучшить читабельность программы.
// ❌ ПЛОХО ↴
// Трудно предположить тип 'value' без аннотации.
const value = await rpc.getSomeValue().transform();
// ✅ ХОРОШО ↴
// Можно с первого взгляда определить тип 'value'.
const value: string[] = await rpc.getSomeValue().transform();
Необходимость аннотации определяется рецензентом кода.
Вопрос о том, следует ли включать аннотации типа возвращаемого значения для функций и методов, зависит от автора кода. Рецензенты могут запросить аннотации для уточнения сложных типов возвращаемых данных, которые трудно понять. В проектах может существовать локальная политика, согласно которой всегда требуется указывать возвращаемые типы, но это не является общим требованием стиля TypeScript.
Явная типизация неявных возвращаемых значений функций и методов имеет два преимущества:
TypeScript поддерживает типы null
и undefined
. Nullable-типы могут быть созданы как union-типы (string|null
), что также относится и к undefined
. Специального синтаксиса для объединений с null
и undefined
не существует.
В TypeScript коде для обозначения отсутствия значения можно использовать undefined
или null
, при этом нет общих рекомендаций для предпочтения одного другому. Множество JavaScript API используют undefined
(например, Map.get
), в то время как во многих DOM API и Google API используется null
(например, Element.getAttribute
), поэтому подходящее обозначение отсутствия значения зависит от контекста.
Псевдонимы типов не должны включать |null
или |undefined
в union-тип. Псевдонимы, допускающие значение null
, обычно указывают на то, что значения null
проходят через слишком много слоев приложения и это затуманивает источник исходной проблемы, которая привела к значению null
. Они также делают неясной ситуацию, когда конкретные значения в классе или интерфейсе могут отсутствовать.
Вместо этого код должен добавлять |null
или |undefined
только тогда, когда псевдоним фактически используется. В коде рекомендуется работать с null
в непосредственной близости от места их возникновения, используя вышеуказанные приемы.
// ❌ ПЛОХО ↴
// Плохо
type CoffeeResponse = Latte|Americano|undefined;
class CoffeeService {
getLatte(): CoffeeResponse { ... };
}
// ✅ ХОРОШО ↴
// Лучше
type CoffeeResponse = Latte|Americano;
class CoffeeService {
getLatte(): CoffeeResponse|undefined { ... };
}
// ✅ ХОРОШО ↴
// Наилучший вариант
type CoffeeResponse = Latte|Americano;
class CoffeeService {
getLatte(): CoffeeResponse {
return assert(fetchResponse(), 'Кофеварка сломана, подайте заявку');
};
}
|undefined
Также TypeScript поддерживает специальную конструкцию для опциональных параметров и полей, используя ?
:
// ✅ ХОРОШО ↴
interface CoffeeOrder {
sugarCubes: number;
milk?: Whole|LowFat|HalfHalf;
}
function pourCoffee(volume?: Milliliter) { ... }
Опциональные параметры неявно включают |undefined
в свой тип. Однако они отличаются тем, что их можно не указывать при составлении выражения или вызове метода. Например, {sugarCubes: 1}
является валидным CoffeeOrder
поскольку milk
является опциональным.
Используйте опциональные поля (в интерфейсах или классах) и параметры вместо |undefined
типов.
Для классов лучше вообще избегать этого приёма и инициализировать как можно больше полей.
// ✅ ХОРОШО ↴
class MyClass {
field = '';
}
Система типов TypeScript является структурной, а не номинальной. Т.е. значение соответствует типу, если оно имеет, по крайней мере, все требуемые типом свойства и типы свойств совпадают рекурсивно.
Используйте структурную типизацию в коде там, где это уместно. Вне тестов для определения структурных типов используйте интерфейсы, а не классы. В тестовом коде может быть полезно иметь Mock-объекты, структурно соответствующие тестируемому коду, без введения дополнительного интерфейса.
При предоставлении реализации, основанной на структуре, явно указывайте тип в объявлении элемента (это позволяет более точно проверить тип и сообщить об ошибке).
// ✅ ХОРОШО ↴
const foo: Foo = {
a: 123,
b: 'abc',
}
// ❌ ПЛОХО ↴
const badFoo = {
a: 123,
b: 'abc',
}
Почему?
Приведенный выше объект
badFoo
полагается на вывод типа. ВbadFoo
могут быть добавлены дополнительные поля, а тип будет выводиться на основе самого объекта.При передаче
badFoo
в функцию, которая принимаетFoo
, ошибка будет возникать на месте вызова функции, а не на месте объявления объекта. Это также существенно при изменении описания интерфейса в обширной кодовой базе.
// ✅ ХОРОШО ↴ interface Animal { sound: string; name: string; } function makeSound(animal: Animal) {} /** * 'cat' имеет выводимый тип '{sound: string}' */ const cat = { sound: 'meow', }; /** * 'cat' не соответствует требуемому для функции типу, * поэтому компилятор TypeScript выдает ошибку здесь, * что может быть очень далеко от места определения 'cat'. */ makeSound(cat); /** * Horse имеет структурный тип, и ошибка типа возникает здесь, а не в вызове функции, * поскольку 'horse' не соответствует требованиям типа 'Animal' */ const horse: Animal = { sound: 'niegh', }; const dog: Animal = { sound: 'bark', name: 'MrPickles', }; makeSound(dog); makeSound(horse);
TypeScript поддерживает псевдонимы типов для присвоения имени всему выражению описывающему тип. Это может быть использовано для именования примитивов, объединений, кортежей и любых других типов.
Однако, при объявлении типов для объектов, используйте интерфейсы вместо псевдонима типа, для выражения, представленного объектным литералом.
// ✅ ХОРОШО ↴
interface User {
firstName: string;
lastName: string;
}
// ❌ ПЛОХО ↴
type User = {
firstName: string,
lastName: string,
}
Почему?
Эти формы почти эквивалентны, поэтому следуя принципу выбора только одной из двух форм, для предотвращения вариативности, нам стоит выбрать одну из них. Кроме того, существуют также интересные технические причины, по которым предпочтение отдается интерфейсу. На той странице также приводятся слова руководителя команды разработчиков TypeScript: "Честно говоря, я считаю, что на самом деле это должны быть просто интерфейсы для всего, что они могут моделировать. Нет особой выгоды в псевдонимах типов, когда существует так много проблем с их отображением и производительностью".
Array<T>
Для простых типов (содержащих только буквенно-цифровые символы и точку) используйте синтаксический сахар для массивов, T[]
, а не более длинную форму Array<T>
.
Для чего-то более сложного используйте более длинную форму Array<T>
.
Эти правила применяются на каждом уровне вложенности, т.е. простой T[]
, вложенный в более сложный тип, все равно будет написан как T[]
, т.е. с использованием синтаксического сахара.
Это также относится к readonly T[]
и ReadonlyArray<T>
.
// ✅ ХОРОШО ↴
const a: string[];
const b: readonly string[];
const c: ns.MyObj[];
const d: Array<string|number>;
const e: ReadonlyArray<string|number>;
const f: InjectionToken<string[]>; // Используйте синтаксический сахар для вложенных типов
// ❌ ПЛОХО ↴
const a: Array<string>; // синтаксический сахар короче
const b: ReadonlyArray<string>;
const c: {n: number, s: string}[]; // фигурные/круглые скобки ухудшают читабельность
const d: (string|number)[];
const e: readonly (string|number)[];
{[key: string]: T}
)В JavaScript принято использовать объект в качестве ассоциативного массива (он же карта (map), хеш-таблица, или словарь). В TypeScript такие объекты могут быть типизированы с использованием индексной сигнатуры ([k: string]: T
):
// ✅ ХОРОШО ↴
const fileSizes: {[fileName: string]: number} = {};
fileSizes['readme.txt'] = 541;
В TypeScript укажите осмысленное обозначение для ключа. (Обозначение существует только для документации; в остальном оно не используется.)
// ❌ ПЛОХО ↴
const users: {[key: string]: number} = ...;
// ✅ ХОРОШО ↴
const users: {[userName: string]: number} = ...;
Вместо использования одного из тех вариантов, рассмотрите возможность использования
Map
иSet
типов ES6. Объекты JavaScript обладают довольно неожиданным нежелательным поведением, а типы ES6 более явно передают ваши намерения. Также,Set
могут хранить значения, аMap
еще и ключи, отличные отstring
.
Встроенный в TypeScript тип Record<Keys, ValueType>
позволяет создавать типы с определенным набором ключей. Это отличается от ассоциативных массивов тем, что ключи известны статически. См. рекомендации по этому вопросу ниже.
В 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
, рассмотрите один из вариантов:
unknown
Используйте интерфейсы, встраиваемый объектный тип или псевдоним типа:
// ✅ ХОРОШО ↴
// Используйте декларируемые интерфейсы для представления серверного JSON.
declare interface MyUserJson {
name: string;
email: string;
}
// Используйте псевдонимы типов для тех типов, которые приходится писать многократно.
type MyType = number|string;
// Или используйте встраиваемый объектный тип для возврата комплексных значений.
function getTwoThings(): {something: number, other: string} {
// ...
return {something, other};
}
// Используйте дженерик там, где в ином случае библиотека указала бы `any`,
// чтобы обозначить, что ей все равно, с каким типом работает пользователь (но обратите
// внимание на раздел "Возвращаемый тип представлен только дженериком" представленный ниже).
function nicestElement<T>(items: T[]): T {
// Поиск наиболее подходящего элемента в items.
// Код может также накладывать ограничения на T, например <T extends HTMLElement>.
}
unknown
вместо any
Тип any
позволяет присваивать значение любого другого типа и разыменовывать любые его свойства. Часто такое поведение не является необходимым или желательным и код просто нуждается в обозначении неизвестности типа. В такой ситуации используйте встроенный тип unknown
- он точнее описывает суть концепции и гораздо безопаснее, поскольку не позволяет разыменовывать произвольные свойства.
// ✅ ХОРОШО ↴
// Можно присваивать любое значение (включая null или undefined), но нельзя
// использовать его без сужения типа или приведения.
const val: unknown = value;
// ❌ ПЛОХО ↴
const danger: any = value /* результат произвольного выражения */;
danger.whoops(); // Этот доступ к переменной абсолютно бесконтролен
Чтобы благополучно использовать значения типа unknown
, следует сужать тип с помощью защитников типа (type guards).
any
Иногда использование any
вполне оправдано, например, в тестах для создания Mock-объектов. В таких случаях добавьте комментарий, который подавляет предупреждение линтера, и задокументируйте, почему это решение оправдано.
// ✅ ХОРОШО ↴
// Этому тесту нужна только частичная реализация BookService,
// и если мы что-то упустили, тест очевидно провалится
// Это намеренно небезопасный частичный Mock-объект
// tslint:disable-next-line:no-any
const mockBookService = ({get() { return mockBook; }} as any) as BookService;
// Корзина покупателя (класс ShoppingCart) в этом тесте не используется
// tslint:disable-next-line:no-any
const component = new MyComponent(mockBookService, /* неиспользуемый ShoppingCart */ null as any);
Если у вас возникнет соблазн создать парный тип, то используйте вместо него кортежный тип:
// ❌ ПЛОХО ↴
interface Pair {
first: string;
second: string;
}
function splitInHalf(input: string): Pair {
...
return {first: x, second: y};
}
// ✅ ХОРОШО ↴
function splitInHalf(input: string): [string, string] {
...
return [x, y];
}
// Используйте это как:
const [leftHalf, rightHalf] = splitInHalf('my string');
Однако часто бывает яснее, если свойствам даются осмысленные имена.
Если объявление интерфейса (interface
) слишком обременительно, можно использовать встраиваемый объектным литералом тип:
// ✅ ХОРОШО ↴
function splitHostPort(address: string): {host: string, port: number} {
...
}
// Используйте это как:
const address = splitHostPort(userAddress);
use(address.port);
// Вы также можете использовать деструктуризацию, чтобы получить поведение, подобное разложению кортежа на отдельные переменные:
const {host, port} = splitHostPort(userAddress);
Есть несколько типов, связанных с JavaScript примитивами, которые не рекомендуется когда-либо использовать:
String
, Boolean
, и Number
имеют несколько иное значение, чем соответствующие примитивные типы string
, boolean
, and number
. Всегда используйте версию со строчными буквами.Object
имеет сходство с {}
и object
, но является несколько менее строгим. Используйте {}
для типа, который включает в себя всё, кроме null
и undefined
, или строчный object
для того, чтобы дополнительно исключить другие примитивные типы (три упомянутых выше, плюс symbol
и bigint
).Кроме того, никогда не вызывайте типы-обертки в качестве конструкторов (с помощью new
).
Избегайте создания API у которых возвращаемый тип представлен только дженериком. При работе с существующими API у которых возвращаемый тип представлен только дженериком, всегда явно указывайте дженерик [11].
Для любого вопроса о стиле, который не решен окончательно этой спецификацией, делайте то, что уже делает другой код в том же файле (будьте последовательны). Если это не решит проблему, рассмотрите возможность подражания другим файлам в том же каталоге.
В основном, инженеры обычно лучше знают, что необходимо в их коде, поэтому если есть несколько вариантов и выбор зависит от ситуации, мы должны позволить принимать решения на месте. Поэтому рекомендуемым ответом по умолчанию здесь является "оставить это как есть".
Следующие пункты являются теми исключительными моментами, на основании которых мы имеем некоторые всеобщие правила. Оцените ваше предложение по составлению руководства по стилю с учетом следующего:
В коде рекомендуется избегать шаблонов, которые известны как вызывающие проблемы, особенно для пользователей, только начинающих изучать язык.
Примеры:
any
легко злоупотребить (действительно ли эта переменная может быть и числом и вызываться как функция?), поэтому у нас есть рекомендации по его использованию.namespace
) создает проблемы с оптимизациями Closure.private
, попытаются скрыть имена своих функций с помощью подчеркивания.Код в различных проектах рекомендуется разрабатывать единообразно, с учетом незначительных отклонений.
Когда есть два варианта, которые эквивалентны в поверхностном смысле, стоит рассмотреть возможность выбора одного из них, просто чтобы не развивались расхождения без причины и избежать бессмысленных дебатов в обзорах кода.
Обычно нам также стоит соответствовать стилю JavaScript, потому что люди часто пишут на обоих языках вместе.
Примеры:
x as T
синтаксис по сравнению с эквивалентным синтаксисом <T>x
(запрещено).Array<[number, number]>
по сравнению с [number, number][]
.Код рекомендуется писать так, чтобы он был поддерживаемым в долгосрочной перспективе.
Код обычно живет дольше, чем над ним работает его автор и команда специалистов по TypeScript должна обеспечить работоспособность всего кода Google в будущем.
Примеры:
Рецензенты кода должны быть сосредоточены на улучшении качества кода, а не на соблюдении произвольных правил.
Часто хорошим знаком считается, если есть возможность реализовать ваше правило в качестве автоматической проверки. Это также способствует принципу №3.
Если это действительно не имеет большого значения — если это не совсем понятная часть языка или если это позволяет избежать ошибки, которая вряд ли возникнет — вероятно, это стоит оставить без изменений.
Прим. пер.: В оригинале используются термины MUST и SHOULD которые зачастую переводят буквально как должен. При этом MUST носит обязательный характер, а SHOULD - рекомендательный. Т.к. в русском языке такие термины, как: "должен", "обязан", "стоит", "необходимо" многими воспринимаются как имеющими строго обязательный характер, при буквальном переводе это может ввести в заблуждение. Поэтому для большего понимания эти термины были адаптированы как:
Такая адаптация вполне совместима с оригинальным стандартом RFC 2119 и не нарушает его. ↩︎
Прим. пер.: В оригинале в этом абзаце присутствует несколько вероятных ошибок:
$
в оригинале был указан знак \(
, но такой символ не может быть в имени идентификатора и поэтому в переводе указан более корректный вариант с $
;[\)\w]+
и поэтому, с учетом прошлого пункта, в переводе было указано более корректное [$\w]+
.Прим. пер.: Такое соглашение было популяризовано Cycle.js и также применяется в Angular. ↩︎
Прим. пер.: С переводом руководства "Google JavaScript Style Guide" вы можете ознакомиться тут: https://rostislavdugin.github.io/styleguide/jsguide.html ↩︎
Прим. пер.: В оригинале, далее в данном абзаце предлагается ознакомиться со страницей "Тестирование и приватная видимость", если вы хотите получить доступ к защищенным полям из теста. К сожалению, предоставленная сокращенная go-ссылка: go/typescript-testing#export-private-visibility, доступна только из внутреннего окружения Google. ↩︎
Прим. пер.: В оригинале, в данном предложении предоставлена сокращенная go-ссылка: go/tsjs-practices/iteration, которая к сожалению доступна только из внутреннего окружения Google. ↩︎
Прим. пер.: В блоке оператора switch
непустые группы операторов case
не допускаются к проваливанию компилятором при активной опции noFallthroughCasesInSwitch
. Подробнее вы можете ознакомиться тут: https://www.typescriptlang.org/tsconfig#noFallthroughCasesInSwitch. ↩︎
Automatic Semicolon Insertion (ASI) — с англ. переводится как "автоматическая вставка точки с запятой". ↩︎
Прим. пер.: В TypeScript 5.0 появился функционал декораторов, который доступен по умолчанию и имеет несколько отличий от ранее существовавших экспериментальных декораторов. С отличиями вы можете ознакомиться тут: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#differences-with-experimental-legacy-decorators. ↩︎
Google Code Search - проект поисковой системы по исходному коду программ, позволяющий использовать в поисковых запросах регулярные выражения. Репозиторий проекта размещен по адресу: https://github.com/google/codesearch ↩︎
Прим. пер.: Данная проблема под названием "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
, что может сбить с толку и привести к нежелательным последствиям. ↩︎