Skip to Content
TypeScript语言特性

语言特性

本节说明哪些特性可以或不可以使用,以及使用时的附加约束。

本风格指南中未讨论的语言特性可以使用,且不提供使用建议。

局部变量声明

使用 const 和 let

始终使用 constlet 来声明变量。默认使用 const,除非变量需要被重新赋值。永远不要使用 var

const foo = otherValue; // 如果 "foo" 不会改变则使用。 let bar = someValue; // 如果 "bar" 之后会被赋值则使用。

constlet 是块级作用域(Block Scoped)的,与大多数其他语言中的变量一样。JavaScript 中的 var 是函数级作用域(Function Scoped)的,这可能导致难以理解的 bug。不要使用它。

var foo = someValue; // 不要使用——var 的作用域规则复杂且容易导致 bug。

变量禁止在声明之前使用。

每个声明只声明一个变量

每个局部变量声明只声明一个变量:不使用像 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]; // 等价于 Array(2): const c = []; c.length = 2; // [0, 0, 0, 0, 0] Array.from<number>({length: 5}).fill(0);

不要在数组上定义属性

不要在数组上定义或使用非数字属性(length 除外)。改用 Map(或 Object)。

使用展开语法

使用展开语法(Spread Syntax) [...foo]; 是浅拷贝或连接可迭代对象的便捷简写。

const foo = [ 1, ]; const foo2 = [ ...foo, 6, 7, ]; const foo3 = [ 5, ...foo, ]; foo2[1] === 6; foo3[1] === 1;

使用展开语法时,被展开的值必须与正在创建的内容匹配。创建数组时,只展开可迭代对象。原始值(包括 nullundefined禁止被展开。

const foo = [7]; const bar = [5, ...(shouldUseFoo && foo)]; // 可能是 undefined // 创建了 {0: 'a', 1: 'b', 2: 'c'} 但没有 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'];

数组解构

数组字面量可以用在赋值的左侧来执行解构(Destructuring)(例如从单个数组或可迭代对象中解包多个值)。可以包含最后一个”剩余”元素(... 和变量名之间没有空格)。如果元素未使用,应省略。

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 可能来自某个父级原型! }

要么使用 if 语句显式过滤值,要么使用 for (... of Object.keys(...))

for (const x in someObj) { if (!someObj.hasOwnProperty(x)) continue; // 现在 x 确实是在 someObj 上定义的 } for (const x of Object.keys(someObj)) { // 注意:用的是 _of_! // 现在 x 确实是在 someObj 上定义的 } for (const [key, value] of Object.entries(someObj)) { // 注意:用的是 _of_! // 现在 key 确实是在 someObj 上定义的 }

使用展开语法

使用展开语法 {...bar} 是创建对象浅拷贝的便捷简写。在对象初始化中使用展开语法时,后面的值会替换相同键的较早的值。

const foo = { num: 1, }; const foo2 = { ...foo, num: 5, }; const foo3 = { num: 5, ...foo, } foo2.num === 5; foo3.num === 1;

使用展开语法时,被展开的值必须与正在创建的内容匹配。也就是说,创建对象时,只能展开对象;数组和原始值(包括 nullundefined禁止被展开。避免展开具有非 Object 原型的对象(例如类定义、类实例、函数),因为其行为不直观(只会浅拷贝可枚举的非原型属性)。

const foo = {num: 7}; const bar = {num: 5, ...(shouldUseFoo && foo)}; // 可能是 undefined // 创建了 {0: 'a', 1: 'b', 2: 'c'} 但没有 length const fooStrings = ['a', 'b', 'c']; const ids = {...fooStrings};
const foo = shouldUseFoo ? {num: 7} : {}; const bar = {num: 5, ...foo};

计算属性名

计算属性名(Computed Property Name)(例如 {['key' + foo()]: 42})是允许的,且被视为字典风格(带引号的)键(即不得与不带引号的键混合使用),除非计算属性是一个 symbol (例如 [Symbol.iterator])。

对象解构

对象解构模式可以用在赋值的左侧来执行解构并从单个对象中解包多个值。

解构后的对象也可以用作函数参数,但应保持尽可能简单:单层的不带引号的简写属性。更深层的嵌套和计算属性不得在参数解构中使用。在解构参数的左侧指定任何默认值({str = 'some default'} = {},而不是 {str} = {str: 'some default'}),如果解构对象本身是可选的,它必须默认为 {}

示例:

interface Options { /** 执行某操作的次数。 */ num?: number; /** 要处理的字符串。 */ 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 { }; // 不必要的分号

相比之下,包含类表达式(Class Expression)的语句必须以分号结尾:

export const Baz = class extends Bar { method(): number { return this.x; } }; // 这里需要分号,因为这是一个语句,不是声明
exports const Baz = class extends Bar { method(): number { return this.x; } }

既不鼓励也不反对在类声明的大括号与其他类内容之间使用空行:

// 大括号周围没有空格——可以。 class Baz { method(): number { return this.x; } } // 两个大括号周围各有一个空格——也可以。 class Foo { method(): number { return this.x; } }

类方法声明

类方法声明禁止使用分号来分隔各个方法声明:

class Foo { doThing() { console.log("A"); } }
class Foo { doThing() { console.log("A"); }; // <-- 不必要的 }

方法声明应与周围代码之间用一个空行分隔:

class Foo { doThing() { console.log("A"); } getOtherThing(): number { return 4; } }
class Foo { doThing() { console.log("A"); } getOtherThing(): number { return 4; } }
重写 toString

toString 方法可以被重写,但必须总是成功执行且不能有可见的副作用。

提示:特别注意,不要从 toString 调用其他方法,因为异常情况可能导致无限循环。

静态方法

避免私有静态方法

在不影响可读性的情况下,优先使用模块局部函数而非私有静态方法。

不要依赖动态分派

代码不应该依赖静态方法的动态分派(Dynamic Dispatch)。静态方法应该只在定义它的基类本身上调用。静态方法不应该在可能是构造函数或子类构造函数的动态实例变量上调用(如果这样做了,必须使用 @nocollapse 定义),且禁止在未自行定义该方法的子类上直接调用。

不允许的写法:

// 下面示例的上下文(这个类本身是没问题的) class Base { /** @nocollapse */ static foo() {} } class Sub extends Base {} // 不推荐:不要动态调用静态方法 function callFoo(cls: typeof Base) { cls.foo(); } // 不允许:不要在未自行定义该方法的子类上调用静态方法 Sub.foo(); // 不允许:不要在静态方法中访问 this。 class MyClass { static foo() { return this.staticField; } } MyClass.staticField = 1;
避免在静态上下文中引用 this

代码禁止在静态上下文中使用 this

JavaScript 允许通过 this 访问静态字段。与其他语言不同,静态字段也是可继承的。

class ShoeStore { static storage: Storage = ...; static isAvailable(s: Shoe) { // 不好:不要在静态方法中使用 `this`。 return this.storage.has(s.id); } } class EmptyShoeStore extends ShoeStore { static storage: Storage = EMPTY_STORE; // 覆盖了 ShoeStore 的 storage }

为什么?

这段代码通常令人意外:作者可能不会预期静态字段可以通过 this 指针访问,也可能会惊讶地发现它们可以被覆盖——这个特性并不常用。

这段代码还鼓励了一种拥有大量静态状态的反模式,这会导致可测试性问题。

构造函数

构造函数调用必须使用括号,即使没有传递参数:

const x = new Foo;
const x = new Foo();

省略括号可能导致微妙的错误。以下两行是不等价的:

new Foo().Bar(); new Foo.Bar();

不需要提供空的构造函数或仅仅委托给父类的构造函数,因为 ES2015 在未指定构造函数时会提供默认的类构造函数。然而,带有参数属性(Parameter Property)、可见性修饰符(Visibility Modifier)或参数装饰器(Parameter Decorator)的构造函数不应该被省略,即使构造函数主体为空。

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 字段

不要使用私有字段(Private Field)(也称为私有标识符):

class Clazz { #ident = 1; }

相反,使用 TypeScript 的可见性注解:

class Clazz { private ident = 1; }

为什么?

私有标识符在被 TypeScript 降级编译时会导致显著的输出体积增加和性能下降,且在 ES2015 之前不受支持。它们只能降级到 ES2015,不能更低。同时,在使用静态类型检查来强制可见性时,它们也没有提供实质性的好处。

使用 readonly

将在构造函数之外永远不会被重新赋值的属性标记为 readonly 修饰符(这些不必是深度不可变的)。

参数属性

与其将一个显而易见的初始化器显式传递给类成员,不如使用 TypeScript 的参数属性(Parameter Property) 

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[] = []; }

提示:在构造函数完成后,永远不要向实例添加或删除属性,因为这会严重影响虚拟机优化类的“形状(Shape)”的能力。稍后可能被填充的可选字段应显式初始化为 undefined,以防止后续的形状变化。

在类词法作用域外使用的属性

在其包含类的词法作用域外使用的属性(例如从模板中使用的 Angular 组件属性)禁止使用 private 可见性,因为它们在包含类的词法作用域之外被使用。

根据问题中的属性,适当使用 protectedpublic。Angular 和 AngularJS 的模板属性应使用 protected,而 Polymer 应使用 public

TypeScript 代码禁止使用 obj['foo'] 来绕过属性的可见性。

为什么?

当属性为 private 时,你向自动化系统和人类都声明了该属性的访问范围仅限于声明类的方法,它们会依赖这一点。例如,未使用代码检查会标记一个看起来未使用的私有属性,即使其他文件设法绕过了可见性限制。

虽然 obj['foo'] 看起来可以在 TypeScript 编译器中绕过可见性,但这种模式可能会因构建规则的重新安排而被打破,并且也违反了优化兼容性

getter 和 setter

类成员的 getter 和 setter(也称为访问器(Accessor))可以使用。getter 方法必须是一个纯函数(Pure Function) (即结果一致且无副作用:getter 禁止改变可观察状态)。它们也可用于限制内部或冗长实现细节的可见性(如下所示)。

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++; // 不好:getter 改变了可观察状态 } }

如果使用访问器来隐藏类属性,隐藏的属性可以使用任何完整单词作为前缀或后缀,如 internalwrapped。使用这些私有属性时,尽可能通过访问器来访问值。至少一个属性的访问器必须是非平凡的:不要仅为了隐藏属性而定义“直通”访问器。相反,将属性设为公开(或考虑将其设为 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 = ''; // 这两个访问器都没有逻辑,所以直接把 bar 设为公开即可。 get bar() { return this.barInternal; } set bar(value: string) { this.barInternal = value; } }

getter 和 setter 禁止使用 Object.defineProperty 来定义,因为这会干扰属性重命名。

计算属性

计算属性(Computed Property)只能在类中属性为 symbol 时使用。不允许使用字典风格的属性(即带引号或计算的非 symbol 键)(参见不混合键类型的原因)。应为任何逻辑上可迭代的类定义 [Symbol.iterator] 方法。除此之外,应谨慎使用 Symbol

提示:注意使用任何其他内置 symbol(例如 Symbol.isConcatSpreadable),因为它们不会被编译器 polyfill,因此在旧版浏览器中将无法工作。

可见性

限制属性、方法和整个类型的可见性有助于保持代码的解耦。

  • 尽可能限制符号的可见性。
  • 考虑将私有方法转换为同一文件中但在任何类之外的非导出函数,并将私有属性移到单独的非导出类中。
  • TypeScript 符号默认是公开的。永远不要使用 public 修饰符,除非声明非只读的公开参数属性(在构造函数中)。
class Foo { public bar = new Bar(); // 不好:不需要 public 修饰符 constructor(public readonly baz: Baz) {} // 不好:readonly 已经意味着它是一个属性,默认就是 public }
class Foo { bar = new Bar(); // 好的:不需要 public 修饰符 constructor(public baz: Baz) {} // 允许使用 public 修饰符 }

另见导出可见性

不允许的类模式

不要直接操作 prototype

class 关键字比定义 prototype 属性提供了更清晰、更可读的类定义。普通实现代码没有理由去操作这些对象。Mixin 和修改内置对象的原型是明确禁止的。

例外:框架代码(如 Polymer 或 Angular)可能需要使用 prototype,并且不应为了避免这样做而采用更糟糕的变通方法。

函数

术语

函数有多种不同的类型,它们之间有细微的区别。本指南使用以下术语,与 MDN  一致:

  • “函数声明(Function Declaration)”:使用 function 关键字的声明(即非表达式)
  • “函数表达式(Function Expression)”:使用 function 关键字的表达式,通常用于赋值或作为参数传递
  • “箭头函数(Arrow Function)”:使用 => 语法的表达式
  • “块体(Block Body)”:带大括号的箭头函数右侧
  • “简洁体(Concise Body)”:不带大括号的箭头函数右侧

方法和类/构造函数不在本节讨论范围内。

命名函数优先使用函数声明

定义命名函数时,优先使用函数声明而非箭头函数或函数表达式。

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(但这是不推荐的)或生成器函数(没有箭头语法)时使用。

箭头函数体

根据情况使用带简洁体(即表达式)或块体的箭头函数。

// 顶层函数使用函数声明。 function someFunction() { // 块体是可以的: const receipts = books.map((b: Book) => { const receipt = payMoney(b.price); recordTransaction(receipt); return receipt; }); // 如果使用返回值,简洁体也可以: const longThings = myValues.filter(v => v.length > 1000).map(v => String(v)); function payMoney(amount: number) { // 函数声明可以使用,但不能访问 `this`。 } // 嵌套箭头函数可以赋值给 const。 const computeTax = (amount: number) => amount * 0.12; }

只有在实际使用函数返回值时才使用简洁体。块体确保返回类型为 void,从而防止潜在的副作用。

// 不好:如果函数返回值未被使用,请使用块体。 myPromise.then(v => console.log(v)); // 不好:类型检查通过,但返回值仍然泄漏了。 let f: () => void; f = () => 1;
// 好的:返回值未使用,使用块体。 myPromise.then(v => { console.log(v); }); // 好的:代码可以使用块来提高可读性。 const transformed = [1, 2, 3].map(v => { const intermediate = someComplicatedExpr(v); const more = acrossManyLines(intermediate); return worthWrapping(more); }); // 好的:显式的 `void` 确保没有泄漏的返回值 myPromise.then(v => void console.log(v));

提示:当结果未使用时,可以使用 void 运算符确保带表达式体的箭头函数返回 undefined

重绑定 this

函数表达式和函数声明禁止使用 this,除非它们专门用于重绑定 this 指针。大多数情况下可以通过使用箭头函数或显式参数来避免重绑定 this

function clickHandler() { // 不好:在这个上下文中 `this` 是什么? this.textContent = 'Hello'; } // 不好:`this` 指针引用被隐式设置为 document.body。 document.body.onclick = clickHandler;
// 好的:从箭头函数中显式引用对象。 document.body.onclick = () => { document.body.textContent = 'hello'; }; // 或者:使用显式参数 const setTextFn = (e: HTMLElement) => { e.textContent = 'hello'; }; document.body.onclick = setTextFn.bind(null, document.body);

优先使用箭头函数而非其他绑定 this 的方式,例如 f.bind(this)goog.bind(f, this)const self = this

优先传递箭头函数作为回调

回调可能被以意外的参数调用,这些参数虽然能通过类型检查,但仍会导致逻辑错误。

避免将命名回调传递给高阶函数,除非你确定两个函数的调用签名都是稳定的。特别注意不常用的可选参数。

// 不好:参数没有被显式传递,导致可选的 `radix` 参数 // 获取了数组索引 0、1 和 2,从而产生了意外行为。 const numbers = ['11', '5', '10'].map(parseInt); // > [11, NaN, 2];

相反,推荐传递一个箭头函数来显式转发参数给命名回调。

// 好的:参数被显式传递给回调 const numbers = ['11', '5', '3'].map((n) => parseInt(n)); // > [11, 5, 3] // 好的:函数是本地定义的,专为用作回调而设计 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() { // 问题:`this` 在回调中未被保留。回调中的 `this` // 将不是 DelayHandler 的实例。 setTimeout(this.patienceTracker, 5000); } private patienceTracker() { this.waitedPatiently = true; } }
// 箭头函数通常不应该是属性。 class DelayHandler { constructor() { // 不好:这段代码看起来像是忘了绑定 `this`。 setTimeout(this.patienceTracker, 5000); } private patienceTracker = () => { this.waitedPatiently = true; } }
// 在调用时显式管理 `this`。 class DelayHandler { constructor() { // 如果可能,使用匿名函数。 setTimeout(() => { this.patienceTracker(); }, 5000); } private patienceTracker() { this.waitedPatiently = true; } }

事件处理器

当不需要卸载处理器时(例如事件由类本身发出),事件处理器可以使用箭头函数。如果处理器需要卸载,箭头函数属性是正确的方式,因为它们自动捕获 this 并提供稳定的引用用于卸载。

// 事件处理器可以是匿名函数或箭头函数属性。 class Component { onAttached() { // 事件由此类发出,无需卸载。 this.addEventListener('click', () => { this.listener(); }); // this.listener 是一个稳定的引用,我们之后可以卸载它。 window.addEventListener('onbeforeunload', this.listener); } onDetached() { // 事件由 window 发出。如果我们不卸载,this.listener 会 // 因为被绑定而保持对 `this` 的引用,导致内存泄漏。 window.removeEventListener('onbeforeunload', this.listener); } // 存储在属性中的箭头函数自动绑定到 `this`。 private listener = () => { confirm('Do you want to exit the page?'); } }

不要在安装事件处理器的表达式中使用 bind,因为它会创建一个无法卸载的临时引用。

// 绑定监听器会创建一个临时引用,阻止卸载。 class Component { onAttached() { // 这创建了一个我们无法卸载的临时引用 window.addEventListener('onbeforeunload', this.listener.bind(this)); } onDetached() { // 这个 bind 创建了一个不同的引用,所以这行什么也不做。 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) {}
// 不好:增加计数器的副作用 let globalCounter = 0; function newId(index = globalCounter++) {} // 不好:暴露共享的可变状态,可能在函数调用之间引入意外的耦合 class Foo { private readonly defaultPaths: string[]; frobnicate(paths = defaultPaths) {} }

谨慎使用默认参数。当有超过少量没有自然顺序的可选参数时,优先使用解构来创建可读的 API。

适当时优先使用 rest 和 spread

使用*剩余参数(Rest Parameter)*而非访问 arguments。永远不要将局部变量或参数命名为 arguments,因为这会令人困惑地遮蔽内置名称。

function variadic(array: string[], ...numbers: number[]) {}

使用函数展开语法而非 Function.prototype.apply

函数格式化

不允许在函数体的开头或结尾有空行。

在函数体内可以少量使用单个空行来创建语句的逻辑分组

生成器(Generator)应将 * 附加到 functionyield 关键字上,如 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');

原始值字面量

字符串字面量

使用单引号

普通字符串字面量使用单引号(')而非双引号(")作为定界符。

提示:如果字符串包含单引号字符,考虑使用模板字符串(Template String)以避免转义引号。

不使用行续写

在普通字符串字面量或模板字符串字面量中都不要使用行续写(Line Continuation)(即在字符串字面量内使用反斜杠结束一行)。即使 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';
模板字面量

在复杂的字符串拼接中使用模板字面量(Template Literal)(以 ` 定界)而非字符串拼接,特别是涉及多个字符串字面量时。模板字面量可以跨多行。

如果模板字面量跨多行,它不需要遵循外层代码块的缩进,但如果额外的空白不影响结果,也可以缩进。

示例:

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}`; }

数字字面量

数字可以用十进制、十六进制、八进制或二进制表示。对于十六进制、八进制和二进制,分别使用 0x0o0b 前缀,且使用小写字母。永远不要包含前导零,除非紧跟 xob

类型强制转换

TypeScript 代码可以使用 String()Boolean()(注意:不带 new!)函数、字符串模板字面量或 !! 来进行类型强制转换(Type Coercion)。

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 而非 NaNNumber('Infinity')Number('-Infinity') 会分别返回 Infinity-Infinity。此外,指数表示法如 Number('1e+309')Number('-1e+309') 可能溢出为 Infinity。这些情况可能需要特殊处理。

const aNumber = Number('123'); if (!isFinite(aNumber)) throw new Error(...);

代码禁止使用一元加号(+)来将字符串转换为数字。解析数字可能失败,有令人意外的边界情况,而且可能是代码异味(Code Smell)(在错误的层级解析)。一元加号在代码审查中太容易被忽略。

const x = +y;

代码也禁止使用 parseIntparseFloat 来解析数字,除非是非十进制字符串(见下文)。这两个函数都会忽略字符串中的尾随字符,这可能掩盖错误条件(例如将 12 dwarves 解析为 12)。

const n = parseInt(someString, 10); // 容易出错, const f = parseFloat(someString); // 无论是否传递了基数。

需要使用特定基数解析的代码在调用 parseInt 之前必须检查其输入是否只包含该基数的适当数字;

if (!/^[a-fA-F0-9]+$/.test(someString)) throw new Error(...); // 需要解析十六进制。 // tslint:disable-next-line:ban const n = parseInt(someString, 16); // 仅在基数 != 10 时允许

使用 Number() 后跟 Math.floorMath.trunc(如果可用)来解析整数:

let f = Number(someString); if (isNaN(f)) handleError(); f = Math.floor(f);
隐式强制转换

不要在具有隐式布尔强制转换的条件子句中使用显式布尔强制转换。这些包括 ifforwhile 语句中的条件。

const foo: MyInterface|null = ...; if (!!foo) {...} while (!!foo) {...}
const foo: MyInterface|null = ...; if (foo) {...} while (foo) {...}

与显式转换一样,枚举类型的值(包括枚举类型和其他类型的联合类型)禁止被隐式强制转换为布尔值,而必须使用比较运算符进行显式比较。

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

其他类型的值可以隐式强制转换为布尔值,也可以使用比较运算符进行显式比较:

// 显式比较 > 0 是可以的: if (arr.length > 0) {...} // 依赖布尔强制转换也可以: if (arr.length) {...}

控制结构

控制流语句和代码块

控制流语句(ifelsefordowhile 等)始终使用大括号包裹代码块,即使代码体只包含一条语句。非空代码块的第一条语句必须从新行开始。

for (let i = 0; i < x; i++) { doSomethingWith(i); } if (x) { doSomethingWithALongMethodNameThatForcesANewLine(x); }
if (x) doSomethingWithALongMethodNameThatForcesANewLine(x); for (let i = 0; i < x; i++) doSomethingWith(i);

**例外:**能放在一行的 if 语句可以省略代码块。

if (x) x.doFoo();
控制语句中的赋值

优先避免在控制语句中赋值变量。在控制语句中,赋值容易被误认为相等性检查。

if (x = someFunction()) { // 赋值容易被误认为相等性检查 // ... }
x = someFunction(); if (x) { // ... }

在控制语句内部的赋值更可取的情况下,用额外的括号包裹赋值以表明是有意为之。

while ((x = someFunction())) { // 双重括号表示赋值是有意为之的 // ... }
遍历容器

优先使用 for (... of someArr) 来遍历数组。也允许使用 Array.prototype.forEach 和普通的 for 循环:

for (const x of someArr) { // x 是 someArr 的一个值。 } for (let i = 0; i < someArr.length; i++) { // 如果需要索引则显式计数,否则使用 for/of 形式。 const x = someArr[i]; // ... } for (const [i, x] of someArr.entries()) { // 上述的替代版本。 }

for-in 循环只能用于字典风格的对象(更多信息参见下文)。不要使用 for (... in ...) 来遍历数组,因为它会违反直觉地给出数组的索引(作为字符串!),而不是值:

for (const x in someArray) { // x 是索引! }

应在 for-in 循环中使用 Object.prototype.hasOwnProperty 来排除不需要的原型属性。如果可能,优先使用 for-of 配合 Object.keysObject.valuesObject.entries,而不是 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); }

分组括号

只有当作者和审查者一致认为,没有这些括号代码不会被误解,且这些括号也不会使代码更易读时,才可以省略可选的分组括号。不应假设每个读者都记住了完整的运算符优先级表。

不要在 deletetypeofvoidreturnthrowcaseinofyield 后面的整个表达式周围使用不必要的括号。

异常处理

异常(Exception)是语言的重要组成部分,应在出现异常情况时使用。

自定义异常提供了一种从函数传递额外错误信息的好方法。应在原生 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 的拒绝值,因为 Promise.reject(obj) 等价于 async 函数中的 throw obj;

// 不好:无法获取堆栈跟踪。 throw 'oh noes!'; // 对于 Promise new Promise((resolve, reject) => void reject('oh noes!')); Promise.reject(); Promise.reject('oh noes!');

相反,只抛出 Error(或其子类):

// 只抛出 Error throw new Error('oh noes!'); // ... 或 Error 的子类型。 class MyError extends Error {} throw new MyError('my oh noes!'); // 对于 Promise new Promise((resolve) => resolve()); // 没有 reject 是可以的。 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) { // 所有抛出的错误必须是 Error 的子类型。不要处理 // 其他可能的值,除非你知道它们会被抛出。 assertIsError(e); displayError(e.message); // 或重新抛出: throw e; }

异常处理器禁止防御性地处理非 Error 类型,除非确知被调用的 API 违反了上述规则抛出非 Error。在这种情况下,应包含注释以明确说明非 Error 的来源。

try { badApiThrowingStrings(); } catch (e: unknown) { // 注意:该不良 API 抛出字符串而非错误对象。 if (typeof e === 'string') { ... } }

为什么?

避免过度防御性编程 。在大多数代码中不存在的问题上重复相同的防御措施会导致无用的样板代码。

空的 catch 块

对捕获的异常什么都不做很少是正确的。当确实不需要在 catch 块中采取任何操作时,需要在注释中说明原因。

try { return handleNumericResponse(response); } catch (e: unknown) { // 响应不是数字。继续按文本处理。 } 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: // 无需操作。 }

在 switch 块内,每个语句组要么以 breakreturn 语句或抛出异常来突然终止。非空语句组(case ...禁止贯穿(Fall Through)(由编译器强制执行):

switch (x) { case X: doSomething(); // 贯穿——不允许! case Y: // ... }

空语句组允许贯穿:

switch (x) { case X: case Y: doSomething(); break; default: // 无需操作。 }

相等性检查

始终使用三重等号(===)和不等于(!==)。双重相等运算符会导致容易出错的类型强制转换,难以理解且 JavaScript 虚拟机执行更慢。另见 JavaScript 相等性比较表 

if (foo == 'bar' || baz != bam) { // 由于类型强制转换,行为难以理解。 }
if (foo === 'bar' || baz !== bam) { // 一切正常。 }

**例外:**与字面量 null 值的比较可以使用 ==!= 运算符来同时覆盖 nullundefined 值。

if (foo == null) { // 当 foo 为 null 或 undefined 时触发。 }

类型断言和非空断言

类型断言(Type Assertion)(x as SomeType)和非空断言(Non-nullability Assertion)(y!)是不安全的。两者都只是使 TypeScript 编译器静默,但不会插入任何运行时检查来匹配这些断言,因此它们可能导致你的程序在运行时崩溃。

因此,不应该在没有明显或显式理由的情况下使用类型断言和非空断言。

而不是以下写法:

(x as Foo).foo(); y!.bar();

当你想要断言类型或非空性时,最好的方式是显式编写执行该检查的运行时检查。

// 假设 Foo 是一个类。 if (x instanceof Foo) { x.foo(); } if (y) { y.bar(); }

有时由于代码的某些局部属性,你可以确定断言形式是安全的。在这些情况下,你应该添加说明来解释为什么你认为不安全的行为是可以接受的:

// x 是 Foo,因为 ... (x as Foo).foo(); // y 不可能为 null,因为 ... y!.bar();

如果类型断言或非空断言背后的理由是显而易见的,注释可以不必要。例如,生成的 proto 代码总是可空的,但也许在代码的上下文中众所周知某些字段总是由后端提供的。请使用你的判断。

类型断言语法

类型断言必须使用 as 语法(而非尖括号语法)。这在访问成员时强制在断言周围使用括号。

const x = (<Foo>z).length; const y = <Foo>z.length;
// z 必须是 Foo 因为 ... const x = (z as Foo).length;
双重断言

根据 TypeScript 手册 ,TypeScript 只允许转换为更具体更不具体的类型的类型断言。添加不满足此条件的类型断言(x as Foo)会得到错误:“类型 ‘X’ 到类型 ‘Y’ 的转换可能是错误的,因为两种类型都没有与另一种充分重叠。”

如果你确定断言是安全的,可以执行双重断言(Double Assertion)。这涉及通过 unknown 进行转换,因为它比所有类型都更不具体。

// x 在这里是 Foo,因为... (x as unknown as Foo).fooMethod();

使用 unknown(而不是 any{})作为中间类型。

类型断言和对象字面量

使用类型注解(: Foo)而非类型断言(as Foo)来指定对象字面量的类型。这允许在接口字段随时间变化时检测重构 bug。

interface Foo { bar: number; baz?: string; // 曾经是 "bam",后来重命名为 "baz"。 } const foo = { bar: 123, bam: 'abc', // 没有错误! } as Foo; function func() { return { bar: 123, bam: 'abc', // 没有错误! } as Foo; }
interface Foo { bar: number; baz?: string; } const foo: Foo = { bar: 123, bam: 'abc', // 会报错 "bam" 未在 Foo 上定义。 }; function func(): Foo { return { bar: 123, bam: 'abc', // 会报错 "bam" 未在 Foo 上定义。 }; }

保持 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 块扩展到覆盖整个循环是可以的。

装饰器

装饰器(Decorator)是带有 @ 前缀的语法,如 @MyDecorator

不要定义新的装饰器。只使用框架定义的装饰器:

  • Angular(例如 @Component@NgModule 等)
  • Polymer(例如 @property

为什么?

我们通常希望避免使用装饰器,因为它们曾经是一个实验性特性,后来与 TC39 提案产生了分歧,并且存在已知的不会被修复的 bug。

使用装饰器时,装饰器必须紧接在它所装饰的符号之前,中间不能有空行:

/** JSDoc 注释放在装饰器之前 */ @Component({...}) // 注意:装饰器之后没有空行。 class MyComp { @Input() myField: string; // 字段上的装饰器可以在同一行... @Input() myOtherField: string; // ... 或换行。 }

禁止的特性

原始类型的包装类

TypeScript 代码禁止实例化原始类型 StringBooleanNumber 的包装类(Wrapper Class)。包装类有令人意外的行为,例如 new Boolean(false) 的求值结果为 true

const s = new String('hello'); const b = new Boolean(false); const n = new Number(5);

包装器可以作为函数调用来进行强制转换(这优于使用 + 或拼接空字符串)或创建 symbol。参见类型强制转换了解更多信息。

自动分号插入

不要依赖自动分号插入(Automatic Semicolon Insertion, ASI)。使用分号显式结束所有语句。这可以防止由于不正确的分号插入而导致的 bug,并确保与 ASI 支持有限的工具(例如 clang-format)的兼容性。

Const 枚举

代码禁止使用 const enum;改用普通的 enum

为什么?

TypeScript 枚举本身已经不可变;const enum 是一个与优化相关的独立语言特性,它使得枚举对模块的 JavaScript 用户不可见。

Debugger 语句

生产代码中禁止包含 debugger 语句。

function debugMe() { debugger; }

with

不要使用 with 关键字。它使你的代码更难理解,且自 ES5 起已在严格模式中被禁止 

动态代码求值

不要使用 evalFunction(...string) 构造函数(代码加载器除外)。这些特性可能是危险的,并且在使用严格内容安全策略(Content Security Policy) 的环境中根本无法工作。

非标准特性

不要使用非标准的 ECMAScript 或 Web 平台特性。

这包括:

  • 已被标记为废弃或已从 ECMAScript / Web 平台中完全移除的旧特性(参见 MDN 
  • 尚未标准化的新 ECMAScript 特性
    • 避免使用当前 TC39 工作草案中的特性或当前处于提案流程 中的特性
    • 仅使用当前 ECMA-262 规范中定义的 ECMAScript 特性
  • 已提出但尚未完成的 Web 标准:
  • 非标准的语言”扩展”(例如某些外部转译器提供的)

针对特定 JavaScript 运行时的项目,例如仅限最新 Chrome、Chrome 扩展、Node.JS、Electron,显然可以使用这些 API。在考虑专有且仅在某些浏览器中实现的 API 表面时要谨慎;考虑是否有通用库可以为你抽象此 API 表面。

修改内置对象

永远不要修改内置类型,无论是向其构造函数还是原型添加方法。避免依赖这样做的库。

除非绝对必要(例如第三方 API 要求),否则不要向全局对象添加符号。

Last updated on