Google TypeScript Style Guide
-TypeScript Style Guide
-go/tsstyle
-This guide is based on the internal Google TypeScript style guide, but it has +been slightly adjusted to remove Google-internal sections. Google's internal environment has different constraints on TypeScript than you might find outside -of Google. The advice here is specifically useful for people authoring code -they intend to import into Google, but otherwise may not apply in your external -environment. +of Google. The advice here is specifically useful for people authoring code they +intend to import into Google, but otherwise may not apply in your external +environment.
There is no automatic deployment process for this version as it's pushed -on-demand by volunteers. -
Introduction
+ + + +Terminology notes
This Style Guide uses RFC 2119 terminology when using the phrases must, must not, should, should not, -and may. All examples given are non-normative and serve only to illustrate the -normative language of the style guide.
+and may. The terms prefer and avoid correspond to should and should +not, respectively. Imperative and declarative statements are prescriptive and +correspond to must. -Syntax
+Guide notes
-Identifiers
+All examples given are non-normative and serve only to illustrate the +normative language of the style guide. That is, while the examples are in Google +Style, they may not illustrate the only stylish way to represent the code. +Optional formatting choices made in examples must not be enforced as rules.
-Identifiers must use only ASCII letters, digits, underscores (for constants -and structured test method names), and the '\(' sign. Thus each valid identifier -name is matched by the regular expression `[\)\w]+`.
+ -Style | -Category | -
---|---|
UpperCamelCase
- |
-class / interface / type / enum / decorator / type -parameters | -
lowerCamelCase
- |
-variable / parameter / function / method / property / -module alias | -
CONSTANT_CASE
- |
-global constant values, including enum values. See -Constants below. | -
#ident |
-private identifiers are never used. | -
Abbreviations
+ -
-Treat abbreviations like acronyms in names as whole words, i.e. use
-loadHttpUrl
, not , unless required by a platform name (e.g.
-loadHTTPURL
XMLHttpRequest
).
Dollar sign
+File encoding: UTF-8
-Identifiers should not generally use $
, except when aligning with naming
-conventions for third party frameworks. See below for more on
-using $
with Observable
values.
Source files are encoded in UTF-8.
-Type parameters
+ -Type parameters, like in Array<T>
, may use a single upper case character
-(T
) or UpperCamelCase
.
Whitespace characters
-Test names
+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 are +escaped.
-Test method names in Closure testSuite
s and similar xUnit-style test
-frameworks may be structured with _
separators, e.g. testX_whenY_doesZ()
.
Special escape sequences
-_
prefix/suffix
+For any character that has a special escape sequence (\'
, \"
, \\
, \b
,
+\f
, \n
, \r
, \t
, \v
), that sequence is used rather than the
+corresponding numeric escape (e.g \x0a
, \u000a
, or \u{a}
). Legacy octal
+escapes are never used.
Identifiers must not use _
as a prefix or suffix.
Non-ASCII characters
-This also means that _
must not be used as an identifier by itself (e.g. to
-indicate a parameter is unused).
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.
--Tip: If you only need some of the elements from an array (or TypeScript -tuple), you can insert extra commas in a destructuring statement to ignore -in-between elements:
+// Perfectly clear, even without a comment. +const units = 'μs'; -
-const [a, , b] = [1, 5, 10]; // a <- 1, b <- 10 +// Use escapes for non-printable characters. +const output = '\ufeff' + content; // byte order mark
Imports
- -Module namespace imports are lowerCamelCase
while files are snake_case
,
-which means that imports correctly will not match in casing style, such as
// Hard to read and prone to mistakes, even with the comment.
+const units = '\u03bcs'; // Greek letter mu, 's'
-import * as fooBar from './foo_bar';
+// The reader has no idea what this is.
+const output = '\ufeff' + content;
-Some libraries might commonly use a namespace import prefix that violates this
-naming scheme, but overbearingly common open source use makes the violating
-style more readable. The only libraries that currently fall under this exception
-are:
+
-
+
-Constants
+
-Immutable: CONSTANT_CASE
indicates that a value is intended to not be
-changed, and may 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 be
-modified.
+
-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 a static readonly
property of a class.
+
-class Foo {
- private static readonly MY_SPECIAL_NUMBER = 5;
+
- bar() {
- return 2 * Foo.MY_SPECIAL_NUMBER;
- }
-}
-
+Source file structure
-Global: Only symbols declared on the module level, static fields of module
-level classes, and values of module level enums, may use CONST_CASE
. If a
-value 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 nested
-in a function) then it must use lowerCamelCase
.
+Files consist of the following, in order:
-If a value is an arrow function that implements an interface, then it may be
-declared lowerCamelCase
.
+
+- Copyright information, if present
+- JSDoc with
@fileoverview
, if present
+- Imports, if present
+- The file’s implementation
+
-Aliases
+Exactly one blank line separates each section that is present.
-When creating a local-scope alias of an existing symbol, use the format of the
-existing identifier. The local alias must match the existing naming and format
-of the source. For variables use const
for your local aliases, and for class
-fields use the readonly
attribute.
+
-
-Note: If you're creating an alias just to expose it to a template in your
-framework of choice, remember to also apply the proper
-access modifiers.
-
+Copyright information
-const {Foo} = SomeType;
-const CAPACITY = 5;
+
-class Teapot {
- readonly BrewStateEnum = BrewStateEnum;
- readonly CAPACITY = CAPACITY;
-}
-
+If license or copyright information is necessary in a file, add it in a JSDoc at
+the top of the file.
-Naming style
+
-TypeScript expresses information in types, so names should not be decorated
-with information that is included in the type. (See also
-Testing Blog
- for more about what
-not to include.)
+@fileoverview
JSDoc
-Some concrete examples of this rule:
+A file may have a top-level @fileoverview
JSDoc. If present, it may provide a
+description of the file's content, its uses, or information about its
+dependencies. Wrapped lines are not indented.
-
-- Do not use trailing or leading underscores for private properties or
-methods.
-- Do not use the
opt_
prefix for optional parameters.
-
-- For accessors, see accessor rules
-below.
-
-- Do not mark interfaces specially (
IMyInterface
or
-MyFooInterface
) unless it's idiomatic in its
-environment. When
-introducing an interface for a class, give it a name that expresses why the
-interface exists in the first place (e.g. class TodoItem
and interface
-TodoItemStorage
if the interface expresses the format used for
-storage/serialization in JSON).
-- Suffixing
Observable
s with $
is a common external convention and can
-help resolve confusion regarding observable values vs concrete values.
-Judgement on whether this is a useful convention is left up to individual
-teams, but should be consistent within projects.
-
+Example:
-Descriptive names
+/**
+ * @fileoverview Description of file. Lorem ipsum dolor sit amet, consectetur
+ * adipiscing elit, sed do eiusmod tempor incididunt.
+ */
+
-Names must be descriptive and clear to a new reader. Do not use abbreviations
-that are ambiguous or unfamiliar to readers outside your project, and do not
-abbreviate by deleting letters within a word.
+
-
-- Exception: Variables that are in scope for 10 lines or fewer, including
-arguments that are not part of an exported API, may use short (e.g.
-single letter) variable names.
-
+Imports
-File encoding: UTF-8
+There are four variants of import statements in ES6 and TypeScript:
-For 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
-
+
+
+
+Import type
+Example
+Use for
+
+
-// Hard to read and prone to mistakes, even with the comment.
-const units = '\u03bcs'; // Greek letter mu, 's'
+
+
+module[module_import]
+
+import * as foo from
+'...';
+TypeScript imports
+
+
+
+named[destructuring_import]
+
+import {SomeThing}
+from '...';
+TypeScript imports
+
+
+
+default
-// The reader has no idea what this is.
-const output = '\ufeff' + content;
-
+
+import SomeThing
+from '...';
+
+Only for other
+external code that
+requires them
+
+
+side-effect
-No line continuations
-Do not use line continuations (that is, ending a line inside a string literal
-with a backslash) in either ordinary or template string literals. Even though
-ES5 allows this, it can lead to tricky errors if any trailing whitespace comes
-after the slash, and is less obvious to readers.
-Disallowed:
+
+import '...';
-const LONG_STRING = 'This is a very long string that far exceeds the 80 \
- column limit. It unfortunately contains long stretches of spaces due \
- to how the continued lines are indented.';
-
-Instead, write
-const LONG_STRING = 'This is a very long string that far exceeds the 80 ' +
- 'column limit. It does not contain long stretches of spaces since ' +
- 'the concatenated strings are cleaner.';
+
+Only to import
+libraries for their
+side-effects on load
+(such as custom
+elements)
+
+
+
+
+// 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';
-Comments & Documentation
+
-JSDoc vs comments
+Import paths
-There are two types of comments, JSDoc (/** ... */
) and non-JSDoc ordinary
-comments (// ...
or /* ... */
).
+TypeScript code must use paths to import other TypeScript code. Paths may be
+relative, i.e. starting with .
or ..
,
+ or rooted at the base directory, e.g.
+root/path/to/file
.
-
-- Use
/** JSDoc */
comments for documentation, i.e. comments a user of the
-code should read.
-- Use
// line comments
for implementation comments, i.e. comments that only
-concern the implementation of the code itself.
-
+Code should use relative imports (./foo
) rather than absolute imports
+path/to/foo
when referring to files within the same (logical) project as this
+allows to move the project around without introducing changes in these imports.
-JSDoc comments are understood by tools (such as editors and documentation
-generators), while ordinary comments are only for other humans.
+Consider limiting the number of parent steps (../../../
) as those can make
+module and path structures hard to understand.
-JSDoc rules follow the JavaScript style
+import {Symbol1} from 'path/from/root';
+import {Symbol2} from '../parent/file';
+import {Symbol3} from './sibling';
+
-In general, follow the
+
-JavaScript style guide's rules for JSDoc,
-sections 7.1 - 7.5. The remainder of this section describes exceptions to those
-rules.
+
-Document all top-level exports of modules
+Namespace versus named imports
-Use /** JSDoc */
comments to communicate information to the users of your
-code. Avoid merely restating the property or parameter name. You should also
-document all properties and methods (exported/public or not) whose purpose is
-not immediately obvious from their name, as judged by your reviewer.
+Both namespace and named imports can be used.
-Exception: Symbols that are only exported to be consumed by tooling, such as
-@NgModule classes, do not require comments.
+Prefer named imports for symbols used frequently in a file or for symbols that
+have clear names, for example Jasmine's describe
and it
. Named imports can
+be aliased to clearer names as needed with as
.
-Omit comments that are redundant with TypeScript
+Prefer namespace imports when using many different symbols from large APIs. A
+namespace import, despite using the *
character, is not comparable to a
+wildcard
import as seen in other languages. Instead, namespace imports give a
+name to all the exports of a module, and each exported symbol from the module
+becomes a property on the module name. Namespace imports can aid readability for
+exported symbols that have common names like Model
or Controller
without the
+need 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';
-For example, do not declare types in @param
or @return
blocks, do not write
-@implements
, @enum
, @private
, @override
etc. on code that uses the
-implements
, enum
, private
, override
etc. keywords.
+let item: TableviewItem|undefined;
+
-Make comments that actually add information
+// Better: use the module for namespacing.
+import * as tableview from './tableview';
-For non-exported symbols, sometimes the name and type of the function or
-parameter is enough. Code will usually benefit from more documentation than
-just variable names though!
+let item: tableview.Item|undefined;
+
-
-Avoid comments that just restate the parameter name and type, e.g.
+import * as testing from './testing';
-/** @param fooBarService The Bar service for the Foo application. */
-
-Because of this rule, @param
and @return
lines are only required when
-they add information, and may otherwise be omitted.
+// 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();
+ });
+});
+
+
+Special case: Apps JSPB protos
+ +Apps JSPB protos must use named imports, even when it leads to long import +lines.
+ +This rule exists to aid in build performance and dead code elimination since
+often .proto
files contain many message
s that are not all needed together.
+By leveraging destructured imports the build system can create finer grained
+dependencies on Apps JSPB messages while preserving the ergonomics of path based
+imports.
// 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) {...}
+
+
+
+
+Renaming imports
+ +Code should fix name collisions by using a namespace import or renaming the
+exports themselves. Code may rename imports (import {SomeThing as
+SomeOtherThing}
) if needed.
Three examples where renaming can be helpful:
+ +-
+
- If it's necessary to avoid collisions with other imported symbols. +
- If the imported symbol name is generated. +
- If importing symbols whose names are unclear by themselves, renaming can
+improve code clarity. For example, when using RxJS the
from
function might +be more readable when renamed toobservableFrom
.
+
Exports
+ +Use named exports in all code:
+ +// Use named exports:
+export class Foo { ... }
+
+
+Do not use default exports. This ensures that all imports follow a uniform +pattern.
+ +// Do not use default exports:
+export default class Foo { ... } // BAD!
+
+
+Why?
+ +Default exports provide no canonical name, which makes central maintenance +difficult with relatively little benefit to code owners, including potentially +decreased readability:
+ +import Foo from './bar'; // Legal.
+import Bar from './bar'; // Also legal.
+
+
+Named exports have the benefit of erroring when import statements try to import
+something that hasn't been declared. In foo.ts
:
const foo = 'blah';
+export default foo;
+
+
+And in bar.ts
:
import {fizz} from './foo';
+
+
+Results in error TS2614: Module '"./foo"' has no exported member 'fizz'.
While
+bar.ts
:
import fizz from './foo';
+
+
+Results in fizz === foo
, which is probably unexpected and difficult to debug.
Additionally, default exports encourage people to put everything into one big +object 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. We
+also have a perhaps needless second scope (the class Foo
) that can be
+ambiguously 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
+}
+
+
+Export visibility
+ +TypeScript does not support restricting the visibility for exported symbols. +Only export symbols that are used outside of the module. Generally minimize the +exported API surface of modules.
+ + + +Mutable exports
+ + + +Regardless of technical support, mutable exports can create hard to understand
+and debug code, in particular with re-exports across multiple modules. One way
+to paraphrase this style point is that export 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, they +should 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 do +the conditional check, then the export. Make sure that all exports are final +after the module's body has executed.
+ +function pickApi() {
+ if (useOtherApi()) return OtherApi;
+ return RegularApi;
+}
+export const SomeApi = pickApi();
+
+
+
+
+Container classes
+ +Do not create container classes with static methods or properties for the sake +of 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; }
+
+
+
+
+Import and export type
+ +Import type
+ +You may use import 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 not +insert runtime loads for type references. So why annotate type imports?
+ +The TypeScript compiler can run in 2 modes:
+ +-
+
- In development mode, we typically want quick iteration loops. The compiler
+transpiles to JavaScript without full type information. This is much faster,
+but requires
import type
in certain cases.
+ - In production mode, we want correctness. The compiler type checks everything
+and ensures
import type
is used correctly.
+
Note: If you need to force a runtime load for side effects, use import '...';
.
+See
Export type
+ +Use export 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.
+See
+isolatedModules
docs.
export type
might also seem useful to avoid ever exporting a value symbol for
+an API. However it does not give guarantees, either: downstream code might still
+import an API through a different path. A better way to split & guarantee type
+vs value usages of an API is to actually split the symbols into e.g.
+UserService
and AjaxUserService
. This is less error prone and also better
+communicates intent.
Use modules not namespaces
+ +TypeScript supports two methods to organize code: namespaces and modules,
+but namespaces are disallowed. That
+is, your code must refer to code in other files using imports and exports of
+the form import {foo} from 'bar';
Your code must not use the namespace Foo { ... }
construct. namespace
s
+may only be used when required to interface with external, third party code.
+To semantically namespace your code, use separate files.
Code must not use require
(as in import 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 use +themodule
keyword in the formmodule Foo { ... }
. Don't use that either. +Always use ES6 imports.
Language features
+ +This section delineates which features may or may not be used, and any +additional constraints on their use.
+ +Language features which are not discussed in this style guide may be used with +no recommendations of their usage.
+ + + +Local variable declarations
+ + + +Use const and let
+ +Always use const
or let
to declare variables. Use const
by default, unless
+a variable needs to be reassigned. Never use var
.
const foo = otherValue; // Use if "foo" never changes.
+let bar = someValue; // Use if "bar" is ever assigned into later on.
+
+
+const
and let
are block scoped, like variables in most other languages.
+var
in JavaScript is function scoped, which can cause difficult to understand
+bugs. Don't use it.
var foo = someValue; // Don't use - var scoping is complex and causes bugs.
+
+
+Variables must not be used before their declaration.
+ + + +One variable per declaration
+ +Every local variable declaration declares only one variable: declarations such
+as let a = 1, b = 2;
are not used.
Array literals
+ + + +Do not use the Array
constructor
+
+Do not use the Array()
constructor, with or without new
. It has confusing
+and 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, or from
to
+initialize an Array
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 properties on arrays
+ +Do not define or use non-numeric properties on an array (other than length
).
+Use a Map
(or Object
) instead.
Using spread syntax
+ +Using spread syntax [...foo];
is a convenient shorthand for shallow-copying or
+concatenating 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 spread must match what is being
+created. When creating an array, only spread iterables. Primitives (including
+null
and undefined
) 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 length
+const fooStrings = ['a', 'b', 'c'];
+const ids = {...fooStrings};
+
+
+const foo = shouldUseFoo ? [7] : [];
+const bar = [5, ...foo];
+const fooStrings = ['a', 'b', 'c'];
+const ids = [...fooStrings, 'd', 'e'];
+
+
+
+
+Array destructuring
+ +Array literals may be used on the left-hand side of an assignment to perform
+destructuring (such as when unpacking multiple values from a single array or
+iterable). A final rest
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 []
as
+the default value if a destructured array parameter is optional, and provide
+default 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 allows +naming the individual elements and specifying a different type for each.
+ + + + + +Object literals
+ + + +Do not use the Object
constructor
+
+The Object
constructor is disallowed. Use an object literal ({}
or {a: 0,
+b: 1, c: 2}
) instead.
Iterating objects
+ +Iterating objects with for (... in ...)
is error prone. It will include
+enumerable properties from the prototype chain.
Do not use unfiltered for (... in ...)
statements:
for (const x in someObj) {
+ // x could come from some parent prototype!
+}
+
+
+Either filter values explicitly with an if
statement, or use for (... of
+Object.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
+ +Using spread syntax {...bar}
is a convenient shorthand for creating a shallow
+copy of an object. When using spread syntax in object initialization, later
+values 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 spread must match what is being
+created. That is, when creating an object, only objects may be spread; arrays
+and primitives (including null
and undefined
) must not be spread. Avoid
+spreading 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 length
+const fooStrings = ['a', 'b', 'c'];
+const ids = {...fooStrings};
+
+
+const foo = shouldUseFoo ? {num: 7} : {};
+const bar = {num: 5, ...foo};
+
+
+
+
+Computed property names
+ +Computed property names (e.g. {['key' + foo()]: 42}
) are allowed, and are
+considered dict-style (quoted) keys (i.e., must not be mixed with non-quoted
+keys) unless the computed property is a
+symbol
+(e.g. [Symbol.iterator]
).
Object destructuring
+ +Object destructuring patterns may be used on the left-hand side of an assignment +to perform destructuring and unpack multiple values from a single object.
+ +Destructured objects may also be used as function parameters, but should be kept
+as simple as possible: a single level of unquoted shorthand properties. Deeper
+levels of nesting and computed properties may not be used in parameter
+destructuring. Specify any default values in the left-hand-side of the
+destructured parameter ({str = 'some default'} = {}
, rather than
+{str} = {str: 'some default'}
), and if a
+destructured 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'}) {}
+
+
+
+
+
+
+
+
+Classes
+ +Class declarations
+ +Class declarations must not be terminated with semicolons:
+ +class Foo {
+}
+
+
+class Foo {
+}; // Unnecessary semicolon
+
+
+In contrast, statements that contain class expressions must be terminated with +a 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 class +declaration 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 declarations
+ +Class method declarations must not use a semicolon to separate individual +method 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 blank +line:
+ +class Foo {
+ doThing() {
+ console.log("A");
+ }
+
+ getOtherThing(): number {
+ return 4;
+ }
+}
+
+
+class Foo {
+ doThing() {
+ console.log("A");
+ }
+ getOtherThing(): number {
+ return 4;
+ }
+}
+
+
+
+
+Overriding toString
+ +The toString
method may be overridden, but must always succeed and never have
+visible side effects.
Tip: Beware, in particular, of calling other methods from toString, since +exceptional conditions could lead to infinite loops.
+ + + +Static methods
+ +Avoid private static methods
+ +Where it does not interfere with readability, prefer module-local functions over +private static methods.
+ +Do not rely on dynamic dispatch
+ +Code should not rely on dynamic dispatch of static
+methods. Static methods should only be called on the base class
+itself (which defines it directly). Static methods should not be called on
+variables containing a dynamic instance that may be either the constructor or a
+subclass constructor (and must be defined with @nocollapse
if this is done),
+and must not be called directly on a subclass that doesn’t define the method
+itself.
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 dynamically
+function callFoo(cls: typeof Base) {
+ cls.foo();
+}
+
+// Disallowed: don't call static methods on subclasses that don't define it themselves
+Sub.foo();
-/**
- * POSTs the request to start coffee brewing.
- * @param amountLitres The amount to brew. Must fit the pot size!
- */
-brew(amountLitres: number, logger: Logger) {
- // ...
+// Disallowed: don't access this in static methods.
+class MyClass {
+ static foo() {
+ return this.staticField;
+ }
}
-
-
+MyClass.staticField = 1;
+
-Parameter property comments
+Avoid static this
references
-A
-parameter property
-is a constructor parameter that is prefixed by one of the modifiers private
,
-protected
, public
, or readonly
. A parameter property declares both a
-parameter and an instance property, and implicitly assigns into it. For example,
-constructor(private readonly foo: Foo)
, declares that the constructor takes a
-parameter foo
, but also declares a private readonly property foo
, and
-assigns the parameter into that property before executing the remainder of the
-constructor.
Code must not use this
in a static context.
To document these fields, use JSDoc's @param
annotation. Editors display the
-description on constructor calls and property accesses.
JavaScript allows accessing static fields through this
. Different from other
+languages, static fields are also inherited.
/** 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[]) {}
+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
}
-/** 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;
+Why?
+
+This code is generally surprising: authors might not expect that static fields
+can be accessed through the this pointer, and might be surprised to find that
+they 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.
+
+
+
+
+
+Constructors
+
+Constructor calls must 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 not
+equivalent:
+
+new Foo().Bar();
+new Foo.Bar();
+
+
+It is unnecessary to provide an empty constructor or one that simply delegates
+into its parent class because ES2015 provides a default class constructor if one
+is not specified. However constructors with parameter properties, visibility
+modifiers or parameter decorators should not be omitted even if the body of
+the 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 below
+by 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);
+ }
+}
+
+
+Class members
+
+No #private fields
+
+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 and
+performance regressions when down-leveled by TypeScript, and are unsupported
+before ES2015. They can only be downleveled to ES2015, not lower. At the same
+time, they do not offer substantial benefits when static type checking is used
+to enforce visibility.
+
+
+
+Use readonly
+
+Mark properties that are never reassigned outside of the constructor with the
+readonly
modifier (these need not be deeply immutable).
+
+Parameter properties
+
+Rather than plumbing an obvious initializer through to a class member, use a
+TypeScript
+parameter 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.
+
+Field initializers
+
+If a class member is not a parameter, initialize it where it's declared, which
+sometimes 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 the
+constructor is finished, since it significantly hinders VMs’ ability to optimize
+classes' shape
. Optional fields that may be filled in later should be
+explicitly initialized to undefined
to prevent later shape changes.
+
+Properties used outside of class lexical scope
+
+Properties used from outside the lexical scope of their containing class, such
+as an Angular component's properties used from a template, must not use
+private
visibility, as they are used outside of the lexical scope of their
+containing class.
+
+Use either protected
or public
as appropriate to the property in question.
+Angular and AngularJS template properties should use protected
, but Polymer
+should use public
.
+
+TypeScript code must not use obj['foo']
to bypass the visibility of a
+property.
+
+
+
+Why?
+
+When a property is private
, you are declaring to both automated systems and
+humans that the property accesses are scoped to the methods of the declaring
+class, and they will rely on that. For example, a check for unused code will
+flag a private property that appears to be unused, even if some other file
+manages to bypass the visibility restriction.
+
+Though it might appear that obj['foo']
can bypass visibility in the TypeScript
+compiler, this pattern can be broken by rearranging the build rules, and also
+violates optimization compatibility.
+
+
+
+
+
+Getters and setters
+
+
+
+Getters and setters, also known as accessors, for class members may be used.
+The getter method must be a
+pure function (i.e., result is
+consistent and has no side effects: getters must not change observable state).
+They are also useful as a means of restricting the visibility of internal or
+verbose 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
}
}
-Comments when calling a function
+If an accessor is used to hide a class property, the hidden property may be
+prefixed or suffixed with any whole word, like internal
or wrapped
. When
+using these private properties, access the value through the accessor whenever
+possible. At least one accessor for a property must be non-trivial: do not
+define pass-through
accessors only for the purpose of hiding a property.
+Instead, make the property public (or consider making it readonly
rather than
+just defining a getter with no setter).
-If needed, document parameters at call sites inline using block comments. Also
-consider named parameters using object literals and destructuring. The exact
-formatting and placement of the comment is not prescribed.
+class Foo {
+ private wrappedBar = '';
+ get bar() {
+ return this.wrappedBar || 'bar';
+ }
-// Inline block comments for parameters that'd be hard to understand:
-new Percolator().brew(/* amountLitres= */ 5);
-// Also consider using named arguments and destructuring parameters (in brew's declaration):
-new Percolator().brew({amountLitres: 5});
+ set bar(wrapped: string) {
+ this.wrappedBar = wrapped.trim();
+ }
+}
-/** An ancient {@link CoffeeBrewer} */
-export class Percolator implements CoffeeBrewer {
- /**
- * Brews coffee.
- * @param amountLitres The amount to brew. Must fit the pot size!
- */
- brew(amountLitres: number) {
- // This implementation creates terrible coffee, but whatever.
- // TODO(b/12345): Improve percolator brewing.
+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;
}
}
-Place documentation prior to decorators
+Getters and setters must not be defined using Object.defineProperty
, since
+this interferes with property renaming.
-When a class, method, or property have both decorators like @Component
and
-JsDoc, 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.
+Computed properties
-/** Component that prints "bar". */
-@Component({
- selector: 'foo',
- template: 'bar',
-})
-export class FooComponent {}
-
-
+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 not
+allowed (see
+rationale for not mixing key types. A
+[Symbol.iterator]
method should be defined for any classes that are logically
+iterable. Beyond this, Symbol
should be used sparingly.
-Language Rules
+Tip: be careful of using any other built-in symbols (e.g.
+Symbol.isConcatSpreadable
) as they are not polyfilled by the compiler and will
+therefore not work in older browsers.
-TypeScript language features which are not discussed in this style guide may
-be used with no recommendations of their usage.
+
-Visibility
+Visibility
Restricting visibility of properties, methods, and entire types helps with
keeping code decoupled.
@@ -462,288 +1369,476 @@ Visibility
}
-See also export visibility below.
+See also export visibility.
-Constructors
+Disallowed class patterns
-Constructor calls must use parentheses, even when no arguments are passed:
+
-const x = new Foo;
+Do not manipulate prototype
s directly
+
+The class
keyword allows clearer and more readable class definitions than
+defining prototype
properties. Ordinary implementation code has no business
+manipulating these objects. Mixins and modifying the prototypes of builtin
+objects are explicitly forbidden.
+
+Exception: Framework code (such as Polymer, or Angular) may need to use prototype
s, and should not resort
+to even-worse workarounds to avoid doing so.
+
+
+
+
+
+
+
+Functions
+
+Terminology
+
+There are many different types of functions, with subtle distinctions between
+them. This guide uses the following terminology, which aligns with
+MDN:
+
+
+function declaration
: a declaration (i.e. not an expression) using the
+function
keyword
+function expression
: an expression, typically used in an assignment or
+passed as a parameter, using the function
keyword
+arrow function
: an expression using the =>
syntax
+block 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 for named functions
+
+Prefer function declarations over arrow functions or function expressions when
+defining named functions.
+
+function foo() {
+ return 42;
+}
-const x = new Foo();
+const foo = () => 42;
-It is unnecessary to provide an empty constructor or one that simply delegates
-into its parent class because ES2015 provides a default class constructor if one
-is not specified. However constructors with parameter properties, visibility
-modifiers or parameter decorators should not be omitted even if the body of
-the constructor is empty.
+Arrow functions may be used, for example, when an explicit type annotation is
+required.
-class UnnecessaryConstructor {
- constructor() {}
+interface SearchFunction {
+ (source: string, subString: string): boolean;
}
+
+const fooSearch: SearchFunction = (source, subString) => { ... };
-class UnnecessaryConstructorOverride extends Base {
- constructor(value: number) {
- super(value);
- }
-}
+
+
+
+
+
+
+Nested functions
+
+Functions nested within other methods or functions may use function
+declarations or arrow functions, as appropriate. In method bodies in particular,
+arrow functions are preferred because they have access to the outer this
.
+
+
+
+Do not use function expressions
+
+Do not use function expressions. Use arrow functions instead.
+
+bar(() => { this.doSomething(); })
-class DefaultConstructor {
-}
+bar(function() { ... })
+
-class ParameterProperties {
- constructor(private myService) {}
-}
+Exception: Function expressions may be used only if code has to
+dynamically rebind this
(but this is discouraged), or for
+generator functions (which do not have an arrow syntax).
-class ParameterDecorators {
- constructor(@SideEffectDecorator myService) {}
-}
+
-class NoInstantiation {
- private constructor() {}
+Arrow function bodies
+
+Use arrow functions with concise bodies (i.e. expressions) or block bodies as
+appropriate.
+
+// 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;
}
-Class Members
+Only use a concise body if the return value of the function is actually used.
+The block body makes sure the return type is void
then and prevents potential
+side effects.
-No #private
fields
+// 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;
+
-Do not use private fields (also known as private identifiers):
+// 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 value
+myPromise.then(v => void console.log(v));
+
-class Clazz {
- #ident = 1;
+Tip: The void
operator can be used to ensure an arrow function with an
+expression body returns undefined
when the result is unused.
+
+Rebinding this
+
+Function expressions and function declarations must not use this
unless they
+specifically exist to rebind the this
pointer. Rebinding this
can in most
+cases 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;
-Instead, use TypeScript's visibility annotations:
+// Good: explicitly reference the object from an arrow function.
+document.body.onclick = () => { document.body.textContent = 'hello'; };
+// Alternatively: take an explicit parameter
+const setTextFn = (e: HTMLElement) => { e.textContent = 'hello'; };
+document.body.onclick = setTextFn.bind(null, document.body);
+
-class Clazz {
- private ident = 1;
-}
+Prefer arrow functions over other approaches to binding this
, such as
+f.bind(this)
, goog.bind(f, this)
, or const self = this
.
+
+Prefer passing arrow functions as callbacks
+
+Callbacks can be invoked with unexpected arguments that can pass a type check
+but still result in logical errors.
+
+Avoid passing a named callback to a higher-order function, unless you are sure
+of the stability of both functions' call signatures. Beware, in particular, of
+less-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];
-
-Why?
+Instead, prefer passing an arrow-function that explicitly forwards parameters to
+the named callback.
- Private identifiers cause substantial emit size and
-performance regressions when down-leveled by TypeScript, and are unsupported
-before ES2015. They can only be downleveled to ES2015, not lower. At the same
-time, they do not offer substantial benefits when static type checking is used
-to enforce visibility.
+// GOOD: Arguments are explicitly passed to the callback
+const numbers = ['11', '5', '3'].map((n) => parseInt(n));
+// > [11, 5, 3]
-
+// GOOD: Function is locally defined and is designed to be used as a callback
+function dayFilter(element: string|null|undefined) {
+ return element != null && element.endsWith('day');
+}
-Use readonly
+const days = ['tuesday', undefined, 'juice', 'wednesday'].filter(dayFilter);
+
-Mark properties that are never reassigned outside of the constructor with the
-readonly
modifier (these need not be deeply immutable).
+Arrow functions as properties
-Parameter properties
+Classes usually should not contain properties initialized to arrow functions.
+Arrow function properties require the calling function to understand that the
+callee's this
is already bound, which increases confusion about what this
+is, and call sites and references using such handlers look broken (i.e. require
+non-local knowledge to determine that they are correct). Code should always
+use arrow functions to call instance methods (const handler = (x) => {
+this.listener(x); };
), and should not obtain or pass references to instance
+methods (const handler = this.listener; handler(x);
).
-Rather than plumbing an obvious initializer through to a class member, use a
-TypeScript
-parameter property.
+
+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 Foo {
- private readonly barService: BarService;
+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;
+ }
+}
+
- constructor(barService: BarService) {
- this.barService = barService;
+// 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;
}
}
-class Foo {
- constructor(private readonly barService: BarService) {}
+// Explicitly manage `this` at call time.
+class DelayHandler {
+ constructor() {
+ // Use anonymous functions if possible.
+ setTimeout(() => {
+ this.patienceTracker();
+ }, 5000);
+ }
+ private patienceTracker() {
+ this.waitedPatiently = true;
+ }
}
-If the parameter property needs documentation,
-use an @param
JSDoc tag.
+Event handlers
-Field initializers
+Event handlers may use arrow functions when there is no need to uninstall the
+handler (for example, if the event is emitted by the class itself). If the
+handler requires uninstallation, arrow function properties are the right
+approach, because they automatically capture this
and provide a stable
+reference to uninstall.
-If a class member is not a parameter, initialize it where it's declared, which
-sometimes lets you drop the constructor entirely.
+// 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?');
+ }
+}
+
-class Foo {
- private readonly userList: string[];
- constructor() {
- this.userList = [];
+Do not use bind
in the expression that installs an event handler, because it
+creates 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?');
}
}
-class Foo {
- private readonly userList: string[] = [];
+
+
+Parameter initializers
+
+Optional function parameters may be given a default initializer to use when
+the argument is omitted. Initializers must not have any observable side
+effects. Initializers should be kept as simple as possible.
+
+function process(name: string, extraContext: string[] = []) {}
+function activate(index = 0) {}
+
+
+// BAD: side effect of incrementing the counter
+let globalCounter = 0;
+function newId(index = globalCounter++) {}
+
+// BAD: exposes shared mutable state, which can introduce unintended coupling
+// between function calls
+class Foo {
+ private readonly defaultPaths: string[];
+ frobnicate(paths = defaultPaths) {}
}
-Properties used outside of class lexical scope
+Use default parameters sparingly. Prefer
+destructuring to create readable APIs when
+there are more than a small handful of optional parameters that do not have a
+natural order.
+
+
-Properties used from outside the lexical scope of their containing class, such
-as an Angular component's properties used from a template, must not use
-private
visibility, as they are used outside of the lexical scope of their
-containing class.
+
-Use either protected
or public
as appropriate to the property in question.
-Angular and AngularJS template properties should use protected
, but Polymer
-should use public
.
+Prefer rest and spread when appropriate
-TypeScript code must not use obj['foo']
to bypass the visibility of a
-property. See
-testing and private visibility
-if you want to access protected fields from a test.
+Use a rest parameter instead of accessing arguments
. Never name a local
+variable or parameter arguments
, which confusingly shadows the built-in name.
-
-Why?
+function variadic(array: string[], ...numbers: number[]) {}
+
-When a property is private
, you are declaring to both automated systems and
-humans that the property accesses are scoped to the methods of the declaring
-class, and they will rely on that. For example, a check for unused code will
-flag a private property that appears to be unused, even if some other file
-manages to bypass the visibility restriction.
+
-Though it might appear that obj['foo']
can bypass visibility in the TypeScript
-compiler, this pattern can be broken by rearranging the build rules,
-and also violates optimization compatibility.
-
+Use function spread syntax instead of Function.prototype.apply
.
-Getters and Setters (Accessors)
+
-Getters and setters for class members may be used. The getter method must be
-a pure function (i.e., result is
-consistent and has no side effects). They are also useful as a means of
-restricting the visibility of internal or verbose implementation details (shown
-below).
+Formatting functions
-class Foo {
- constructor(private readonly someService: SomeService) {}
+Blank lines at the start or end of the function body are not allowed.
- get someMember(): string {
- return this.someService.someVariable;
- }
+A single blank line may be used within function bodies sparingly to create
+logical groupings of statements.
- set someMember(newValue: string) {
- this.someService.someVariable = newValue;
- }
-}
-
+Generators should attach the *
to the function
and yield
keywords, as in
+function* foo()
and yield* iter
, rather than function *foo()
or
+yield *iter
.
-If an accessor is used to hide a class property, the hidden property may be
-prefixed or suffixed with any whole word, like internal
or wrapped
. When
-using these private properties, access the value through the accessor whenever
-possible. At least one accessor for a property must be non-trivial: do not
-define pass-through
accessors only for the purpose of hiding a property.
-Instead, make the property public (or consider making it readonly
rather than
-just defining a getter with no setter).
+Parentheses around the left-hand side of a single-argument arrow function are
+recommended but not required.
-class Foo {
- private wrappedBar = '';
- get bar() {
- return this.wrappedBar || 'bar';
- }
+Do not put a space after the ...
in rest or spread syntax.
- set bar(wrapped: string) {
- this.wrappedBar = wrapped.trim();
- }
-}
+function myFunction(...elements: number[]) {}
+myFunction(...array, ...iterable, ...generator());
-class Bar {
- private barInternal = '';
- // Neither of these accessors have logic, so just make bar public.
- get bar() {
- return this.barInternal;
- }
+this
- set bar(value: string) {
- this.barInternal = value;
- }
-}
-
+Only use this
in class constructors and methods, functions that have an
+explicit this
type declared (e.g. function func(this: ThisType, ...)
), or in
+arrow functions defined in a scope where this
may be used.
-Static this references
+Never use this
to refer to the global object, the context of an eval
, the
+target of an event, or unnecessarily call()
ed or apply()
ed functions.
-Code must not use this
in a static context.
+this.alert('Hello');
+
-JavaScript allows accessing static fields through this
. Different from other
-languages, static fields are also inherited.
+Interfaces
-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
-}
-
+Primitive literals
-
-Why?
+
-This code is generally surprising: authors might not expect that static fields
-can be accessed through the this pointer, and might be surprised to find that
-they can be overridden - this feature is not commonly used.
+String literals
-This code also encourages an anti-pattern of having substantial static state,
-which causes problems with testability.
+
-
+Use single quotes
-Primitive Types & Wrapper Classes
+Ordinary string literals are delimited with single quotes ('
), rather than
+double quotes ("
).
-TypeScript code must not instantiate the wrapper classes for the primitive
-types String
, Boolean
, and Number
. Wrapper classes have surprising
-behavior, such as new Boolean(false)
evaluating to true
.
+Tip: if a string contains a single quote character, consider using a template
+string to avoid having to escape the quote.
-const s = new String('hello');
-const b = new Boolean(false);
-const n = new Number(5);
-
+
-const s = 'hello';
-const b = false;
-const n = 5;
-
+No line continuations
-Array constructor
+Do not use line continuations (that is, ending a line inside a string literal
+with a backslash) in either ordinary or template string literals. Even though
+ES5 allows this, it can lead to tricky errors if any trailing whitespace comes
+after the slash, and is less obvious to readers.
-TypeScript code must not use the Array()
constructor, with or without new
.
-It has confusing and contradictory usage:
+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 a = new Array(2); // [undefined, undefined]
-const b = new Array(2, 3); // [2, 3];
+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';
+
+Template literals
-Instead, always use bracket notation to initialize arrays, or from
to
-initialize an Array
with a certain size:
+Use template literals (delimited with `
) over complex string
+concatenation, particularly if multiple string literals are involved. Template
+literals may span multiple lines.
-const a = [2];
-const b = [2, 3];
+If a template literal spans multiple lines, it does not need to follow the
+indentation of the enclosing block, though it may if the added whitespace does
+not matter.
-// Equivalent to Array(2):
-const c = [];
-c.length = 2;
+Example:
-// [0, 0, 0, 0, 0]
-Array.from<number>({length: 5}).fill(0);
+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}`;
+}
-Type coercion
+
+
+Number literals
+
+Numbers may be specified in decimal, hex, octal, or binary. Use exactly 0x
,
+0o
, and 0b
prefixes, with lowercase letters, for hex, octal, and binary,
+respectively. Never include a leading zero unless it is immediately followed by
+x
, o
, or b
.
+
+Type coercion
TypeScript code may use the String()
and Boolean()
(note: no new
!)
functions, string template literals, or !!
to coerce types.
@@ -784,8 +1879,9 @@ Type coercion
enabled = level !== undefined && level !== SupportLevel.NONE;
-
-Why?
+
+
+Why?
For most purposes, it doesn't matter what number or string value an enum name is
mapped to at runtime, because values of enum types are referred to by name in
@@ -795,8 +1891,9 @@
Type coercion
particular, 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 code
that uses an enum value may not even know whether it's the first declared value
-or not.
-
+or not.
+
+
Using string concatenation to cast to string is discouraged, as we check that
operands to the plus operator are of matching types.
@@ -814,6 +1911,8 @@ Type coercion
if (!isFinite(aNumber)) throw new Error(...);
+
+
Code must not use unary plus (+
) to coerce strings to numbers. Parsing
numbers can fail, has surprising corner cases, and can be a code smell (parsing
at the wrong layer). A unary plus is too easy to miss in code reviews given
@@ -848,7 +1947,9 @@
Type coercion
f = Math.floor(f);
-Implicit coercion
+
+
+Implicit coercion
Do not use explicit boolean coercions in conditional clauses that have implicit
boolean coercion. Those are the conditions in an if
, for
and while
@@ -899,205 +2000,22 @@
Implicit coercion
// Explicitly comparing > 0 is OK:
if (arr.length > 0) {...}
-// so is relying on boolean coercion:
-if (arr.length) {...}
-
-
-Variables
-
-Always use const
or let
to declare variables. Use const
by default, unless
-a variable needs to be reassigned. Never use var
.
-
-const foo = otherValue; // Use if "foo" never changes.
-let bar = someValue; // Use if "bar" is ever assigned into later on.
-
-
-const
and let
are block scoped, like variables in most other languages.
-var
in JavaScript is function scoped, which can cause difficult to understand
-bugs. Don't use it.
-
-var foo = someValue; // Don't use - var scoping is complex and causes bugs.
-
-
-Variables must not be used before their declaration.
-
-Exceptions
-
-Instantiate Errors using new
-
-Always use new Error()
when instantiating exceptions, instead of just calling
-Error()
. Both forms create a new Error
instance, but using new
is more
-consistent with how other objects are instantiated.
-
-throw new Error('Foo is not a valid bar.');
-
-
-throw Error('Foo is not a valid bar.');
-
-
-Only throw Errors
-
-JavaScript (and thus TypeScript) allow throwing arbitrary values. However if the
-thrown value is not an Error
, it does not get a stack trace filled in, making
-debugging hard.
-
-// bad: does not get a stack trace.
-throw 'oh noes!';
-
-
-Instead, only throw (subclasses of) Error
:
-
-// Throw only Errors
-throw new Error('oh noes!');
-// ... or subtypes of Error.
-class MyError extends Error {}
-throw new MyError('my oh noes!');
-
-
-Catching & rethrowing
-
-When catching errors, code should assume that all thrown errors are instances
-of 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.
- assert(e, isInstanceOf(Error));
- displayError(e.message);
- // or rethrow:
- throw e;
-}
-
-
-
-
-Exception handlers must not defensively handle non-Error
types unless the
-called API is conclusively known to throw non-Error
s in violation of the above
-rule. In that case, a comment should be included to specifically identify where
-the non-Error
s originate.
-
-try {
- badApiThrowingStrings();
-} catch (e: unknown) {
- // Note: bad API throws strings instead of errors.
- if (typeof e === 'string') { ... }
-}
-
-
-
-Why?
-
-Avoid overly defensive programming. Repeating the same
-defenses against a problem that will not exist in most code leads to
-boiler-plate code that is not useful.
-
-
-Iterating objects
-
-Iterating objects with for (... in ...)
is error prone. It will include
-enumerable properties from the prototype chain.
-
-Do not use unfiltered for (... in ...)
statements:
-
-for (const x in someObj) {
- // x could come from some parent prototype!
-}
-
-
-Either filter values explicitly with an if
statement, or use for (... of
-Object.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
-}
-
-
-Iterating containers
-
-Do not use for (... in ...)
to iterate over arrays. It will counterintuitively
-give the array's indices (as strings!), not values:
-
-for (const x in someArray) {
- // x is the index!
-}
-
-
-Prefer for (... of someArr)
to iterate over arrays,
-go/tsjs-practices/iteration. Array.prototype.forEach
and vanilla for
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.
-}
-
-
-Using the spread operator
-
-Using the spread operator [...foo]; {...bar}
is a convenient shorthand for
-copying arrays and objects. When using the spread operator on objects, later
-values 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;
-
+// so is relying on boolean coercion:
+if (arr.length) {...}
-When using the spread operator, the value being spread must match what is
-being created. That is, when creating an object, only objects may be used with
-the spread operator; when creating an array, only spread iterables. Primitives,
-including null
and undefined
, must not be spread.
-
-const foo = {num: 7};
-const bar = {num: 5, ...(shouldUseFoo && foo)}; // might be undefined
+
-// Creates {0: 'a', 1: 'b', 2: 'c'} but has no length
-const fooStrings = ['a', 'b', 'c'];
-const ids = {...fooStrings};
-
+Control structures
-const foo = shouldUseFoo ? {num: 7} : {};
-const bar = {num: 5, ...foo};
-const fooStrings = ['a', 'b', 'c'];
-const ids = [...fooStrings, 'd', 'e'];
-
+
-Control flow statements & blocks
+Control flow statements and blocks
-Control flow statements always use blocks for the containing code.
+Control flow statements (if
, else
, for
, do
, while
, etc) always use
+braced blocks for the containing code, even if the body contains only a single
+statement. The first statement of a non-empty block must begin on its own line.
for (let i = 0; i < x; i++) {
doSomethingWith(i);
@@ -1114,12 +2032,12 @@ Control flow statements & blocks
-The exception is that if
statements fitting on one line may elide the block.
+Exception: if
statements fitting on one line may elide the block.
if (x) x.doFoo();
-Assignment in control statements
+Assignment in control statements
Prefer to avoid assignment of variables inside control statements. Assignment
can be easily mistaken for equality checks inside control statements.
@@ -1146,325 +2064,279 @@ Assignment in control statements
-Switch Statements
+
-All switch
statements must contain a default
statement group, even if it
-contains no code.
+Iterating containers
-switch (x) {
- case Y:
- doSomethingElse();
- break;
- default:
- // nothing to do.
-}
-
+Prefer for (... of someArr)
to iterate over arrays. Array.prototype.forEach
and vanilla for
+loops are also allowed:
-Non-empty statement groups (case ...
) must not fall through (enforced by the
-compiler):
+for (const x of someArr) {
+ // x is a value of someArr.
+}
-switch (x) {
- case X:
- doSomething();
- // fall through - not allowed!
- case Y:
- // ...
+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.
}
-Empty statement groups are allowed to fall through:
+for
-in
loops may only be used on dict-style objects (see
+below for more info). Do not
+use for (... in ...)
to iterate over arrays as it will counterintuitively give
+the array's indices (as strings!), not values:
-switch (x) {
- case X:
- case Y:
- doSomething();
- break;
- default: // nothing to do.
+for (const x in someArray) {
+ // x is the index!
}
-Equality Checks
+Object.prototype.hasOwnProperty
should be used in for
-in
loops to exclude
+unwanted prototype properties. Prefer for
-of
with Object.keys
,
+Object.values
, or Object.entries
over for
-in
when possible.
-Always use triple equals (===
) and not equals (!==
). The double equality
-operators cause error prone type coercions that are hard to understand and
-slower to implement for JavaScript Virtual Machines. See also the
-JavaScript equality table.
-
-if (foo == 'bar' || baz != bam) {
- // Hard to understand behaviour due to type coercion.
+for (const key in obj) {
+ if (!obj.hasOwnProperty(key)) continue;
+ doWork(key, obj[key]);
}
-
-
-if (foo === 'bar' || baz !== bam) {
- // All good here.
+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);
}
-Exception: Comparisons to the literal null
value may use the ==
and
-!=
operators to cover both null
and undefined
values.
+
-if (foo == null) {
- // Will trigger when foo is null or undefined.
-}
-
+Grouping parentheses
-Keep try blocks focused
+Optional grouping parentheses are omitted only when the author and reviewer
+agree that there is no reasonable chance that the code will be misinterpreted
+without them, nor would they have made the code easier to read. It is not
+reasonable to assume that every reader has the entire operator precedence table
+memorized.
-Limit the amount of code inside a try block, if this can be done without hurting
-readability.
+Do not use unnecessary parentheses around the entire expression following
+delete
, typeof
, void
, return
, throw
, case
, in
, of
, or yield
.
-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 learn
-which method throws exceptions. Some inline calls that do not throw exceptions
-could stay inside because they might not be worth the extra complication of a
-temporary variable.
+Exception handling
-Exception: There may be performance issues if try blocks are inside a loop.
-Widening try blocks to cover a whole loop is ok.
+Exceptions are an important part of the language and should be used whenever
+exceptional cases occur.
-Function Declarations
+Custom exceptions provide a great way to convey additional error information
+from functions. They should be defined and used wherever the native Error
type
+is insufficient.
-Prefer function foo() { ... }
to declare top-level named functions.
+Prefer throwing exceptions over ad-hoc error-handling approaches (such as
+passing an error container reference type, or returning an object with an error
+property).
-Top-level arrow functions may be used, for example to provide an explicit type
-annotation.
+Instantiate errors using new
-interface SearchFunction {
- (source: string, subString: string): boolean;
-}
+Always use new Error()
when instantiating exceptions, instead of just calling
+Error()
. Both forms create a new Error
instance, but using new
is more
+consistent with how other objects are instantiated.
-const fooSearch: SearchFunction = (source, subString) => { ... };
+throw new Error('Foo is not a valid bar.');
-
-Note the difference between function declarations (function foo() {}
)
-discussed here, and function expressions (doSomethingWith(function()
-{});
) discussed below.
-
-
-Function Expressions
+throw Error('Foo is not a valid bar.');
+
-Use arrow functions in expressions
+Only throw errors
-Always use arrow functions instead of pre-ES6 function expressions defined with
-the function
keyword.
+JavaScript (and thus TypeScript) allow throwing or rejecting a Promise with
+arbitrary values. However if the thrown or rejected value is not an Error
, it
+does not populate stack trace information, making debugging hard. This treatment
+extends to Promise
rejection values as Promise.reject(obj)
is equivalent to
+throw obj;
in async functions.
-bar(() => { this.doSomething(); })
+// bad: does not get a stack trace.
+throw 'oh noes!';
+// For promises
+new Promise((resolve, reject) => void reject('oh noes!'));
+Promise.reject();
+Promise.reject('oh noes!');
-bar(function() { ... })
-
+Instead, only throw (subclasses of) Error
:
-Function expressions (defined with the function
keyword) may only be used if
-code has to dynamically rebind the this
pointer, but code should not rebind
-the this
pointer in general. Code in regular functions (as opposed to arrow
-functions and methods) should not access this
.
+// Throw only Errors
+throw new Error('oh noes!');
+// ... or subtypes of Error.
+class MyError extends Error {}
+throw new MyError('my oh noes!');
+// For promises
+new Promise((resolve) => resolve()); // No reject is OK.
+new Promise((resolve, reject) => void reject(new Error('oh noes!')));
+Promise.reject(new Error('oh noes!'));
+
-Expression bodies vs block bodies
+Catching and rethrowing
-Use arrow functions with expressions or blocks as their body as appropriate.
+When catching errors, code should assume that all thrown errors are instances
+of Error
.
-// Top level functions use function declarations.
-function someFunction() {
- // Block arrow function bodies, i.e. bodies with => { }, are fine:
- const receipts = books.map((b: Book) => {
- const receipt = payMoney(b.price);
- recordTransaction(receipt);
- return receipt;
- });
+
- // Expression 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 don't access `this` in them.
- }
+function assertIsError(e: unknown): asserts e is Error {
+ if (!(e instanceof Error)) throw new Error("e is not an Error");
+}
- // Nested arrow functions may be assigned to a const.
- const computeTax = (amount: number) => amount * 0.12;
+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;
}
-Only use an expression body if the return value of the function is actually
-used.
+
-// BAD: use a block ({ ... }) if the return value of the function is not used.
-myPromise.then(v => console.log(v));
-
+Exception handlers must not defensively handle non-Error
types unless the
+called API is conclusively known to throw non-Error
s in violation of the above
+rule. In that case, a comment should be included to specifically identify where
+the non-Error
s originate.
-// 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);
-});
+try {
+ badApiThrowingStrings();
+} catch (e: unknown) {
+ // Note: bad API throws strings instead of errors.
+ if (typeof e === 'string') { ... }
+}
-Rebinding this
+
-Function expressions must not use this
unless they specifically exist to
-rebind the this
pointer. Rebinding this
can in most cases be avoided by
-using arrow functions or explicit parameters.
+Why?
-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;
-
+Avoid
+overly defensive programming.
+Repeating the same defenses against a problem that will not exist in most code
+leads to boiler-plate code that is not useful.
-// Good: explicitly reference the object from an arrow function.
-document.body.onclick = () => { document.body.textContent = 'hello'; };
-// Alternatively: take an explicit parameter
-const setTextFn = (e: HTMLElement) => { e.textContent = 'hello'; };
-document.body.onclick = setTextFn.bind(null, document.body);
-
+
-Arrow functions as properties
+
-Classes usually should not contain properties initialized to arrow functions.
-Arrow function properties require the calling function to understand that the
-callee's this
is already bound, which increases confusion about what this
-is, and call sites and references using such handlers look broken (i.e. require
-non-local knowledge to determine that they are correct). Code should always
-use arrow functions to call instance methods (const handler = (x) => {
-this.listener(x); };
), and should not obtain or pass references to instance
-methods (const handler = this.listener; handler(x);
).
+Empty catch blocks
-
-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.
-
+It is very rarely correct to do nothing in response to a caught exception. When
+it truly is appropriate to take no action whatsoever in a catch block, the
+reason this is justified is explained in a comment.
-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);
+ try {
+ return handleNumericResponse(response);
+ } catch (e: unknown) {
+ // Response is not numeric. Continue to handle as text.
}
- private patienceTracker() {
- this.waitedPatiently = true;
- }
-}
+ return handleTextResponse(response);
-// 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;
+Disallowed:
+
+ try {
+ shouldFail();
+ fail('expected an error');
+ } catch (expected: unknown) {
}
-}
-// Explicitly manage `this` at call time.
-class DelayHandler {
- constructor() {
- // Use anonymous functions if possible.
- setTimeout(() => {
- this.patienceTracker();
- }, 5000);
- }
- private patienceTracker() {
- this.waitedPatiently = true;
- }
+Tip: Unlike in some other languages, patterns like the above simply don’t work
+since this will catch the error thrown by fail
. Use assertThrows()
instead.
+
+
+
+Switch statements
+
+All switch
statements must contain a default
statement group, even if it
+contains no code. The default
statement group must be last.
+
+switch (x) {
+ case Y:
+ doSomethingElse();
+ break;
+ default:
+ // nothing to do.
}
-Event Handlers
-
-Event handlers may use arrow functions when there is no need to uninstall the
-handler (for example, if the event is emitted by the class itself). If the
-handler requires uninstallation, arrow function properties are the right
-approach, because they automatically capture this
and provide a stable
-reference to uninstall.
+Within a switch block, each statement group either terminates abruptly with a
+break
, a return
statement, or by throwing an exception. Non-empty statement
+groups (case ...
) must not fall through (enforced by the compiler):
-// 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?');
- }
+switch (x) {
+ case X:
+ doSomething();
+ // fall through - not allowed!
+ case Y:
+ // ...
}
-Do not use bind
in the expression that installs an event handler, because it
-creates a temporary reference that can't be uninstalled.
+Empty statement groups are allowed to fall through:
-// 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?');
- }
+switch (x) {
+ case X:
+ case Y:
+ doSomething();
+ break;
+ default: // nothing to do.
}
-Automatic Semicolon Insertion
+
-Do not rely on Automatic Semicolon Insertion (ASI). Explicitly terminate all
-statements using a semicolon. This prevents bugs due to incorrect semicolon
-insertions and ensures compatibility with tools with limited ASI support (e.g.
-clang-format).
+
+
+Equality checks
+
+Always use triple equals (===
) and not equals (!==
). The double equality
+operators cause error prone type coercions that are hard to understand and
+slower to implement for JavaScript Virtual Machines. See also the
+JavaScript equality table.
+
+if (foo == 'bar' || baz != bam) {
+ // Hard to understand behaviour due to type coercion.
+}
+
-@ts-ignore
+if (foo === 'bar' || baz !== bam) {
+ // All good here.
+}
+
-Do not use @ts-ignore
nor variants @ts-expect-error
or @ts-nocheck
. They
-superficially seem to be an easy way to fix
a compiler error, but in practice,
-a specific compiler error is often caused by a larger problem that can be fixed
-more directly.
+Exception: Comparisons to the literal null
value may use the ==
and
+!=
operators to cover both null
and undefined
values.
-For example, if you are using @ts-ignore
to suppress a type error, then it's
-hard to predict what types the surrounding code will end up seeing. For many
-type errors, the advice in how to best use any
is useful.
+if (foo == null) {
+ // Will trigger when foo is null or undefined.
+}
+
-Type and Non-nullability Assertions
+Type and non-nullability assertions
Type assertions (x as SomeType
) and non-nullability assertions (y!
) are
unsafe. Both only silence the TypeScript compiler, but do not insert any runtime
@@ -1510,7 +2382,7 @@
Type and Non-nullability Assertions
nullable, but perhaps it is well-known in the context of the code that certain
fields are always provided by the backend. Use your judgement.
-Type Assertions Syntax
+Type assertion syntax
Type assertions must use the as
syntax (as opposed to the angle brackets
syntax). This enforces parentheses around the assertion when accessing a member.
@@ -1523,7 +2395,25 @@ Type Assertions Syntax
const x = (z as Foo).length;
-Type Assertions and Object Literals
+Double assertions
+
+From the
+TypeScript handbook,
+TypeScript only allows type assertions which convert to a more specific or
+less specific version of a type. Adding a type assertion (x as Foo
) which
+does 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 a double assertion. This
+involves casting through unknown
since it is less specific than all types.
+
+// x is a Foo here, because...
+(x as unknown as Foo).fooMethod();
+
+
+Use unknown
(instead of any
or {}
) as the intermediate type.
+
+Type assertions and object literals
Use type annotations (: Foo
) instead of type assertions (as Foo
) to specify
the type of an object literal. This allows detecting refactoring bugs when the
@@ -1565,118 +2455,37 @@
Type Assertions and Object Literals
}
-Member property declarations
-
-Interface and class declarations must use a semicolon to separate individual
-member declarations:
-
-interface Foo {
- memberA: string;
- memberB: number;
-}
-
-
-Interfaces specifically must not use a comma to separate fields, for symmetry
-with class declarations:
+Keep try blocks focused
-interface Foo {
- memberA: string,
- memberB: number,
-}
-
-
-Inline object type declarations must use a comma as a separator:
-
-type SomeTypeAlias = {
- memberA: string,
- memberB: number,
-};
-
-let someProperty: {memberC: string, memberD: number};
-
-
-Optimization compatibility for property access
-
-Code must not mix quoted property access with dotted property access:
-
-// Bad: code must use either non-quoted or quoted access for any property
-// consistently across the entire application:
-console.log(x['someField']);
-console.log(x.someField);
-
-
-Properties that are external to the application, e.g. properties on JSON objects
-or external APIs, must be accessed using .dotted
notation, and must be
-declared as so-called ambient properties, using the declare
modifier.
-
-
-// Good: using "declare" to declare types that are external to the application,
-// so that their properties are not renamed.
-declare interface ServerInfoJson {
- appVersion: string;
- user: UserJson; // Note: UserJson must also use `declare`!
-}
-// serverResponse must be ServerInfoJson as per the application's contract.
-const data = JSON.parse(serverResponse) as ServerInfoJson;
-console.log(data.appVersion); // Type safe & renaming safe!
-
-
-Optimization compatibility for module object imports
-
-When importing a module object, directly access properties on the module object
-rather than passing it around. This ensures that modules can be analyzed and
-optimized. Treating
-module imports as namespaces is fine.
-
-import * as utils from 'utils';
-class A {
- readonly utils = utils; // <--- BAD: passing around the module object
-}
-
+Limit the amount of code inside a try block, if this can be done without hurting
+readability.
-import * as utils from 'utils';
-class A {
- readonly utils = {method1: utils.method1, method2: utils.method2};
+try {
+ const result = methodThatMayThrow();
+ use(result);
+} catch (error: unknown) {
+ // ...
}
-or, more tersely:
-
-import {method1, method2} from 'utils';
-class A {
- readonly utils = {method1, method2};
+let result;
+try {
+ result = methodThatMayThrow();
+} catch (error: unknown) {
+ // ...
}
+use(result);
-Exception
-
-This optimization compatibility rule applies to all web apps. It does not apply
-to code that only runs server side (e.g. in NodeJS for a test runner). It is
-still strongly encouraged to always declare all types and avoid mixing quoted
-and unquoted property access, for code hygiene.
-
-Const Enums
-
-Code must not use const enum
; use plain enum
instead.
-
-
-Why?
-
-TypeScript enums already cannot be mutated; const enum
is a separate language
-feature related to optimization that makes the enum invisible to
-JavaScript users of the module.
-
-
-Debugger statements
-
-Debugger statements must not be included in production code.
+Moving the non-throwable lines out of the try/catch block helps the reader learn
+which method throws exceptions. Some inline calls that do not throw exceptions
+could stay inside because they might not be worth the extra complication of a
+temporary variable.
-function debugMe() {
- debugger;
-}
-
+Exception: There may be performance issues if try blocks are inside a loop.
+Widening try blocks to cover a whole loop is ok.
-Decorators
+Decorators
Decorators are syntax with an @
prefix, like @MyDecorator
.
@@ -1688,13 +2497,17 @@ Decorators
Polymer (e.g. @property
)
-
-Why?
+
-We generally want to avoid decorators, because they were an experimental
-feature that have since diverged from the TC39 proposal and have known bugs that
-won't be fixed.
-
+
+
+Why?
+
+We generally want to avoid decorators, because they were an experimental feature
+that have since diverged from the TC39 proposal and have known bugs that won't
+be fixed.
+
+
When using decorators, the decorator must immediately precede the symbol it
decorates, with no empty lines between:
@@ -1709,386 +2522,372 @@ Decorators
}
-Source Organization
+Disallowed features
-Modules
+
-Import Paths
+Wrapper objects for primitive types
-TypeScript code must use paths to import other TypeScript code. Paths may be
-relative, i.e. starting with .
or ..
,
- or rooted at the base directory, e.g.
-root/path/to/file
.
+TypeScript code must not instantiate the wrapper classes for the primitive
+types String
, Boolean
, and Number
. Wrapper classes have surprising
+behavior, such as new Boolean(false)
evaluating to true
.
-Code should use relative imports (./foo
) rather than absolute imports
-path/to/foo
when referring to files within the same (logical) project as this
-allows to move the project around without introducing changes in these imports.
+const s = new String('hello');
+const b = new Boolean(false);
+const n = new Number(5);
+
-Consider limiting the number of parent steps (../../../
) as those can make
-module and path structures hard to understand.
+The wrappers may be called as functions for coercing (which is preferred over
+using +
or concatenating the empty string) or creating symbols. See
+type coercion for more information.
-import {Symbol1} from 'path/from/root';
-import {Symbol2} from '../parent/file';
-import {Symbol3} from './sibling';
-
+
-Namespaces vs Modules
+Automatic Semicolon Insertion
-TypeScript supports two methods to organize code: namespaces and modules,
-but namespaces are disallowed. That
-is, your code must refer to code in other files using imports and exports of
-the form import {foo} from 'bar';
+Do not rely on Automatic Semicolon Insertion (ASI). Explicitly end all
+statements using a semicolon. This prevents bugs due to incorrect semicolon
+insertions and ensures compatibility with tools with limited ASI support (e.g.
+clang-format).
-Your code must not use the namespace Foo { ... }
construct. namespace
s
-may only be used when required to interface with external, third party code.
-To semantically namespace your code, use separate files.
+Const enums
-Code must not use require
(as in import x = require('...');
) for imports.
-Use ES6 module syntax.
+Code must not use const enum
; use plain enum
instead.
-// Bad: do not use namespaces:
-namespace Rocket {
- function launch() { ... }
-}
+
-// Bad: do not use <reference>
-/// <reference path="..."/>
+Why?
-// Bad: do not use require()
-import x = require('mydep');
-
+TypeScript enums already cannot be mutated; const enum
is a separate language
+feature related to optimization that makes the enum invisible to
+JavaScript users of the module.
-
-NB: TypeScript namespace
s used to be called internal modules and used to use
-the module
keyword in the form module Foo { ... }
. Don't use that either.
-Always use ES6 imports.
-
+
-Exports
+Debugger statements
-Use named exports in all code:
+Debugger statements must not be included in production code.
-// Use named exports:
-export class Foo { ... }
+function debugMe() {
+ debugger;
+}
-Do not use default exports. This ensures that all imports follow a uniform
-pattern.
+
-// Do not use default exports:
-export default class Foo { ... } // BAD!
-
+with
-
-Why?
+Do not use the with
keyword. It makes your code harder to understand and
+has been banned in strict mode since ES5.
-Default exports provide no canonical name, which makes central maintenance
-difficult with relatively little benefit to code owners, including potentially
-decreased readability:
+
-import Foo from './bar'; // Legal.
-import Bar from './bar'; // Also legal.
-
+Dynamic code evaluation
-Named exports have the benefit of erroring when import statements try to import
-something that hasn't been declared. In foo.ts
:
+Do not use eval
or the Function(...string)
constructor (except for code
+loaders). These features are potentially dangerous and simply do not work in
+environments using strict
+Content Security Policies.
-const foo = 'blah';
-export default foo;
-
+
-And in bar.ts
:
+Non-standard features
-import {fizz} from './foo';
-
+Do not use non-standard ECMAScript or Web Platform features.
-Results in error TS2614: Module '"./foo"' has no exported member 'fizz'.
While
-bar.ts
:
+This includes:
-import fizz from './foo';
-
+
+- Old features that have been marked deprecated or removed entirely from
+ECMAScript / the Web Platform (see
+MDN)
+- New ECMAScript features that are not yet standardized
+
+- Avoid using features that are in current TC39 working draft or currently
+in the proposal process
+- Use only ECMAScript features defined in the current ECMA-262
+specification
+
+- Proposed but not-yet-complete web standards:
+
+- WHATWG proposals that have not completed the
+proposal process.
+
+- Non-standard language “extensions” (such as those provided by some external
+transpilers)
+
-Results in fizz === foo
, which is probably unexpected and difficult to debug.
+Projects targeting specific JavaScript runtimes, such as latest-Chrome-only,
+Chrome extensions, Node.JS, Electron, can obviously use those APIs. Use caution
+when considering an API surface that is proprietary and only implemented in some
+browsers; consider whether there is a common library that can abstract this API
+surface away for you.
-Additionally, default exports encourage people to put everything into one big
-object to namespace it all together:
+
-export default class Foo {
- static SOME_CONSTANT = ...
- static someHelpfulFunction() { ... }
- ...
-}
-
+Modifying builtin objects
-With the above pattern, we have file scope, which can be used as a namespace. We
-also have a perhaps needless second scope (the class Foo
) that can be
-ambiguously used as both a type and a value in other files.
+Never modify builtin types, either by adding methods to their constructors or to
+their prototypes. Avoid depending on libraries that do
+this.
-Instead, prefer use of file scope for namespacing, as well as named exports:
+Do not add symbols to the global object unless absolutely necessary (e.g.
+required by a third-party API).
-export const SOME_CONSTANT = ...
-export function someHelpfulFunction()
-export class Foo {
- // only class stuff here
-}
-
+
-
+Naming
-Export visibility
+Identifiers
-TypeScript does not support restricting the visibility for exported symbols.
-Only export symbols that are used outside of the module. Generally minimize the
-exported API surface of modules.
+Identifiers must use only ASCII letters, digits, underscores (for constants
+and structured test method names), and (rarely) the '$' sign.
-Mutable Exports
+Naming style
-Regardless of technical support, mutable exports can create hard to understand
-and debug code, in particular with re-exports across multiple modules. One way
-to paraphrase this style point is that export let
is not allowed.
+TypeScript expresses information in types, so names should not be decorated
+with information that is included in the type. (See also
+Testing Blog
+ for more about what
+not to include.)
+
+Some concrete examples of this rule:
+
+
+- Do not use trailing or leading underscores for private properties or
+methods.
+- Do not use the
opt_
prefix for optional parameters.
+
+- For accessors, see accessor rules
+below.
+
+- Do not mark interfaces specially (
IMyInterface
or
+MyFooInterface
) unless it's idiomatic in its
+environment. When
+introducing an interface for a class, give it a name that expresses why the
+interface exists in the first place (e.g. class TodoItem
and interface
+TodoItemStorage
if the interface expresses the format used for
+storage/serialization in JSON).
+- Suffixing
Observable
s with $
is a common external convention and can
+help resolve confusion regarding observable values vs concrete values.
+Judgement on whether this is a useful convention is left up to individual
+teams, but should be consistent within projects.
+
-
+
-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 */);
-
+Descriptive names
-
+Names must be descriptive and clear to a new reader. Do not use abbreviations
+that are ambiguous or unfamiliar to readers outside your project, and do not
+abbreviate by deleting letters within a word.
-If one needs to support externally accessible and mutable bindings, they
-should instead use explicit getter functions.
+
+- Exception: Variables that are in scope for 10 lines or fewer, including
+arguments that are not part of an exported API, may use short (e.g.
+single letter) variable names.
+
-let foo = 3;
-window.setTimeout(() => {
- foo = 4;
-}, 1000 /* ms */);
-// Use an explicit getter to access the mutable export.
-export function getFoo() { return foo; };
+// 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.
-For the common pattern of conditionally exporting either of two values, first do
-the conditional check, then the export. Make sure that all exports are final
-after the module's body has executed.
-
-function pickApi() {
- if (useOtherApi()) return OtherApi;
- return RegularApi;
-}
-export const SomeApi = pickApi();
+// 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".
-Container Classes
+
-Do not create container classes with static methods or properties for the sake
-of namespacing.
+Camel case
-export class Container {
- static FOO = 1;
- static bar() { return 1; }
-}
-
+
+Treat abbreviations like acronyms in names as whole words, i.e. use
+loadHttpUrl
, not loadHTTPURL
, unless required by a platform name (e.g.
+XMLHttpRequest
).
-Instead, export individual constants and functions:
+Dollar sign
-export const FOO = 1;
-export function bar() { return 1; }
-
+Identifiers should not generally use $
, except when required by naming
+conventions for third party frameworks. See above for more on
+using $
with Observable
values.
-Imports
+
-There are four variants of import statements in ES6 and TypeScript:
+Rules by identifier type
-
+Most identifier names should follow the casing in the table below, based on the
+identifier's type.
-Import type
-Example
-Use for
+Style
+Category
-module
-
-import * as foo from
-'...';
-TypeScript imports
+ UpperCamelCase
+
+class / interface / type / enum / decorator / type
+parameters / component functions in TSX / JSXElement type
+parameter
-destructuring
-
-import {SomeThing} from
-'...';
-TypeScript imports
+ lowerCamelCase
+variable / parameter / function / method / property /
+module alias
-default
+ CONSTANT_CASE
-import SomeThing from
-'...';
-Only for other external code that
-requires them
+global constant values, including enum values. See
+Constants below.
-side-effect
-
-
-import '...';
-
-
-Only to import libraries for
-their side-effects on load (such
-as custom elements)
+#ident
+private identifiers are never used.
-// 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';
-
+Type parameters
-
+Type parameters, like in Array<T>
, may use a single upper case character
+(T
) or UpperCamelCase
.
-Module versus destructuring imports
+
-Both module and destructuring imports have advantages depending on the
-situation.
+Test names
-Despite the *
, a module import is not comparable to a wildcard
import as
-seen in other languages. Instead, module imports give a name to the entire
-module and each symbol reference mentions the module, which can make code more
-readable and gives autocompletion on all symbols in a module. They also require
-less import churn (all symbols are available), fewer name collisions, and allow
-terser names in the module that's imported. Module imports are particularly
-useful when using many different symbols from large APIs.
+Test method names inxUnit-style test frameworks may be structured with _
separators, e.g.
+testX_whenY_doesZ()
.
-Destructuring imports give local names for each imported symbol. They allow
-terser code when using the imported symbol, which is particularly useful for
-very commonly used symbols, such as Jasmine's describe
and it
.
+_
prefix/suffix
-// Bad: overlong import statement of needlessly namespaced names.
-import {TableViewItem, TableViewHeader, TableViewRow, TableViewModel,
- TableViewRenderer} from './tableview';
-let item: TableViewItem = ...;
-
+Identifiers must not use _
as a prefix or suffix.
-// Better: use the module for namespacing.
-import * as tableview from './tableview';
-let item: tableview.Item = ...;
-
+This also means that _
must not be used as an identifier by itself (e.g. to
+indicate a parameter is unused).
-import * as testing from './testing';
+
+Tip: If you only need some of the elements from an array (or TypeScript
+tuple), you can insert extra commas in a destructuring statement to ignore
+in-between elements:
-// All tests will use the same three functions repeatedly.
-// When importing only a few symbols that are used very frequently, also
-// consider importing the symbols directly (see below).
-testing.describe('foo', () => {
- testing.it('bar', () => {
- testing.expect(...);
- testing.expect(...);
- });
-});
+const [a, , b] = [1, 5, 10]; // a <- 1, b <- 10
+
-// Better: give local names for these common functions.
-import {describe, it, expect} from './testing';
+
-describe('foo', () => {
- it('bar', () => {
- expect(...);
- expect(...);
- });
-});
-...
-
+Imports
-Renaming imports
+Module namespace imports are lowerCamelCase
while files are snake_case
,
+which means that imports correctly will not match in casing style, such as
-Code should fix name collisions by using a module import or renaming the
-exports themselves. Code may rename imports (import {SomeThing as
-SomeOtherThing}
) if needed.
+import * as fooBar from './foo_bar';
+
-Three examples where renaming can be helpful:
+Some libraries might commonly use a namespace import prefix that violates this
+naming scheme, but overbearingly common open source use makes the violating
+style more readable. The only libraries that currently fall under this exception
+are:
-
-- If it's necessary to avoid collisions with other imported symbols.
-- If the imported symbol name is generated.
-- If importing symbols whose names are unclear by themselves, renaming can
-improve code clarity. For example, when using RxJS the
from
function might
-be more readable when renamed to observableFrom
.
-
+
-Import & export type
+Constants
-Do not use import type {...}
or export type {...}
.
+Immutable: CONSTANT_CASE
indicates that a value is intended to not be
+changed, and may 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 be
+modified.
-import type {Foo};
-export type {Bar};
-export type {Bar} from './bar';
+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.
-Instead, just use regular imports and exports:
+A constant can also be a static readonly
property of a class.
+
+class Foo {
+ private static readonly MY_SPECIAL_NUMBER = 5;
-import {Foo} from './foo';
-export {Bar} from './bar';
+ bar() {
+ return 2 * Foo.MY_SPECIAL_NUMBER;
+ }
+}
-Note: this does not apply to export
as applied to a type definition, i.e.
-export type Foo = ...;
.
+Global: Only symbols declared on the module level, static fields of module
+level classes, and values of module level enums, may use CONST_CASE
. If a
+value 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 nested
+in a function) then it must use lowerCamelCase
.
-export type Foo = string;
-
+If a value is an arrow function that implements an interface, then it may be
+declared lowerCamelCase
.
-TypeScript tooling automatically distinguishes symbols used as types vs symbols
-used as values and only generates runtime loads for the latter.
+
-
-Why?
+
-TypeScript tooling automatically handles the distinction and does not insert
-runtime loads for type references. This gives a better developer UX: toggling
-back and forth between import type
and import
is bothersome. At the same
-time, import type
gives no guarantees: your code might still have a hard
-dependency on some import through a different transitive path.
+
-If you need to force a runtime load for side effects, use import '...';
. See
-
+Aliases
-export type
might seem useful to avoid ever exporting a value symbol for an
-API. However it does not give guarantees either: downstream code might still
-import an API through a different path. A better way to split & guarantee type
-vs value usages of an API is to actually split the symbols into e.g.
-UserService
and AjaxUserService
. This is less error prone and also better
-communicates intent.
+When creating a local-scope alias of an existing symbol, use the format of the
+existing identifier. The local alias must match the existing naming and format
+of the source. For variables use const
for your local aliases, and for class
+fields use the readonly
attribute.
-
+
+Note: If you're creating an alias just to expose it to a template in your
+framework of choice, remember to also apply the proper
+access modifiers.
+
-Organize By Feature
+const {BrewStateEnum} = SomeType;
+const CAPACITY = 5;
-Organize packages by feature, not by type. For example, an online shop should
-have packages named products
, checkout
, backend
, not views
, models
,
-controllers
.
+class Teapot {
+ readonly BrewStateEnum = BrewStateEnum;
+ readonly CAPACITY = CAPACITY;
+}
+
-Type System
+Type system
-Type Inference
+Type inference
Code may rely on type inference as implemented by the TypeScript compiler for
all type expressions (variables, fields, return types, etc).
@@ -2108,6 +2907,10 @@
Type Inference
const x: Set<string> = new Set();
+Explicitly specifying types may be required to prevent generic type parameters
+from being inferred as unknown
. For example, initializing generic types with
+no values (e.g. empty arrays, objects, Map
s, or Set
s).
+
const x = new Set<string>();
@@ -2124,7 +2927,9 @@ Type Inference
Whether an annotation is required is decided by the code reviewer.
-Return types
+
+
+Return types
Whether to include return type annotations for functions and methods is up to
the code author. Reviewers may ask for annotations to clarify complex return
@@ -2140,11 +2945,15 @@
Return types
that change the return type of the function.
-Null vs Undefined
+
+
+Undefined and null
-TypeScript supports null
and undefined
types. Nullable types can be
+
TypeScript supports undefined
and null
types. Nullable types can be
constructed as a union type (string|null
); similarly with undefined
. There
-is no special syntax for unions of null
and undefined
.
+is no special syntax for unions of undefined
and null
.
+
+
TypeScript code can use either undefined
or null
to denote absence of a
value, there is no general guidance to prefer one over the other. Many
@@ -2152,7 +2961,7 @@
Null vs Undefined
use null
(e.g. Element.getAttribute
), so the appropriate absent value
depends on the context.
-Nullable/undefined type aliases
+Nullable/undefined type aliases
Type aliases must not include |null
or |undefined
in a union type.
Nullable aliases typically indicate that null values are being passed around
@@ -2180,17 +2989,9 @@
Nullable/undefined type aliases
}
-// Best
-type CoffeeResponse = Latte|Americano;
-
-class CoffeeService {
- getLatte(): CoffeeResponse {
- return assert(fetchResponse(), 'Coffee maker is broken, file a ticket');
- };
-}
-
+
-Optionals vs |undefined
type
+Prefer optional over |undefined
In addition, TypeScript supports a special construct for optional parameters and
fields, using ?
:
@@ -2219,17 +3020,14 @@ Optionals vs |undefined
type
}
-Structural Types vs Nominal Types
+
+
+Use structural types
TypeScript's type system is structural, not nominal. That is, a value matches a
type if it has at least all the properties the type requires and the properties'
types match, recursively.
-Use structural typing where appropriate in your code. Outside of test code, use
-interfaces to define structural types, not classes. In test code it can be
-useful to have mock implementations structurally match the code under test
-without introducing an extra interface.
-
When providing a structural-based implementation, explicitly include the type at
the declaration of the symbol (this allows more precise type checking and error
reporting).
@@ -2246,8 +3044,33 @@ Structural Types vs Nominal Types
-
-Why?
+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?
The badFoo
object above relies on type inference. Additional fields could be
added to badFoo
and the type is inferred based on the object itself.
@@ -2294,9 +3117,11 @@ Structural Types vs Nominal Types
-
+
-Interfaces vs Type Aliases
+
+
+Prefer interfaces over type literal aliases
TypeScript supports
type aliases
@@ -2318,8 +3143,9 @@
Interfaces vs Type Aliases
}
-
-Why?
+
+
+Why?
These forms are nearly equivalent, so under the principle of just choosing one
out of two forms to prevent variation, we should choose one. Additionally, there
@@ -2327,37 +3153,49 @@
Interfaces vs Type Aliases
interesting technical reasons to prefer interface.
That page quotes the TypeScript team lead: Honestly, my take is that it should
really just be interfaces for anything that they can model. There is no benefit
-to type aliases when there are so many issues around display/perf.
-
+to type aliases when there are so many issues around display/perf.
+
+
-Array<T>
Type
+Array<T>
Type
For simple types (containing just alphanumeric characters and dot), use the
-syntax sugar for arrays, T[]
, rather than the longer form Array<T>
.
+syntax sugar for arrays, T[]
or readonly T[]
, rather than the longer form
+Array<T>
or ReadonlyArray<T>
.
+
+For multi-dimensional non-readonly
arrays of simple types, use the syntax
+sugar form (T[][]
, T[][][]
, and so on) rather than the longer form.
For anything more complex, use the longer form Array<T>
.
These rules apply at each level of nesting, i.e. a simple T[]
nested in a more
complex type would still be spelled as T[]
, using the syntax sugar.
-This also applies for readonly T[]
vs 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[]>; // Use syntax sugar for nested types.
+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[]>;
-const a: Array<string>; // the syntax sugar is shorter
-const b: ReadonlyArray<string>;
-const c: {n: number, s: string}[]; // the braces/parens make it harder to read
-const d: (string|number)[];
-const e: readonly (string|number)[];
+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[])[];
-Indexable Types / index signatures ({[key: string]: T}
)
+Indexable types / index signatures ({[key: string]: T}
)
In JavaScript, it's common to use an object as an associative array (aka map
,
hash
, or dict
). Such objects can be typed using an
@@ -2390,7 +3228,7 @@
Indexable Types / index signatures ({[k
keys are statically known. See advice on that
below.
-Mapped & Conditional Types
+Mapped and conditional types
TypeScript's
mapped types
@@ -2434,10 +3272,9 @@
Mapped & Conditional Types
Mapped & conditional types may be used, subject to these considerations.
-
-For example, TypeScript's builtin Pick<T, Keys>
type allows creating a new
+For example, TypeScript's builtin Pick<T, Keys>
type allows creating a new
type by subsetting another type T
, but simple interface extension can often be
-easier to understand.
+easier to understand.
interface User {
shoeSize: number;
@@ -2469,20 +3306,23 @@ Mapped & Conditional Types
Using interfaces here makes the grouping of properties explicit, improves IDE
support, allows better optimization, and arguably makes the code easier to
-understand.
-
+understand.
-any
Type
+any
Type
TypeScript's any
type is a super and subtype of all other types, and allows
dereferencing all properties. As such, any
is dangerous - it can mask severe
programming errors, and its use undermines the value of having static types in
the first place.
-
-Consider not to use any
. In circumstances where you
-want to use any
, consider one of:
-
+
+
+
+
+Consider not to use any
. In circumstances where you want to use any
,
+consider one of:
+
+
- Provide a more specific type
@@ -2490,7 +3330,7 @@ any
Type
- Suppress the lint warning and document why
-Providing a more specific type
+Providing a more specific type
Use interfaces , an
inline object type, or a type alias:
@@ -2519,7 +3359,7 @@ Providing a more specific type
}
-Using unknown
over any
+Using unknown
over any
The any
type allows assignment into any other type and dereferencing any
property off it. Often this behaviour is not necessary or desirable, and code
@@ -2536,12 +3376,16 @@
Using unknown
over any
danger.whoops(); // This access is completely unchecked!
-
-To safely use unknown
values, narrow the type using a
-type guard
-
+
+
+To safely use unknown
values, narrow the type using a
+type guard
-Suppressing any
lint warnings
+
+
+
+
+Suppressing any
lint warnings
Sometimes using any
is legitimate, for example in tests to construct a mock
object. In such cases, add a comment that suppresses the lint warning, and
@@ -2557,7 +3401,47 @@
Suppressing any
lint warnings
const component = new MyComponent(mockBookService, /* unused ShoppingCart */ null as any);
-Tuple Types
+
+
+{}
Type
+
+The {}
type, also known as an empty interface type, represents a interface
+with no properties. An empty interface type has no specified properties and
+therefore 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 code should not use {}
for most use cases. {}
represents any
+non-nullish primitive or object type, which is rarely appropriate. Prefer one of
+the following more-descriptive types:
+
+
+unknown
can hold any value, including null
or undefined
, and is
+generally more appropriate for opaque values.
+Record<string, T>
is better for dictionary-like objects, and provides
+better type safety by being explicit about the type T
of contained values
+(which may itself be unknown
).
+object
excludes primitives as well, leaving only non-nullish functions and
+objects, but without any other assumptions about what properties may be
+available.
+
+
+Tuple types
If you are tempted to create a Pair type, instead use a tuple type:
@@ -2597,7 +3481,7 @@ Tuple Types
const {host, port} = splitHostPort(userAddress);
-Wrapper types
+Wrapper types
There are a few types related to JavaScript primitives that should not ever be
used:
@@ -2614,20 +3498,474 @@ Wrapper types
Further, never invoke the wrapper types as constructors (with new
).
-Return type only generics
+Return type only generics
Avoid creating APIs that have return type only generics. When working with
existing APIs that have return type only generics always explicitly specify the
generics.
-Consistency
+
+
+
+
+Toolchain requirements
+
+Google style requires using a number of tools in specific ways, outlined here.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+TypeScript compiler
+
+All TypeScript files must pass type checking using the standard
+ tool chain.
+
+@ts-ignore
+
+Do not use @ts-ignore
nor the variants @ts-expect-error
or @ts-nocheck
.
+
+
+
+Why?
+
+They superficially seem to be an easy way to fix
a compiler error, but in
+practice, a specific compiler error is often caused by a larger problem that can
+be fixed more directly.
+
+For example, if you are using @ts-ignore
to suppress a type error, then it's
+hard to predict what types the surrounding code will end up seeing. For many
+type errors, the advice in how to best use any
is useful.
+
+
+
+
+
+You may use @ts-expect-error
in unit tests, though you generally should not.
+@ts-expect-error
suppresses all errors. It's easy to accidentally over-match
+and suppress more serious errors. Consider one of:
+
+
+- When testing APIs that need to deal with unchecked values at runtime, add
+casts to the expected type or to
any
and add an explanatory comment. This
+limits error suppression to a single expression.
+- Suppress the lint warning and document why, similar to
+suppressing
any
lint warnings.
+
+
+
+
+
+
+Conformance
+
+Google TypeScript includes several conformance frameworks,
+
+tsetse and
+tsec.
+
+
+
+These rules are commonly used to enforce critical restrictions (such as defining
+globals, which could break the codebase) and security patterns (such as using
+eval
or assigning to innerHTML
), or more loosely to improve code quality.
+
+Google-style TypeScript must abide by any applicable global or framework-local
+conformance rules.
+
+
+
+
+
+
+
+Comments and documentation
+
+
+
+JSDoc versus comments
+
+There are two types of comments, JSDoc (/** ... */
) and non-JSDoc ordinary
+comments (// ...
or /* ... */
).
+
+
+- Use
/** JSDoc */
comments for documentation, i.e. comments a user of the
+code should read.
+- Use
// line comments
for implementation comments, i.e. comments that only
+concern the implementation of the code itself.
+
+
+JSDoc comments are understood by tools (such as editors and documentation
+generators), while ordinary comments are only for other humans.
+
+
+
+Multi-line comments
+
+Multi-line comments are indented at the same level as the surrounding code. They
+must 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.
+
+
+
+JSDoc general form
+
+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, it must use the
+multi-line style with /**
and */
on their own lines.
+
+Many tools extract metadata from JSDoc comments to perform code validation and
+optimization. As such, these comments must be well-formed.
+
+Markdown
+
+JSDoc is written in Markdown, though it may include HTML when necessary.
+
+This means that tooling parsing JSDoc will ignore plain text formatting, so if
+you 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
+ */
+
+
+JSDoc tags
+
+Google style allows a subset of JSDoc tags. Most tags must occupy their own line, with the tag at the beginning
+of 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 wrapping
+
+Line-wrapped block tags are indented four spaces. Wrapped description text may
+be lined up with the description on previous lines, but this horizontal
+alignment 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.
+
+Document all top-level exports of modules
+
+Use /** JSDoc */
comments to communicate information to the users of your
+code. Avoid merely restating the property or parameter name. You should also
+document all properties and methods (exported/public or not) whose purpose is
+not 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.
+
+Class comments
+
+JSDoc comments for classes should provide the reader with enough information to
+know how and when to use the class, as well as any additional considerations
+necessary to correctly use the class. Textual descriptions may be omitted on the
+constructor.
+
+
+
+Method and function comments
+
+Method, parameter, and return descriptions may be omitted if they are obvious
+from 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 method
+does. This phrase is not an imperative sentence, but instead is written in the
+third person, as if there is an implied This method ...
before it.
+
+Parameter property comments
+
+A
+parameter property
+is a constructor parameter that is prefixed by one of the modifiers private
,
+protected
, public
, or readonly
. A parameter property declares both a
+parameter and an instance property, and implicitly assigns into it. For example,
+constructor(private readonly foo: Foo)
, declares that the constructor takes a
+parameter foo
, but also declares a private readonly property foo
, and
+assigns the parameter into that property before executing the remainder of the
+constructor.
+
+To document these fields, use JSDoc's @param
annotation. Editors display the
+description 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
+
+JSDoc type annotations are redundant in TypeScript source code. Do not declare
+types in @param
or @return
blocks, do not write @implements
, @enum
,
+@private
, @override
etc. on code that uses the implements
, enum
,
+private
, override
etc. keywords.
+
+
+
+Make comments that actually add information
+
+For non-exported symbols, sometimes the name and type of the function or
+parameter is enough. Code will usually benefit from more documentation than
+just 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 when
+they add information, and may 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) {
+ // ...
+}
+
+
+
+
+
+Comments when calling a function
+
+“Parameter name” comments should be used whenever the method name and parameter
+value do not sufficiently convey the meaning of the parameter.
+
+Before adding these comments, consider refactoring the method to instead accept
+an interface and destructure it to greatly improve call-site
+readability.
+
+Parameter name
comments go before the parameter value, and include the
+parameter name and a =
suffix:
+
+someFunction(obviousParam, /* shouldRender= */ true, /* name= */ 'hello');
+
+
+Existing code may use a legacy parameter name comment style, which places these
+comments ~after~ the parameter value and omits the =
. Continuing to use this
+style within the file for consistency is acceptable.
+
+someFunction(obviousParam, true /* shouldRender */, 'hello' /* name */);
+
+
+Place documentation prior to decorators
+
+When a class, method, or property have both decorators like @Component
and
+JsDoc, 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 {}
+
+
+
+
+
+Policies
+
+
+
+Consistency
For any style question that isn't settled definitively by this specification, do
what the other code in the same file is already doing (be consistent
). If that
doesn't resolve the question, consider emulating the other files in the same
directory.
-Goals
+Brand new files must use Google Style, regardless of the style choices of
+other files in the same package. When adding new code to a file that is not in
+Google Style, reformatting the existing code first is recommended, subject to
+the advice below. If this reformatting is not
+done, then new code should be as consistent as possible with existing code in
+the same file, but must not violate the style guide.
+
+
+
+Reformatting existing code
+
+You will occasionally encounter files in the codebase that are not in proper
+Google Style. These may have come from an acquisition, or may have been written
+before Google Style took a position on some issue, or may be in non-Google Style
+for any other reason.
+
+When updating the style of existing code, follow these guidelines.
+
+
+- It is not required to change all existing code to meet current style
+guidelines. Reformatting existing code is a trade-off between code churn and
+consistency. Style rules evolve over time and these kinds of tweaks to
+maintain compliance would create unnecessary churn. However, if significant
+changes are being made to a file it is expected that the file will be in
+Google Style.
+- Be careful not to allow opportunistic style fixes to muddle the focus of a
+CL. If you find yourself making a lot of style changes that aren’t critical
+to the central focus of a CL, promote those changes to a separate CL.
+
+
+
+
+
+
+
+
+Deprecation
+
+Mark deprecated methods, classes or interfaces with an @deprecated
JSDoc
+annotation. A deprecation comment must include simple, clear directions for
+people to fix their call sites.
+
+
+
+
+
+Generated code: mostly exempt
+
+Source code generated by the build process is not required to be in Google
+Style. However, any generated identifiers that will be referenced from
+hand-written source code must follow the naming requirements. As a special
+exception, such identifiers are allowed to contain underscores, which may help
+to avoid conflicts with hand-written identifiers.
+
+
+
+
+
+Style guide goals
In general, engineers usually know best about what's needed in their code, so if
there are multiple options and the choice is situation dependent, we should let
@@ -2640,20 +3978,7 @@
Goals
Code should avoid patterns that are known to cause problems, especially
for users new to the language.
-Examples:
-
-
-- The
any
type is easy to misuse (is that variable really both a
-number and callable as a function?), so we have recommendations for how
-to use it.
-- TypeScript
namespace
causes trouble for Closure optimization.
-- Periods within filenames make them ugly/confusing to import from
-JavaScript.
-- Static functions in classes optimize confusingly, while often file-level
-functions accomplish the same goal.
-- Users unaware of the
private
keyword will attempt to obfuscate their
-function names with underscores.
-
+
Code across
projects should be consistent across
irrelevant variations.
@@ -2662,9 +3987,6 @@ Goals
should consider choosing one just so we don't divergently evolve for no
reason and avoid pointless debates in code reviews.
-We should usually match JavaScript style as well, because people often write
-both languages together.
-
Examples:
@@ -2682,9 +4004,9 @@ Goals
- We use software to automate changes to code, so code is autoformatted so
it's easy for software to meet whitespace rules.
-- We require a single set of Closure compilation flags, so a given TS
-library can be written assuming a specific set of flags, and users can
-always safely use a shared library.
+- We require a single set of compiler flags, so a given TS library can be
+written assuming a specific set of flags, and users can always safely
+use a shared library.
- Code must import the libraries it uses (
strict deps
) so that a
refactor in a dependency doesn't change the dependencies of its users.
- We ask users to write tests. Without tests we cannot have confidence
@@ -2703,6 +4025,39 @@
Goals
probably worth leaving out.
+
+
+
+
+