Merge branch 'dev'

This commit is contained in:
TinkerRobot
2021-09-02 16:50:40 +08:00
10 changed files with 2454 additions and 0 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
# VS Code Config Files
.vscode/
# JetBrains Webstorm Config Files
.idea/

View File

@@ -13,3 +13,4 @@
google-python-styleguide/contents
google-shell-styleguide/contents
google-javascript-styleguide/contents
google-typescript-styleguide/contents

View File

@@ -1046,6 +1046,8 @@ JavaScript中的类型
//出现this未定义警告
goog.bind(function() { this.someProperty; });
.. _js-comments:
注释
----------

View File

@@ -0,0 +1,52 @@
一致性
################################################################################
对于本文中并未明确解释的任何与代码风格有关的问题,都应当与同一文件中其它代码的现有写法 **保持一致** 。如果问题仍未得到解决,则应当参考同一文件夹下其它文件的写法。
.. _ts-goals:
目标
********************************************************************************
通常情况下,程序员自己是最了解他们的代码需求的人。所以,对于那些答案不唯一、而且最优解取决于实际场景的问题,一般应当由当事人根据情况自行决定解决方案。因此,对于这类问题,默认回答往往都是“不管了”。
以下几点则是其中的特例,它们解释了为什么要在这篇风格指南中编写全局性的规范。对于程序员自行规定的代码风格,应当根据以下几个原则对其进行评估:
1. **应当避免使用已知的会导致问题的代码范式,尤其是对于这门语言的新手而言**
例如:
* ``any`` 是一个容易被误用的类型(某个变量 *真的* 可以既是一个数字,同时还可以作为函数被调用吗?),因此关于它的用法,指南中提出了一些建议。
* TypeScript 的命名空间会为闭包优化带来问题。
* 在文件名中使用句点 ``.`` 会让导入语句的样式变得不美观且令人困惑。
* 类中的静态函数对优化十分不友好,同样的功能完全可以由文件级函数实现。
* 不熟悉 ``private`` 关键字的用户会试图使用下划线将函数名变得混乱难懂。
2. **跨项目的代码应当保持一致的用法**
如果有两种语义上等价只是形式上不同的写法,应当只选择其中的一种,以避免代码中发生无意义的发散演化,同时也避免在代码审查的过程中进行无意义的争辩。
除此之外,还应当尽可能与 JavaScript 的代码风格保持一致,因为大部分程序员都会同时使用两种语言。
例如:
* 变量名的首字母大小写风格。
* ``x as T`` 语法和等价的 ``<T>x`` 语法(后者不允许使用)。
* ``Array<[number, number]>````[number, number][]``
3. **代码应当具有长期可维护性**
代码的生命周期往往比其原始作者为其工作的时间要长,而 TypeScript 团队必须保证谷歌的所有工作在未来依然能顺利进行。
例如:
* 使用自动化工具修改代码,所有的代码均经过自动格式化以符合空格样式的规范。
* 规定了一组 Clousure 编译器标识,使 TS 代码库在编写过程中无需考虑编译选项的问题,也让用户能够安全地使用共享库。
* 代码在使用其它库时必须进行导入(严格依赖),以便依赖项中的重构不会改变其用户的依赖项。
* 用户必须编写测试。如果没有测试,就无法保证对语言或 google3 库中的改动不会破坏用户现有的代码。
4. **代码审查员应当着力于提高代码质量,而非强制推行各种规则**
如果能够将规范实现为自动化检查工具,这通常都是一个好的做法。这对上文中的第三条原则也有所帮助。
对于确实无关紧要的问题,例如语言中十分罕见的边界情况,或者避免了一个不太可能发生的 Bug ,等等,不妨直接无视之。

View File

@@ -0,0 +1,13 @@
TypeScript 风格指南
################################################################################
.. toctree::
:caption: 目录
:numbered:
preface
syntax
language
source_organization
type_system
consistency

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
前言
################################################################################
.. _ts-introduction:
简介
********************************************************************************
这份风格指南基于谷歌的内部版本,并在此基础上做了一些修改,使其具有更广泛的适用性。指南并非定期自动部署,而是由志愿者们根据需求进行维护与更新。
指南中的内容包括代码规范与最佳实践两部分。读者可根据所在团队的需求加以参考和选用。
指南中对 *必须、禁止、应当、不应、可以* 等词语的用法遵循 `RFC 2119 <https://datatracker.ietf.org/doc/html/rfc2119>`_ 中的定义。文中的所有示例均非适合实际项目的正式用法,只用于对指南中的内容加以说明。
.. _ts-about:
翻译信息
********************************************************************************
.. _ts-about-last-update:
上次更新日期
================================================================================
2021 年 09 月 02 日。
.. _ts-about-author:
作者
================================================================================
* `TinkerRobot <https://github.com/tinkerrobot>`_
.. _ts-about-original:
原文链接
================================================================================
`Google TypeScript Style Guide <https://google.github.io/styleguide/tsguide.html>`_
.. _ts-about-translation:
中文版链接
================================================================================
`谷歌 TypeScript 风格指南 <https://zh-google-styleguide.readthedocs.io/en/latest/google-typescript-styleguide/>`_
.. _ts-about-changelog:
修订历史
================================================================================
* **2021 年 09 月 02 日:** `TinkerRobot <https://github.com/tinkerrobot>`_ 提交了第一个版本。

View File

@@ -0,0 +1,331 @@
代码管理
################################################################################
.. _ts-modules:
模块
********************************************************************************
.. _import-paths:
导入路径
================================================================================
TypeScript 代码必须使用路径进行导入。这里的路径既可以是相对路径,以 ``.````..`` 开头,也可以是从项目根目录开始的绝对路径,如 ``root/path/to/file``
在引用逻辑上属于同一项目的文件时,应使用相对路径 ``./foo`` ,不要使用绝对路径 ``path/to/foo``
应尽可能地限制父层级的数量(避免出现诸如 ``../../../`` 的路径),过多的层级会导致模块和路径结构难以理解。
.. code-block:: typescript
import {Symbol1} from 'google3/path/from/root';
import {Symbol2} from '../parent/file';
import {Symbol3} from './sibling';
.. _namespaces-vs-modules:
用 命名空间 还是 模块?
================================================================================
在 TypeScript 有两种组织代码的方式命名空间namespace和模块module
不允许使用命名空间,在 TypeScript 中必须使用模块(即 `ES6 模块 <http://exploringjs.com/es6/ch_modules.html>`_ )。也就是说,在引用其它文件中的代码时必须以 ``import {foo} from 'bar'`` 的形式进行导入和导出。
不允许使用 ``namespace Foo { ... }`` 的形式组织代码。命名空间只能在所用的外部第三方库有要求时才能使用。如果需要在语义上对代码划分命名空间,应当通过分成不同文件的方式实现。
不允许在导入时使用 ``require`` 关键字(形如 ``import x = require('...');`` )。应当使用 ES6 的模块语法。
.. code-block:: typescript
// 不要这样做!不要使用命名空间!
namespace Rocket {
function launch() { ... }
}
// 不要这样做!不要使用 <reference>
/// <reference path="..."/>
// 不要这样做!不要使用 require()
import x = require('mydep');
.. tip::
TypeScript 的命名空间早期也被称为内部模块并使用 ``module`` 关键字,形如 ``module Foo { ... }`` 。不要使用这种用法。任何时候都应当使用 ES6 的导入语法。
.. _ts-exports:
导出
********************************************************************************
代码中必须使用具名的导出声明。
.. code-block:: typescript
// Use named exports:
export class Foo { ... }
不要使用默认导出,这样能保证所有的导入语句都遵循统一的范式:
.. code-block:: typescript
// 不要这样做!不要使用默认导出!
export default class Foo { ... }
为什么?因为默认导出并不为被导出的符号提供一个标准的名称,这增加了维护的难度和降低可读性的风险,同时并未带来明显的益处。如下面的例子所示:
.. code-block:: typescript
// 默认导出会造成如下的弊端
import Foo from './bar'; // 这个语句是合法的。
import Bar from './bar'; // 这个语句也是合法的。
具名导出的一个优势是,当代码中试图导入一个并未被导出的符号时,这段代码会报错。例如,假设在 ``foo.ts`` 中有如下的导出声明:
.. code-block:: typescript
// 不要这样做!
const foo = 'blah';
export default foo;
如果在 ``bar.ts`` 中有如下的导入语句:
.. code-block:: typescript
// 编译错误!
import {fizz} from './foo';
会导致编译错误: ``error TS2614: Module '"./foo"' has no exported member 'fizz'`` 。反之,如果在 ``bar.ts`` 中的导入语句为:
.. code-block:: typescript
// 不要这样做!这定义了一个多余的变量 fizz
import fizz from './foo';
结果是 ``fizz === foo`` ,这往往不符合预期,且难以调试。
此外,默认导出会鼓励程序员将所有内容全部置于一个巨大的对象当中,这个对象实际上充当了命名空间的角色:
.. code-block:: typescript
// 不要这样做!
export default class Foo {
static SOME_CONSTANT = ...
static someHelpfulFunction() { ... }
...
}
显然,这个文件中具有文件作用域,它可以被用做命名空间。但是,这里创建了第二个作用域——类 ``Foo`` ,这个类在其它文件中具有歧义:它既可以被视为类型,又可以被视为值。
因此,应当使用文件作用域作为实质上的命名空间,同时使用具名的导出声明:
.. code-block:: typescript
// 应当这样做!
export const SOME_CONSTANT = ...
export function someHelpfulFunction()
export class Foo {
// 只有类 Foo 中的内容
}
.. _ts-export-visibility:
导出可见性
================================================================================
TypeScript 不支持限制导出符号的可见性。因此,不要导出不用于模块以外的符号。一般来说,应当尽量减小模块的外部 API 的规模。
.. _ts-mutable-exports:
可变导出
================================================================================
虽然技术上可以实现,但是可变导出会造成难以理解和调试的代码,尤其是对于在多个模块中经过了多次重新导出的符号。这条规则的一个例子是,不允许使用 ``export let``
.. code-block:: typescript
// 不要这样做!
export let foo = 3;
// 在纯 ES6 环境中,变量 foo 是一个可变值,导入了 foo 的代码会观察到它的值在一秒钟之后发生了改变。
// 在 TypeScript 中,如果 foo 被另一个文件重新导出了,导入该文件的代码则不会观察到变化。
window.setTimeout(() => {
foo = 4;
}, 1000 /* ms */);
如果确实需要允许外部代码对可变值进行访问,应当提供一个显式的取值器。
.. code-block:: typescript
// 应当这样做!
let foo = 3;
window.setTimeout(() => {
foo = 4;
}, 1000 /* ms */);
// 使用显式的取值器对可变导出进行访问。
export function getFoo() { return foo; };
有一种常见的编程情景是,要根据某种特定的条件从两个值中选取其中一个进行导出:先检查条件,然后导出。这种情况下,应当保证模块中的代码执行完毕后,导出的结果就是确定的。
.. code-block:: typescript
function pickApi() {
if (useOtherApi()) return OtherApi;
return RegularApi;
}
export const SomeApi = pickApi();
.. _ts-container-classes:
容器类
================================================================================
不要为了实现命名空间创建含有静态方法或属性的容器类。
.. code-block:: typescript
// 不要这样做!
export class Container {
static FOO = 1;
static bar() { return 1; }
}
应当将这些方法和属性设为单独导出的常数和函数。
.. code-block:: typescript
// 应当这样做!
export const FOO = 1;
export function bar() { return 1; }
.. _ts-imports-source-organization:
导入
********************************************************************************
在 ES6 和 TypeScript 中,导入语句共有四种变体:
======================================== ======================================== ========================================
导入类型 示例 用途
======================================== ======================================== ========================================
模块 ``import * as foo from '...';`` TypeScript 导入方式
解构 ``import {SomeThing} from '...';`` TypeScript 导入方式
默认 ``import SomeThing from '...';`` 只用于外部代码的特殊需求
副作用 ``import '...';`` 只用于加载某些库的副作用(例如自定义元素)
======================================== ======================================== ========================================
.. code-block:: typescript
// 应当这样做!从这两种变体中选择较合适的一种(见下文)。
import * as ng from '@angular/core';
import {Foo} from './foo';
// 只在有需要时使用默认导入。
import Button from 'Button';
// 有时导入某些库是为了其代码执行时的副作用。
import 'jasmine';
import '@polymer/paper-button';
.. _ts-module-versus-destructuring-imports:
选择模块导入还是解构导入?
================================================================================
根据使用场景的不同,模块导入和解构导入分别有其各自的优势。
虽然模块导入语句中出现了通配符 ``*`` ,但模块导入并不能因此被视为其它语言中的通配符导入。相反地,模块导入语句为整个模块提供了一个名称,模块中的所有符号都通过这个名称进行访问,这为代码提供了更好的可读性,同时令模块中的所有符号可以进行自动补全。模块导入减少了导入语句的数量(模块中的所有符号都可以使用),降低了命名冲突的出现几率,同时还允许为被导入的模块提供一个简洁的名称。在从一个大型 API 中导入多个不同的符号时,模块导入语句尤其有用。
解构导入语句则为每一个被导入的符号提供一个局部的名称,这样在使用被导入的符号时,代码可以更简洁。对那些十分常用的符号,例如 Jasmine 的 ``describe````it`` 来说,这一点尤其有用。
.. code-block:: typescript
// 不要这样做!无意义地使用命名空间中的名称使得导入语句过于冗长。
import {TableViewItem, TableViewHeader, TableViewRow, TableViewModel,
TableViewRenderer} from './tableview';
let item: TableViewItem = ...;
.. code-block:: typescript
// 应当这样做!使用模块作为命名空间。
import * as tableview from './tableview';
let item: tableview.Item = ...;
.. code-block:: typescript
import * as testing from './testing';
// 所有的测试都只会重复地使用相同的三个函数。
// 如果只需要导入少数几个符号,而这些符号的使用频率又非常高的话,
// 也可以考虑使用解构导入语句直接导入这几个符号(见下文)。
testing.describe('foo', () => {
testing.it('bar', () => {
testing.expect(...);
testing.expect(...);
});
});
.. code-block:: typescript
// 这样做更好!为这几个常用的函数提供局部变量名。
import {describe, it, expect} from './testing';
describe('foo', () => {
it('bar', () => {
expect(...);
expect(...);
});
});
...
.. _ts-renaming-imports:
重命名导入
================================================================================
在代码中,应当通过使用模块导入或重命名导出解决命名冲突。此外,在需要时,也可以使用重命名导入(例如 ``import {SomeThing as SomeOtherThing}`` )。
在以下几种情况下,重命名导入可能较为有用:
1. 避免与其它导入的符号产生命名冲突。
2. 被导入符号的名称是自动生成的。
3. 被导入符号的名称不能清晰地描述其自身,需要通过重命名提高代码的可读性,如将 RxJS 的 ``from`` 函数重命名为 ``observableFrom``
.. _ts-import-export-type:
``import type`` 和 ``export type``
================================================================================
不要使用 ``import type ... from`` 或者 ``export type ... from``
.. tip::
这一规则不适用于导出类型定义,如 ``export type Foo = ...;``
.. code-block:: typescript
// 不要这样做!
import type {Foo} from './foo';
export type {Bar} from './bar';
应当使用常规的导入语句。
.. code-block:: typescript
// 应当这样做!
import {Foo} from './foo';
export {Bar} from './bar';
TypeScript 的工具链会自动区分用作类型的符号和用作值的符号。对于类型引用,工具链不会生成运行时加载的代码。这样做的原因是为了提供更好的开发体验,否则在 ``import type````import`` 之间反复切换会非常繁琐。同时, ``import type`` 并不提供任何保证,因为代码仍然可以通过其它的途径导入同一个依赖。
如果需要在运行时加载代码以执行其副作用,应使用 ``import '...'`` ,参见 :ref:`ts-imports-source-organization` 一节。
使用 ``export type`` 似乎可以避免将某个用作值的符号导出为 API。然而``import type`` 类似, ``export type`` 也不提供任何保证,因为外部代码仍然可以通过其它途径导入。如果需要拆分对 API 作为值的使用和作为类型的使用,并保证二者不被混用的话,应当显式地将其拆分成不同的符号,例如 ``UserService````AjaxUserService`` ,这样不容易造成错误,同时能更好地表达设计思路。
.. _ts-organize-by-feature:
根据特征组织代码
********************************************************************************
应当根据特征而非类型组织代码。例如,一个在线商城的代码应当按照 ``products`` ``checkout`` ``backend`` 等分类,而不是 ``views`` ``models`` ``controllers``

View File

@@ -0,0 +1,363 @@
语法规范
################################################################################
.. _ts-identifiers:
标识符
********************************************************************************
.. _ts-naming:
命名规范
================================================================================
在 TypeScript 中,标识符只能使用 ASCII 码表中的字母、数字、下划线与 ``(``。因此,合法的标识符可以使用正则表达式 ``[\)\w]+`` 进行匹配。根据标识符的用途不同,使用的命名法也不同,如下表所示:
======================================== ========================================
命名法 分类
======================================== ========================================
帕斯卡命名法( ``UpperCamelCase`` 类、接口、类型、枚举、装饰器、类型参数
驼峰式命名法( ``lowerCamelCase`` 变量、参数、函数、方法、属性、模块别名
全大写下划线命名法( ``CONSTANT_CASE`` 全局常量、枚举值
私有成员命名法( ``#ident`` 不允许使用
======================================== ========================================
.. _ts-abbreviations:
缩写
--------------------------------------------------------------------------------
缩写应被视为一个词。例如,应使用 ``loadHttpUrl``,而非 ``loadHTTPURL``。平台有特殊要求的标识符例外,如 ``XMLHttpRequest``
.. _ts-dollar-sign:
美元符号 \$
--------------------------------------------------------------------------------
一般情况下,标识符不应使用 `$`,除非为了与第三方框架的命名规范保持一致。关于 `$` 的使用,可参见 :ref:`ts-naming-style` 一节对 ``Observable`` 类型的说明。
.. _ts-type-parameters:
类型参数
--------------------------------------------------------------------------------
形如 ``Array<T>`` 的类型参数既可以使用单个大写字母(如 ``T``),也可以使用帕斯卡命名法(如 ``UpperCamelCase``)。
.. _ts-test-names:
测试用例
--------------------------------------------------------------------------------
无论是在 `Closure <https://github.com/google/closure-library>`_ 库的 ``testSuites`` 还是 `xUnit <https://xunit.net/>`_ 风格的测试框架中,都可以使用 ``_`` 作为标识符的分隔符,例如 ``testX_whenY_doesZ()``
.. _ts-underscore-prefix-suffix:
``_`` 前缀与后缀
--------------------------------------------------------------------------------
标识符禁止使用下划线 ``_`` 作为前缀或后缀。这也意味着,禁止使用单个下划线 ``_`` 作为标识符(例如:用来表示未被使用的参数)。
如果需要从数组或元组中取出某个或某几个特定的元素的话,可以在解构语句中插入额外的逗号,忽略掉不需要的元素:
.. code-block:: typescript
const [a, , b] = [1, 5, 10]; // a <- 1, b <- 10
.. _ts-imports:
导入模块
--------------------------------------------------------------------------------
导入模块的命名空间时使用驼峰命名法(``lowerCamelCase``),文件名则使用蛇形命名法(``snake_case``)。例如:
.. code-block:: typescript
import * as fooBar from './foo_bar';
一些库可能会在导入命名空间时使用某种特定的前缀,这与这里规定的命名规范有所冲突。然而,由于其中的一些库已经被广泛使用,因此遵循它们的特殊规则反而能够获得更好的可读性。这些特例包括:
* `jQuery <https://jquery.com/>`_使用 ``$`` 前缀。
* `three.js <https://threejs.org/>`_,使用 ``THREE`` 前缀。
.. _ts-constants:
常量
--------------------------------------------------------------------------------
常量命名(``CONSTANT_CASE``表示某个值不可被修改。它还可以用于虽然技术上可以实现但是用户不应当试图修改的值比如并未进行深度冻结deep frozen的值。
.. code-block:: typescript
const UNIT_SUFFIXES = {
'milliseconds': 'ms',
'seconds': 's',
};
// UNIT_SUFFIXES 使用了常量命名,
// 这意味着用户不应试图修改它,
// 即使它实际上是一个可变的值。
这里所说的常量,也包括类中的静态只读属性:
.. code-block:: typescript
class Foo {
private static readonly MY_SPECIAL_NUMBER = 5;
bar() {
return 2 * Foo.MY_SPECIAL_NUMBER;
}
}
.. _ts-others:
其他
--------------------------------------------------------------------------------
如果某个值在程序的整个运行生命周期中会被多次实例化或被用户以任何方式进行修改,则它必须使用驼峰式命名法。
如果某个值是作为某个接口的实现的箭头函数,则它也可以使用驼峰式命名法。
.. _ts-aliases:
别名
================================================================================
在为一个已有的标识符创建具有局部作用域的别名时,别名的命名方式应当与现有的标识符和现有的命名规范保持一致。声明别名时,应使用 ``const`` (如果它是一个变量)或 ``readonly`` (如果它是类里的一个字段)。
.. code-block:: typescript
const {Foo} = SomeType;
const CAPACITY = 5;
class Teapot {
readonly BrewStateEnum = BrewStateEnum;
readonly CAPACITY = CAPACITY;
}
.. _ts-naming-style:
命名风格
================================================================================
TypeScript 中的类型表达了丰富的信息,因此在起名时不应与类型中所携带的信息重复。(关于更多在起名时应避免的内容,可参见谷歌的 `Testing Blog <https://testing.googleblog.com/2017/10/code-health-identifiernamingpostforworl.html>`_。)
这里有几个具体的例子:
* 不要为私有属性或方法名添加下划线 `_` 前缀或后缀。
* 不要为可选参数添加 `opt_` 前缀。
* 关于在存取器中的特例,参见后文 :ref:`name-and-order-of-includes`
* 除非在项目中已成惯例,否则不要显式地标记接口类型(例如不要使用 ``IMyInterface`` 或者 ``MyFooInterface`` )。在为类添加接口时,接口名称中应包含创建这一接口的原因。(例如,在为类 ``TodoItem`` 创建一个将其转为 JSON 格式以用于存储或者序列化的接口时,可以将这一接口命名为 ``TodoItemStorage`` 。)
* 对于 ``Observable`` 类型的值,通常的惯例是使用 ``$`` 前缀将其与一般类型的值进行区分,使之不致混淆。各个团队可以在与项目内部的现有做法保持一致的前提下,自行决定是否采用这一做法。
.. _ts-descriptive-names:
描述性命名
================================================================================
命名应当具有描述性且易于读者理解。不要使用对项目以外的用户而言含糊不清或并不熟悉的缩写,不要通过删减单词中的字母来强行创造缩写。
这一规则的例外是,对不超过十行的作用域中的变量,以及内部 API 的参数,可以使用短变量名(例如 ``i````j`` 等只有单个字母的变量名)。
.. _ts-file-encoding:
文件编码
********************************************************************************
使用 UTF-8 文件编码。
对于非 ASCII 字符,应使用实际的 Unicode 字符(例如 ```` )。对于非输出字符,使用对应的十六进制编码或 Unicode 转义编码(如 ``\u221e`` ),并添加注释进行说明。
.. code-block:: typescript
// 应当这样做!即使没有注释也十分易懂。
const units = 'μs';
// 应当这样做!对非输出字符进行转义。
const output = '\ufeff' + content; // 字节顺序标记Byte Order MarkBOM
.. code-block:: typescript
// 不要这样做!即使加上注释也不太好读,而且容易出错。
const units = '\u03bcs'; // Greek letter mu, 's'
// 不要省略注释!读者在缺少注释的情况下很难理解这个字符的含义。
const output = '\ufeff' + content;
.. _ts-comments-documentation:
注释与文档
********************************************************************************
.. _ts-jsdoc-vs-comments:
用 JSDoc 还是 注释?
================================================================================
TypesScript 中有两种类型的注释JSDoc ``/** ... */`` 和普通注释 ``// ... 或者 /* ... */``
* 对于文档,也就是用户应当阅读的注释,使用 ``/** JSDoc */``
* 对于实现说明,也就是只和代码本身的实现细节有关的注释,使用 ``// 行注释``
JSDoc 注释能够为工具(例如编辑器或文档生成器)所识别,而普通注释只能供人阅读。
.. _ts-jsdoc-rules-follow-the-js-style:
JSDoc 规范
================================================================================
JSDoc 的规范大部分遵循 JavaScript 风格指南中的规定。具体地说,遵循 JavaScript 风格指南中 :ref:`js-comments` 一节的规则。本节的剩余部分只对与这些规则不一致的部分进行说明。
.. _ts-document-all-top-level-exports-of-modules:
对所有导出的顶层模块进行注释
================================================================================
使用 ``/** JSDoc */`` 注释为代码的用户提供信息。这些注释应当言之有物,切忌仅仅将属性名或参数名重抄一遍。如果代码的审核人认为某个属性或方法的作用不能从它的名字上一目了然地看出来的话,这些属性和方法同样应当使用 ``/** JSDoc */`` 注释添加说明文档,无论它们是否被导出,是公开还是私有的。
.. _ts-omit-comments-that-are-redundant-with-ts:
省略对于 TypeScript 而言多余的注释
================================================================================
例如,不要在 ``@param````@return`` 注释中声明类型,不要在使用了 ``implements````enum````private`` 等关键字的地方添加 ``@implements````@enum````@private`` 等注释。
.. _ts-do-not-use-override:
不要使用 ``@override``
================================================================================
不要在 TypeScript 代码中使用 ``@override`` 注释。 ``@override`` 并不会被编译器视为强制性约束,这会导致注释与实现上的不一致性。如果纯粹为了文档添加这一注释,反而令人困惑。
.. _ts-make-comments-that-actually-add-information:
注释必须言之有物
================================================================================
虽然大多数情况下文档对代码十分有益,但对于那些并不用于导出的符号,有时其函数或参数的名称与类型便足以描述自身了。
注释切忌照抄参数类型和参数名,如下面的反面示例:
.. code-block:: typescript
// 不要这样做!这个注释没有任何有意义的内容。
/** @param fooBarService Foo 应用的 Bar 服务 */
因此,只有当需要添加额外信息时才使用 ``@param````@return`` 注释,其它情况下直接省略即可。
.. code-block:: typescript
/**
* 发送 POST 请求,开始煮咖啡
* @param amountLitres 煮咖啡的量,注意和煮锅的尺寸对应!
*/
brew(amountLitres: number, logger: Logger) {
// ...
}
.. _ts-parameter-property-comments:
参数属性注释
================================================================================
通过为构造函数的参数添加访问限定符,参数属性同时创建了构造函数参数和类成员。例如,如下的构造函数
.. code-block:: typescript
class Foo {
constructor(private readonly bar: Bar) { }
}
``Foo`` 类创建了 ``Bar`` 类型的成员 ``bar``
如果要为这些成员添加文档,应使用 JSDoc 的 ``@param`` 注释,这样编辑器会在调用构造函数和访问属性时显示对应的文档描述信息。
.. code-block:: typescript
/** 这个类演示了如何为参数属性添加文档 */
class ParamProps {
/**
* @param percolator 煮咖啡所用的咖啡壶。
* @param beans 煮咖啡所用的咖啡豆。
*/
constructor(
private readonly percolator: Percolator,
private readonly beans: CoffeeBean[]) {}
}
.. code-block:: typescript
/** 这个类演示了如何为普通成员添加文档 */
class OrdinaryClass {
/** 下次调用 brew() 时所用的咖啡豆。 */
nextBean: CoffeeBean;
constructor(initialBean: CoffeeBean) {
this.nextBean = initialBean;
}
}
.. _ts-comments-when-calling-a-function:
函数调用注释
================================================================================
如果有需要,可以在函数的调用点使用行内的 ``/* 块注释 */`` 为参数添加文档,或者使用字面量对象为参数添加名称并在函数声明中进行解构。注释的格式和位置没有明确的规定。
.. code-block:: typescript
// 使用行内块注释为难以理解的参数添加说明:
new Percolator().brew(/* amountLitres= */ 5);
// 或者使用字面量对象为参数命名,并在函数 brew 的声明中将参数解构:
new Percolator().brew({amountLitres: 5});
.. code-block:: typescript
/** 一个古老的咖啡壶 {@link CoffeeBrewer} */
export class Percolator implements CoffeeBrewer {
/**
* 煮咖啡。
* @param amountLitres 煮咖啡的量,注意必须和煮锅的尺寸对应!
*/
brew(amountLitres: number) {
// 这个实现煮出来的咖啡味道差极了,不管了。
// TODO(b/12345): 优化煮咖啡的过程。
}
}
.. _ts-place-documentation-prior-to-decorators:
将文档置于装饰器之前
================================================================================
文档、方法或者属性如果同时具有装饰器(例如 ``@Component``)和 JSDoc 注释,应当将 JSDoc 置于装饰器之前。
禁止将 JSDoc 置于装饰器和被装饰的对象之间。
.. code-block:: typescript
// 不要这样做JSDoc 被放在装饰器 @Component 和类 FooComponent 中间了!
@Component({
selector: 'foo',
template: 'bar',
})
/** 打印 "bar" 的组件。 */
export class FooComponent {}
应当将 JSDoc 置于装饰器之前。
.. code-block:: typescript
/** 打印 "bar" 的组件。 */
@Component({
selector: 'foo',
template: 'bar',
})
export class FooComponent {}

View File

@@ -0,0 +1,485 @@
类型系统
################################################################################
.. _ts-type-inference:
类型推导
********************************************************************************
对于所有类型的表达式(包括变量、字段、返回值,等等),都可以依赖 TypeScript 编译器所实现的类型推导。 google3 编译器会拒绝所有缺少类型记号又无法推导出其类型的代码,以保证所有的代码都具有类型(即使其中可能包括显式的 ``any`` 类型)。
.. code-block:: typescript
const x = 15; // x 的类型可以推导得出.
当变量或参数被初始化为 ``string`` ``number`` ``boolean`` ``RegExp`` 正则表达式字面量或 ``new`` 表达式时,由于明显能够推导出类型,因此应当省略类型记号。
.. code-block:: typescript
// 不要这样做!添加 boolean 记号对提高可读性没有任何帮助!
const x: boolean = true;
.. code-block:: typescript
// 不要这样做Set 类型显然可以从初始化语句中推导得出。
const x: Set<string> = new Set();
.. code-block:: typescript
// 应当这样做!依赖 TypeScript 的类型推导。
const x = new Set<string>();
对于更为复杂的表达式,类型记号有助于提高代码的可读性。此时是否使用类型记号应当由代码审查员决定。
.. _ts-return-types:
返回类型
================================================================================
代码的作者可以自由决定是否在函数和方法中使用类型记号标明返回类型。代码审查员 *可以* 要求对难以理解的复杂返回类型使用类型记号进行阐明。项目内部 *可以* 自行规定必须标明返回值,本文作为一个通用的 TypeScript 风格指南,不做硬性要求。
显式地标明函数和方法的返回值有两个优点:
* 能够生成更精确的文档,有助于读者理解代码。
* 如果未来改变了函数的返回类型的话,可以让因此导致的潜在的错误更快地暴露出来。
.. _ts-null-vs-undefined:
Null 还是 Undefined
********************************************************************************
TypeScript 支持 ``null````undefined`` 类型。可空类型可以通过联合类型实现,例如 ``string | null`` 。对于 ``undefined`` 也是类似的。对于 ``null````undefined`` 的联合类型,并无特殊的语法。
TypeScript 代码中可以使用 ``undefined`` 或者 ``null`` 标记缺少的值,这里并无通用的规则约定应当使用其中的某一种。许多 JavaScript API 使用 ``undefined`` (例如 ``Map.get`` ),然而 DOM 和 Google API 中则更多地使用 ``null`` (例如 ``Element.getAttribute`` ),因此,对于 ``null````undefined`` 的选择取决于当前的上下文。
.. _ts-nullable-undefined-type-aliases:
可空/未定义类型别名
================================================================================
*不允许* 为包括 ``|null````|undefined`` 的联合类型创建类型别名。这种可空的别名通常意味着空值在应用中会被层层传递,并且它掩盖了导致空值出现的源头。另外,这种别名也让类或接口中的某个值何时有可能为空变得不确定。
因此,代码 *必须* 在使用别名时才允许添加 ``|null`` 或者 ``|undefined`` 。同时,代码 *应当* 在空值出现位置的附近对其进行处理。
.. code-block:: typescript
// 不要这样做!不要在创建别名的时候包含 undefined
type CoffeeResponse = Latte|Americano|undefined;
class CoffeeService {
getLatte(): CoffeeResponse { ... };
}
.. code-block:: typescript
// 应当这样做!在使用别名的时候联合 undefined
type CoffeeResponse = Latte|Americano;
class CoffeeService {
getLatte(): CoffeeResponse|undefined { ... };
}
.. code-block:: typescript
// 这样做更好!使用断言对可能的空值进行处理!
type CoffeeResponse = Latte|Americano;
class CoffeeService {
getLatte(): CoffeeResponse {
return assert(fetchResponse(), 'Coffee maker is broken, file a ticket');
};
}
.. _ts-optionals-vs-undefined-type:
可选参数 还是 ``undefined`` 类型?
================================================================================
TypeScript 支持使用 ``?`` 创建可选参数和可选字段,例如:
.. code-block:: typescript
interface CoffeeOrder {
sugarCubes: number;
milk?: Whole|LowFat|HalfHalf;
}
function pourCoffee(volume?: Milliliter) { ... }
可选参数实际上隐式地向类型中联合了 ``|undefined`` 。不同之处在于,在构造类实例或调用方法时,可选参数可以被直接省略。例如, ``{sugarCubes: 1}`` 是一个合法的 ``CoffeeOrder`` ,因为 ``milk`` 字段是可选的。
应当使用可选字段(对于类或者接口)和可选参数而非联合 ``|undefined`` 类型。
对于类,应当尽可能避免使用可选字段,尽可能初始化每一个字段。
.. code-block:: typescript
class MyClass {
field = '';
}
.. _ts-structural-types-vs-nominal-types:
结构类型 与 指名类型
********************************************************************************
TypeScript 的类型系统使用的是结构类型而非指名类型。具体地说,一个值,如果它拥有某个类型的所有属性,且所有属性的类型能够递归地一一匹配,则这个值与这个类型也是匹配的。
在代码中,可以在适当的场景使用结构类型。具体地说,在测试代码之外,应当使用接口而非类对结构类型进行定义。在测试代码中,由于经常要创建 Mock 对象用于测试,此时不引入额外的接口往往较为方便。
在提供基于结构类型的实现时,应当在符号的声明位置显式地包含其类型,使类型检查和错误检测能够更准确地工作。
.. code-block:: typescript
// 应当这样做!
const foo: Foo = {
a: 123,
b: 'abc',
}
.. code-block:: typescript
// 不要这样做!
const badFoo = {
a: 123,
b: 'abc',
}
为什么要这样做?
这是因为在上文中, ``badFoo`` 对象的类型依赖于类型推导。 ``badFoo`` 对象中可能添加额外的字段,此时类型推导的结果就有可能发生变化。
如果将 ``badFoo`` 传给接收 ``Foo`` 类型参数的函数,错误提示会出现在函数调用的位置,而非对象声明的位置。在大规模的代码仓库中修改接口时,这一点区别会很重要。
.. code-block:: typescript
interface Animal {
sound: string;
name: string;
}
function makeSound(animal: Animal) {}
/**
* 'cat' 的类型会被推导为 '{sound: string}'
*/
const cat = {
sound: 'meow',
};
/**
* 'cat' 的类型并不满足函数参数的要求,
* 因此 TypeScript 编译器会在这里报错,
* 而这里有可能离 'cat' 的定义相当远。
*/
makeSound(cat);
/**
* Horse 具有结构类型,因此这里会提示类型错误,而函数调用点不会报错。
* 这是因为 'horse' 不满足接口 'Animal' 的类型约定。
*/
const horse: Animal = {
sound: 'niegh',
};
const dog: Animal = {
sound: 'bark',
name: 'MrPickles',
};
makeSound(dog);
makeSound(horse);
.. _ts-interface-vs-type-aliases:
接口 还是 类型别名?
********************************************************************************
TypeScript 支持使用 `类型别名 <https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-aliases>`_ 为类型命名。这一功能可以用于基本类型、联合类型、元组以及其它类型。
然而,当需要声明用于对象的类型时,应当使用接口,而非对象字面量表达式的类型别名。
.. code-block:: typescript
// 应当这样做!
interface User {
firstName: string;
lastName: string;
}
.. code-block:: typescript
// 不要这样做!
type User = {
firstName: string,
lastName: string,
}
为什么?
这两种形式是几乎等价的,因此,基于从两个形式中只选择其中一种以避免项目中出现变种的原则,这里选择了更常见的接口形式。另外,这里选择接口还有一个 `有趣的技术原因 <https://ncjamieson.com/prefer-interfaces/>`_ 。这篇博文引用了 TypeScript 团队负责人的话:“老实说,我个人的意见是对于任何可以建模的对象都应当使用接口。相比之下,使用类型别名没有任何优势,尤其是类型别名有许多的显示和性能问题”。
.. _ts-array-type:
``Array<T>`` 类型
********************************************************************************
对于简单类型(名称中只包含字母、数字和点 ``.`` 的类型),应当使用数组的语法糖 ``T[]`` ,而非更长的 ``Array<T>`` 形式。
对于其它复杂的类型,则应当使用较长的 ``Array<T>``
这条规则也适用于 ``readonly T[]````ReadonlyArray<T>``
.. code-block:: typescript
// 应当这样做!
const a: string[];
const b: readonly string[];
const c: ns.MyObj[];
const d: Array<string|number>;
const e: ReadonlyArray<string|number>;
.. code-block:: typescript
// 不要这样做!
const f: Array<string>; // 语法糖写法更短。
const g: ReadonlyArray<string>;
const h: {n: number, s: string}[]; // 大括号和中括号让这行代码难以阅读。
const i: (string|number)[];
const j: readonly (string|number)[];
.. _ts-indexable-type:
索引类型 ``{[key: string]: number}``
********************************************************************************
在 JavaScript 中,使用对象作为关联数组(又称“映射表”、“哈希表”或者“字典”)是一种常见的做法:
.. code-block:: typescript
const fileSizes: {[fileName: string]: number} = {};
fileSizes['readme.txt'] = 541;
在 TypeScript 中,应当为键提供一个有意义的标签名。(当然,这个标签只有在文档中有实际意义,在其它场合是无用的。)
.. code-block:: typescript
// 不要这样做!
const users: {[key: string]: number} = ...;
.. code-block:: typescript
// 应当这样做!
const users: {[userName: string]: number} = ...;
然而,相比使用上面的这种形式,在 TypeScript 中应当考虑使用 ES6 新增的 ``Map````Set`` 类型。因为 JavaScript 对象有一些 `令人困惑又不符合预期的行为 <http://2ality.com/2012/01/objects-as-maps.html>`_ ,而 ES6 的新增类型能够更明确地表达程序员的设计思路。此外, ``Map`` 类型的键和 ``Set`` 类型的元素都允许使用 ``string`` 以外的其他类型。
TypeScript 内建的 ``Record<Keys, ValueType>`` 允许使用已定义的一组键创建类型。它与关联数组的不同之处在于键是静态确定的。关于它的使用建议,参见 :ref:`ts-mapped-conditional-types` 一节。
.. _ts-mapped-conditional-types:
映射类型与条件类型
********************************************************************************
TypeScript 中的 `映射类型 <https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types>`_`条件类型 <https://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types>`_ 让程序员能够在已有类型的基础上构建出新的类型。在 TypeScript 的标准库中有许多类型运算符都是基于这一机制(例如 ``Record````Partial````Readonly`` 等等)。
TypeScript 类型系统的这一特性让创建新类型变得简洁,还程序员在设计代码抽象时,既能实现强大的功能,同时海能保证类型安全。然而,它们也有一些缺点:
* 相较于显式地指定属性与类型间关系(例如使用接口和继承,参见下文中的例子),类型运算符需要读者在头脑中自行对后方的类型表达式进行求值。本质上说,这增加了程序的理解难度,尤其是在类型推导和类型表达式有可能横跨数个文件的情况下。
* 映射类型与条件类型的求值模型并没有明确的规范,且经常随着 TypeScript 编译器的版本更新而发生变化,因此并不总是易于理解,尤其是与类型推导一同使用时。因此,代码有可能只是碰巧能够通过编译或者给出正确的结果。在这种情况下,使用类型运算符增加了代码未来的维护成本。
* 映射类型与条件类型最为强大之处在于,它们能够从复杂且/或推导的类型中派生出新的类型。然而从另一方面看,这样做也很容易导致程序难于理解与维护。
* 有些语法工具并不能够很好地支持类型系统的这一特性。例如,一些 IDE 的“查找引用”功能(以及依赖于它的“重命名重构”)无法发现位于 ``Pick<T, Keys>`` 类型中的属性,因而在查找结果中不会将其设为高亮。
因此,推荐的代码规范如下:
* 任何使用都应当使用最简单的类型构造方式进行表达。
* 一定程度的重复或冗余,往往好过复杂的类型表达式带来的长远维护成本。
* 映射类型和条件类型必须在符合上述理念的情况下使用。
例如TypeScript 内建的 ``Pick<T, Keys>`` 类型允许以类型 ``T`` 的子集创建新的类型。然而,使用接口和继承的方式实现往往更易于理解。
.. code-block:: typescript
interface User {
shoeSize: number;
favoriteIcecream: string;
favoriteChocolate: string;
}
// FoodPreferences 类型拥有 favoriteIcecream 和 favoriteChocolate但不包括 shoeSize。
type FoodPreferences = Pick<User, 'favoriteIcecream'|'favoriteChocolate'>;
这种写法等价于显式地写出 ``FoodPreferences`` 的属性:
.. code-block:: typescript
interface FoodPreferences {
favoriteIcecream: string;
favoriteChocolate: string;
}
为了减少重复,可以让 ``User`` 继承 ``FoodPreferences`` ,或者在 ``User`` 中嵌套一个类型为 ``FoodPrefences`` 的字段(这样做可能更好):
.. code-block:: typescript
interface FoodPreferences { /* 同上 */ }
interface User extends FoodPreferences {
shoeSize: number;
// 这样 User 也包括了 FoodPreferences 的字段。
}
使用接口让属性的分类变得清晰IDE 的支持更完善,方便进一步优化,同时使得代码更易于理解。
.. _ts-any-type:
``any`` 类型
********************************************************************************
TypeScript 的 ``any`` 类型是所有其它类型的超类,又是所有其它类型的子类,同时还允许解引用一切属性。因此,使用 ``any`` 十分危险——它会掩盖严重的程序错误,并且它从根本上破坏了对应的值“具有静态属性”的原则。
尽可能 *不要* 使用 ``any`` 。如果出现了需要使用 ``any`` 的场景,可以考虑下列的解决方案:
* :ref:`ts-provide-a-more-specific-type`
* :ref:`ts-using-unknown-over-any`
* :ref:`ts-suppress-the-lint-warning`
.. _ts-provide-a-more-specific-type:
提供一个更具体的类型
================================================================================
使用接口、内联对象类型、或者类型别名:
.. code-block:: typescript
// 声明接口类型以表示服务端发送的 JSON。
declare interface MyUserJson {
name: string;
email: string;
}
// 对重复出现的类型使用类型别名。
type MyType = number|string;
// 或者对复杂的返回类型使用内联对象类型。
function getTwoThings(): {something: number, other: string} {
// ...
return {something, other};
}
// 使用泛型,有些库在这种情况下可能会使用 any 表示
// 这里并不考虑函数所作用于的参数类型。
// 注意,对于这种写法,“只有泛型的返回类型”一节有更详细的规范。
function nicestElement<T>(items: T[]): T {
// 在 items 中查找最棒的元素。
// 这里还可以进一步为泛型参数 T 添加限制,例如 <T extends HTMLElement>。
}
.. _ts-using-unknown-over-any:
使用 ``unknown`` 而非 ``any``
================================================================================
``any`` 类型的值可以赋给其它任何类型,还可以对其解引用任意属性。一般来说,这个行为不是必需的,也不符合期望,此时代码试图表达的内容其实是“该类型是未知的”。在这种情况下,应当使用内建的 ``unknown`` 类型。它能够表达相同的语义,并且,因为 ``unknown`` 不能解引用任意属性,它较 ``any`` 而言更为安全。
.. code-block:: typescript
// 应当这样做!
// 可以将任何值(包括 null 和 undefined赋给 val
// 但在缩窄类型或者类型转换之前并不能使用它。
const val: unknown = value;
.. code-block:: typescript
// 不要这样做!
const danger: any = value /* 这是任意一个表达式的结果 */;
danger.whoops(); // 完全未经检查的访问!
.. _ts-suppress-the-lint-warning:
关闭 Lint 工具对 ``any`` 的警告
================================================================================
有时使用 ``any`` 是合理的,例如用于在测试中构造 Mock 对象。在这种情况下,应当添加注释关闭 Lint 工具对此的警告,并添加文档对使用 any 的合理性进行说明。
.. code-block:: typescript
// 这个测试只需要部分地实现 BookService否则测试会失败。
// 所以,这里有意地使用了一个不安全的部分实现 Mock 对象。
// tslint:disable-next-line:no-any
const mockBookService = ({get() { return mockBook; }} as any) as BookService;
// 购物车在这个测试里并未使用。
// tslint:disable-next-line:no-any
const component = new MyComponent(mockBookService, /* unused ShoppingCart */ null as any);
.. _ts-tuple-types:
元组类型
********************************************************************************
应当使用元组类型代替常见的 ``Pair`` 类型的写法:
.. code-block:: typescript
// 不要这样做!
interface Pair {
first: string;
second: string;
}
function splitInHalf(input: string): Pair {
// ...
return {first: x, second: y};
}
.. code-block:: typescript
// 应当这样做!
function splitInHalf(input: string): [string, string] {
// ...
return [x, y];
}
// 这样使用:
const [leftHalf, rightHalf] = splitInHalf('my string');
然而通常情况下,为属性提供一个有意义的名称往往能让代码更加清晰。
如果为此声明一个接口过于繁重的话,可以使用内联对象字面量类型:
.. code-block:: typescript
function splitHostPort(address: string): {host: string, port: number} {
// ...
}
// 这样使用:
const address = splitHostPort(userAddress);
use(address.port);
// 也可以使用解构进行形如元组的操作:
const {host, port} = splitHostPort(userAddress);
.. _ts-wrapper-types:
包装类型
********************************************************************************
不要使用如下几种类型,它们是 JavaScript 中基本类型的包装类型:
* ``String````Boolean````Number`` 。它们的含义和对应的基本类型 ``string````boolean````number`` 略有不同。任何时候,都应当使用后者。
* ``Object`` 。它和 ``{}````object`` 类似,但包含的范围略微更大。应当使用 ``{}`` 表示“包括除 ``null````undefined`` 之外所有类型”的类型,使用 ``object`` 表示“所有基本类型以外”的类型(这里的“所有基本类型”包括上文中提到的基本类型, ``symbol````bigint`` )。
此外,不要将包装类型用作构造函数。
.. _ts-return-type-only-generics:
只有泛型的返回类型
********************************************************************************
不要创建返回类型只有泛型的 API。如果现有的 API 中存在这种情况,使用时应当显式地标明泛型参数类型。