This guide is based on the internal Google TypeScript style guide, but it hasbeen slightly adjusted to remove Google-internal sections. Google's internalenvironment has different constraints on TypeScript than you might find outsideof Google. The advice here is specifically useful for people authoring code theyintend to import into Google, but otherwise may not apply in your externalenvironment.
There is no automatic deployment process for this version as it's pushedon-demand by volunteers.
This Style Guide usesRFC 2119terminology when using the phrasesmust,must not,should,should not,andmay. The termsprefer andavoid correspond toshould andshouldnot, respectively. Imperative and declarative statements are prescriptive andcorrespond tomust.
All examples given arenon-normative and serve only to illustrate thenormative language of the style guide. That is, while the examples are in GoogleStyle, they may not illustrate theonly stylish way to represent the code.Optional formatting choices made in examples must not be enforced as rules.
Source files are encoded inUTF-8.
Aside from the line terminator sequence, the ASCII horizontal space character(0x20) is the only whitespace character that appears anywhere in a source file.This implies that all other whitespace characters in string literals areescaped.
For any character that has a special escape sequence (\'
,\"
,\\
,\b
,\f
,\n
,\r
,\t
,\v
), that sequence is used rather than thecorresponding numeric escape (e.g\x0a
,\u000a
, or\u{a}
). Legacy octalescapes are never used.
For the remaining non-ASCII characters, use the actual Unicode character (e.g.∞
). For non-printable characters, the equivalent hex or Unicode escapes (e.g.\u221e
) can be used along with an explanatory comment.
// Perfectly clear, even without a comment.const units = 'μs';// Use escapes for non-printable characters.const output = '\ufeff' + content; // byte order mark
// Hard to read and prone to mistakes, even with the comment.const units = '\u03bcs'; // Greek letter mu, 's'// The reader has no idea what this is.const output = '\ufeff' + content;
Files consist of the following,in order:
@fileoverview
, if presentExactly one blank line separates each section that is present.
If license or copyright information is necessary in a file, add it in a JSDoc atthe top of the file.
@fileoverview
JSDocA file may have a top-level@fileoverview
JSDoc. If present, it may provide adescription of the file's content, its uses, or information about itsdependencies. Wrapped lines are not indented.
Example:
/** * @fileoverview Description of file. Lorem ipsum dolor sit amet, consectetur * adipiscing elit, sed do eiusmod tempor incididunt. */
There are four variants of import statements in ES6 and TypeScript:
Import type | Example | Use for |
---|---|---|
module[module_import] | import * as foo from'...'; | TypeScript imports |
named[destructuring_import] | import {SomeThing}from '...'; | TypeScript imports |
default | import SomeThingfrom '...'; | Only for otherexternal code thatrequires them |
side-effect | import '...'; | Only to importlibraries for theirside-effects on load(such as customelements) |
// Good: choose between two options as appropriate (see below).import * as ng from '@angular/core';import {Foo} from './foo';// Only when needed: default imports.import Button from 'Button';// Sometimes needed to import libraries for their side effects:import 'jasmine';import '@polymer/paper-button';
TypeScript codemust use paths to import other TypeScript code. Pathsmay berelative, i.e. starting with.
or..
, or rooted at the base directory, e.g.root/path/to/file
.
Codeshould use relative imports (./foo
) rather than absolute importspath/to/foo
when referring to files within the same (logical) project as thisallows to move the project around without introducing changes in these imports.
Consider limiting the number of parent steps (../../../
) as those can makemodule and path structures hard to understand.
import {Symbol1} from 'path/from/root';import {Symbol2} from '../parent/file';import {Symbol3} from './sibling';
Both namespace and named imports can be used.
Prefer named imports for symbols used frequently in a file or for symbols thathave clear names, for example Jasmine'sdescribe
andit
. Named imports canbe aliased to clearer names as needed withas
.
Prefer namespace imports when using many different symbols from large APIs. Anamespace import, despite using the*
character, is not comparable to awildcard
import as seen in other languages. Instead, namespace imports give aname to all the exports of a module, and each exported symbol from the modulebecomes a property on the module name. Namespace imports can aid readability forexported symbols that have common names likeModel
orController
without theneed to declare aliases.
// Bad: overlong import statement of needlessly namespaced names.import {Item as TableviewItem, Header as TableviewHeader, Row as TableviewRow, Model as TableviewModel, Renderer as TableviewRenderer} from './tableview';let item: TableviewItem|undefined;
// Better: use the module for namespacing.import * as tableview from './tableview';let item: tableview.Item|undefined;
import * as testing from './testing';// Bad: The module name does not improve readability.testing.describe('foo', () => { testing.it('bar', () => { testing.expect(null).toBeNull(); testing.expect(undefined).toBeUndefined(); });});
// Better: give local names for these common functions.import {describe, it, expect} from './testing';describe('foo', () => { it('bar', () => { expect(null).toBeNull(); expect(undefined).toBeUndefined(); });});
Apps JSPB protos must use named imports, even when it leads to long importlines.
This rule exists to aid in build performance and dead code elimination sinceoften.proto
files contain manymessage
s that are not all needed together.By leveraging destructured imports the build system can create finer graineddependencies on Apps JSPB messages while preserving the ergonomics of path basedimports.
// Good: import the exact set of symbols you need from the proto file.import {Foo, Bar} from './foo.proto';function copyFooBar(foo: Foo, bar: Bar) {...}
Codeshould fix name collisions by using a namespace import or renaming theexports themselves. Codemay rename imports (import {SomeThing asSomeOtherThing}
) if needed.
Three examples where renaming can be helpful:
from
function mightbe more readable when renamed toobservableFrom
.Use named exports in all code:
// Use named exports:export class Foo { ... }
Do not use default exports. This ensures that all imports follow a uniformpattern.
// Do not use default exports:export default class Foo { ... } // BAD!
Why?
Default exports provide no canonical name, which makes central maintenancedifficult with relatively little benefit to code owners, including potentiallydecreased readability:
import Foo from './bar'; // Legal.import Bar from './bar'; // Also legal.
Named exports have the benefit of erroring when import statements try to importsomething that hasn't been declared. Infoo.ts
:
const foo = 'blah';export default foo;
And inbar.ts
:
import {fizz} from './foo';
Results inerror TS2614: Module '"./foo"' has no exported member 'fizz'.
Whilebar.ts
:
import fizz from './foo';
Results infizz === foo
, which is probably unexpected and difficult to debug.
Additionally, default exports encourage people to put everything into one bigobject to namespace it all together:
export default class Foo { static SOME_CONSTANT = ... static someHelpfulFunction() { ... } ...}
With the above pattern, we have file scope, which can be used as a namespace. Wealso have a perhaps needless second scope (the classFoo
) that can beambiguously used as both a type and a value in other files.
Instead, prefer use of file scope for namespacing, as well as named exports:
export const SOME_CONSTANT = ...export function someHelpfulFunction()export class Foo { // only class stuff here}
TypeScript does not support restricting the visibility for exported symbols.Only export symbols that are used outside of the module. Generally minimize theexported API surface of modules.
Regardless of technical support, mutable exports can create hard to understandand debug code, in particular with re-exports across multiple modules. One wayto paraphrase this style point is thatexport let
is not allowed.
export let foo = 3;// In pure ES6, foo is mutable and importers will observe the value change after a second.// In TS, if foo is re-exported by a second file, importers will not see the value change.window.setTimeout(() => { foo = 4;}, 1000 /* ms */);
If one needs to support externally accessible and mutable bindings, theyshould instead use explicit getter functions.
let foo = 3;window.setTimeout(() => { foo = 4;}, 1000 /* ms */);// Use an explicit getter to access the mutable export.export function getFoo() { return foo; };
For the common pattern of conditionally exporting either of two values, first dothe conditional check, then the export. Make sure that all exports are finalafter the module's body has executed.
function pickApi() { if (useOtherApi()) return OtherApi; return RegularApi;}export const SomeApi = pickApi();
Do not create container classes with static methods or properties for the sakeof namespacing.
export class Container { static FOO = 1; static bar() { return 1; }}
Instead, export individual constants and functions:
export const FOO = 1;export function bar() { return 1; }
You may useimport type {...}
when you use the imported symbol only as a type.Use regular imports for values:
import type {Foo} from './foo';import {Bar} from './foo';import {type Foo, Bar} from './foo';
Why?
The TypeScript compiler automatically handles the distinction and does notinsert runtime loads for type references. So why annotate type imports?
The TypeScript compiler can run in 2 modes:
import type
in certain cases.import type
is used correctly.Note: If you need to force a runtime load for side effects, useimport '...';
.See
Useexport type
when re-exporting a type, e.g.:
export type {AnInterface} from './foo';
Why?
export type
is useful to allow type re-exports in file-by-file transpilation.SeeisolatedModules
docs.
export type
might also seem useful to avoid ever exporting a value symbol foran API. However it does not give guarantees, either: downstream code might stillimport an API through a different path. A better way to split & guarantee typevs value usages of an API is to actually split the symbols into e.g.UserService
andAjaxUserService
. This is less error prone and also bettercommunicates intent.
TypeScript supports two methods to organize code:namespaces andmodules,but namespaces are disallowed. Thatis, your codemust refer to code in other files using imports and exports ofthe formimport {foo} from 'bar';
Your codemust not use thenamespace Foo { ... }
construct.namespace
smay only be used when required to interface with external, third party code.To semantically namespace your code, use separate files.
Codemust not userequire
(as inimport x = require('...');
) for imports.Use ES6 module syntax.
// Bad: do not use namespaces:namespace Rocket { function launch() { ... }}// Bad: do not use <reference>/// <reference path="..."/>// Bad: do not use require()import x = require('mydep');
NB: TypeScript
namespace
s used to be called internal modules and used to usethemodule
keyword in the formmodule Foo { ... }
. Don't use that either.Always use ES6 imports.
This section delineates which features may or may not be used, and anyadditional constraints on their use.
Language features which are not discussed in this style guidemay be used withno recommendations of their usage.
Always useconst
orlet
to declare variables. Useconst
by default, unlessa variable needs to be reassigned. Never usevar
.
const foo = otherValue; // Use if "foo" never changes.let bar = someValue; // Use if "bar" is ever assigned into later on.
const
andlet
are block scoped, like variables in most other languages.var
in JavaScript is function scoped, which can cause difficult to understandbugs. Don't use it.
var foo = someValue; // Don't use - var scoping is complex and causes bugs.
Variablesmust not be used before their declaration.
Every local variable declaration declares only one variable: declarations suchaslet a = 1, b = 2;
are not used.
Array
constructorDo not use theArray()
constructor, with or withoutnew
. It has confusingand contradictory usage:
const a = new Array(2); // [undefined, undefined]const b = new Array(2, 3); // [2, 3];
Instead, always use bracket notation to initialize arrays, orfrom
toinitialize anArray
with a certain size:
const a = [2];const b = [2, 3];// Equivalent to Array(2):const c = [];c.length = 2;// [0, 0, 0, 0, 0]Array.from<number>({length: 5}).fill(0);
Do not define or use non-numeric properties on an array (other thanlength
).Use aMap
(orObject
) instead.
Using spread syntax[...foo];
is a convenient shorthand for shallow-copying orconcatenating iterables.
const foo = [ 1,];const foo2 = [ ...foo, 6, 7,];const foo3 = [ 5, ...foo,];foo2[1] === 6;foo3[1] === 1;
When using spread syntax, the value being spreadmust match what is beingcreated. When creating an array, only spread iterables. Primitives (includingnull
andundefined
)must not be spread.
const foo = [7];const bar = [5, ...(shouldUseFoo && foo)]; // might be undefined// Creates {0: 'a', 1: 'b', 2: 'c'} but has no lengthconst 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'];
Array literals may be used on the left-hand side of an assignment to performdestructuring (such as when unpacking multiple values from a single array oriterable). A finalrest
element may be included (with no space between the...
and the variable name). Elements should be omitted if they are unused.
const [a, b, c, ...rest] = generateResults();let [, b,, d] = someArray;
Destructuring may also be used for function parameters. Always specify[]
asthe default value if a destructured array parameter is optional, and providedefault values on the left hand side:
function destructured([a = 4, b = 2] = []) { … }
Disallowed:
function badDestructuring([a, b] = [4, 2]) { … }
Tip: For (un)packing multiple values into a function’s parameter or return,prefer object destructuring to array destructuring when possible, as it allowsnaming the individual elements and specifying a different type for each.
Object
constructorTheObject
constructor is disallowed. Use an object literal ({}
or{a: 0,b: 1, c: 2}
) instead.
Iterating objects withfor (... in ...)
is error prone. It will includeenumerable properties from the prototype chain.
Do not use unfilteredfor (... in ...)
statements:
for (const x in someObj) { // x could come from some parent prototype!}
Either filter values explicitly with anif
statement, or usefor (... ofObject.keys(...))
.
for (const x in someObj) { if (!someObj.hasOwnProperty(x)) continue; // now x was definitely defined on someObj}for (const x of Object.keys(someObj)) { // note: for _of_! // now x was definitely defined on someObj}for (const [key, value] of Object.entries(someObj)) { // note: for _of_! // now key was definitely defined on someObj}
Using spread syntax{...bar}
is a convenient shorthand for creating a shallowcopy of an object. When using spread syntax in object initialization, latervalues replace earlier values at the same key.
const foo = { num: 1,};const foo2 = { ...foo, num: 5,};const foo3 = { num: 5, ...foo,}foo2.num === 5;foo3.num === 1;
When using spread syntax, the value being spreadmust match what is beingcreated. That is, when creating an object, only objects may be spread; arraysand primitives (includingnull
andundefined
)must not be spread. Avoidspreading objects that have prototypes other than the Object prototype (e.g.class definitions, class instances, functions) as the behavior is unintuitive(only enumerable non-prototype properties are shallow-copied).
const foo = {num: 7};const bar = {num: 5, ...(shouldUseFoo && foo)}; // might be undefined// Creates {0: 'a', 1: 'b', 2: 'c'} but has no lengthconst fooStrings = ['a', 'b', 'c'];const ids = {...fooStrings};
const foo = shouldUseFoo ? {num: 7} : {};const bar = {num: 5, ...foo};
Computed property names (e.g.{['key' + foo()]: 42}
) are allowed, and areconsidered dict-style (quoted) keys (i.e., must not be mixed with non-quotedkeys) unless the computed property is asymbol(e.g.[Symbol.iterator]
).
Object destructuring patterns may be used on the left-hand side of an assignmentto perform destructuring and unpack multiple values from a single object.
Destructured objects may also be used as function parameters, but should be keptas simple as possible: a single level of unquoted shorthand properties. Deeperlevels of nesting and computed properties may not be used in parameterdestructuring. Specify any default values in the left-hand-side of thedestructured parameter ({str = 'some default'} = {}
, rather than{str} = {str: 'some default'}
), and if adestructured object is itself optional, it must default to{}
.
Example:
interface Options { /** The number of times to do something. */ num?: number; /** A string to do stuff to. */ str?: string;}function destructured({num, str = 'default'}: Options = {}) {}
Disallowed:
function nestedTooDeeply({x: {num, str}}: {x: Options}) {}function nontrivialDefault({num, str}: Options = {num: 42, str: 'default'}) {}
Class declarationsmust not be terminated with semicolons:
class Foo {}
class Foo {}; // Unnecessary semicolon
In contrast, statements that contain class expressionsmust be terminated witha semicolon:
export const Baz = class extends Bar { method(): number { return this.x; }}; // Semicolon here as this is a statement, not a declaration
exports const Baz = class extends Bar { method(): number { return this.x; }}
It is neither encouraged nor discouraged to have blank lines separating classdeclaration braces from other class content:
// No spaces around braces - fine.class Baz { method(): number { return this.x; }}// A single space around both braces - also fine.class Foo { method(): number { return this.x; }}
Class method declarationsmust not use a semicolon to separate individualmethod declarations:
class Foo { doThing() { console.log("A"); }}
class Foo { doThing() { console.log("A"); }; // <-- unnecessary}
Method declarations should be separated from surrounding code by a single blankline:
class Foo { doThing() { console.log("A"); } getOtherThing(): number { return 4; }}
class Foo { doThing() { console.log("A"); } getOtherThing(): number { return 4; }}
ThetoString
method may be overridden, but must always succeed and never havevisible side effects.
Tip: Beware, in particular, of calling other methods from toString, sinceexceptional conditions could lead to infinite loops.
Where it does not interfere with readability, prefer module-local functions overprivate static methods.
Codeshould not rely on dynamic dispatch of staticmethods. Static methodsshould only be called on the base classitself (which defines it directly). Static methodsshould not be called onvariables containing a dynamic instance that may be either the constructor or asubclass constructor (andmust be defined with@nocollapse
if this is done),andmust not be called directly on a subclass that doesn’t define the methoditself.
Disallowed:
// Context for the examples below (this class is okay by itself)class Base { /** @nocollapse */ static foo() {}}class Sub extends Base {}// Discouraged: don't call static methods dynamicallyfunction callFoo(cls: typeof Base) { cls.foo();}// Disallowed: don't call static methods on subclasses that don't define it themselvesSub.foo();// Disallowed: don't access this in static methods.class MyClass { static foo() { return this.staticField; }}MyClass.staticField = 1;
this
referencesCodemust not usethis
in a static context.
JavaScript allows accessing static fields throughthis
. Different from otherlanguages, static fields are also inherited.
class ShoeStore { static storage: Storage = ...; static isAvailable(s: Shoe) { // Bad: do not use `this` in a static method. return this.storage.has(s.id); }}class EmptyShoeStore extends ShoeStore { static storage: Storage = EMPTY_STORE; // overrides storage from ShoeStore}
Why?
This code is generally surprising: authors might not expect that static fieldscan be accessed through the this pointer, and might be surprised to find thatthey can be overridden - this feature is not commonly used.
This code also encourages an anti-pattern of having substantial static state,which causes problems with testability.
Constructor callsmust use parentheses, even when no arguments are passed:
const x = new Foo;
const x = new Foo();
Omitting parentheses can lead to subtle mistakes. These two lines are notequivalent:
new Foo().Bar();new Foo.Bar();
It is unnecessary to provide an empty constructor or one that simply delegatesinto its parent class because ES2015 provides a default class constructor if oneis not specified. However constructors with parameter properties, visibilitymodifiers or parameter decoratorsshould not be omitted even if the body ofthe constructor is empty.
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() {}}
The constructor should be separated from surrounding code both above and belowby a single blank line:
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); }}
Do not use private fields (also known as private identifiers):
class Clazz { #ident = 1;}
Instead, use TypeScript's visibility annotations:
class Clazz { private ident = 1;}
Why?
Private identifiers cause substantial emit size andperformance regressions when down-leveled by TypeScript, and are unsupportedbefore ES2015. They can only be downleveled to ES2015, not lower. At the sametime, they do not offer substantial benefits when static type checking is usedto enforce visibility.
Mark properties that are never reassigned outside of the constructor with thereadonly
modifier (these need not be deeply immutable).
Rather than plumbing an obvious initializer through to a class member, use aTypeScriptparameter property.
class Foo { private readonly barService: BarService; constructor(barService: BarService) { this.barService = barService; }}
class Foo { constructor(private readonly barService: BarService) {}}
If the parameter property needs documentation,use an@param
JSDoc tag.
If a class member is not a parameter, initialize it where it's declared, whichsometimes lets you drop the constructor entirely.
class Foo { private readonly userList: string[]; constructor() { this.userList = []; }}
class Foo { private readonly userList: string[] = [];}
Tip: Properties should never be added to or removed from an instance after theconstructor is finished, since it significantly hinders VMs’ ability to optimizeclasses'shape
. Optional fields that may be filled in later should beexplicitly initialized toundefined
to prevent later shape changes.
Properties used from outside the lexical scope of their containing class, suchas an Angular component's properties used from a template,must not useprivate
visibility, as they are used outside of the lexical scope of theircontaining class.
Use eitherprotected
orpublic
as appropriate to the property in question.Angular and AngularJS template properties should useprotected
, but Polymershould usepublic
.
TypeScript codemust not useobj['foo']
to bypass the visibility of aproperty.
Why?
When a property isprivate
, you are declaring to both automated systems andhumans that the property accesses are scoped to the methods of the declaringclass, and they will rely on that. For example, a check for unused code willflag a private property that appears to be unused, even if some other filemanages to bypass the visibility restriction.
Though it might appear thatobj['foo']
can bypass visibility in the TypeScriptcompiler, this pattern can be broken by rearranging the build rules, and alsoviolatesoptimization compatibility.
Getters and setters, also known as accessors, for class membersmay be used.The getter methodmust be apure function (i.e., result isconsistent and has no side effects: gettersmust not change observable state).They are also useful as a means of restricting the visibility of internal orverbose implementation details (shown below).
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++; // Bad: getter changes observable state }}
If an accessor is used to hide a class property, the hidden propertymay beprefixed or suffixed with any whole word, likeinternal
orwrapped
. Whenusing these private properties, access the value through the accessor wheneverpossible. At least one accessor for a propertymust be non-trivial: do notdefinepass-through
accessors only for the purpose of hiding a property.Instead, make the property public (or consider making itreadonly
rather thanjust defining a getter with no setter).
class Foo { private wrappedBar = ''; get bar() { return this.wrappedBar || 'bar'; } set bar(wrapped: string) { this.wrappedBar = wrapped.trim(); }}
class Bar { private barInternal = ''; // Neither of these accessors have logic, so just make bar public. get bar() { return this.barInternal; } set bar(value: string) { this.barInternal = value; }}
Getters and settersmust not be defined usingObject.defineProperty
, sincethis interferes with property renaming.
Computed properties may only be used in classes when the property is a symbol.Dict-style properties (that is, quoted or computed non-symbol keys) are notallowed (seerationale for not mixing key types. A[Symbol.iterator]
method should be defined for any classes that are logicallyiterable. Beyond this,Symbol
should be used sparingly.
Tip: be careful of using any other built-in symbols (e.g.Symbol.isConcatSpreadable
) as they are not polyfilled by the compiler and willtherefore not work in older browsers.
Restricting visibility of properties, methods, and entire types helps withkeeping code decoupled.
public
modifierexcept when declaring non-readonly public parameter properties (inconstructors).class Foo { public bar = new Bar(); // BAD: public modifier not needed constructor(public readonly baz: Baz) {} // BAD: readonly implies it's a property which defaults to public}
class Foo { bar = new Bar(); // GOOD: public modifier not needed constructor(public baz: Baz) {} // public modifier allowed}
See alsoexport visibility.
prototype
s directlyTheclass
keyword allows clearer and more readable class definitions thandefiningprototype
properties. Ordinary implementation code has no businessmanipulating these objects. Mixins and modifying the prototypes of builtinobjects are explicitly forbidden.
Exception: Framework code (such as Polymer, or Angular) may need to useprototype
s, and should not resortto even-worse workarounds to avoid doing so.
There are many different types of functions, with subtle distinctions betweenthem. This guide uses the following terminology, which aligns withMDN:
function declaration: a declaration (i.e. not an expression) using the
function
keywordfunction expression: an expression, typically used in an assignment orpassed as a parameter, using the
function
keywordarrow function: an expression using the
=>
syntaxblock body: right hand side of an arrow function with braces
concise body: right hand side of an arrow function without braces
Methods and classes/constructors are not covered in this section.
Prefer function declarations over arrow functions or function expressions whendefining named functions.
function foo() { return 42;}
const foo = () => 42;
Arrow functionsmay be used, for example, when an explicit type annotation isrequired.
interface SearchFunction { (source: string, subString: string): boolean;}const fooSearch: SearchFunction = (source, subString) => { ... };
Functions nested within other methods or functionsmay use functiondeclarations or arrow functions, as appropriate. In method bodies in particular,arrow functions are preferred because they have access to the outerthis
.
Do not use function expressions. Use arrow functions instead.
bar(() => { this.doSomething(); })
bar(function() { ... })
Exception: Function expressionsmay be usedonly if code has todynamically rebindthis
(but this isdiscouraged), or forgenerator functions (which do not have an arrow syntax).
Use arrow functions with concise bodies (i.e. expressions) or block bodies asappropriate.
// Top level functions use function declarations.function someFunction() { // Block bodies are fine: const receipts = books.map((b: Book) => { const receipt = payMoney(b.price); recordTransaction(receipt); return receipt; }); // Concise bodies are fine, too, if the return value is used: const longThings = myValues.filter(v => v.length > 1000).map(v => String(v)); function payMoney(amount: number) { // function declarations are fine, but must not access `this`. } // Nested arrow functions may be assigned to a const. const computeTax = (amount: number) => amount * 0.12;}
Only use a concise body if the return value of the function is actually used.The block body makes sure the return type isvoid
then and prevents potentialside effects.
// BAD: use a block body if the return value of the function is not used.myPromise.then(v => console.log(v));// BAD: this typechecks, but the return value still leaks.let f: () => void;f = () => 1;
// GOOD: return value is unused, use a block body.myPromise.then(v => { console.log(v);});// GOOD: code may use blocks for readability.const transformed = [1, 2, 3].map(v => { const intermediate = someComplicatedExpr(v); const more = acrossManyLines(intermediate); return worthWrapping(more);});// GOOD: explicit `void` ensures no leaked return valuemyPromise.then(v => void console.log(v));
Tip: Thevoid
operator can be used to ensure an arrow function with anexpression body returnsundefined
when the result is unused.
this
Function expressions and function declarationsmust not usethis
unless theyspecifically exist to rebind thethis
pointer. Rebindingthis
can in mostcases be avoided by using arrow functions or explicit parameters.
function clickHandler() { // Bad: what's `this` in this context? this.textContent = 'Hello';}// Bad: the `this` pointer reference is implicitly set to document.body.document.body.onclick = clickHandler;
// Good: explicitly reference the object from an arrow function.document.body.onclick = () => { document.body.textContent = 'hello'; };// Alternatively: take an explicit parameterconst setTextFn = (e: HTMLElement) => { e.textContent = 'hello'; };document.body.onclick = setTextFn.bind(null, document.body);
Prefer arrow functions over other approaches to bindingthis
, such asf.bind(this)
,goog.bind(f, this)
, orconst self = this
.
Callbacks can be invoked with unexpected arguments that can pass a type checkbut still result in logical errors.
Avoid passing a named callback to a higher-order function, unless you are sureof the stability of both functions' call signatures. Beware, in particular, ofless-commonly-used optional parameters.
// BAD: Arguments are not explicitly passed, leading to unintended behavior// when the optional `radix` argument gets the array indices 0, 1, and 2.const numbers = ['11', '5', '10'].map(parseInt);// > [11, NaN, 2];
Instead, prefer passing an arrow-function that explicitly forwards parameters tothe named callback.
// GOOD: Arguments are explicitly passed to the callbackconst numbers = ['11', '5', '3'].map((n) => parseInt(n));// > [11, 5, 3]// GOOD: Function is locally defined and is designed to be used as a callbackfunction dayFilter(element: string|null|undefined) { return element != null && element.endsWith('day');}const days = ['tuesday', undefined, 'juice', 'wednesday'].filter(dayFilter);
Classes usuallyshould not contain properties initialized to arrow functions.Arrow function properties require the calling function to understand that thecallee'sthis
is already bound, which increases confusion about whatthis
is, and call sites and references using such handlers look broken (i.e. requirenon-local knowledge to determine that they are correct). Codeshould alwaysuse arrow functions to call instance methods (const handler = (x) => {this.listener(x); };
), andshould not obtain or pass references to instancemethods ().const handler = this.listener; handler(x);
Note: in some specific situations, e.g. when binding functions in a template,arrow functions as properties are useful and create much more readable code.Use judgement with this rule. Also, see the
Event Handlers
section below.
class DelayHandler { constructor() { // Problem: `this` is not preserved in the callback. `this` in the callback // will not be an instance of DelayHandler. setTimeout(this.patienceTracker, 5000); } private patienceTracker() { this.waitedPatiently = true; }}
// Arrow functions usually should not be properties.class DelayHandler { constructor() { // Bad: this code looks like it forgot to bind `this`. setTimeout(this.patienceTracker, 5000); } private patienceTracker = () => { this.waitedPatiently = true; }}
// Explicitly manage `this` at call time.class DelayHandler { constructor() { // Use anonymous functions if possible. setTimeout(() => { this.patienceTracker(); }, 5000); } private patienceTracker() { this.waitedPatiently = true; }}
Event handlersmay use arrow functions when there is no need to uninstall thehandler (for example, if the event is emitted by the class itself). If thehandler requires uninstallation, arrow function properties are the rightapproach, because they automatically capturethis
and provide a stablereference to uninstall.
// Event handlers may be anonymous functions or arrow function properties.class Component { onAttached() { // The event is emitted by this class, no need to uninstall. this.addEventListener('click', () => { this.listener(); }); // this.listener is a stable reference, we can uninstall it later. window.addEventListener('onbeforeunload', this.listener); } onDetached() { // The event is emitted by window. If we don't uninstall, this.listener will // keep a reference to `this` because it's bound, causing a memory leak. window.removeEventListener('onbeforeunload', this.listener); } // An arrow function stored in a property is bound to `this` automatically. private listener = () => { confirm('Do you want to exit the page?'); }}
Do not usebind
in the expression that installs an event handler, because itcreates a temporary reference that can't be uninstalled.
// Binding listeners creates a temporary reference that prevents uninstalling.class Component { onAttached() { // This creates a temporary reference that we won't be able to uninstall window.addEventListener('onbeforeunload', this.listener.bind(this)); } onDetached() { // This bind creates a different reference, so this line does nothing. window.removeEventListener('onbeforeunload', this.listener.bind(this)); } private listener() { confirm('Do you want to exit the page?'); }}
Optional function parametersmay be given a default initializer to use whenthe argument is omitted. Initializersmust not have any observable sideeffects. Initializersshould be kept as simple as possible.
function process(name: string, extraContext: string[] = []) {}function activate(index = 0) {}
// BAD: side effect of incrementing the counterlet globalCounter = 0;function newId(index = globalCounter++) {}// BAD: exposes shared mutable state, which can introduce unintended coupling// between function callsclass Foo { private readonly defaultPaths: string[]; frobnicate(paths = defaultPaths) {}}
Use default parameters sparingly. Preferdestructuring to create readable APIs whenthere are more than a small handful of optional parameters that do not have anatural order.
Use arest parameter instead of accessingarguments
. Never name a localvariable or parameterarguments
, which confusingly shadows the built-in name.
function variadic(array: string[], ...numbers: number[]) {}
Use function spread syntax instead ofFunction.prototype.apply
.
Blank lines at the start or end of the function body are not allowed.
A single blank linemay be used within function bodies sparingly to createlogical groupings of statements.
Generators should attach the*
to thefunction
andyield
keywords, as infunction* foo()
andyield* iter
, rather than orfunction *foo()
.yield *iter
Parentheses around the left-hand side of a single-argument arrow function arerecommended but not required.
Do not put a space after the...
in rest or spread syntax.
function myFunction(...elements: number[]) {}myFunction(...array, ...iterable, ...generator());
Only usethis
in class constructors and methods, functions that have anexplicitthis
type declared (e.g.function func(this: ThisType, ...)
), or inarrow functions defined in a scope wherethis
may be used.
Never usethis
to refer to the global object, the context of aneval
, thetarget of an event, or unnecessarilycall()
ed orapply()
ed functions.
this.alert('Hello');
Ordinary string literals are delimited with single quotes ('
), rather thandouble quotes ("
).
Tip: if a string contains a single quote character, consider using a templatestring to avoid having to escape the quote.
Do not useline continuations (that is, ending a line inside a string literalwith a backslash) in either ordinary or template string literals. Even thoughES5 allows this, it can lead to tricky errors if any trailing whitespace comesafter the slash, and is less obvious to readers.
Disallowed:
const LONG_STRING = 'This is a very very very very very very very long string. \ It inadvertently contains long stretches of spaces due to how the \ continued lines are indented.';
Instead, write
const LONG_STRING = 'This is a very very very very very very long string. ' + 'It does not contain long stretches of spaces because it uses ' + 'concatenated strings.';const SINGLE_STRING = 'http://it.is.also/acceptable_to_use_a_single_long_string_when_breaking_would_hinder_search_discoverability';
Use template literals (delimited with`
) over complex stringconcatenation, particularly if multiple string literals are involved. Templateliterals may span multiple lines.
If a template literal spans multiple lines, it does not need to follow theindentation of the enclosing block, though it may if the added whitespace doesnot matter.
Example:
function arithmetic(a: number, b: number) { return `Here is a table of arithmetic operations:${a} + ${b} = ${a + b}${a} - ${b} = ${a - b}${a} * ${b} = ${a * b}${a} / ${b} = ${a / b}`;}
Numbers may be specified in decimal, hex, octal, or binary. Use exactly0x
,0o
, and0b
prefixes, with lowercase letters, for hex, octal, and binary,respectively. Never include a leading zero unless it is immediately followed byx
,o
, orb
.
TypeScript codemay use theString()
andBoolean()
(note: nonew
!)functions, string template literals, or!!
to coerce types.
const bool = Boolean(false);const str = String(aNumber);const bool2 = !!str;const str2 = `result: ${bool2}`;
Values of enum types (including unions of enum types and other types)must notbe converted to booleans withBoolean()
or!!
, and must instead be comparedexplicitly with comparison operators.
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;
Why?
For most purposes, it doesn't matter what number or string value an enum name ismapped to at runtime, because values of enum types are referred to by name insource code. Consequently, engineers are accustomed to not thinking about this,and so situations where itdoes matter are undesirable because they will besurprising. Such is the case with conversion of enums to booleans; inparticular, by default, the first declared enum value is falsy (because it is 0)while the others are truthy, which is likely to be unexpected. Readers of codethat uses an enum value may not even know whether it's the first declared valueor not.
Using string concatenation to cast to string is discouraged, as we check thatoperands to the plus operator are of matching types.
Codemust useNumber()
to parse numeric values, andmust check its returnforNaN
values explicitly, unless failing to parse is impossible from context.
Note:Number('')
,Number(' ')
, andNumber('\t')
would return0
insteadofNaN
.Number('Infinity')
andNumber('-Infinity')
would returnInfinity
and-Infinity
respectively. Additionally, exponential notation such asNumber('1e+309')
andNumber('-1e+309')
can overflow intoInfinity
. Thesecases may require special handling.
const aNumber = Number('123');if (!isFinite(aNumber)) throw new Error(...);
Codemust not use unary plus (+
) to coerce strings to numbers. Parsingnumbers can fail, has surprising corner cases, and can be a code smell (parsingat the wrong layer). A unary plus is too easy to miss in code reviews giventhis.
const x = +y;
Code alsomust not useparseInt
orparseFloat
to parse numbers, except fornon-base-10 strings (see below). Both of those functions ignore trailingcharacters in the string, which can shadow error conditions (e.g. parsing12dwarves
as12
).
const n = parseInt(someString, 10); // Error prone,const f = parseFloat(someString); // regardless of passing a radix.
Code that requires parsing with a radixmust check that its input containsonly appropriate digits for that radix before calling intoparseInt
;
if (!/^[a-fA-F0-9]+$/.test(someString)) throw new Error(...);// Needed to parse hexadecimal.// tslint:disable-next-line:banconst n = parseInt(someString, 16); // Only allowed for radix != 10
UseNumber()
followed byMath.floor
orMath.trunc
(where available) toparse integer numbers:
let f = Number(someString);if (isNaN(f)) handleError();f = Math.floor(f);
Do not use explicit boolean coercions in conditional clauses that have implicitboolean coercion. Those are the conditions in anif
,for
andwhile
statements.
const foo: MyInterface|null = ...;if (!!foo) {...}while (!!foo) {...}
const foo: MyInterface|null = ...;if (foo) {...}while (foo) {...}
As with explicit conversions, values of enum types (includingunions of enum types and other types)must not be implicitly coerced tobooleans, and must instead be compared explicitly with comparison operators.
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) {...}
Other types of values may be either implicitly coerced to booleans or comparedexplicitly with comparison operators:
// Explicitly comparing > 0 is OK:if (arr.length > 0) {...}// so is relying on boolean coercion:if (arr.length) {...}
Control flow statements (if
,else
,for
,do
,while
, etc) always usebraced blocks for the containing code, even if the body contains only a singlestatement. The first statement of a non-empty block must begin on its own line.
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);
Exception:if
statements fitting on one linemay elide the block.
if (x) x.doFoo();
Prefer to avoid assignment of variables inside control statements. Assignmentcan be easily mistaken for equality checks inside control statements.
if (x = someFunction()) { // Assignment easily mistaken with equality check // ...}
x = someFunction();if (x) { // ...}
In cases where assignment inside the control statement is preferred, enclose theassignment in additional parenthesis to indicate it is intentional.
while ((x = someFunction())) { // Double parenthesis shows assignment is intentional // ...}
Preferfor (... of someArr)
to iterate over arrays.Array.prototype.forEach
and vanillafor
loops are also allowed:
for (const x of someArr) { // x is a value of someArr.}for (let i = 0; i < someArr.length; i++) { // Explicitly count if the index is needed, otherwise use the for/of form. const x = someArr[i]; // ...}for (const [i, x] of someArr.entries()) { // Alternative version of the above.}
for
-in
loops may only be used on dict-style objects (seebelow for more info). Do notusefor (... in ...)
to iterate over arrays as it will counterintuitively givethe array's indices (as strings!), not values:
for (const x in someArray) { // x is the index!}
Object.prototype.hasOwnProperty
should be used infor
-in
loops to excludeunwanted prototype properties. Preferfor
-of
withObject.keys
,Object.values
, orObject.entries
overfor
-in
when possible.
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);}
Optional grouping parentheses are omitted only when the author and revieweragree that there is no reasonable chance that the code will be misinterpretedwithout them, nor would they have made the code easier to read. It isnotreasonable to assume that every reader has the entire operator precedence tablememorized.
Do not use unnecessary parentheses around the entire expression followingdelete
,typeof
,void
,return
,throw
,case
,in
,of
, oryield
.
Exceptions are an important part of the language and should be used wheneverexceptional cases occur.
Custom exceptions provide a great way to convey additional error informationfrom functions. They should be defined and used wherever the nativeError
typeis insufficient.
Prefer throwing exceptions over ad-hoc error-handling approaches (such aspassing an error container reference type, or returning an object with an errorproperty).
new
Always usenew Error()
when instantiating exceptions, instead of just callingError()
. Both forms create a newError
instance, but usingnew
is moreconsistent with how other objects are instantiated.
throw new Error('Foo is not a valid bar.');
throw Error('Foo is not a valid bar.');
JavaScript (and thus TypeScript) allow throwing or rejecting a Promise witharbitrary values. However if the thrown or rejected value is not anError
, itdoes not populate stack trace information, making debugging hard. This treatmentextends toPromise
rejection values asPromise.reject(obj)
is equivalent tothrow obj;
in async functions.
// bad: does not get a stack trace.throw 'oh noes!';// For promisesnew Promise((resolve, reject) => void reject('oh noes!'));Promise.reject();Promise.reject('oh noes!');
Instead, only throw (subclasses of)Error
:
// Throw only Errorsthrow new Error('oh noes!');// ... or subtypes of Error.class MyError extends Error {}throw new MyError('my oh noes!');// For promisesnew Promise((resolve) => resolve()); // No reject is OK.new Promise((resolve, reject) => void reject(new Error('oh noes!')));Promise.reject(new Error('oh noes!'));
When catching errors, codeshould assume that all thrown errors are instancesofError
.
function assertIsError(e: unknown): asserts e is Error { if (!(e instanceof Error)) throw new Error("e is not an Error");}try { doSomething();} catch (e: unknown) { // All thrown errors must be Error subtypes. Do not handle // other possible values unless you know they are thrown. assertIsError(e); displayError(e.message); // or rethrow: throw e;}
Exception handlersmust not defensively handle non-Error
types unless thecalled API is conclusively known to throw non-Error
s in violation of the aboverule. In that case, a comment should be included to specifically identify wherethe non-Error
s originate.
try { badApiThrowingStrings();} catch (e: unknown) { // Note: bad API throws strings instead of errors. if (typeof e === 'string') { ... }}
Why?
Avoidoverly defensive programming.Repeating the same defenses against a problem that will not exist in most codeleads to boiler-plate code that is not useful.
It is very rarely correct to do nothing in response to a caught exception. Whenit truly is appropriate to take no action whatsoever in a catch block, thereason this is justified is explained in a comment.
try { return handleNumericResponse(response); } catch (e: unknown) { // Response is not numeric. Continue to handle as text. } return handleTextResponse(response);
Disallowed:
try { shouldFail(); fail('expected an error'); } catch (expected: unknown) { }
Tip: Unlike in some other languages, patterns like the above simply don’t worksince this will catch the error thrown byfail
. UseassertThrows()
instead.
Allswitch
statementsmust contain adefault
statement group, even if itcontains no code. Thedefault
statement group must be last.
switch (x) { case Y: doSomethingElse(); break; default: // nothing to do.}
Within a switch block, each statement group either terminates abruptly with abreak
, areturn
statement, or by throwing an exception. Non-empty statementgroups (case ...
)must not fall through (enforced by the compiler):
switch (x) { case X: doSomething(); // fall through - not allowed! case Y: // ...}
Empty statement groups are allowed to fall through:
switch (x) { case X: case Y: doSomething(); break; default: // nothing to do.}
Always use triple equals (===
) and not equals (!==
). The double equalityoperators cause error prone type coercions that are hard to understand andslower to implement for JavaScript Virtual Machines. See also theJavaScript equality table.
if (foo == 'bar' || baz != bam) { // Hard to understand behaviour due to type coercion.}
if (foo === 'bar' || baz !== bam) { // All good here.}
Exception: Comparisons to the literalnull
valuemay use the==
and!=
operators to cover bothnull
andundefined
values.
if (foo == null) { // Will trigger when foo is null or undefined.}
Type assertions (x as SomeType
) and non-nullability assertions (y!
) areunsafe. Both only silence the TypeScript compiler, but do not insert any runtimechecks to match these assertions, so they can cause your program to crash atruntime.
Because of this, youshould not use type and non-nullability assertionswithout an obvious or explicit reason for doing so.
Instead of the following:
(x as Foo).foo();y!.bar();
When you want to assert a type or non-nullability the best answer is toexplicitly write a runtime check that performs that check.
// assuming Foo is a class.if (x instanceof Foo) { x.foo();}if (y) { y.bar();}
Sometimes due to some local property of your code you can be sure that theassertion form is safe. In those situations, youshould add clarification toexplain why you are ok with the unsafe behavior:
// x is a Foo, because ...(x as Foo).foo();// y cannot be null, because ...y!.bar();
If the reasoning behind a type or non-nullability assertion is obvious, thecommentsmay not be necessary. For example, generated proto code is alwaysnullable, but perhaps it is well-known in the context of the code that certainfields are always provided by the backend. Use your judgement.
Type assertionsmust use theas
syntax (as opposed to the angle bracketssyntax). This enforces parentheses around the assertion when accessing a member.
const x = (<Foo>z).length;const y = <Foo>z.length;
// z must be Foo because ...const x = (z as Foo).length;
From theTypeScript handbook,TypeScript only allows type assertions which convert to amore specific orless specific version of a type. Adding a type assertion (x as Foo
) whichdoes not meet this criteria will give the error:Conversion of type 'X' to type'Y' may be a mistake because neither type sufficiently overlaps with the other.
If you are sure an assertion is safe, you can perform adouble assertion. Thisinvolves casting throughunknown
since it is less specific than all types.
// x is a Foo here, because...(x as unknown as Foo).fooMethod();
Useunknown
(instead ofany
or{}
) as the intermediate type.
Use type annotations (: Foo
) instead of type assertions (as Foo
) to specifythe type of an object literal. This allows detecting refactoring bugs when thefields of an interface change over time.
interface Foo { bar: number; baz?: string; // was "bam", but later renamed to "baz".}const foo = { bar: 123, bam: 'abc', // no error!} as Foo;function func() { return { bar: 123, bam: 'abc', // no error! } as Foo;}
interface Foo { bar: number; baz?: string;}const foo: Foo = { bar: 123, bam: 'abc', // complains about "bam" not being defined on Foo.};function func(): Foo { return { bar: 123, bam: 'abc', // complains about "bam" not being defined on Foo. };}
Limit the amount of code inside a try block, if this can be done without hurtingreadability.
try { const result = methodThatMayThrow(); use(result);} catch (error: unknown) { // ...}
let result;try { result = methodThatMayThrow();} catch (error: unknown) { // ...}use(result);
Moving the non-throwable lines out of the try/catch block helps the reader learnwhich method throws exceptions. Some inline calls that do not throw exceptionscould stay inside because they might not be worth the extra complication of atemporary variable.
Exception: There may be performance issues if try blocks are inside a loop.Widening try blocks to cover a whole loop is ok.
Decorators are syntax with an@
prefix, like@MyDecorator
.
Do not define new decorators. Only use the decorators defined byframeworks:
@Component
,@NgModule
, etc.)@property
)Why?
We generally want to avoid decorators, because they were an experimental featurethat have since diverged from the TC39 proposal and have known bugs that won'tbe fixed.
When using decorators, the decoratormust immediately precede the symbol itdecorates, with no empty lines between:
/** JSDoc comments go before decorators */@Component({...}) // Note: no empty line after the decorator.class MyComp { @Input() myField: string; // Decorators on fields may be on the same line... @Input() myOtherField: string; // ... or wrap.}
TypeScript codemust not instantiate the wrapper classes for the primitivetypesString
,Boolean
, andNumber
. Wrapper classes have surprisingbehavior, such asnew Boolean(false)
evaluating totrue
.
const s = new String('hello');const b = new Boolean(false);const n = new Number(5);
The wrappers may be called as functions for coercing (which is preferred overusing+
or concatenating the empty string) or creating symbols. Seetype coercion for more information.
Do not rely on Automatic Semicolon Insertion (ASI). Explicitly end allstatements using a semicolon. This prevents bugs due to incorrect semicoloninsertions and ensures compatibility with tools with limited ASI support (e.g.clang-format).
Codemust not useconst enum
; use plainenum
instead.
Why?
TypeScript enums already cannot be mutated;const enum
is a separate languagefeature related to optimization that makes the enum invisible toJavaScript users of the module.
Debugger statementsmust not be included in production code.
function debugMe() { debugger;}
with
Do not use thewith
keyword. It makes your code harder to understand andhas been banned in strict mode since ES5.
Do not useeval
or theFunction(...string)
constructor (except for codeloaders). These features are potentially dangerous and simply do not work inenvironments using strictContent Security Policies.
Do not use non-standard ECMAScript or Web Platform features.
This includes:
Projects targeting specific JavaScript runtimes, such as latest-Chrome-only,Chrome extensions, Node.JS, Electron, can obviously use those APIs. Use cautionwhen considering an API surface that is proprietary and only implemented in somebrowsers; consider whether there is a common library that can abstract this APIsurface away for you.
Never modify builtin types, either by adding methods to their constructors or totheir prototypes. Avoid depending on libraries that dothis.
Do not add symbols to the global object unless absolutely necessary (e.g.required by a third-party API).
Identifiersmust use only ASCII letters, digits, underscores (for constantsand structured test method names), and (rarely) the '$' sign.
TypeScript expresses information in types, so namesshould not be decoratedwith information that is included in the type. (See alsoTesting Blog for more about whatnot to include.)
Some concrete examples of this rule:
opt_
prefix for optional parameters.IMyInterface
MyFooInterface
class TodoItem
andinterfaceTodoItemStorage
if the interface expresses the format used forstorage/serialization in JSON).Observable
s with$
is a common external convention and canhelp resolve confusion regarding observable values vs concrete values.Judgement on whether this is a useful convention is left up to individualteams, butshould be consistent within projects.Namesmust be descriptive and clear to a new reader. Do not use abbreviationsthat are ambiguous or unfamiliar to readers outside your project, and do notabbreviate by deleting letters within a word.
// Good identifiers:errorCount // No abbreviation.dnsConnectionIndex // Most people know what "DNS" stands for.referrerUrl // Ditto for "URL".customerId // "Id" is both ubiquitous and unlikely to be misunderstood.
// Disallowed identifiers:n // Meaningless.nErr // Ambiguous abbreviation.nCompConns // Ambiguous abbreviation.wgcConnections // Only your group knows what this stands for.pcReader // Lots of things can be abbreviated "pc".cstmrId // Deletes internal letters.kSecondsPerDay // Do not use Hungarian notation.customerID // Incorrect camelcase of "ID".
Treat abbreviations like acronyms in names as whole words, i.e. useloadHttpUrl
, not, unless required by a platform name (e.g.loadHTTPURL
XMLHttpRequest
).
Identifiersshould not generally use$
, except when required by namingconventions for third party frameworks.See above for more onusing$
withObservable
values.
Most identifier names should follow the casing in the table below, based on theidentifier's type.
Style | Category |
---|---|
UpperCamelCase | class / interface / type / enum / decorator / typeparameters / component functions in TSX / JSXElement typeparameter |
lowerCamelCase | variable / parameter / function / method / property /module alias |
CONSTANT_CASE | global constant values, including enum values. SeeConstants below. |
#ident | private identifiers are never used. |
Type parameters, like inArray<T>
,may use a single upper case character(T
) orUpperCamelCase
.
Test method names inxUnit-style test frameworksmay be structured with_
separators, e.g.testX_whenY_doesZ()
.
_
prefix/suffixIdentifiers must not use_
as a prefix or suffix.
This also means that_
must not be used as an identifier by itself (e.g. toindicate a parameter is unused).
Tip: If you only need some of the elements from an array (or TypeScripttuple), you can insert extra commas in a destructuring statement to ignorein-between elements:
const [a, , b] = [1, 5, 10]; // a <- 1, b <- 10
Module namespace imports arelowerCamelCase
while files aresnake_case
,which means that imports correctly will not match in casing style, such as
import * as fooBar from './foo_bar';
Some libraries might commonly use a namespace import prefix that violates thisnaming scheme, but overbearingly common open source use makes the violatingstyle more readable. The only libraries that currently fall under this exceptionare:
Immutable:CONSTANT_CASE
indicates that a value isintended to not bechanged, andmay be used for values that can technically be modified (i.e.values that are not deeply frozen) to indicate to users that they must not bemodified.
const UNIT_SUFFIXES = { 'milliseconds': 'ms', 'seconds': 's',};// Even though per the rules of JavaScript UNIT_SUFFIXES is// mutable, the uppercase shows users to not modify it.
A constant can also be astatic readonly
property of a class.
class Foo { private static readonly MY_SPECIAL_NUMBER = 5; bar() { return 2 * Foo.MY_SPECIAL_NUMBER; }}
Global: Only symbols declared on the module level, static fields of modulelevel classes, and values of module level enums,may useCONST_CASE
. If avalue can be instantiated more than once over the lifetime of the program (e.g.a local variable declared within a function, or a static field on a class nestedin a function) then itmust uselowerCamelCase
.
If a value is an arrow function that implements an interface, then itmay bedeclaredlowerCamelCase
.
When creating a local-scope alias of an existing symbol, use the format of theexisting identifier. The local aliasmust match the existing naming and formatof the source. For variables useconst
for your local aliases, and for classfields use thereadonly
attribute.
Note: If you're creating an alias just to expose it to a template in yourframework of choice, remember to also apply the properaccess modifiers.
const {BrewStateEnum} = SomeType;const CAPACITY = 5;class Teapot { readonly BrewStateEnum = BrewStateEnum; readonly CAPACITY = CAPACITY;}
Codemay rely on type inference as implemented by the TypeScript compiler forall type expressions (variables, fields, return types, etc).
const x = 15; // Type inferred.
Leave out type annotations for trivially inferred types: variables or parametersinitialized to astring
,number
,boolean
,RegExp
literal ornew
expression.
const x: boolean = true; // Bad: 'boolean' here does not aid readability
// Bad: 'Set' is trivially inferred from the initializationconst x: Set<string> = new Set();
Explicitly specifying types may be required to prevent generic type parametersfrom being inferred asunknown
. For example, initializing generic types withno values (e.g. empty arrays, objects,Map
s, orSet
s).
const x = new Set<string>();
For more complex expressions, type annotations can help with readability of theprogram:
// Hard to reason about the type of 'value' without an annotation.const value = await rpc.getSomeValue().transform();
// Can tell the type of 'value' at a glance.const value: string[] = await rpc.getSomeValue().transform();
Whether an annotation is required is decided by the code reviewer.
Whether to include return type annotations for functions and methods is up tothe code author. Reviewersmay ask for annotations to clarify complex returntypes that are hard to understand. Projectsmay have a local policy to alwaysrequire return types, but this is not a general TypeScript style requirement.
There are two benefits to explicitly typing out the implicit return values offunctions and methods:
TypeScript supportsundefined
andnull
types. Nullable types can beconstructed as a union type (string|null
); similarly withundefined
. Thereis no special syntax for unions ofundefined
andnull
.
TypeScript code can use eitherundefined
ornull
to denote absence of avalue, there is no general guidance to prefer one over the other. ManyJavaScript APIs useundefined
(e.g.Map.get
), while many DOM and Google APIsusenull
(e.g.Element.getAttribute
), so the appropriate absent valuedepends on the context.
Type aliasesmust not include|null
or|undefined
in a union type.Nullable aliases typically indicate that null values are being passed aroundthrough too many layers of an application, and this clouds the source of theoriginal issue that resulted innull
. They also make it unclear when specificvalues on a class or interface might be absent.
Instead, codemust only add|null
or|undefined
when the alias is actuallyused. Codeshould deal with null values close to where they arise, using theabove techniques.
// Badtype CoffeeResponse = Latte|Americano|undefined;class CoffeeService { getLatte(): CoffeeResponse { ... };}
// Bettertype CoffeeResponse = Latte|Americano;class CoffeeService { getLatte(): CoffeeResponse|undefined { ... };}
|undefined
In addition, TypeScript supports a special construct for optional parameters andfields, using?
:
interface CoffeeOrder { sugarCubes: number; milk?: Whole|LowFat|HalfHalf;}function pourCoffee(volume?: Milliliter) { ... }
Optional parameters implicitly include|undefined
in their type. However, theyare different in that they can be left out when constructing a value or callinga method. For example,{sugarCubes: 1}
is a validCoffeeOrder
becausemilk
is optional.
Use optional fields (on interfaces or classes) and parameters rather than a|undefined
type.
For classes preferably avoid this pattern altogether and initialize as manyfields as possible.
class MyClass { field = '';}
TypeScript's type system is structural, not nominal. That is, a value matches atype if it has at least all the properties the type requires and the properties'types match, recursively.
When providing a structural-based implementation, explicitly include the type atthe declaration of the symbol (this allows more precise type checking and errorreporting).
const foo: Foo = { a: 123, b: 'abc',}
const badFoo = { a: 123, b: 'abc',}
Use interfaces to define structural types, not classes
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',}
Why?
ThebadFoo
object above relies on type inference. Additional fields could beadded tobadFoo
and the type is inferred based on the object itself.
When passing abadFoo
to a function that takes aFoo
, the error will be atthe function call site, rather than at the object declaration site. This is alsouseful when changing the surface of an interface across broad codebases.
interface Animal { sound: string; name: string;}function makeSound(animal: Animal) {}/** * 'cat' has an inferred type of '{sound: string}' */const cat = { sound: 'meow',};/** * 'cat' does not meet the type contract required for the function, so the * TypeScript compiler errors here, which may be very far from where 'cat' is * defined. */makeSound(cat);/** * Horse has a structural type and the type error shows here rather than the * function call. 'horse' does not meet the type contract of 'Animal'. */const horse: Animal = { sound: 'niegh',};const dog: Animal = { sound: 'bark', name: 'MrPickles',};makeSound(dog);makeSound(horse);
TypeScript supportstype aliasesfor naming a type expression. This can be used to name primitives, unions,tuples, and any other types.
However, when declaring types for objects, use interfaces instead of a typealias for the object literal expression.
interface User { firstName: string; lastName: string;}
type User = { firstName: string, lastName: string,}
Why?
These forms are nearly equivalent, so under the principle of just choosing oneout of two forms to prevent variation, we should choose one. Additionally, thereare alsointeresting technical reasons to prefer interface.That page quotes the TypeScript team lead:Honestly, my take is that it shouldreally just be interfaces for anything that they can model. There is no benefitto type aliases when there are so many issues around display/perf.
Array<T>
TypeFor simple types (containing just alphanumeric characters and dot), use thesyntax sugar for arrays,T[]
orreadonly T[]
, rather than the longer formArray<T>
orReadonlyArray<T>
.
For multi-dimensional non-readonly
arrays of simple types, use the syntaxsugar form (T[][]
,T[][][]
, and so on) rather than the longer form.
For anything more complex, use the longer formArray<T>
.
These rules apply at each level of nesting, i.e. a simpleT[]
nested in a morecomplex type would still be spelled asT[]
, using the syntax sugar.
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[]>; // Use syntax sugar for nested types.let i: ReadonlyArray<string[]>;let j: Array<readonly string[]>;
let a: Array<string>; // The syntax sugar is shorter.let b: ReadonlyArray<string>;let c: Array<ns.MyObj>;let d: Array<string[]>;let e: {n: number, s: string}[]; // The braces make it harder to read.let f: (string|number)[]; // Likewise with parens.let g: readonly (string | number)[];let h: InjectionToken<Array<string>>;let i: readonly string[][];let j: (readonly string[])[];
{[key: string]: T}
)In JavaScript, it's common to use an object as an associative array (akamap
,hash
, ordict
). Such objects can be typed using anindex signature([k: string]: T
) in TypeScript:
const fileSizes: {[fileName: string]: number} = {};fileSizes['readme.txt'] = 541;
In TypeScript, provide a meaningful label for the key. (The label only existsfor documentation; it's unused otherwise.)
const users: {[key: string]: number} = ...;
const users: {[userName: string]: number} = ...;
Rather than using one of these, consider using the ES6
Map
andSet
typesinstead. JavaScript objects havesurprising undesirable behaviorsand the ES6 types more explicitly convey your intent. Also,Map
s can bekeyed by—andSet
s can contain—types other thanstring
.
TypeScript's builtinRecord<Keys, ValueType>
type allows constructing typeswith a defined set of keys. This is distinct from associative arrays in that thekeys are statically known. See advice on thatbelow.
TypeScript'smapped typesandconditional typesallow specifying new types based on other types. TypeScript's standard libraryincludes several type operators based on these (Record
,Partial
,Readonly
etc).
These type system features allow succinctly specifying types and constructingpowerful yet type safe abstractions. They come with a number of drawbacksthough:
accidentallycompile or seem to give the right results. This increasesfuture support cost of code using type operators.
Pick<T, Keys>
type, and Code Search won'thyperlink them.The style recommendation is:
For example, TypeScript's builtinPick<T, Keys>
type allows creating a newtype by subsetting another typeT
, but simple interface extension can often beeasier to understand.
interface User { shoeSize: number; favoriteIcecream: string; favoriteChocolate: string;}// FoodPreferences has favoriteIcecream and favoriteChocolate, but not shoeSize.type FoodPreferences = Pick<User, 'favoriteIcecream'|'favoriteChocolate'>;
This is equivalent to spelling out the properties onFoodPreferences
:
interface FoodPreferences { favoriteIcecream: string; favoriteChocolate: string;}
To reduce duplication,User
could extendFoodPreferences
, or (possiblybetter) nest a field for food preferences:
interface FoodPreferences { /* as above */ }interface User extends FoodPreferences { shoeSize: number; // also includes the preferences.}
Using interfaces here makes the grouping of properties explicit, improves IDEsupport, allows better optimization, and arguably makes the code easier tounderstand.
any
TypeTypeScript'sany
type is a super and subtype of all other types, and allowsdereferencing all properties. As such,any
is dangerous - it can mask severeprogramming errors, and its use undermines the value of having static types inthe first place.
Considernot to useany
. In circumstances where you want to useany
,consider one of:
Use interfaces , aninline object type, or a type alias:
// Use declared interfaces to represent server-side JSON.declare interface MyUserJson { name: string; email: string;}// Use type aliases for types that are repetitive to write.type MyType = number|string;// Or use inline object types for complex returns.function getTwoThings(): {something: number, other: string} { // ... return {something, other};}// Use a generic type, where otherwise a library would say `any` to represent// they don't care what type the user is operating on (but note "Return type// only generics" below).function nicestElement<T>(items: T[]): T { // Find the nicest element in items. // Code can also put constraints on T, e.g. <T extends HTMLElement>.}
unknown
overany
Theany
type allows assignment into any other type and dereferencing anyproperty off it. Often this behaviour is not necessary or desirable, and codejust needs to express that a type is unknown. Use the built-in typeunknown
inthat situation — it expresses the concept and is much safer as it does not allowdereferencing arbitrary properties.
// Can assign any value (including null or undefined) into this but cannot// use it without narrowing the type or casting.const val: unknown = value;
const danger: any = value /* result of an arbitrary expression */;danger.whoops(); // This access is completely unchecked!
To safely useunknown
values, narrow the type using atype guard
any
lint warningsSometimes usingany
is legitimate, for example in tests to construct a mockobject. In such cases, add a comment that suppresses the lint warning, anddocument why it is legitimate.
// This test only needs a partial implementation of BookService, and if// we overlooked something the test will fail in an obvious way.// This is an intentionally unsafe partial mock// tslint:disable-next-line:no-anyconst mockBookService = ({get() { return mockBook; }} as any) as BookService;// Shopping cart is not used in this test// tslint:disable-next-line:no-anyconst component = new MyComponent(mockBookService, /* unused ShoppingCart */ null as any);
{}
TypeThe{}
type, also known as anempty interface type, represents a interfacewith no properties. An empty interface type has no specified properties andtherefore any non-nullish value is assignable to it.
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 codeshould not use{}
for most use cases.{}
represents anynon-nullish primitive or object type, which is rarely appropriate. Prefer one ofthe following more-descriptive types:
unknown
can hold any value, includingnull
orundefined
, and isgenerally more appropriate for opaque values.Record<string, T>
is better for dictionary-like objects, and providesbetter type safety by being explicit about the typeT
of contained values(which may itself beunknown
).object
excludes primitives as well, leaving only non-nullish functions andobjects, but without any other assumptions about what properties may beavailable.If you are tempted to create a Pair type, instead use a tuple type:
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];}// Use it like:const [leftHalf, rightHalf] = splitInHalf('my string');
However, often it's clearer to provide meaningful names for the properties.
If declaring aninterface
is too heavyweight, you can use an inline objectliteral type:
function splitHostPort(address: string): {host: string, port: number} { ...}// Use it like:const address = splitHostPort(userAddress);use(address.port);// You can also get tuple-like behavior using destructuring:const {host, port} = splitHostPort(userAddress);
There are a few types related to JavaScript primitives thatshould not ever beused:
String
,Boolean
, andNumber
have slightly different meaning from thecorresponding primitive typesstring
,boolean
, andnumber
. Always usethe lowercase version.Object
has similarities to both{}
andobject
, but is slightly looser.Use{}
for a type that include everything exceptnull
andundefined
,or lowercaseobject
to further exclude the other primitive types (thethree mentioned above, plussymbol
andbigint
).Further, never invoke the wrapper types as constructors (withnew
).
Avoid creating APIs that have return type only generics. When working withexisting APIs that have return type only generics always explicitly specify thegenerics.
Google style requires using a number of tools in specific ways, outlined here.
All TypeScript files must pass type checking using the standard tool chain.
Do not use@ts-ignore
nor the variants@ts-expect-error
or@ts-nocheck
.
Why?
They superficially seem to be an easy way tofix
a compiler error, but inpractice, a specific compiler error is often caused by a larger problem that canbe fixed more directly.
For example, if you are using@ts-ignore
to suppress a type error, then it'shard to predict what types the surrounding code will end up seeing. For manytype errors, the advice inhow to best useany
is useful.
You may use@ts-expect-error
in unit tests, though you generallyshould not.@ts-expect-error
suppresses all errors. It's easy to accidentally over-matchand suppress more serious errors. Consider one of:
any
and add an explanatory comment. Thislimits error suppression to a single expression.any
lint warnings.Google TypeScript includes severalconformance frameworks,tsetse andtsec.
These rules are commonly used to enforce critical restrictions (such as definingglobals, which could break the codebase) and security patterns (such as usingeval
or assigning toinnerHTML
), or more loosely to improve code quality.
Google-style TypeScript must abide by any applicable global or framework-localconformance rules.
There are two types of comments, JSDoc (/** ... */
) and non-JSDoc ordinarycomments (// ...
or/* ... */
).
/** JSDoc */
comments for documentation, i.e. comments a user of thecode should read.// line comments
for implementation comments, i.e. comments that onlyconcern the implementation of the code itself.JSDoc comments are understood by tools (such as editors and documentationgenerators), while ordinary comments are only for other humans.
Multi-line comments are indented at the same level as the surrounding code. Theymust use multiple single-line comments (//
-style), not block comment style(/* */
).
// This is// fine
/* * This should * use multiple * single-line comments *//* This should use // */
Comments are not enclosed in boxes drawn with asterisks or other characters.
The basic formatting of JSDoc comments is as seen in this example:
/** * Multiple lines of JSDoc text are written here, * wrapped normally. * @param arg A number to do something to. */function doSomething(arg: number) { … }
or in this single-line example:
/** This short jsdoc describes the function. */function doSomething(arg: number) { … }
If a single-line comment overflows into multiple lines, itmust use themulti-line style with/**
and*/
on their own lines.
Many tools extract metadata from JSDoc comments to perform code validation andoptimization. As such, these commentsmust be well-formed.
JSDoc is written in Markdown, though itmay include HTML when necessary.
This means that tooling parsing JSDoc will ignore plain text formatting, so ifyou did this:
/** * Computes weight based on three factors: * items sent * items received * last timestamp */
it will be rendered like this:
Computes weight based on three factors: items sent items received last timestamp
Instead, write a Markdown list:
/** * Computes weight based on three factors: * * - items sent * - items received * - last timestamp */
Google style allows a subset of JSDoc tags. Most tags must occupy their own line, with the tag at the beginningof the line.
/** * The "param" tag must occupy its own line and may not be combined. * @param left A description of the left param. * @param right A description of the right param. */function add(left: number, right: number) { ... }
/** * The "param" tag must occupy its own line and may not be combined. * @param left @param right */function add(left: number, right: number) { ... }
Line-wrapped block tags are indented four spaces. Wrapped description textmaybe lined up with the description on previous lines, but this horizontalalignment is discouraged.
/** * Illustrates line wrapping for long param/return descriptions. * @param foo This is a param with a particularly long description that just * doesn't fit on one line. * @return This returns something that has a lengthy description too long to fit * in one line. */exports.method = function(foo) { return 5;};
Do not indent when wrapping a@desc
or@fileoverview
description.
Use/** JSDoc */
comments to communicate information to the users of yourcode. Avoid merely restating the property or parameter name. Youshould alsodocument all properties and methods (exported/public or not) whose purpose isnot immediately obvious from their name, as judged by your reviewer.
Exception: Symbols that are only exported to be consumed by tooling, such as@NgModule classes, do not require comments.
JSDoc comments for classes should provide the reader with enough information toknow how and when to use the class, as well as any additional considerationsnecessary to correctly use the class. Textual descriptions may be omitted on theconstructor.
Method, parameter, and return descriptions may be omitted if they are obviousfrom the rest of the method’s JSDoc or from the method name and type signature.
Method descriptions begin with a verb phrase that describes what the methoddoes. This phrase is not an imperative sentence, but instead is written in thethird person, as if there is an impliedThis method ...
before it.
Aparameter propertyis a constructor parameter that is prefixed by one of the modifiersprivate
,protected
,public
, orreadonly
. A parameter property declares both aparameter and an instance property, and implicitly assigns into it. For example,constructor(private readonly foo: Foo)
, declares that the constructor takes aparameterfoo
, but also declares a private readonly propertyfoo
, andassigns the parameter into that property before executing the remainder of theconstructor.
To document these fields, use JSDoc's@param
annotation. Editors display thedescription on constructor calls and property accesses.
/** This class demonstrates how parameter properties are documented. */class ParamProps { /** * @param percolator The percolator used for brewing. * @param beans The beans to brew. */ constructor( private readonly percolator: Percolator, private readonly beans: CoffeeBean[]) {}}
/** This class demonstrates how ordinary fields are documented. */class OrdinaryClass { /** The bean that will be used in the next call to brew(). */ nextBean: CoffeeBean; constructor(initialBean: CoffeeBean) { this.nextBean = initialBean; }}
JSDoc type annotations are redundant in TypeScript source code. Do not declaretypes in@param
or@return
blocks, do not write@implements
,@enum
,@private
,@override
etc. on code that uses theimplements
,enum
,private
,override
etc. keywords.
For non-exported symbols, sometimes the name and type of the function orparameter is enough. Code willusually benefit from more documentation thanjust variable names though!
Avoid comments that just restate the parameter name and type, e.g.
/** @param fooBarService The Bar service for the Foo application. */
Because of this rule,@param
and@return
lines are only required whenthey add information, andmay otherwise be omitted.
/** * POSTs the request to start coffee brewing. * @param amountLitres The amount to brew. Must fit the pot size! */brew(amountLitres: number, logger: Logger) { // ...}
“Parameter name” comments should be used whenever the method name and parametervalue do not sufficiently convey the meaning of the parameter.
Before adding these comments, consider refactoring the method to instead acceptan interface and destructure it to greatly improve call-sitereadability.
Parameter name
comments go before the parameter value, and include theparameter name and a=
suffix:
someFunction(obviousParam, /* shouldRender= */ true, /* name= */ 'hello');
Existing code may use a legacy parameter name comment style, which places thesecomments ~after~ the parameter value and omits the=
. Continuing to use thisstyle within the file for consistency is acceptable.
someFunction(obviousParam, true /* shouldRender */, 'hello' /* name */);
When a class, method, or property have both decorators like@Component
andJsDoc, please make sure to write the JsDoc before the decorator.
Do not write JsDoc between the Decorator and the decorated statement.
@Component({ selector: 'foo', template: 'bar',})/** Component that prints "bar". */export class FooComponent {}
Write the JsDoc block before the Decorator.
/** Component that prints "bar". */@Component({ selector: 'foo', template: 'bar',})export class FooComponent {}
For any style question that isn't settled definitively by this specification, dowhat the other code in the same file is already doing (be consistent
). If thatdoesn't resolve the question, consider emulating the other files in the samedirectory.
Brand new filesmust use Google Style, regardless of the style choices ofother files in the same package. When adding new code to a file that is not inGoogle Style, reformatting the existing code first is recommended, subject tothe advicebelow. If this reformatting is notdone, then new codeshould be as consistent as possible with existing code inthe same file, butmust not violate the style guide.
You will occasionally encounter files in the codebase that are not in properGoogle Style. These may have come from an acquisition, or may have been writtenbefore Google Style took a position on some issue, or may be in non-Google Stylefor any other reason.
When updating the style of existing code, follow these guidelines.
Mark deprecated methods, classes or interfaces with an@deprecated
JSDocannotation. A deprecation comment must include simple, clear directions forpeople to fix their call sites.
Source code generated by the build process is not required to be in GoogleStyle. However, any generated identifiers that will be referenced fromhand-written source code must follow the naming requirements. As a specialexception, such identifiers are allowed to contain underscores, which may helpto avoid conflicts with hand-written identifiers.
In general, engineers usually know best about what's needed in their code, so ifthere are multiple options and the choice is situation dependent, we should letdecisions be made locally. So the default answer should beleave it out
.
The following points are the exceptions, which are the reasons we have someglobal rules. Evaluate your style guide proposal against the following:
Code should avoid patterns that are known to cause problems, especiallyfor users new to the language.
Code acrossprojects should be consistent acrossirrelevant variations.
When there are two options that are equivalent in a superficial way, weshould consider choosing one just so we don't divergently evolve for noreason and avoid pointless debates in code reviews.
Examples:
x as T
syntax vs the equivalent<T>x
syntax (disallowed).Array<[number, number]>
vs[number, number][]
.Code should be maintainable in the long term.
Code usually lives longer than the original author works on it, and theTypeScript team must keep all of Google working into the future.
Examples:
strict deps) so that arefactor in a dependency doesn't change the dependencies of its users.
Code reviewers should be focused on improving the quality of the code, notenforcing arbitrary rules.
If it's possible to implement your rule as anautomated check that is often a good sign.This also supports principle 3.
If it really just doesn't matter that much -- if it's an obscure corner ofthe language or if it avoids a bug that is unlikely to occur -- it'sprobably worth leaving out.