From fd8c54856d66df2e2643e048344ae04d2cfdde9a Mon Sep 17 00:00:00 2001 From: vincent Date: Wed, 9 Nov 2022 14:35:00 -0500 Subject: [PATCH] update ts styleguide with latest google version --- tsguide.html | 801 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 536 insertions(+), 265 deletions(-) diff --git a/tsguide.html b/tsguide.html index bf9410d..bda9817 100644 --- a/tsguide.html +++ b/tsguide.html @@ -11,26 +11,34 @@

Google TypeScript Style Guide

-

TypeScript style guide

+

TypeScript Style Guide

-
-This is the external guide that's based on the internal Google version but has been adjusted for the broader audience. There is no automatic deployment process for this version as it's pushed on-demand by volunteers. +

go/tsstyle

-

It contains both rules and best practices. Choose those that work best for your team. +

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

There is no automatic deployment process for this version as it's pushed +on-demand by volunteers.

-

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.

+

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.

Syntax

Identifiers

-

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]+`.

+

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]+`.

@@ -54,8 +62,10 @@ parameters module alias - - + + @@ -64,24 +74,35 @@ module alias
CONSTANT_CASEglobal constant values, including enum valuesCONSTANT_CASE +global constant values, including enum values. See +Constants below.
#ident
-
    -
  • Abbreviations: -Treat abbreviations like acronyms in names as whole words, i.e. use -loadHttpUrl, not loadHTTPURL, unless required by a platform name -(e.g. XMLHttpRequest).

  • -
  • Dollar sign: Identifiers should not generally use $, except when -aligning with naming conventions for third party frameworks. -See below for more on using $ with Observable values.

  • -
  • Type parameters: Type parameters, like in Array<T>, may use a single -upper case character (T) or UpperCamelCase.

  • -
  • Test names: Test method names in Closure testSuites and similar -xUnit-style test frameworks may be structured with _ separators, e.g. -testX_whenY_doesZ().

  • -
  • _ prefix/suffix: Identifiers must not use _ as a prefix or suffix.

    +

    Abbreviations

    -

    This also means that _ must not be used as an identifier by itself (e.g. -to indicate a parameter is unused).

  • -
+

+Treat abbreviations like acronyms in names as whole words, i.e. use +loadHttpUrl, not loadHTTPURL, unless required by a platform name (e.g. +XMLHttpRequest).

+ +

Dollar sign

+ +

Identifiers should not generally use $, except when aligning with naming +conventions for third party frameworks. See below for more on +using $ with Observable values.

+ +

Type parameters

+ +

Type parameters, like in Array<T>, may use a single upper case character +(T) or UpperCamelCase.

+ +

Test names

+ +

Test method names in Closure testSuites and similar xUnit-style test +frameworks may be structured with _ separators, e.g. testX_whenY_doesZ().

+ +

_ prefix/suffix

+ +

Identifiers must not use _ as a prefix or suffix.

+ +

This also means that _ must not be used as an identifier by itself (e.g. to +indicate a parameter is unused).

Tip: If you only need some of the elements from an array (or TypeScript @@ -92,27 +113,30 @@ in-between elements:

-
    -
  • Imports: Module namespace imports are lowerCamelCase while files are -snake_case, which means that imports correctly will not match in casing -style, such as

    +

    Imports

    + +

    Module namespace imports are lowerCamelCase while files are snake_case, +which means that imports correctly will not match in casing style, such as

    import * as fooBar from './foo_bar';
     
    -

    Some libraries might commonly use a namespace import prefix that violates -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:

    +

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

    +
+ +

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',
@@ -133,25 +157,29 @@ not be modified.

}
-

If a value can be instantiated more than once over the lifetime of the -program, or if users mutate it in any way, it must use lowerCamelCase.

+

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.

-

If a value is an arrow function that implements an interface, then it can be -declared lowerCamelCase.

-
  • - -
  • - +

    If a value is an arrow function that implements an interface, then it may be +declared lowerCamelCase.

    Aliases

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

    -
    
    -const {Foo} = SomeType;
    +
    +

    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.

    +
    + +
    const {Foo} = SomeType;
     const CAPACITY = 5;
     
     class Teapot {
    @@ -223,6 +251,27 @@ const units = '\u03bcs'; // Greek letter mu, 's'
     const output = '\ufeff' + content;
     
    +

    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:

    + +
    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.';
    +
    +

    Comments & Documentation

    JSDoc vs comments

    @@ -260,17 +309,11 @@ not immediately obvious from their name, as judged by your reviewer.

    Omit comments that are redundant with TypeScript

    +

    +

    For example, do not declare types in @param or @return blocks, do not write -@implements, @enum, @private etc. on code that uses the implements, -enum, private etc. keywords.

    - -

    Do not use @override

    - -

    Do not use @override in TypeScript source code.

    - -

    @override is not enforced by the compiler, which is surprising and leads to -annotations and implementation going out of sync. Including it purely for -documentation purposes is confusing.

    +@implements, @enum, @private, @override etc. on code that uses the +implements, enum, private, override etc. keywords.

    Make comments that actually add information

    @@ -284,7 +327,7 @@ just variable names though!

    /** @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.

    +they add information, and may otherwise be omitted.

    /**
      * POSTs the request to start coffee brewing.
    @@ -298,10 +341,15 @@ brew(amountLitres: number, logger: Logger) {
     
     

    Parameter property comments

    -

    A parameter property is when a class declares a field and a constructor -parameter in a single declaration, by marking a parameter in the constructor. -E.g. constructor(private readonly foo: Foo), declares that the class has a -foo field.

    +

    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.

    @@ -382,6 +430,9 @@ export class FooComponent {}

    Language Rules

    +

    TypeScript language features which are not discussed in this style guide may +be used with no recommendations of their usage.

    +

    Visibility

    Restricting visibility of properties, methods, and entire types helps with @@ -415,7 +466,7 @@ constructors).

  • Constructors

    -

    Constructor calls must use parentheses, even when no arguments are passed:

    +

    Constructor calls must use parentheses, even when no arguments are passed:

    const x = new Foo;
     
    @@ -425,9 +476,9 @@ constructors).

    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, modifiers or -parameter decorators should not be omitted even if the body of the constructor -is empty.

    +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() {}
    @@ -475,7 +526,7 @@ class NoInstantiation {
     }
     
    -
    +
    Why?

    Private identifiers cause substantial emit size and @@ -495,7 +546,7 @@ to enforce visibility.

    Rather than plumbing an obvious initializer through to a class member, use a TypeScript -parameter property.

    +parameter property.

    class Foo {
       private readonly barService: BarService;
    @@ -535,18 +586,20 @@ sometimes lets you drop the constructor entirely.

    Properties used outside of class lexical scope

    Properties used from outside the lexical scope of their containing class, such -as an AngularJS controller's properties used from a template, must not use +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.

    -

    Prefer public visibility for these properties, however protected visibility -can also be used as needed. For example, Angular and Polymer template properties -should use public, but AngularJS should use protected.

    +

    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 not use obj['foo'] to bypass the visibility of a -property

    +

    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.

    -
    +
    Why?

    When a property is private, you are declaring to both automated systems and @@ -555,15 +608,15 @@ 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 may appear that obj['foo'] can bypass visibility in the TypeScript +

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

    -

    Getters and setters for class members may be used. The getter method must be a -pure function (i.e., result is +

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

    @@ -581,10 +634,10 @@ below).

    }
    -

    If an accessor is used to hide a class property, the hidden property may be +

    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 +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).

    @@ -614,11 +667,44 @@ just defining a getter with no setter).

    }
    +

    Static this references

    + +

    Code must not use this in a static context.

    + +

    JavaScript allows accessing static fields through this. Different from other +languages, static fields are also inherited.

    + +
    class ShoeStore {
    +  static storage: Storage = ...;
    +
    +  static isAvailable(s: Shoe) {
    +    // Bad: do not use `this` in a static method.
    +    return this.storage.has(s.id);
    +  }
    +}
    +
    +class EmptyShoeStore extends ShoeStore {
    +  static storage: Storage = EMPTY_STORE;  // overrides storage from ShoeStore
    +}
    +
    + +
    +Why? + +

    This code is generally surprising: authors might not expect that static 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.

    + +
    +

    Primitive Types & Wrapper Classes

    -

    TypeScript code must not instantiate the wrapper classes for the primitive types -String, Boolean, and Number. Wrapper classes have surprising behaviour, -such as new Boolean(false) evaluating to true.

    +

    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.

    const s = new String('hello');
     const b = new Boolean(false);
    @@ -632,7 +718,7 @@ const n = 5;
     
     

    Array constructor

    -

    TypeScript code must not use the Array() constructor, with or without new. +

    TypeScript code must not use the Array() constructor, with or without new. It has confusing and contradictory usage:

    @@ -659,7 +745,7 @@ Array.from<number>({length: 5}).fill(0);

    Type coercion

    -

    TypeScript code may use the String() and Boolean() (note: no new!) +

    TypeScript code may use the String() and Boolean() (note: no new!) functions, string template literals, or !! to coerce types.

    const bool = Boolean(false);
    @@ -668,29 +754,75 @@ const bool2 = !!str;
     const str2 = `result: ${bool2}`;
     
    +

    Values of enum types (including unions of enum types and other types) must not +be converted to booleans with Boolean() or !!, and must instead be compared +explicitly with comparison operators.

    + +
    enum SupportLevel {
    +  NONE,
    +  BASIC,
    +  ADVANCED,
    +}
    +
    +const level: SupportLevel = ...;
    +let enabled = Boolean(level);
    +
    +const maybeLevel: SupportLevel|undefined = ...;
    +enabled = !!maybeLevel;
    +
    + +
    enum SupportLevel {
    +  NONE,
    +  BASIC,
    +  ADVANCED,
    +}
    +
    +const level: SupportLevel = ...;
    +let enabled = level !== SupportLevel.NONE;
    +
    +const maybeLevel: SupportLevel|undefined = ...;
    +enabled = level !== undefined && level !== SupportLevel.NONE;
    +
    + +
    +Why? + +

    For most purposes, it doesn't matter what number or string value an enum name is +mapped to at runtime, because values of enum types are referred to by name in +source code. Consequently, engineers are accustomed to not thinking about this, +and so situations where it does matter are undesirable because they will be +surprising. Such is the case with conversion of enums to booleans; in +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. +

    +

    Using string concatenation to cast to string is discouraged, as we check that operands to the plus operator are of matching types.

    -

    Code must use Number() to parse numeric values, and must check its return +

    Code must use Number() to parse numeric values, and must check its return for NaN values explicitly, unless failing to parse is impossible from context.

    Note: Number(''), Number(' '), and Number('\t') would return 0 instead of NaN. Number('Infinity') and Number('-Infinity') would return Infinity -and -Infinity respectively. These cases may require special handling.

    +and -Infinity respectively. Additionally, exponential notation such as +Number('1e+309') and Number('-1e+309') can overflow into Infinity. These +cases may require special handling.

    const aNumber = Number('123');
    -if (isNaN(aNumber)) throw new Error(...);  // Handle NaN if the string might not contain a number
    -assertFinite(aNumber, ...);                // Optional: if NaN cannot happen because it was validated before.
    +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 this.

    +

    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 +this.

    const x = +y;
     
    -

    Code must also not use parseInt or parseFloat to parse numbers, except for +

    Code also must not use parseInt or parseFloat to parse numbers, except for non-base-10 strings (see below). Both of those functions ignore trailing characters in the string, which can shadow error conditions (e.g. parsing 12 dwarves as 12).

    @@ -699,8 +831,8 @@ dwarves
    as 12).

    const f = parseFloat(someString); // regardless of passing a radix.
    -

    Code that must parse using a radix must check that its input is a number -before calling into parseInt;

    +

    Code that requires parsing with a radix must check that its input contains +only appropriate digits for that radix before calling into parseInt;

    if (!/^[a-fA-F0-9]+$/.test(someString)) throw new Error(...);
     // Needed to parse hexadecimal.
    @@ -716,6 +848,8 @@ if (isNaN(f)) handleError();
     f = Math.floor(f);
     
    +

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

    @@ -730,7 +864,38 @@ if (foo) {...} while (foo) {...} -

    Code may use explicit comparisons:

    +

    As with explicit conversions, values of enum types (including +unions of enum types and other types) must not be implicitly coerced to +booleans, and must instead be compared explicitly with comparison operators.

    + +
    enum SupportLevel {
    +  NONE,
    +  BASIC,
    +  ADVANCED,
    +}
    +
    +const level: SupportLevel = ...;
    +if (level) {...}
    +
    +const maybeLevel: SupportLevel|undefined = ...;
    +if (level) {...}
    +
    + +
    enum SupportLevel {
    +  NONE,
    +  BASIC,
    +  ADVANCED,
    +}
    +
    +const level: SupportLevel = ...;
    +if (level !== SupportLevel.NONE) {...}
    +
    +const maybeLevel: SupportLevel|undefined = ...;
    +if (level !== undefined && level !== SupportLevel.NONE) {...}
    +
    + +

    Other types of values may be either implicitly coerced to booleans or compared +explicitly with comparison operators:

    // Explicitly comparing > 0 is OK:
     if (arr.length > 0) {...}
    @@ -754,10 +919,12 @@ 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.

    +

    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.

    @@ -768,6 +935,67 @@ consistent with how other objects are instantiated.

    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-Errors in violation of the above +rule. In that case, a comment should be included to specifically identify where +the non-Errors 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 @@ -805,8 +1033,9 @@ give the array's indices (as strings!), not values:

    }
    -

    Use for (... of someArr) or vanilla for loops with indices to iterate over -arrays.

    +

    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.
    @@ -822,38 +1051,6 @@ for (const [i, x] of someArr.entries()) {
     }
     
    -

    Do not use Array.prototype.forEach, Set.prototype.forEach, and -Map.prototype.forEach. They make code harder to debug and defeat some useful -compiler checks (e.g. reachability).

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

    Consider this code:

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

    You can recognize that this code is fine: x isn't null and it doesn't change -before it is accessed. But the compiler cannot know that this .forEach() call -doesn't hang on to the closure that was passed in and call it at some later -point, maybe after x was set to null, so it flags this code as an error. The -equivalent for-of loop is fine.

    - -

    -See the error and non-error in the playground -

    - -

    In practice, variations of this limitation of control flow analysis show up in -more complex codepaths where it is more surprising. -

    -

    Using the spread operator

    Using the spread operator [...foo]; {...bar} is a convenient shorthand for @@ -879,10 +1076,10 @@ foo3.num === 1; -

    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, may never be spread.

    +

    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
    @@ -900,32 +1097,58 @@ const ids = [...fooStrings, 'd', 'e'];
     
     

    Control flow statements & blocks

    -

    Control flow statements spanning multiple lines always use blocks for the -containing code.

    +

    Control flow statements always use blocks for the containing code.

    for (let i = 0; i < x; i++) {
       doSomethingWith(i);
    -  andSomeMore();
     }
    +
     if (x) {
    -  doSomethingWithALongMethodName(x);
    +  doSomethingWithALongMethodNameThatForcesANewLine(x);
     }
     
    if (x)
    -  x.doFoo();
    -for (let i = 0; i < x; i++)
    -  doSomethingWithALongMethodName(i);
    +  doSomethingWithALongMethodNameThatForcesANewLine(x);
    +
    +for (let i = 0; i < x; i++) doSomethingWith(i);
     
    -

    The exception is that if statements fitting on one line may elide the block.

    +

    The exception is that if statements fitting on one line may elide the block.

    if (x) x.doFoo();
     
    -

    Switch Statements

    +

    Assignment in control statements

    -

    All switch statements must contain a default statement group, even if it +

    Prefer to avoid assignment of variables inside control statements. Assignment +can be easily mistaken for equality checks inside control statements.

    + +
    if (x = someFunction()) {
    +  // Assignment easily mistaken with equality check
    +  // ...
    +}
    +
    + +
    x = someFunction();
    +if (x) {
    +  // ...
    +}
    +
    + +

    In cases where assignment inside the control statement is preferred, enclose the +assignment in additional parenthesis to indicate it is intentional.

    + +
    while ((x = someFunction())) {
    +  // Double parenthesis shows assignment is intentional
    +  // ...
    +}
    +
    +
    + +

    Switch Statements

    + +

    All switch statements must contain a default statement group, even if it contains no code.

    switch (x) {
    @@ -937,7 +1160,7 @@ contains no code.

    }
    -

    Non-empty statement groups (case ...) may not fall through (enforced by the +

    Non-empty statement groups (case ...) must not fall through (enforced by the compiler):

    switch (x) {
    @@ -977,45 +1200,50 @@ slower to implement for JavaScript Virtual Machines. See also the
     }
     
    -

    Exception: Comparisons to the literal null value may use the == and != -operators to cover both null and undefined values.

    +

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

    Keep try blocks focused

    + +

    Limit the amount of code inside a try block, if this can be done without hurting +readability.

    + +
    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: There may be performance issues if try blocks are inside a loop. +Widening try blocks to cover a whole loop is ok.

    +

    Function Declarations

    -

    Use function foo() { ... } to declare named functions, including functions in -nested scopes, e.g. within another function.

    +

    Prefer function foo() { ... } to declare top-level named functions.

    -

    Use function declarations instead of assigning a function expression into a -local variable (const x = function() {...};). TypeScript already disallows -rebinding functions, so preventing overwriting a function declaration by using -const is unnecessary.

    - -

    Exception: Use arrow functions assigned to variables instead of function -declarations if the function accesses the outer scope's this.

    - -
    function foo() { ... }
    -
    - -
    // Given the above declaration, this won't compile:
    -foo = () => 3;  // ERROR: Invalid left-hand side of assignment expression.
    -
    -// So declarations like this are unnecessary.
    -const foo = function() { ... }
    -
    - -
    -

    Note the difference between function declarations (function foo() {}) -discussed here, and function expressions (doSomethingWith(function() -{});) discussed below.

    -
    - -

    Top level arrow functions may be used to explicitly declare that a function -implements an interface.

    +

    Top-level arrow functions may be used, for example to provide an explicit type +annotation.

    interface SearchFunction {
       (source: string, subString: string): boolean;
    @@ -1024,6 +1252,12 @@ implements an interface.

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

    Note the difference between function declarations (function foo() {}) +discussed here, and function expressions (doSomethingWith(function() +{});) discussed below.

    +
    +

    Function Expressions

    Use arrow functions in expressions

    @@ -1037,7 +1271,7 @@ the function keyword.

    bar(function() { ... })
     
    -

    Function expressions (defined with the function keyword) may only be used if +

    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.

    @@ -1061,6 +1295,9 @@ function someFunction() { function payMoney(amount: number) { // function declarations are fine, but don't access `this` in them. } + + // Nested arrow functions may be assigned to a const. + const computeTax = (amount: number) => amount * 0.12; }
    @@ -1085,7 +1322,7 @@ const transformed = [1, 2, 3].map(v => {

    Rebinding this

    -

    Function expressions must not use this unless they specifically exist to +

    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.

    @@ -1164,9 +1401,9 @@ class DelayHandler {

    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 must be uninstalled, arrow function properties are the right approach, -because they automatically capture this and provide a stable reference to -uninstall.

    +handler requires uninstallation, arrow function properties are the right +approach, because they automatically capture this and provide a stable +reference to uninstall.

    // Event handlers may be anonymous functions or arrow function properties.
     class Component {
    @@ -1218,9 +1455,10 @@ clang-format).

    @ts-ignore

    -

    Do not use @ts-ignore. It superficially seems 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.

    +

    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.

    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 @@ -1268,20 +1506,21 @@ y!.bar();

    If the reasoning behind a type or non-nullability assertion is obvious, the -comments may not be necessary. For example, generated proto code is always +comments may not be necessary. For example, generated proto code is always 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 assertions must use the as syntax (as opposed to the angle brackets +

    Type assertions must use the as syntax (as opposed to the angle brackets syntax). This enforces parentheses around the assertion when accessing a member.

    const x = (<Foo>z).length;
     const y = <Foo>z.length;
     
    -
    const x = (z as Foo).length;
    +
    // z must be Foo because ...
    +const x = (z as Foo).length;
     

    Type Assertions and Object Literals

    @@ -1328,8 +1567,8 @@ function func(): Foo {

    Member property declarations

    -

    Interface and class declarations must use the ; character to separate -individual member declarations:

    +

    Interface and class declarations must use a semicolon to separate individual +member declarations:

    interface Foo {
       memberA: string;
    @@ -1337,8 +1576,8 @@ individual member declarations:

    }
    -

    Interfaces specifically must not use the , character to separate fields, for -symmetry with class declarations:

    +

    Interfaces specifically must not use a comma to separate fields, for symmetry +with class declarations:

    interface Foo {
       memberA: string,
    @@ -1346,7 +1585,7 @@ symmetry with class declarations:

    }
    -

    Inline object type declarations must use the comma as a separator:

    +

    Inline object type declarations must use a comma as a separator:

    type SomeTypeAlias = {
       memberA: string,
    @@ -1358,7 +1597,7 @@ let someProperty: {memberC: string, memberD: number};
     
     

    Optimization compatibility for property access

    -

    Code must not mix quoted property access with dotted 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:
    @@ -1366,17 +1605,18 @@ console.log(x['someField']);
     console.log(x.someField);
     
    -

    Code must not rely on disabling renaming, but must rather declare all properties -that are external to the application to prevent renaming:

    +

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

    -

    Prefer for code to account for a possible property-renaming optimization, and -declare all properties that are external to the application to prevent renaming:

    - -
    // Good: declaring an interface
    +
    // 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;
    +  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!
     
    @@ -1388,15 +1628,23 @@ rather than passing it around. This ensures that modules can be analyzed and optimized. Treating module imports as namespaces is fine.

    -
    import {method1, method2} from 'utils';
    +
    import * as utils from 'utils';
     class A {
    -  readonly utils = {method1, method2};
    +  readonly utils = utils;  // <--- BAD: passing around the module object
     }
     
    -
    import * as utils from 'utils';
    +
    import * as utils from 'utils';
     class A {
    -  readonly utils = utils;
    +  readonly utils = {method1: utils.method1, method2: utils.method2};
    +}
    +
    + +

    or, more tersely:

    + +
    import {method1, method2} from 'utils';
    +class A {
    +  readonly utils = {method1, method2};
     }
     
    @@ -1407,15 +1655,21 @@ 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.

    -

    Enums

    +

    Const Enums

    -

    Always use enum and not const enum. 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.

    +

    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.

    +

    Debugger statements must not be included in production code.

    function debugMe() {
       debugger;
    @@ -1434,7 +1688,7 @@ frameworks:

  • Polymer (e.g. @property)
  • -
    +
    Why?

    We generally want to avoid decorators, because they were an experimental @@ -1442,7 +1696,7 @@ 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 +

    When using decorators, the decorator must immediately precede the symbol it decorates, with no empty lines between:

    /** JSDoc comments go before decorators */
    @@ -1461,18 +1715,19 @@ class MyComp {
     
     

    Import Paths

    -

    TypeScript code must use paths to import other TypeScript code. Paths may be +

    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.

    Code should use relative imports (./foo) rather than absolute imports -path/to/foo when referring to files within the same (logical) project.

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

    Consider limiting the number of parent steps (../../../) as those can make module and path structures hard to understand.

    -
    import {Symbol1} from 'google3/path/from/root';
    +
    import {Symbol1} from 'path/from/root';
     import {Symbol2} from '../parent/file';
     import {Symbol3} from './sibling';
     
    @@ -1480,16 +1735,15 @@ import {Symbol3} from './sibling';

    Namespaces vs Modules

    TypeScript supports two methods to organize code: namespaces and modules, -but namespaces are disallowed. google3 code must use TypeScript modules (which -are ECMAScript 6 modules). That +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. namespaces may -only be used when required to interface with external, third party code. To -semantically namespace your code, use separate files.

    +

    Your code must not use the namespace Foo { ... } construct. namespaces +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. +

    Code must not use require (as in import x = require('...');) for imports. Use ES6 module syntax.

    // Bad: do not use namespaces:
    @@ -1525,7 +1779,7 @@ pattern.

    export default class Foo { ... } // BAD!
    -
    +
    Why?

    Default exports provide no canonical name, which makes central maintenance @@ -1593,7 +1847,7 @@ exported API surface of modules.

    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.
    @@ -1605,8 +1859,8 @@ window.setTimeout(() => {
     
     
    -

    If one needs to support externally accessible and mutable bindings, they should -instead use explicit getter functions.

    +

    If one needs to support externally accessible and mutable bindings, they +should instead use explicit getter functions.

    let foo = 3;
     window.setTimeout(() => {
    @@ -1783,25 +2037,29 @@ be more readable when renamed to observableFrom.
     
     

    Import & export type

    -

    Do not use import type ... from or export type ... from.

    +

    Do not use import type {...} or export type {...}.

    -

    Note: this does not apply to exporting type definitions, i.e. export type Foo = -...;.

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

    Instead, just use regular imports:

    +

    Instead, just use regular imports and exports:

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

    Note: this does not apply to export as applied to a type definition, i.e. +export type Foo = ...;.

    + +
    export type Foo = string;
    +
    +

    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 @@ -1832,11 +2090,9 @@ have packages named products, checkout, backend<

    Type Inference

    -

    Code may rely on type inference as implemented by the TypeScript compiler for -all type expressions (variables, fields, return types, etc). The google3 -compiler flags reject code that does not have a type annotation and cannot be -inferred, so all code is guaranteed to be typed (but might use the any type -explicitly).

    +

    Code may rely on type inference as implemented by the TypeScript compiler for +all type expressions (variables, fields, return types, etc). +

    const x = 15;  // Type inferred.
     
    @@ -1856,7 +2112,17 @@ const x: Set<string> = new Set();

    For more complex expressions, type annotations can help with readability of the -program. Whether an annotation is required is decided by the code reviewer.

    +program:

    + +
    // Hard to reason about the type of 'value' without an annotation.
    +const value = await rpc.getSomeValue().transform();
    +
    + +
    // Can tell the type of 'value' at a glance.
    +const value: string[] = await rpc.getSomeValue().transform();
    +
    + +

    Whether an annotation is required is decided by the code reviewer.

    Return types

    @@ -1980,7 +2246,7 @@ reporting).

    }
    -
    +
    Why?

    The badFoo object above relies on type inference. Additional fields could be @@ -2033,7 +2299,7 @@ makeSound(horse);

    Interfaces vs Type Aliases

    TypeScript supports -type aliases +type aliases for naming a type expression. This can be used to name primitives, unions, tuples, and any other types.

    @@ -2052,12 +2318,12 @@ alias for the object literal expression.

    }
    -
    +
    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 -also +are also 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 @@ -2071,6 +2337,9 @@ syntax sugar for arrays, T[], rather than the longer form Arr

    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[];
    @@ -2078,19 +2347,22 @@ 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.
     
    -
    const f: Array<string>;            // the syntax sugar is shorter
    -const g: ReadonlyArray<string>;
    -const h: {n: number, s: string}[]; // the braces/parens make it harder to read
    -const i: (string|number)[];
    -const j: readonly (string|number)[];
    +
    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)[];
     
    -

    Indexable ({[key: string]: number}) Type

    +

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

    +hash, or dict). Such objects can be typed using an +index signature +([k: string]: T) in TypeScript:

    const fileSizes: {[fileName: string]: number} = {};
     fileSizes['readme.txt'] = 541;
    @@ -2121,9 +2393,9 @@ keys are statically known. See advice on that
     

    Mapped & Conditional Types

    TypeScript's -mapped types +mapped types and -conditional types +conditional types allow specifying new types based on other types. TypeScript's standard library includes several type operators based on these (Record, Partial, Readonly etc).

    @@ -2162,7 +2434,7 @@ term cost of complex type expressions.
  • Mapped & conditional types may be used, subject to these considerations.
  • -
    +
    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. @@ -2207,7 +2479,7 @@ dereferencing all properties. As such, any is dangerous - it can ma 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:
    @@ -2220,8 +2492,8 @@ want to use any, consider one of:

    Providing a more specific type

    -

    Use interfaces , an inline object type, or -a type alias:

    +

    Use interfaces , an +inline object type, or a type alias:

    // Use declared interfaces to represent server-side JSON.
     declare interface MyUserJson {
    @@ -2264,7 +2536,7 @@ const val: unknown = value;
     danger.whoops();  // This access is completely unchecked!
     
    -
    +
    To safely use unknown values, narrow the type using a type guard
    @@ -2327,7 +2599,7 @@ const {host, port} = splitHostPort(userAddress);

    Wrapper types

    -

    There are a few types related to JavaScript primitives that should never be +

    There are a few types related to JavaScript primitives that should not ever be used:

      @@ -2416,8 +2688,7 @@ 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 -that changes that we make to the language, or google3-wide library -changes, don't break users.
    • +that changes that we make to the language, don't break users.
  • Code reviewers should be focused on improving the quality of the code, not enforcing arbitrary rules.