Google TypeScript 风格指南
本指南基于 Google 内部的 TypeScript 风格指南,但经过稍微调整以删除 Google 内部的部分。Google 的内部环境对 TypeScript 的约束与您在 Google 外部可能遇到的情况不同。这里的建议对于编写打算导入到 Google 的代码的人特别有用,但在您的外部环境中可能不适用。
此版本没有自动部署流程,由志愿者按需推送。
简介
术语说明
本风格指南在使用必须、不得、应该、不应该和可以等短语时,采用 RFC 2119 术语。术语推荐和避免分别对应于应该和不应该。命令式和陈述式语句具有规定性,对应于必须。
指南说明
给出的所有示例都是非规范性的,仅用于说明风格指南的规范性语言。也就是说,虽然这些示例是 Google 风格的,但它们可能没有说明表示代码的唯一时尚方式。示例中使用的可选格式选择不得强制执行为规则。
源文件基础
文件编码:UTF-8
源文件以 UTF-8 编码。
空白字符
除了行终止符序列之外,ASCII 水平空格字符 (0x20) 是源文件中出现的唯一空白字符。这意味着字符串文字中的所有其他空白字符都被转义。
特殊转义序列
对于任何具有特殊转义序列的字符(\'
、\"
、\\
、\b
、\f
、\n
、\r
、\t
、\v
),应使用该序列而不是相应的数字转义(例如 \x0a
、\u000a
或 \u{a}
)。永远不要使用旧的八进制转义。
非 ASCII 字符
对于其余的非 ASCII 字符,请使用实际的 Unicode 字符(例如 ∞
)。对于不可打印字符,可以使用等效的十六进制或 Unicode 转义(例如 \u221e
)以及解释性注释。
// Perfectly clear, even without a comment.
const units = 'μs';
// Use escapes for non-printable characters.
const output = '\ufeff' + content; // byte order mark
// Hard to read and prone to mistakes, even with the comment.
const units = '\u03bcs'; // Greek letter mu, 's'
// The reader has no idea what this is.
const output = '\ufeff' + content;
源文件结构
文件由以下内容组成,按顺序
- 版权信息(如果存在)
- 带有
@fileoverview
的 JSDoc(如果存在) - 导入(如果存在)
- 文件的实现
只有一个空行分隔每个存在的部分。
版权信息
如果文件中需要许可或版权信息,请将其添加到文件顶部的 JSDoc 中。
@fileoverview
JSDoc
一个文件可能有一个顶级的 @fileoverview
JSDoc。如果存在,它可以提供文件内容的描述、其用途或有关其依赖关系的信息。换行不缩进。
示例
/**
* @fileoverview Description of file. Lorem ipsum dolor sit amet, consectetur
* adipiscing elit, sed do eiusmod tempor incididunt.
*/
导入
ES6 和 TypeScript 中有四种导入语句
导入类型 | 示例 | 用于 |
---|---|---|
模块[module_import] | import * as foo from '...'; |
TypeScript 导入 |
命名[destructuring_import] | import {SomeThing} from '...'; |
TypeScript 导入 |
默认 | import SomeThing from '...';
|
仅用于其他需要它们的外部代码 |
副作用 | import '...';
|
仅用于导入库以使其在加载时产生副作用(例如自定义元素) |
// Good: choose between two options as appropriate (see below).
import * as ng from '@angular/core';
import {Foo} from './foo';
// Only when needed: default imports.
import Button from 'Button';
// Sometimes needed to import libraries for their side effects:
import 'jasmine';
import '@polymer/paper-button';
导入路径
TypeScript 代码必须使用路径导入其他 TypeScript 代码。路径可以是相对的,即以 .
或 ..
开头,或者以根目录为根,例如 root/path/to/file
。
代码应该使用相对导入 (./foo
) 而不是绝对导入 path/to/foo
,当引用与同一(逻辑)项目中的文件时,这允许在不更改这些导入的情况下移动项目。
考虑限制父级步骤的数量 (../../../
),因为这些步骤会使模块和路径结构难以理解。
import {Symbol1} from 'path/from/root';
import {Symbol2} from '../parent/file';
import {Symbol3} from './sibling';
命名空间与命名导入
可以使用命名空间导入和命名导入。
对于文件中经常使用的符号或具有清晰名称的符号,例如 Jasmine 的 describe
和 it
,首选命名导入。命名导入可以使用 as
根据需要别名为更清晰的名称。
当使用来自大型 API 的许多不同符号时,首选命名空间导入。命名空间导入,尽管使用了 *
字符,但与在其他语言中看到的“通配符”导入不同。相反,命名空间导入为模块的所有导出提供一个名称,并且来自模块的每个导出的符号都成为模块名称上的一个属性。命名空间导入可以帮助提高具有通用名称(如 Model
或 Controller
)的导出符号的可读性,而无需声明别名。
// Bad: overlong import statement of needlessly namespaced names.
import {Item as TableviewItem, Header as TableviewHeader, Row as TableviewRow,
Model as TableviewModel, Renderer as TableviewRenderer} from './tableview';
let item: TableviewItem|undefined;
// Better: use the module for namespacing.
import * as tableview from './tableview';
let item: tableview.Item|undefined;
import * as testing from './testing';
// Bad: The module name does not improve readability.
testing.describe('foo', () => {
testing.it('bar', () => {
testing.expect(null).toBeNull();
testing.expect(undefined).toBeUndefined();
});
});
// Better: give local names for these common functions.
import {describe, it, expect} from './testing';
describe('foo', () => {
it('bar', () => {
expect(null).toBeNull();
expect(undefined).toBeUndefined();
});
});
特殊情况:Apps JSPB protos
Apps JSPB protos 必须使用命名导入,即使这会导致较长的导入行。
此规则的存在是为了帮助提高构建性能和消除死代码,因为 .proto
文件通常包含许多不需要一起使用的 message
。通过利用解构导入,构建系统可以创建对 Apps JSPB 消息的更细粒度的依赖关系,同时保持基于路径的导入的人体工程学。
// 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) {...}
重命名导入
代码应该通过使用命名空间导入或重命名导出本身来修复名称冲突。如果需要,代码可以重命名导入(import {SomeThing as SomeOtherThing}
)。
重命名可能有帮助的三个示例
- 如果需要避免与其他导入的符号发生冲突。
- 如果导入的符号名称是生成的。
- 如果导入的符号名称本身不清楚,重命名可以提高代码的清晰度。例如,当使用 RxJS 时,将
from
函数重命名为observableFrom
可能会更具可读性。
导出
在所有代码中使用命名导出
// Use named exports:
export class Foo { ... }
不要使用默认导出。这确保所有导入都遵循统一的模式。
// Do not use default exports:
export default class Foo { ... } // BAD!
为什么?
默认导出不提供规范名称,这使得集中维护变得困难,对代码所有者的好处相对较小,包括可能降低可读性
import Foo from './bar'; // Legal.
import Bar from './bar'; // Also legal.
命名导出具有在导入语句尝试导入尚未声明的内容时出错的优点。在 foo.ts
中
const foo = 'blah';
export default foo;
而在 bar.ts
中
import {fizz} from './foo';
导致 error TS2614: Module '"./foo"' has no exported member 'fizz'.
而 bar.ts
import fizz from './foo';
导致 fizz === foo
,这可能是意想不到的并且难以调试。
此外,默认导出鼓励人们将所有内容放入一个大对象中以将它们全部命名空间
export default class Foo {
static SOME_CONSTANT = ...
static someHelpfulFunction() { ... }
...
}
使用上述模式,我们具有文件范围,可以用作命名空间。我们还有一个可能不必要的第二个范围(类 Foo
),可以在其他文件中含糊地用作类型和值。
相反,首选使用文件范围进行命名空间,以及命名导出
export const SOME_CONSTANT = ...
export function someHelpfulFunction()
export class Foo {
// only class stuff here
}
导出可见性
TypeScript 不支持限制导出符号的可见性。仅导出在模块外部使用的符号。通常最小化模块的导出 API 表面。
可变导出
无论技术支持如何,可变导出都可能导致难以理解和调试的代码,尤其是在跨多个模块重新导出时。对此样式点的一种解释是,不允许使用 export let
。
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 */);
如果需要支持外部可访问和可变绑定,它们应该使用显式 getter 函数。
let foo = 3;
window.setTimeout(() => {
foo = 4;
}, 1000 /* ms */);
// Use an explicit getter to access the mutable export.
export function getFoo() { return foo; };
对于有条件地导出两个值之一的常见模式,首先进行条件检查,然后进行导出。确保在模块的主体执行后,所有导出都是最终的。
function pickApi() {
if (useOtherApi()) return OtherApi;
return RegularApi;
}
export const SomeApi = pickApi();
容器类
不要为了命名空间而创建具有静态方法或属性的容器类。
export class Container {
static FOO = 1;
static bar() { return 1; }
}
相反,导出单个常量和函数
export const FOO = 1;
export function bar() { return 1; }
导入和导出类型
导入类型
当您仅将导入的符号用作类型时,可以使用 import type {...}
。对值使用常规导入
import type {Foo} from './foo';
import {Bar} from './foo';
import {type Foo, Bar} from './foo';
为什么?
TypeScript 编译器自动处理区别,并且不会插入类型引用的运行时加载。那么为什么要注释类型导入?
TypeScript 编译器可以在 2 种模式下运行
- 在开发模式下,我们通常需要快速迭代循环。编译器在没有完整类型信息的情况下转换为 JavaScript。这要快得多,但在某些情况下需要
import type
。 - 在生产模式下,我们希望正确性。编译器类型检查所有内容,并确保正确使用
import type
。
注意:如果需要强制运行时加载以产生副作用,请使用 import '...';
。参见
导出类型
重新导出类型时,使用 export type
,例如
export type {AnInterface} from './foo';
为什么?
export type
可用于允许在逐个文件转译中重新导出类型。参见 isolatedModules
文档。
export type
似乎也适用于避免为 API 导出值符号。但是,它也没有提供保证:下游代码可能仍然通过不同的路径导入 API。拆分和保证 API 的类型与值的更好方法实际上是将符号拆分为例如 UserService
和 AjaxUserService
。这不太容易出错,并且也更好地传达了意图。
使用模块而不是命名空间
TypeScript 支持两种组织代码的方法:命名空间和模块,但不允许使用命名空间。也就是说,您的代码必须使用 import {foo} from 'bar';
形式的导入和导出引用其他文件中的代码。
您的代码不得使用 namespace Foo { ... }
结构。namespace
可以仅在需要与外部第三方代码接口时使用。要从语义上对代码进行命名空间,请使用单独的文件。
代码不得使用 require
(如 import x = require('...');
)进行导入。使用 ES6 模块语法。
// 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');
注意:TypeScript
namespace
过去被称为内部模块,并使用module
关键字,形式为module Foo { ... }
。也不要使用它。始终使用 ES6 导入。
语言特性
本节概述了哪些特性可以使用或不能使用,以及对其使用的任何其他约束。
本风格指南中未讨论的语言特性可以使用,对其使用没有任何建议。
局部变量声明
使用 const 和 let
始终使用 const
或 let
来声明变量。默认情况下使用 const
,除非变量需要重新分配。永远不要使用 var
。
const foo = otherValue; // Use if "foo" never changes.
let bar = someValue; // Use if "bar" is ever assigned into later on.
const
和 let
是块作用域的,就像大多数其他语言中的变量一样。JavaScript 中的 var
是函数作用域的,这可能会导致难以理解的错误。不要使用它。
var foo = someValue; // Don't use - var scoping is complex and causes bugs.
变量不得在其声明之前使用。
每个声明一个变量
每个局部变量声明只声明一个变量:不使用诸如 let a = 1, b = 2;
之类的声明。
数组字面量
不要使用 Array
构造函数
不要使用 Array()
构造函数,无论是否使用 new
。它具有令人困惑和矛盾的用法
const a = new Array(2); // [undefined, undefined]
const b = new Array(2, 3); // [2, 3];
相反,始终使用括号表示法初始化数组,或使用 from
初始化具有一定大小的 Array
const a = [2];
const b = [2, 3];
// Equivalent to Array(2):
const c = [];
c.length = 2;
// [0, 0, 0, 0, 0]
Array.from<number>({length: 5}).fill(0);
不要在数组上定义属性
不要在数组上定义或使用非数字属性(length
除外)。改用 Map
(或 Object
)。
使用扩展语法
使用扩展语法 [...foo];
是浅复制或连接可迭代对象的便捷简写形式。
const foo = [
1,
];
const foo2 = [
...foo,
6,
7,
];
const foo3 = [
5,
...foo,
];
foo2[1] === 6;
foo3[1] === 1;
使用扩展语法时,要扩展的值必须与要创建的内容匹配。创建数组时,仅扩展可迭代对象。基元(包括 null
和 undefined
)不得扩展。
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'];
数组解构
数组字面量可以用于赋值语句的左侧,以执行解构(例如,从单个数组或可迭代对象中解包多个值时)。可以包含一个最终的“剩余”元素(...
和变量名之间没有空格)。如果元素未使用,则应省略。
const [a, b, c, ...rest] = generateResults();
let [, b,, d] = someArray;
解构也可以用于函数参数。如果解构的数组参数是可选的,请始终指定 []
作为默认值,并在左侧提供默认值。
function destructured([a = 4, b = 2] = []) { … }
不允许
function badDestructuring([a, b] = [4, 2]) { … }
提示:对于将多个值打包(或解包)到函数的参数或返回值中,尽可能优先使用对象解构而不是数组解构,因为它允许命名单个元素并为每个元素指定不同的类型。
对象字面量
不要使用 Object
构造函数
禁止使用 Object
构造函数。请使用对象字面量({}
或 {a: 0, b: 1, c: 2}
)代替。
迭代对象
使用 for (... in ...)
迭代对象容易出错。它将包含原型链中的可枚举属性。
不要使用未过滤的 for (... in ...)
语句
for (const x in someObj) {
// x could come from some parent prototype!
}
可以使用 if
语句显式过滤值,或者使用 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
}
使用扩展语法
使用展开语法 {...bar}
是创建对象的浅拷贝的便捷方法。在对象初始化中使用展开语法时,后面的值将替换同一键的先前值。
const foo = {
num: 1,
};
const foo2 = {
...foo,
num: 5,
};
const foo3 = {
num: 5,
...foo,
}
foo2.num === 5;
foo3.num === 1;
使用展开语法时,要展开的值必须与正在创建的内容匹配。也就是说,在创建对象时,只能展开对象;数组和基本类型(包括 null
和 undefined
)不得展开。避免展开具有除 Object 原型之外的原型的对象(例如,类定义、类实例、函数),因为该行为不直观(仅浅拷贝可枚举的非原型属性)。
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};
计算属性名
允许使用计算属性名(例如,{['key' + foo()]: 42}
),并且将其视为字典样式的(带引号的)键(即,不得与不带引号的键混合),除非计算属性是 symbol(例如,[Symbol.iterator]
)。
对象解构
对象解构模式可以用于赋值语句的左侧,以执行解构并从单个对象中解包多个值。
解构的对象也可以用作函数参数,但应尽可能保持简单:单层非引号简写属性。更深层次的嵌套和计算属性不能用于参数解构。在解构参数的左侧指定任何默认值({str = 'some default'} = {}
,而不是 {str} = {str: 'some default'}
),如果解构的对象本身是可选的,则其默认值必须为 {}
。
示例
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 = {}) {}
不允许
function nestedTooDeeply({x: {num, str}}: {x: Options}) {}
function nontrivialDefault({num, str}: Options = {num: 42, str: 'default'}) {}
类
类声明
类声明不得以分号结尾
class Foo {
}
class Foo {
}; // Unnecessary 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;
}
}
是否使用空行将类声明的花括号与其他类内容分隔开来,既不鼓励也不反对
// 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 Foo {
doThing() {
console.log("A");
}
}
class Foo {
doThing() {
console.log("A");
}; // <-- unnecessary
}
方法声明应与周围的代码用一个空行隔开
class Foo {
doThing() {
console.log("A");
}
getOtherThing(): number {
return 4;
}
}
class Foo {
doThing() {
console.log("A");
}
getOtherThing(): number {
return 4;
}
}
覆盖 toString
可以覆盖 toString
方法,但必须始终成功且永远不能有可见的副作用。
提示:特别要注意从 toString 调用其他方法,因为异常情况可能导致无限循环。
静态方法
避免使用私有静态方法
在不影响可读性的情况下,优先使用模块局部函数而不是私有静态方法。
不要依赖动态调度
代码不应依赖静态方法的动态调度。静态方法应该仅在基类本身(直接定义它的类)上调用。静态方法不应在包含可能是构造函数或子类构造函数的动态实例的变量上调用(如果这样做,则必须使用 @nocollapse
定义),并且不得直接在未定义该方法的子类上调用。
不允许
// 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();
// Disallowed: don't access this in static methods.
class MyClass {
static foo() {
return this.staticField;
}
}
MyClass.staticField = 1;
避免静态 this
引用
代码不得在静态上下文中使用 this
。
JavaScript 允许通过 this
访问静态字段。与其他语言不同,静态字段也是继承的。
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 指针访问,并且可能会惊讶地发现它们可以被覆盖 - 此功能不常用。
此代码还鼓励使用大量的静态状态的反模式,这会导致可测试性问题。
构造函数
构造函数调用必须使用括号,即使未传递任何参数
const x = new Foo;
const x = new Foo();
省略括号可能导致细微的错误。以下两行并不等价
new Foo().Bar();
new Foo.Bar();
无需提供空构造函数或仅委托给其父类的构造函数,因为如果未指定,ES2015 会提供默认的类构造函数。但是,具有参数属性、可见性修饰符或参数装饰器的构造函数不应省略,即使构造函数的主体为空。
class UnnecessaryConstructor {
constructor() {}
}
class UnnecessaryConstructorOverride extends Base {
constructor(value: number) {
super(value);
}
}
class DefaultConstructor {
}
class ParameterProperties {
constructor(private myService) {}
}
class ParameterDecorators {
constructor(@SideEffectDecorator myService) {}
}
class NoInstantiation {
private constructor() {}
}
构造函数应与周围的代码上下方都用一个空行隔开
class Foo {
myField = 10;
constructor(private readonly ctorParam) {}
doThing() {
console.log(ctorParam.getThing() + myField);
}
}
class Foo {
myField = 10;
constructor(private readonly ctorParam) {}
doThing() {
console.log(ctorParam.getThing() + myField);
}
}
类成员
没有 #private 字段
不要使用私有字段(也称为私有标识符)
class Clazz {
#ident = 1;
}
而是使用 TypeScript 的可见性注解
class Clazz {
private ident = 1;
}
为什么?
当被 TypeScript 降级时,私有标识符会导致大量的 emit 大小和性能回归,并且在 ES2015 之前不受支持。它们只能降级到 ES2015,不能降级到更低版本。同时,当使用静态类型检查来强制可见性时,它们并没有提供实质性的好处。
使用 readonly
使用 readonly
修饰符标记构造函数外部永远不会重新赋值的属性(这些属性不必是深度不可变的)。
参数属性
与其将显式初始化程序传递到类成员,不如使用 TypeScript 的 参数属性。
class Foo {
private readonly barService: BarService;
constructor(barService: BarService) {
this.barService = barService;
}
}
class Foo {
constructor(private readonly barService: BarService) {}
}
如果参数属性需要文档,请使用 @param
JSDoc 标签。
字段初始化器
如果类成员不是参数,请在其声明的地方初始化它,有时可以让您完全删除构造函数。
class Foo {
private readonly userList: string[];
constructor() {
this.userList = [];
}
}
class Foo {
private readonly userList: string[] = [];
}
提示:构造函数完成后,永远不应向实例添加或从中删除属性,因为它会严重阻碍 VM 优化类的“形状”的能力。稍后可能填充的可选字段应显式初始化为 undefined
,以防止稍后的形状更改。
在类词法作用域之外使用的属性
从其包含类的词法作用域之外使用的属性,例如 Angular 组件的从模板使用的属性,不得使用 private
可见性,因为它们在其包含类的词法作用域之外使用。
根据具体属性,使用 protected
或 public
。Angular 和 AngularJS 模板属性应使用 protected
,但 Polymer 应使用 public
。
TypeScript 代码不得使用 obj['foo']
来绕过属性的可见性。
为什么?
当属性为 private
时,您既向自动化系统又向人声明属性访问的范围限定于声明类的 methods,他们将依赖于此。例如,未使用的代码检查将标记一个看似未使用的私有属性,即使某些其他文件设法绕过了可见性限制。
虽然看起来 obj['foo']
可以绕过 TypeScript 编译器中的可见性,但这种模式可以通过重新排列构建规则来打破,并且也违反了 优化兼容性。
Getters 和 Setters
可以使用类成员的 Getters 和 Setters,也称为访问器。getter 方法必须是一个 纯函数(即,结果是一致的并且没有副作用:getters不得更改可观察状态)。它们也可用作限制内部或冗长实现细节的可见性的一种手段(如下所示)。
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
}
}
如果使用访问器来隐藏类属性,则隐藏的属性可以用任何完整单词作为前缀或后缀,例如 internal
或 wrapped
。使用这些私有属性时,尽可能通过访问器访问该值。属性的至少一个访问器必须是非平凡的:不要仅为了隐藏属性而定义“直通”访问器。而是使属性 public (或者考虑将其设为 readonly
而不仅仅是定义一个没有 setter 的 getter)。
class Foo {
private wrappedBar = '';
get bar() {
return this.wrappedBar || 'bar';
}
set bar(wrapped: string) {
this.wrappedBar = wrapped.trim();
}
}
class Bar {
private barInternal = '';
// Neither of these accessors have logic, so just make bar public.
get bar() {
return this.barInternal;
}
set bar(value: string) {
this.barInternal = value;
}
}
不得使用 Object.defineProperty
定义 Getters 和 Setters,因为这会干扰属性重命名。
计算属性
仅当属性是 symbol 时,才可以在类中使用计算属性。不允许使用字典样式的属性(即,带引号或计算的非 symbol 键)(请参阅不混合键类型的理由。应为任何在逻辑上可迭代的类定义一个 [Symbol.iterator]
方法。除此之外,应谨慎使用 Symbol
。
提示:小心使用任何其他内置符号(例如,Symbol.isConcatSpreadable
),因为它们不会被编译器填充,因此无法在旧版本的浏览器中使用。
可见性
限制属性、方法和整个类型的可见性有助于保持代码的解耦。
- 尽可能限制符号的可见性。
- 考虑将私有方法转换为同一文件中但在任何类之外的非导出函数,并将私有属性移动到单独的非导出类中。
- TypeScript 符号默认是 public 的。除非在声明非只读 public 参数属性(在构造函数中)时,否则永远不要使用
public
修饰符。
class Foo {
public bar = new Bar(); // BAD: public modifier not needed
constructor(public readonly baz: Baz) {} // BAD: readonly implies it's a property which defaults to public
}
class Foo {
bar = new Bar(); // GOOD: public modifier not needed
constructor(public baz: Baz) {} // public modifier allowed
}
另请参阅 导出可见性。
不允许的类模式
不要直接操作 prototype
与定义 prototype
属性相比,class
关键字允许更清晰和更可读的类定义。普通的实现代码没有理由操作这些对象。显式禁止 Mixin 和修改内置对象的原型。
例外:框架代码(例如 Polymer 或 Angular)可能需要使用 prototype
,并且不应采取更糟糕的解决方法来避免这样做。
函数
术语
有许多不同类型的函数,它们之间存在细微的区别。本指南使用以下术语,这些术语与 MDN 一致
- “函数声明”:使用
function
关键字的声明(即,不是表达式) - “函数表达式”:使用
function
关键字的表达式,通常用于赋值或作为参数传递 - “箭头函数”:使用
=>
语法的表达式 - “块体”:带有花括号的箭头函数的右侧
- “简洁体”:没有花括号的箭头函数的右侧
本节不包括方法和类/构造函数。
对于命名函数,优先使用函数声明
在定义命名函数时,优先使用函数声明而不是箭头函数或函数表达式。
function foo() {
return 42;
}
const foo = () => 42;
例如,当需要显式类型注解时,可以使用箭头函数。
interface SearchFunction {
(source: string, subString: string): boolean;
}
const fooSearch: SearchFunction = (source, subString) => { ... };
嵌套函数
嵌套在其他方法或函数中的函数可以根据需要使用函数声明或箭头函数。特别是,在方法主体中,首选箭头函数,因为它们可以访问外部 this
。
不要使用函数表达式
不要使用函数表达式。请改用箭头函数。
bar(() => { this.doSomething(); })
bar(function() { ... })
例外:函数表达式可能只在代码需要动态地重新绑定 this
时使用(但这不推荐),或者用于生成器函数(没有箭头函数语法)。
箭头函数体
根据需要,使用带有简洁函数体(即表达式)或块状函数体的箭头函数。
// 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;
}
只有当函数返回值实际被使用时,才使用简洁函数体。块状函数体确保返回类型是 void
,并防止潜在的副作用。
// BAD: use a block body if the return value of the function is not used.
myPromise.then(v => console.log(v));
// BAD: this typechecks, but the return value still leaks.
let f: () => void;
f = () => 1;
// GOOD: return value is unused, use a block body.
myPromise.then(v => {
console.log(v);
});
// GOOD: code may use blocks for readability.
const transformed = [1, 2, 3].map(v => {
const intermediate = someComplicatedExpr(v);
const more = acrossManyLines(intermediate);
return worthWrapping(more);
});
// GOOD: explicit `void` ensures no leaked return value
myPromise.then(v => void console.log(v));
提示:当表达式箭头函数的结果未使用时,可以使用 void
运算符来确保它返回 undefined
。
重新绑定 this
函数表达式和函数声明必须不使用 this
,除非它们明确存在以重新绑定 this
指针。在大多数情况下,可以通过使用箭头函数或显式参数来避免重新绑定 this
。
function clickHandler() {
// Bad: what's `this` in this context?
this.textContent = 'Hello';
}
// Bad: the `this` pointer reference is implicitly set to document.body.
document.body.onclick = clickHandler;
// Good: explicitly reference the object from an arrow function.
document.body.onclick = () => { document.body.textContent = 'hello'; };
// Alternatively: take an explicit parameter
const setTextFn = (e: HTMLElement) => { e.textContent = 'hello'; };
document.body.onclick = setTextFn.bind(null, document.body);
相比其他绑定 this
的方法(例如 f.bind(this)
、goog.bind(f, this)
或 const self = this
),更推荐使用箭头函数。
优先将箭头函数作为回调传递
回调可以被调用时附带意想不到的参数,这些参数可以通过类型检查,但仍然会导致逻辑错误。
避免将命名回调传递给高阶函数,除非您确定这两个函数的调用签名都是稳定的。尤其要注意不常用的可选参数。
// 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];
相反,更喜欢传递一个箭头函数,该函数显式地将参数转发给命名回调。
// 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');
}
const days = ['tuesday', undefined, 'juice', 'wednesday'].filter(dayFilter);
作为属性的箭头函数
类通常不应包含初始化为箭头函数的属性。箭头函数属性要求调用函数理解被调用者的 this
已经被绑定,这会增加关于 this
是什么的困惑,并且使用此类处理程序的调用点和引用看起来会损坏(即,需要非本地知识来确定它们是正确的)。代码应该总是使用箭头函数来调用实例方法(const handler = (x) => { this.listener(x); };
),并且不应该获取或传递对实例方法的引用()。const handler = this.listener; handler(x);
注意:在某些特定情况下,例如在模板中绑定函数时,将箭头函数用作属性非常有用,并且可以创建更易读的代码。使用此规则时请自行判断。另请参阅下面的
事件处理程序
部分。
class DelayHandler {
constructor() {
// Problem: `this` is not preserved in the callback. `this` in the callback
// will not be an instance of DelayHandler.
setTimeout(this.patienceTracker, 5000);
}
private patienceTracker() {
this.waitedPatiently = true;
}
}
// Arrow functions usually should not be properties.
class DelayHandler {
constructor() {
// Bad: this code looks like it forgot to bind `this`.
setTimeout(this.patienceTracker, 5000);
}
private patienceTracker = () => {
this.waitedPatiently = true;
}
}
// Explicitly manage `this` at call time.
class DelayHandler {
constructor() {
// Use anonymous functions if possible.
setTimeout(() => {
this.patienceTracker();
}, 5000);
}
private patienceTracker() {
this.waitedPatiently = true;
}
}
事件处理程序
当不需要卸载处理程序时(例如,如果事件是由类本身发出的),事件处理程序可能使用箭头函数。如果处理程序需要卸载,则箭头函数属性是正确的方法,因为它们会自动捕获 this
并提供稳定的引用来卸载。
// 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?');
}
}
不要在安装事件处理程序的表达式中使用 bind
,因为它会创建一个无法卸载的临时引用。
// 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?');
}
}
参数初始化器
可选的函数参数可能被赋予一个默认初始化器,以便在省略参数时使用。初始化器必须不具有任何可观察的副作用。初始化器应该尽可能简单。
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) {}
}
谨慎使用默认参数。当有少量可选参数且没有自然顺序时,更喜欢使用 解构来创建可读的 API。
适当时,更喜欢 rest 和 spread
使用 rest 参数代替访问 arguments
。永远不要将局部变量或参数命名为 arguments
,这会令人困惑地屏蔽内置名称。
function variadic(array: string[], ...numbers: number[]) {}
使用函数 spread 语法代替 Function.prototype.apply
。
格式化函数
函数体开始或结束处的空行是不允许的。
函数体中可能少量使用单个空行来创建语句的逻辑分组。
生成器应该将 *
附加到 function
和 yield
关键字,如 function* foo()
和 yield* iter
中那样,而不是 或 function *foo()
。yield *iter
建议在单参数箭头函数左侧使用括号,但不是强制性的。
不要在 rest 或 spread 语法中的 ...
后面添加空格。
function myFunction(...elements: number[]) {}
myFunction(...array, ...iterable, ...generator());
this
只在类构造函数和方法、声明了显式 this
类型的函数(例如 function func(this: ThisType, ...)
)或在可能使用 this
的作用域中定义的箭头函数中使用 this
。
永远不要使用 this
来引用全局对象、eval
的上下文、事件的目标,或者不必要地 call()
或 apply()
调用的函数。
this.alert('Hello');
接口
原始字面量
字符串字面量
使用单引号
普通字符串字面量用单引号 ('
) 分隔,而不是双引号 ("
)。
提示:如果字符串包含单引号字符,请考虑使用模板字符串以避免必须转义引号。
没有行继续符
不要在普通字符串或模板字符串字面量中使用行继续符(即,用反斜杠结束字符串字面量中的一行)。即使 ES5 允许这样做,如果斜杠后有任何尾随空格,也可能导致棘手的错误,并且对于读者来说不太明显。
不允许
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.';
而是写成:
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';
模板字面量
使用模板字面量(用 `
分隔)代替复杂的字符串连接,尤其是在涉及多个字符串字面量时。模板字面量可以跨越多行。
如果模板字面量跨越多行,则不需要遵循封闭块的缩进,尽管如果添加的空格无关紧要,则可以这样做。
示例
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}`;
}
数字字面量
数字可以用十进制、十六进制、八进制或二进制指定。对于十六进制、八进制和二进制,分别精确使用 0x
、0o
和 0b
前缀,并使用小写字母。除非紧随其后的是 x
、o
或 b
,否则永远不要包含前导零。
类型强制转换
TypeScript 代码可能使用 String()
和 Boolean()
函数(注意:没有 new
!)、字符串模板字面量或 !!
来强制转换类型。
const bool = Boolean(false);
const str = String(aNumber);
const bool2 = !!str;
const str2 = `result: ${bool2}`;
枚举类型的值(包括枚举类型和其他类型的联合)必须不使用 Boolean()
或 !!
转换为布尔值,而必须使用比较运算符显式比较。
enum SupportLevel {
NONE,
BASIC,
ADVANCED,
}
const level: SupportLevel = ...;
let enabled = Boolean(level);
const maybeLevel: SupportLevel|undefined = ...;
enabled = !!maybeLevel;
enum SupportLevel {
NONE,
BASIC,
ADVANCED,
}
const level: SupportLevel = ...;
let enabled = level !== SupportLevel.NONE;
const maybeLevel: SupportLevel|undefined = ...;
enabled = level !== undefined && level !== SupportLevel.NONE;
为什么?
对于大多数情况,枚举名称在运行时映射到的数字或字符串值并不重要,因为枚举类型的值在源代码中按名称引用。因此,工程师习惯于不考虑这一点,因此确实重要的情况是不希望的,因为它们会令人惊讶。枚举转换为布尔值就是这种情况;特别是,默认情况下,第一个声明的枚举值是 falsy(因为它为 0),而其他值是 truthy,这很可能出乎意料。使用枚举值的代码的读者甚至可能不知道它是否是第一个声明的值。
不鼓励使用字符串连接来强制转换为字符串,因为我们会检查加号运算符的操作数是否具有匹配的类型。
代码必须使用 Number()
解析数值,并且必须显式检查其返回的 NaN
值,除非从上下文中无法解析失败。
注意:Number('')
、Number(' ')
和 Number('\t')
将返回 0
而不是 NaN
。Number('Infinity')
和 Number('-Infinity')
将分别返回 Infinity
和 -Infinity
。此外,诸如 Number('1e+309')
和 Number('-1e+309')
之类的指数表示法可能会溢出到 Infinity
。这些情况可能需要特殊处理。
const aNumber = Number('123');
if (!isFinite(aNumber)) throw new Error(...);
代码必须不使用一元加号 (+
) 将字符串强制转换为数字。解析数字可能会失败,具有令人惊讶的极端情况,并且可能是一种代码异味(在错误的层解析)。鉴于此,一元加号在代码审查中太容易被忽略。
const x = +y;
代码也必须不使用 parseInt
或 parseFloat
来解析数字,除非对于非十进制字符串(见下文)。这两个函数都忽略字符串中的尾随字符,这可能会掩盖错误情况(例如,将 12 dwarves
解析为 12
)。
const n = parseInt(someString, 10); // Error prone,
const f = parseFloat(someString); // regardless of passing a radix.
需要使用基数进行解析的代码必须在调用 parseInt
之前检查其输入是否仅包含该基数的适当数字;
if (!/^[a-fA-F0-9]+$/.test(someString)) throw new Error(...);
// Needed to parse hexadecimal.
// tslint:disable-next-line:ban
const n = parseInt(someString, 16); // Only allowed for radix != 10
使用 Number()
,然后使用 Math.floor
或 Math.trunc
(如果可用)来解析整数数字
let f = Number(someString);
if (isNaN(f)) handleError();
f = Math.floor(f);
隐式强制转换
不要在具有隐式布尔强制转换的条件子句中使用显式布尔强制转换。这些是 if
、for
和 while
语句中的条件。
const foo: MyInterface|null = ...;
if (!!foo) {...}
while (!!foo) {...}
const foo: MyInterface|null = ...;
if (foo) {...}
while (foo) {...}
与显式转换一样,枚举类型的值(包括枚举类型和其他类型的联合)必须不隐式强制转换为布尔值,而必须使用比较运算符显式比较。
enum SupportLevel {
NONE,
BASIC,
ADVANCED,
}
const level: SupportLevel = ...;
if (level) {...}
const maybeLevel: SupportLevel|undefined = ...;
if (level) {...}
enum SupportLevel {
NONE,
BASIC,
ADVANCED,
}
const level: SupportLevel = ...;
if (level !== SupportLevel.NONE) {...}
const maybeLevel: SupportLevel|undefined = ...;
if (level !== undefined && level !== SupportLevel.NONE) {...}
其他类型的值可以隐式强制转换为布尔值,也可以使用比较运算符显式比较
// Explicitly comparing > 0 is OK:
if (arr.length > 0) {...}
// so is relying on boolean coercion:
if (arr.length) {...}
控制结构
控制流语句和块
控制流语句 (if
, else
, for
, do
, while
等) 总是使用带花括号的代码块,即使代码体只包含一个语句。非空代码块的第一个语句必须从单独的一行开始。
for (let i = 0; i < x; i++) {
doSomethingWith(i);
}
if (x) {
doSomethingWithALongMethodNameThatForcesANewLine(x);
}
if (x)
doSomethingWithALongMethodNameThatForcesANewLine(x);
for (let i = 0; i < x; i++) doSomethingWith(i);
例外:适合在一行上的 if
语句可能省略代码块。
if (x) x.doFoo();
控制语句中的赋值
尽量避免在控制语句中赋值变量。在控制语句中,赋值很容易被误认为相等性检查。
if (x = someFunction()) {
// Assignment easily mistaken with equality check
// ...
}
x = someFunction();
if (x) {
// ...
}
如果希望在控制语句内进行赋值,请将赋值放在额外的括号中,以表明这是有意的。
while ((x = someFunction())) {
// Double parenthesis shows assignment is intentional
// ...
}
迭代容器
优先使用 for (... of someArr)
迭代数组。也允许使用 Array.prototype.forEach
和原始的 for
循环。
for (const x of someArr) {
// x is a value of someArr.
}
for (let i = 0; i < someArr.length; i++) {
// Explicitly count if the index is needed, otherwise use the for/of form.
const x = someArr[i];
// ...
}
for (const [i, x] of someArr.entries()) {
// Alternative version of the above.
}
for
-in
循环只能在字典风格的对象上使用(有关更多信息,请参见下面)。不要使用 for (... in ...)
迭代数组,因为它会适得其反地给出数组的索引(作为字符串!),而不是值
for (const x in someArray) {
// x is the index!
}
Object.prototype.hasOwnProperty
应该在 for
-in
循环中使用,以排除不需要的原型属性。如果可能,优先使用带有 Object.keys
、Object.values
或 Object.entries
的 for
-of
代替 for
-in
。
for (const key in obj) {
if (!obj.hasOwnProperty(key)) continue;
doWork(key, obj[key]);
}
for (const key of Object.keys(obj)) {
doWork(key, obj[key]);
}
for (const value of Object.values(obj)) {
doWorkValOnly(value);
}
for (const [key, value] of Object.entries(obj)) {
doWork(key, value);
}
分组括号
仅当作者和审查者都同意没有合理的可能性在没有括号的情况下代码会被误解,或者括号使代码更易于阅读时,才省略可选的分组括号。假设每个读者都记住了整个运算符优先级表是不合理的。
不要在 delete
、typeof
、void
、return
、throw
、case
、in
、of
或 yield
之后的整个表达式周围使用不必要的括号。
异常处理
异常是语言的重要组成部分,应在出现异常情况时使用。
自定义异常提供了一种从函数传递额外错误信息的绝佳方式。如果原生的 Error
类型不足以满足需求,则应定义和使用自定义异常。
优先选择抛出异常,而不是使用临时的错误处理方法(例如,传递错误容器引用类型或返回带有错误属性的对象)。
使用 new
实例化错误
实例化异常时,始终使用 new Error()
,而不是仅调用 Error()
。两种形式都会创建一个新的 Error
实例,但使用 new
与实例化其他对象的方式更加一致。
throw new Error('Foo is not a valid bar.');
throw Error('Foo is not a valid bar.');
只抛出错误
JavaScript(以及 TypeScript)允许抛出或拒绝包含任意值的 Promise。但是,如果抛出或拒绝的值不是 Error
,它不会填充堆栈跟踪信息,从而使调试变得困难。这种处理方式也适用于 Promise
拒绝值,因为在 async 函数中,Promise.reject(obj)
等同于 throw obj;
。
// 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!');
相反,只抛出(Error
的子类)Error
// 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!'));
捕获和重新抛出
捕获错误时,代码应该假定所有抛出的错误都是 Error
的实例。
function assertIsError(e: unknown): asserts e is Error {
if (!(e instanceof Error)) throw new Error("e is not an Error");
}
try {
doSomething();
} catch (e: unknown) {
// All thrown errors must be Error subtypes. Do not handle
// other possible values unless you know they are thrown.
assertIsError(e);
displayError(e.message);
// or rethrow:
throw e;
}
异常处理程序不得防御性地处理非 Error
类型,除非明确知道调用的 API 会抛出违反上述规则的非 Error
。在这种情况下,应包含注释以明确标识非 Error
的来源。
try {
badApiThrowingStrings();
} catch (e: unknown) {
// Note: bad API throws strings instead of errors.
if (typeof e === 'string') { ... }
}
为什么?
避免过度防御性编程。对大多数代码中不会存在的问题重复相同的防御会导致样板代码,而这些代码并没有用处。
空的 catch 块
对捕获的异常不采取任何操作的情况很少见。如果确实适合在 catch 块中不采取任何操作,则应在注释中解释这样做的理由。
try {
return handleNumericResponse(response);
} catch (e: unknown) {
// Response is not numeric. Continue to handle as text.
}
return handleTextResponse(response);
不允许
try {
shouldFail();
fail('expected an error');
} catch (expected: unknown) {
}
提示:与其他一些语言不同,像上面的模式根本不起作用,因为它会捕获 fail
抛出的错误。请改用 assertThrows()
。
Switch 语句
所有 switch
语句必须包含一个 default
语句组,即使它不包含任何代码。default
语句组必须是最后一个。
switch (x) {
case Y:
doSomethingElse();
break;
default:
// nothing to do.
}
在 switch 块中,每个语句组要么以 break
、return
语句或抛出异常来突然终止。非空语句组 (case ...
) 不得穿透(由编译器强制执行)
switch (x) {
case X:
doSomething();
// fall through - not allowed!
case Y:
// ...
}
允许空语句组穿透
switch (x) {
case X:
case Y:
doSomething();
break;
default: // nothing to do.
}
相等性检查
始终使用三等号 (===
) 和不等号 (!==
)。双等号运算符会导致容易出错的类型强制转换,这些转换难以理解,并且 JavaScript 虚拟机实现起来更慢。另请参见 JavaScript 相等性表。
if (foo == 'bar' || baz != bam) {
// Hard to understand behaviour due to type coercion.
}
if (foo === 'bar' || baz !== bam) {
// All good here.
}
例外:与字面量 null
值的比较可以使用 ==
和 !=
运算符来覆盖 null
和 undefined
值。
if (foo == null) {
// Will trigger when foo is null or undefined.
}
类型和非空断言
类型断言 (x as SomeType
) 和非空断言 (y!
) 是不安全的。两者都只会使 TypeScript 编译器静音,但不会插入任何运行时检查来匹配这些断言,因此它们可能导致程序在运行时崩溃。
因此,如果没有明显或明确的理由,你不应该使用类型和非空断言。
取代以下代码:
(x as Foo).foo();
y!.bar();
当你想断言类型或非空性时,最好的答案是显式编写一个执行该检查的运行时检查。
// assuming Foo is a class.
if (x instanceof Foo) {
x.foo();
}
if (y) {
y.bar();
}
有时,由于代码的某些局部属性,你可以确定断言形式是安全的。在这种情况下,你应该添加说明来解释为什么你可以接受不安全的行为
// x is a Foo, because ...
(x as Foo).foo();
// y cannot be null, because ...
y!.bar();
如果类型或非空断言背后的原因很明显,则可能不需要注释。例如,生成的 proto 代码始终可以为空,但也许在代码的上下文中众所周知某些字段始终由后端提供。请自行判断。
类型断言语法
类型断言必须使用 as
语法(而不是尖括号语法)。这会在访问成员时强制在断言周围加上括号。
const x = (<Foo>z).length;
const y = <Foo>z.length;
// z must be Foo because ...
const x = (z as Foo).length;
双重断言
来自 TypeScript 手册,TypeScript 只允许转换为类型的更具体或不太具体版本的类型断言。添加不符合此标准的类型断言 (x as Foo
) 会给出错误:类型 'X' 到类型 'Y' 的转换可能是一个错误,因为这两种类型都没有足够的重叠。
如果你确定断言是安全的,你可以执行双重断言。这涉及到通过 unknown
进行转换,因为它比所有类型都更不具体。
// x is a Foo here, because...
(x as unknown as Foo).fooMethod();
使用 unknown
(而不是 any
或 {}
)作为中间类型。
类型断言和对象字面量
使用类型注解 (: Foo
) 而不是类型断言 (as Foo
) 来指定对象字面量的类型。这允许在接口的字段随时间变化时检测重构错误。
interface Foo {
bar: number;
baz?: string; // was "bam", but later renamed to "baz".
}
const foo = {
bar: 123,
bam: 'abc', // no error!
} as Foo;
function func() {
return {
bar: 123,
bam: 'abc', // no error!
} as Foo;
}
interface Foo {
bar: number;
baz?: string;
}
const foo: Foo = {
bar: 123,
bam: 'abc', // complains about "bam" not being defined on Foo.
};
function func(): Foo {
return {
bar: 123,
bam: 'abc', // complains about "bam" not being defined on Foo.
};
}
保持 try 块的焦点
限制 try 块内的代码量,如果这样做不会损害可读性。
try {
const result = methodThatMayThrow();
use(result);
} catch (error: unknown) {
// ...
}
let result;
try {
result = methodThatMayThrow();
} catch (error: unknown) {
// ...
}
use(result);
将非抛出行的代码移出 try/catch 块有助于读者了解哪些方法会抛出异常。一些不抛出异常的内联调用可以保留在内部,因为它们可能不值得增加临时变量的复杂性。
例外:如果 try 块位于循环中,则可能存在性能问题。扩大 try 块以覆盖整个循环是可以接受的。
装饰器
装饰器是带有 @
前缀的语法,例如 @MyDecorator
。
不要定义新的装饰器。只使用框架定义的装饰器
- Angular (例如
@Component
、@NgModule
等) - Polymer (例如
@property
)
为什么?
我们通常希望避免使用装饰器,因为它们是一个实验性功能,后来与 TC39 提案分道扬镳,并且存在无法修复的已知错误。
使用装饰器时,装饰器必须紧接在它装饰的符号之前,并且两者之间没有空行
/** JSDoc comments go before decorators */
@Component({...}) // Note: no empty line after the decorator.
class MyComp {
@Input() myField: string; // Decorators on fields may be on the same line...
@Input()
myOtherField: string; // ... or wrap.
}
不允许使用的功能
原始类型的包装器对象
TypeScript 代码不得实例化原始类型 String
、Boolean
和 Number
的包装类。包装类具有令人惊讶的行为,例如 new Boolean(false)
的计算结果为 true
。
const s = new String('hello');
const b = new Boolean(false);
const n = new Number(5);
包装器可以作为函数调用来进行强制转换(这比使用 +
或连接空字符串更好)或创建符号。有关更多信息,请参见类型强制转换。
自动分号插入
不要依赖自动分号插入 (ASI)。使用分号显式结束所有语句。这可以防止由于不正确的分号插入而导致的错误,并确保与 ASI 支持有限的工具(例如 clang-format)的兼容性。
常量枚举
代码不得使用 const enum
;请改用普通的 enum
。
为什么?
TypeScript 枚举已经无法被修改;const enum
是一种单独的语言特性,与优化有关,它使枚举对模块的 JavaScript 用户不可见。
调试器语句
调试器语句不得包含在生产代码中。
function debugMe() {
debugger;
}
with
不要使用 with
关键字。它使你的代码更难理解,并且自 ES5 以来已在严格模式下被禁止。
动态代码评估
不要使用 eval
或 Function(...string)
构造函数(代码加载器除外)。这些功能可能很危险,并且在使用严格内容安全策略的环境中根本不起作用。
非标准功能
不要使用非标准的 ECMAScript 或 Web 平台功能。
这包括
- 已被标记为已弃用或已完全从 ECMAScript/Web 平台中删除的旧功能(请参见MDN)
- 尚未标准化的新 ECMAScript 功能
- 避免使用当前 TC39 工作草案或当前处于提案流程中的功能
- 仅使用在当前 ECMA-262 规范中定义的 ECMAScript 功能
- 已提出但尚未完成的 Web 标准
- 尚未完成 提案流程的 WHATWG 提案。
- 非标准语言“扩展”(例如某些外部转换器提供的扩展)
针对特定 JavaScript 运行时(例如仅限最新 Chrome、Chrome 扩展、Node.JS、Electron)的项目显然可以使用这些 API。在考虑专有且仅在某些浏览器中实现的 API 表面时要小心;考虑是否存在一个公共库可以为你抽象掉这个 API 表面。
修改内置对象
永远不要修改内置类型,无论是通过向其构造函数添加方法还是向其原型添加方法。避免依赖于执行此操作的库。
除非绝对必要(例如,第三方 API 要求),否则不要向全局对象添加符号。
命名
标识符
标识符必须仅使用 ASCII 字母、数字、下划线(用于常量和结构化测试方法名称)和(很少)$
符号。
命名风格
TypeScript 在类型中表达信息,因此名称不应使用类型中包含的信息进行装饰。(另请参见 Testing Blog,了解更多关于不包含哪些内容的信息。)
此规则的一些具体示例
- 不要为私有属性或方法使用尾随或前导下划线。
- 不要为可选参数使用
opt_
前缀。- 对于访问器,请参见下面的访问器规则。
- 不要专门标记接口(
或IMyInterface
),除非它在其环境中是惯用的。在为类引入接口时,给它一个表达接口存在原因的名称(例如,如果接口表达用于 JSON 中的存储/序列化的格式,则使用MyFooInterface
class TodoItem
和interface TodoItemStorage
)。 - 在
Observable
s后添加$
后缀是一种常见的外部约定,可以帮助解决关于 observable 值与具体值之间的混淆。 是否认为这是一个有用的约定取决于各个团队的判断,但应该在项目中保持一致。
描述性名称
名称必须具有描述性,并且对于新的读者来说清晰易懂。 不要使用模棱两可或项目外部读者不熟悉的缩写,也不要通过删除单词中的字母来进行缩写。
- 例外:作用域在 10 行或更少行内的变量,包括不属于导出 API 的参数,可以使用短(例如,单个字母)变量名。
// Good identifiers:
errorCount // No abbreviation.
dnsConnectionIndex // Most people know what "DNS" stands for.
referrerUrl // Ditto for "URL".
customerId // "Id" is both ubiquitous and unlikely to be misunderstood.
// Disallowed identifiers:
n // Meaningless.
nErr // Ambiguous abbreviation.
nCompConns // Ambiguous abbreviation.
wgcConnections // Only your group knows what this stands for.
pcReader // Lots of things can be abbreviated "pc".
cstmrId // Deletes internal letters.
kSecondsPerDay // Do not use Hungarian notation.
customerID // Incorrect camelcase of "ID".
驼峰命名法
在命名中,应将缩写(例如首字母缩略词)视为完整的单词,即使用loadHttpUrl
,而不是,除非平台名称要求(例如loadHTTPURL
XMLHttpRequest
)。
美元符号
标识符通常不应使用$
,除非第三方框架的命名约定要求。参见上文,了解有关将$
与Observable
值一起使用的更多信息。
按标识符类型划分的规则
大多数标识符名称应遵循下表中的大小写规则,基于标识符的类型。
风格 | 类别 |
---|---|
UpperCamelCase
|
class / interface / type / enum / decorator / 类型参数 / TSX 中的组件函数 / JSXElement 类型参数 |
lowerCamelCase
|
variable / parameter / function / method / property / module alias |
CONSTANT_CASE
|
全局常量值,包括枚举值。 参见下面的常量。 |
#ident |
从不使用私有标识符。 |
类型参数
类型参数,例如在Array<T>
中,可以使用单个大写字符(T
)或UpperCamelCase
。
测试名称
inxUnit 风格的测试框架中的测试方法名称可以使用_
分隔符进行结构化,例如testX_whenY_doesZ()
。
_
前缀/后缀
标识符不得使用_
作为前缀或后缀。
这也意味着不得将_
本身用作标识符(例如,表示参数未使用)。
提示:如果只需要数组(或 TypeScript 元组)中的某些元素,可以在解构语句中插入额外的逗号以忽略中间元素
const [a, , b] = [1, 5, 10]; // a <- 1, b <- 10
导入
模块命名空间导入是lowerCamelCase
,而文件是snake_case
,这意味着导入在大小写风格上不会正确匹配,例如
import * as fooBar from './foo_bar';
某些库可能经常使用违反此命名方案的命名空间导入前缀,但过度常见的开源使用使违反的风格更具可读性。 目前属于此例外的唯一库是
常量
不可变:CONSTANT_CASE
表示一个值旨在不被更改,并且可以用于技术上可以修改的值(即未深度冻结的值),以向用户表明它们不得被修改。
const UNIT_SUFFIXES = {
'milliseconds': 'ms',
'seconds': 's',
};
// Even though per the rules of JavaScript UNIT_SUFFIXES is
// mutable, the uppercase shows users to not modify it.
常量也可以是类的static readonly
属性。
class Foo {
private static readonly MY_SPECIAL_NUMBER = 5;
bar() {
return 2 * Foo.MY_SPECIAL_NUMBER;
}
}
全局:只有在模块级别声明的符号、模块级别类的静态字段以及模块级别枚举的值,可以使用CONST_CASE
。 如果一个值在程序的生命周期中可以被实例化多次(例如,在函数中声明的局部变量,或嵌套在函数中的类的静态字段),则它必须使用lowerCamelCase
。
如果一个值是实现接口的箭头函数,那么它可以被声明为lowerCamelCase
。
别名
创建现有符号的本地作用域别名时,使用现有标识符的格式。 本地别名必须与源的现有命名和格式匹配。 对于变量,为本地别名使用const
,对于类字段,使用readonly
属性。
注意:如果创建别名只是为了在您选择的框架中将其公开给模板,请记住也应用适当的访问修饰符。
const {BrewStateEnum} = SomeType;
const CAPACITY = 5;
class Teapot {
readonly BrewStateEnum = BrewStateEnum;
readonly CAPACITY = CAPACITY;
}
类型系统
类型推断
代码可以依赖 TypeScript 编译器为所有类型表达式(变量、字段、返回类型等)实现的类型推断。
const x = 15; // Type inferred.
省略对平凡推断类型的类型注解:初始化为string
、number
、boolean
、RegExp
字面量或new
表达式的变量或参数。
const x: boolean = true; // Bad: 'boolean' here does not aid readability
// Bad: 'Set' is trivially inferred from the initialization
const x: Set<string> = new Set();
可能需要显式指定类型,以防止泛型类型参数被推断为unknown
。 例如,使用没有值的泛型类型初始化(例如,空数组、对象、Map
或Set
)。
const x = new Set<string>();
对于更复杂的表达式,类型注解可以帮助提高程序的可读性
// 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();
是否需要注解由代码审查者决定。
返回类型
是否为函数和方法包含返回类型注解由代码作者决定。 审查者可以要求提供注解以阐明难以理解的复杂返回类型。 项目可以具有始终要求返回类型的本地策略,但这不是一般的 TypeScript 样式要求。
显式键入函数和方法的隐式返回值有两个好处
- 更精确的文档,使代码读者受益。
- 如果将来有代码更改会改变函数的返回类型,则可以更快地发现潜在的类型错误。
Undefined 和 null
TypeScript 支持undefined
和null
类型。 可空类型可以构造为联合类型 (string|null
); undefined
也是如此。 没有用于undefined
和null
联合的特殊语法。
TypeScript 代码可以使用undefined
或null
来表示值的缺失,没有通用的指导原则来偏好其中一个。 许多 JavaScript API 使用undefined
(例如Map.get
),而许多 DOM 和 Google API 使用null
(例如Element.getAttribute
),因此适当的缺失值取决于上下文。
可空/未定义类型别名
类型别名不得在联合类型中包含|null
或|undefined
。 可空别名通常表明 null 值正在通过应用程序的太多层传递,这掩盖了导致null
的原始问题的根源。 它们还使类或接口上的特定值何时可能缺失变得不清楚。
相反,代码必须仅在实际使用别名时添加|null
或|undefined
。 代码应该使用上述技术,在 null 值出现的位置附近处理它们。
// Bad
type CoffeeResponse = Latte|Americano|undefined;
class CoffeeService {
getLatte(): CoffeeResponse { ... };
}
// Better
type CoffeeResponse = Latte|Americano;
class CoffeeService {
getLatte(): CoffeeResponse|undefined { ... };
}
优先选择 optional 而不是 |undefined
此外,TypeScript 支持一种特殊的构造,用于可选参数和字段,使用?
interface CoffeeOrder {
sugarCubes: number;
milk?: Whole|LowFat|HalfHalf;
}
function pourCoffee(volume?: Milliliter) { ... }
可选参数隐式地在其类型中包含|undefined
。 但是,它们的区别在于在构造值或调用方法时可以省略它们。 例如,{sugarCubes: 1}
是有效的CoffeeOrder
,因为milk
是可选的。
使用可选字段(在接口或类上)和参数,而不是|undefined
类型。
对于类,最好完全避免这种模式,并尽可能多地初始化字段。
class MyClass {
field = '';
}
使用结构类型
TypeScript 的类型系统是结构化的,而不是名义上的。 也就是说,如果一个值具有类型要求的所有属性并且这些属性的类型递归匹配,则该值与该类型匹配。
提供基于结构的实现时,在符号的声明中显式包含类型(这允许更精确的类型检查和错误报告)。
const foo: Foo = {
a: 123,
b: 'abc',
}
const badFoo = {
a: 123,
b: 'abc',
}
使用接口定义结构类型,而不是类
interface Foo {
a: number;
b: string;
}
const foo: Foo = {
a: 123,
b: 'abc',
}
class Foo {
readonly a: number;
readonly b: number;
}
const foo: Foo = {
a: 123,
b: 'abc',
}
为什么?
上面的badFoo
对象依赖于类型推断。 可以将其他字段添加到badFoo
,并且该类型是基于对象本身推断出来的。
将badFoo
传递给接受Foo
的函数时,错误将出现在函数调用站点,而不是在对象声明站点。 这在跨广泛代码库更改接口的表面时也很有用。
interface Animal {
sound: string;
name: string;
}
function makeSound(animal: Animal) {}
/**
* 'cat' has an inferred type of '{sound: string}'
*/
const cat = {
sound: 'meow',
};
/**
* 'cat' does not meet the type contract required for the function, so the
* TypeScript compiler errors here, which may be very far from where 'cat' is
* defined.
*/
makeSound(cat);
/**
* Horse has a structural type and the type error shows here rather than the
* function call. 'horse' does not meet the type contract of 'Animal'.
*/
const horse: Animal = {
sound: 'niegh',
};
const dog: Animal = {
sound: 'bark',
name: 'MrPickles',
};
makeSound(dog);
makeSound(horse);
优先使用接口而不是类型字面量别名
TypeScript 支持类型别名来命名类型表达式。 这可用于命名原始类型、联合类型、元组和任何其他类型。
但是,当为对象声明类型时,请使用接口而不是对象字面量表达式的类型别名。
interface User {
firstName: string;
lastName: string;
}
type User = {
firstName: string,
lastName: string,
}
为什么?
这些形式几乎是等效的,因此在仅选择两种形式中的一种以防止变化的原则下,我们应该选择一种。 此外,还有一些有趣的技术原因倾向于使用接口。 该页面引用了 TypeScript 团队负责人:老实说,我的看法是,对于任何他们可以建模的东西,都应该真正只是接口。 当存在这么多关于显示/性能的问题时,类型别名没有任何好处。
Array<T>
类型
对于简单类型(仅包含字母数字字符和点),使用数组的语法糖T[]
或readonly T[]
,而不是更长的形式Array<T>
或ReadonlyArray<T>
。
对于简单类型的多维非readonly
数组,使用语法糖形式(T[][]
、T[][][]
等),而不是更长的形式。
对于任何更复杂的情况,使用更长的形式Array<T>
。
这些规则适用于每个嵌套级别,即嵌套在更复杂类型中的简单T[]
仍应拼写为T[]
,使用语法糖。
let a: string[];
let b: readonly string[];
let c: ns.MyObj[];
let d: string[][];
let e: Array<{n: number, s: string}>;
let f: Array<string|number>;
let g: ReadonlyArray<string|number>;
let h: InjectionToken<string[]>; // Use syntax sugar for nested types.
let i: ReadonlyArray<string[]>;
let j: Array<readonly string[]>;
let a: Array<string>; // The syntax sugar is shorter.
let b: ReadonlyArray<string>;
let c: Array<ns.MyObj>;
let d: Array<string[]>;
let e: {n: number, s: string}[]; // The braces make it harder to read.
let f: (string|number)[]; // Likewise with parens.
let g: readonly (string | number)[];
let h: InjectionToken<Array<string>>;
let i: readonly string[][];
let j: (readonly string[])[];
可索引类型/索引签名 ({[key: string]: T}
)
在 JavaScript 中,通常使用对象作为关联数组(也称为map
、hash
或dict
)。 可以使用 TypeScript 中的索引签名 ([k: string]: T
) 对此类对象进行类型化
const fileSizes: {[fileName: string]: number} = {};
fileSizes['readme.txt'] = 541;
在 TypeScript 中,为键提供有意义的标签。 (标签仅用于文档;否则未使用。)
const users: {[key: string]: number} = ...;
const users: {[userName: string]: number} = ...;
与其使用这些中的一个,不如考虑使用 ES6
Map
和Set
类型。 JavaScript 对象具有令人惊讶的不良行为,并且 ES6 类型更明确地传达了您的意图。 此外,Map
s 可以通过string
以外的类型进行键控,并且Set
s 可以包含这些类型。
TypeScript 的内置Record<Keys, ValueType>
类型允许构造具有已定义键集的类型。 这与关联数组的不同之处在于,键是静态已知的。 请参阅下面关于该映射条件类型的建议。
映射类型和条件类型
TypeScript 的映射类型和条件类型允许根据其他类型指定新类型。 TypeScript 的标准库包括基于这些的几个类型运算符 (Record
、Partial
、Readonly
等)。
这些类型系统功能允许简洁地指定类型并构造强大但类型安全的抽象。 但是,它们带来了一些缺点
- 与显式指定属性和类型关系(例如,使用接口和扩展,见下例)相比,类型操作符需要读者在脑海中评估类型表达式。这会使程序更难阅读,尤其是当与类型推断和跨文件边界的表达式结合使用时。
- 映射类型和条件类型的评估模型,特别是与类型推断结合使用时,规范不明确,并非总是被很好地理解,并且经常在 TypeScript 编译器版本中发生变化。代码可能会“意外地”编译或看起来给出正确的结果。这增加了使用类型操作符的代码的未来维护成本。
- 映射类型和条件类型在从复杂和/或推断类型派生类型时最强大。另一方面,这也是它们最容易创建难以理解和维护的程序的时候。
- 一些语言工具不能很好地处理这些类型系统特性。例如,你的 IDE 的查找引用(以及因此的重命名属性重构)将无法在
Pick<T, Keys>
类型中找到属性,并且代码搜索不会对其进行超链接。
样式建议是
- 始终使用可以表达你的代码的最简单的类型构造。
- 少量重复或冗余通常比复杂类型表达式的长期成本要低得多。
- 映射类型和条件类型可以被使用,但须遵守这些考虑因素。
例如,TypeScript 的内置类型 Pick<T, Keys>
允许通过子集化另一个类型 T
来创建一个新类型,但简单的接口扩展通常更容易理解。
interface User {
shoeSize: number;
favoriteIcecream: string;
favoriteChocolate: string;
}
// FoodPreferences has favoriteIcecream and favoriteChocolate, but not shoeSize.
type FoodPreferences = Pick<User, 'favoriteIcecream'|'favoriteChocolate'>;
这等同于明确地写出 FoodPreferences
的属性
interface FoodPreferences {
favoriteIcecream: string;
favoriteChocolate: string;
}
为了减少重复,User
可以扩展 FoodPreferences
,或者(可能更好)嵌套一个用于食物偏好的字段
interface FoodPreferences { /* as above */ }
interface User extends FoodPreferences {
shoeSize: number;
// also includes the preferences.
}
在这里使用接口使属性的分组更加明确,改进了 IDE 支持,允许更好的优化,并且可以说使代码更容易理解。
any
类型
TypeScript 的 any
类型是所有其他类型的超类型和子类型,并允许解引用所有属性。因此,any
是危险的 - 它可以掩盖严重的编程错误,并且它的使用破坏了拥有静态类型的价值。
考虑不要使用 any
。 在你想使用 any
的情况下,考虑以下之一
提供更具体的类型
使用接口、内联对象类型或类型别名
// Use declared interfaces to represent server-side JSON.
declare interface MyUserJson {
name: string;
email: string;
}
// Use type aliases for types that are repetitive to write.
type MyType = number|string;
// Or use inline object types for complex returns.
function getTwoThings(): {something: number, other: string} {
// ...
return {something, other};
}
// Use a generic type, where otherwise a library would say `any` to represent
// they don't care what type the user is operating on (but note "Return type
// only generics" below).
function nicestElement<T>(items: T[]): T {
// Find the nicest element in items.
// Code can also put constraints on T, e.g. <T extends HTMLElement>.
}
使用 unknown
而不是 any
any
类型允许赋值给任何其他类型,并允许解引用它的任何属性。通常,这种行为不是必需的或可取的,代码只需要表达一个类型是未知的。在这种情况下,使用内置类型 unknown
- 它表达了这个概念,并且更安全,因为它不允许解引用任意属性。
// Can assign any value (including null or undefined) into this but cannot
// use it without narrowing the type or casting.
const val: unknown = value;
const danger: any = value /* result of an arbitrary expression */;
danger.whoops(); // This access is completely unchecked!
要安全地使用 unknown
值,请使用 类型守卫来缩小类型范围
抑制 any
lint 警告
有时使用 any
是合法的,例如在测试中构造一个模拟对象。在这种情况下,添加一个注释来抑制 lint 警告,并记录为什么它是合法的。
// This test only needs a partial implementation of BookService, and if
// we overlooked something the test will fail in an obvious way.
// This is an intentionally unsafe partial mock
// tslint:disable-next-line:no-any
const mockBookService = ({get() { return mockBook; }} as any) as BookService;
// Shopping cart is not used in this test
// tslint:disable-next-line:no-any
const component = new MyComponent(mockBookService, /* unused ShoppingCart */ null as any);
{}
类型
{}
类型,也称为空接口类型,表示一个没有属性的接口。一个空接口类型没有指定的属性,因此任何非 nullish 值都可以赋值给它。
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 代码不应该在大多数用例中使用 {}
。{}
表示任何非 nullish 的原始类型或对象类型,这很少是合适的。优先选择以下更具描述性的类型之一
unknown
可以容纳任何值,包括null
或undefined
,并且通常更适合不透明的值。Record<string, T>
更适合类似字典的对象,并通过明确包含值的类型T
(其本身可能是unknown
)来提供更好的类型安全。object
也排除了原始类型,仅留下非 nullish 的函数和对象,但没有关于可能有哪些属性可用的其他假设。
元组类型
如果你想创建一个 Pair 类型,请使用元组类型
interface Pair {
first: string;
second: string;
}
function splitInHalf(input: string): Pair {
...
return {first: x, second: y};
}
function splitInHalf(input: string): [string, string] {
...
return [x, y];
}
// Use it like:
const [leftHalf, rightHalf] = splitInHalf('my string');
然而,通常为属性提供有意义的名称更清晰。
如果声明一个 interface
太过重量级,你可以使用一个内联对象字面量类型
function splitHostPort(address: string): {host: string, port: number} {
...
}
// Use it like:
const address = splitHostPort(userAddress);
use(address.port);
// You can also get tuple-like behavior using destructuring:
const {host, port} = splitHostPort(userAddress);
包装类型
有一些与 JavaScript 原始类型相关的类型不应该被使用
String
、Boolean
和Number
与相应的原始类型string
、boolean
和number
具有稍微不同的含义。始终使用小写版本。Object
与{}
和object
都有相似之处,但稍微宽松一些。对于包含除null
和undefined
之外的所有内容的类型,使用{}
;或者使用小写object
来进一步排除其他原始类型(上面提到的三个,加上symbol
和bigint
)。
此外,永远不要将包装类型作为构造函数调用(使用 new
)。
仅返回类型泛型
避免创建只有返回类型泛型的 API。 当使用现有的具有仅返回类型泛型的 API 时,始终显式指定泛型。
工具链要求
Google 风格要求以特定的方式使用许多工具,这里概述了这些工具。
TypeScript 编译器
所有 TypeScript 文件都必须使用标准工具链通过类型检查。
@ts-ignore
不要使用 @ts-ignore
及其变体 @ts-expect-error
或 @ts-nocheck
。
为什么?
它们表面上看起来是 修复
编译器错误的简单方法,但实际上,特定的编译器错误通常是由更大的问题引起的,可以直接修复。
例如,如果你使用 @ts-ignore
来抑制类型错误,那么很难预测周围的代码最终会看到什么类型。对于许多类型错误,如何最好地使用 any
中的建议很有用。
你可以在单元测试中使用 @ts-expect-error
,尽管通常不应该这样做。@ts-expect-error
会抑制所有错误。很容易意外地过度匹配并抑制更严重的错误。考虑以下之一
- 在测试需要在运行时处理未经检查的值的 API 时,将类型强制转换为预期类型或
any
,并添加解释性注释。 这将错误抑制限制为单个表达式。 - 抑制 lint 警告并记录原因,类似于抑制
any
lint 警告。
一致性
Google TypeScript 包括几个一致性框架,tsetse 和 tsec。
这些规则通常用于强制执行关键限制(例如定义全局变量,这可能会破坏代码库)和安全模式(例如使用 eval
或分配给 innerHTML
),或者更宽松地用于提高代码质量。
Google 风格的 TypeScript 必须遵守任何适用的全局或框架局部一致性规则。
注释和文档
JSDoc 与注释
有两种类型的注释,JSDoc(/** ... */
)和非 JSDoc 普通注释(// ...
或 /* ... */
)。
- 使用
/** JSDoc */
注释进行文档编写,即代码用户应该阅读的注释。 - 使用
// 行注释
进行实现注释,即仅涉及代码本身实现的注释。
JSDoc 注释可以被工具(如编辑器和文档生成器)理解,而普通注释仅供其他人阅读。
多行注释
多行注释的缩进级别与周围代码相同。它们必须使用多个单行注释(//
样式),而不是块注释样式(/* */
)。
// This is
// fine
/*
* This should
* use multiple
* single-line comments
*/
/* This should use // */
注释不包含在用星号或其他字符绘制的框中。
JSDoc 一般形式
JSDoc 注释的基本格式如下例所示
/**
* Multiple lines of JSDoc text are written here,
* wrapped normally.
* @param arg A number to do something to.
*/
function doSomething(arg: number) { … }
或在这个单行示例中
/** This short jsdoc describes the function. */
function doSomething(arg: number) { … }
如果单行注释溢出到多行,则必须使用多行样式,并在单独的行上使用 /**
和 */
。
许多工具从 JSDoc 注释中提取元数据以执行代码验证和优化。因此,这些注释必须格式良好。
Markdown
JSDoc 是用 Markdown 编写的,但必要时可以包含 HTML。
这意味着解析 JSDoc 的工具会忽略纯文本格式,所以如果你这样做
/**
* Computes weight based on three factors:
* items sent
* items received
* last timestamp
*/
它将像这样呈现
Computes weight based on three factors: items sent items received last timestamp
相反,编写一个 Markdown 列表
/**
* Computes weight based on three factors:
*
* - items sent
* - items received
* - last timestamp
*/
JSDoc 标签
Google 风格允许 JSDoc 标签的子集。 大多数标签必须占据自己的行,标签位于行的开头。
/**
* 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) { ... }
换行
换行的块标签缩进四个空格。 换行的描述文本可以与前几行的描述对齐,但不鼓励这种水平对齐。
/**
* 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;
};
包装 @desc
或 @fileoverview
描述时不要缩进。
记录模块的所有顶级导出
使用 /** JSDoc */
注释来向代码用户传达信息。避免仅仅重复属性或参数名称。你应该还记录所有属性和方法(导出的/公共的或不是),如果它们的用途不能从它们的名称中立即看出(由你的审阅者判断)。
例外:仅导出以供工具使用的符号,例如 @NgModule 类,不需要注释。
类注释
类的 JSDoc 注释应该为读者提供足够的信息,以便了解如何以及何时使用该类,以及正确使用该类所需的任何其他考虑因素。 构造函数上的文本描述可以省略。
方法和函数注释
如果从方法的其余 JSDoc 或从方法名称和类型签名中可以明显看出方法、参数和返回描述,则可以省略它们。
方法描述以描述方法做什么的动词短语开始。 这个短语不是一个祈使句,而是以第三人称书写,就好像在它之前有一个隐含的 This method ...
。
参数属性注释
参数属性是一个构造函数参数,它以修饰符 private
、protected
、public
或 readonly
之一为前缀。 参数属性声明了参数和实例属性,并隐式地赋值给它。 例如,constructor(private readonly foo: Foo)
声明构造函数接受一个参数 foo
,但也声明了一个私有只读属性 foo
,并在执行构造函数的其余部分之前将该参数分配给该属性。
要记录这些字段,请使用 JSDoc 的 @param
注释。 编辑器在构造函数调用和属性访问时显示该描述。
/** 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 类型注释
在 TypeScript 源代码中,JSDoc 类型注解是冗余的。不要在 @param
或 @return
代码块中声明类型,不要在使用 implements
, enum
, private
, override
等关键字的代码上编写 @implements
, @enum
, @private
, @override
等。
编写真正增加信息的注释
对于非导出的符号,有时函数或参数的名称和类型就足够了。但是,代码通常会从比变量名更多的文档中受益!
避免仅重述参数名称和类型的注释,例如:
/** @param fooBarService The Bar service for the Foo application. */
由于此规则,只有在
@param
和@return
行添加信息时才需要它们,否则可以省略。/** * POSTs the request to start coffee brewing. * @param amountLitres The amount to brew. Must fit the pot size! */ brew(amountLitres: number, logger: Logger) { // ... }
调用函数时的注释
只要方法名称和参数值不能充分表达参数的含义,就应使用“参数名称”注释。
在添加这些注释之前,请考虑重构该方法以接受一个接口并将其解构,以大大提高调用点的可读性。
“参数名称”注释位于参数值之前,并包含参数名称和一个 =
后缀
someFunction(obviousParam, /* shouldRender= */ true, /* name= */ 'hello');
现有代码可能使用旧式的参数名称注释样式,该样式将这些注释放在参数值~之后~,并省略 =
。为了保持一致性,可以继续在该文件中使用此样式。
someFunction(obviousParam, true /* shouldRender */, 'hello' /* name */);
将文档放在装饰器之前
当类、方法或属性同时具有像 @Component
这样的装饰器和 JsDoc 时,请务必在装饰器之前编写 JsDoc。
不要在装饰器和被装饰的语句之间编写 JsDoc。
@Component({ selector: 'foo', template: 'bar', }) /** Component that prints "bar". */ export class FooComponent {}
在装饰器之前编写 JsDoc 代码块。
/** Component that prints "bar". */ @Component({ selector: 'foo', template: 'bar', }) export class FooComponent {}
策略
一致性
对于任何本规范未明确解决的样式问题,请执行与同一文件中其他代码已执行的操作(“保持一致”)。如果这不能解决问题,请考虑模仿同一目录中的其他文件。
全新的文件必须使用 Google Style,无论同一软件包中其他文件的样式选择如何。当向不符合 Google Style 的文件中添加新代码时,建议首先重新格式化现有代码,但需遵守以下建议。如果未进行此重新格式化,则新代码应尽可能与同一文件中的现有代码保持一致,但不得违反样式指南。
重新格式化现有代码
您偶尔会遇到代码库中不符合正确的 Google Style 的文件。 这些文件可能来自收购,或者可能是在 Google Style 对某些问题采取立场之前编写的,或者可能由于任何其他原因而不符合 Google Style。
更新现有代码的样式时,请遵循以下准则。
- 不需要更改所有现有代码以符合当前的样式准则。 重新格式化现有代码是在代码变动和一致性之间进行权衡。 样式规则会随着时间的推移而演变,并且这些为了保持合规性而进行的调整会造成不必要的变动。 但是,如果对文件进行了重大更改,则期望该文件符合 Google Style。
- 注意不要让机会主义的样式修复混淆了 CL 的重点。 如果您发现自己进行了许多对 CL 的核心重点并不重要的样式更改,请将这些更改提升到单独的 CL 中。
弃用
使用 @deprecated
JSDoc 注释标记已弃用的方法、类或接口。 弃用注释必须包括简单、清晰的指示,以供人们修复其调用点。
生成的代码:基本免除
构建过程生成的源代码不需要符合 Google Style。 但是,任何将从手写源代码引用的生成的标识符都必须遵循命名要求。 作为一个特殊的例外,允许此类标识符包含下划线,这可能有助于避免与手写标识符冲突。
样式指南目标
一般来说,工程师通常最了解他们的代码需要什么,因此如果有多个选项并且选择取决于具体情况,我们应该让决策在本地进行。因此,默认答案应该是“省略”。
以下几点是例外,这是我们制定一些全局规则的原因。根据以下内容评估您的样式指南提案
代码应避免已知会导致问题的模式,尤其是对于不熟悉该语言的用户。
跨项目的代码应在不相关的变体中保持一致。
当有两个表面上等效的选项时,我们应该考虑选择一个,这样我们就不会无缘无故地发散演变,并避免代码审查中毫无意义的争论。
示例
- 名称的大小写样式。
x as T
语法与等效的<T>x
语法(不允许)。Array<[number, number]>
与[number, number][]
。
代码应在长期内可维护。
代码的寿命通常比原始作者的工作时间长,并且 TypeScript 团队必须让 Google 的所有工作在未来继续进行。
示例
- 我们使用软件来自动化代码更改,因此代码是自动格式化的,以便软件可以轻松地满足空格规则。
- 我们要求一组唯一的编译器标志,因此可以编写给定的 TS 库,假设有一组特定的标志,并且用户始终可以安全地使用共享库。
- 代码必须导入它使用的库(“严格的依赖关系”),以便依赖项中的重构不会改变其用户的依赖项。
- 我们要求用户编写测试。如果没有测试,我们就无法确信我们对该语言所做的更改不会破坏用户。
代码审查员应专注于提高代码的质量,而不是执行任意规则。
如果可以将您的规则实现为自动化检查,这通常是一个好兆头。这也支持原则 3。
如果它真的不是那么重要——如果它是该语言的一个模糊角落,或者如果它避免了一个不太可能发生的错误——那么可能值得省略它。