源文件结构
文件由以下部分按顺序组成:
- 版权信息(如有)
- 带有
@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 中有四种导入语句(Import Statement)的变体:
| 导入类型 | 示例 | 用途 |
|---|---|---|
| 模块导入(module)1 | import * as foo from '...'; | TypeScript 导入 |
| 命名导入(named)2 | import {SomeThing} from '...'; | TypeScript 导入 |
| 默认导入(default) | import SomeThing from '...'; | 仅在其他外部代码需要时使用 |
| 副作用导入(side-effect) | import '...'; | 仅用于导入那些在加载时产生副作用的库(例如自定义元素) |
// 好的写法:根据情况在两种方式中选择合适的(见下文)。
import * as ng from '@angular/core';
import {Foo} from './foo';
// 仅在需要时使用:默认导入。
import Button from 'Button';
// 有时需要导入库以获取其副作用:
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';命名空间导入与命名导入
命名空间导入(Namespace Import)和命名导入(Named Import)都可以使用。
对于在文件中频繁使用的符号或名称明确的符号,推荐使用命名导入,例如
Jasmine 的 describe 和 it。命名导入可以根据需要使用 as
重命名为更清晰的名称。
当从大型 API
中使用许多不同符号时,推荐使用命名空间导入。命名空间导入虽然使用了 *
字符,但不等同于其他语言中的“通配符”导入。相反,命名空间导入为模块的所有导出提供了一个名称,每个导出的符号都成为该模块名称的属性。命名空间导入有助于提高常见名称(如
Model 或 Controller)的可读性,而无需声明别名。
// 不好:过长的导入语句,不必要地使用了命名空间化的名称。
import {Item as TableviewItem, Header as TableviewHeader, Row as TableviewRow,
Model as TableviewModel, Renderer as TableviewRenderer} from './tableview';
let item: TableviewItem|undefined;// 更好:使用模块进行命名空间化。
import * as tableview from './tableview';
let item: tableview.Item|undefined;import * as testing from './testing';
// 不好:模块名称没有提高可读性。
testing.describe('foo', () => {
testing.it('bar', () => {
testing.expect(null).toBeNull();
testing.expect(undefined).toBeUndefined();
});
});// 更好:为这些常用函数提供本地名称。
import {describe, it, expect} from './testing';
describe('foo', () => {
it('bar', () => {
expect(null).toBeNull();
expect(undefined).toBeUndefined();
});
});特殊情况:Apps JSPB protos
Apps JSPB protos 必须使用命名导入,即使这会导致很长的导入行。
此规则的目的是帮助构建性能和死代码消除(Dead Code Elimination),因为
.proto 文件通常包含许多不需要同时使用的
message。通过利用解构导入,构建系统可以对 Apps JSPB
消息创建更细粒度的依赖关系,同时保持基于路径导入的便利性。
// 好的写法:从 proto 文件中导入你需要的确切符号集。
import {Foo, Bar} from './foo.proto';
function copyFooBar(foo: Foo, bar: Bar) {...}重命名导入
代码应该通过使用命名空间导入或重命名导出本身来解决名称冲突。代码可以在需要时重命名导入(import {SomeThing as SomeOtherThing})。
以下三种场景中重命名会很有帮助:
- 需要避免与其他导入符号的冲突时。
- 导入的符号名称是自动生成的时。
- 导入符号的名称本身不够清晰时,重命名可以提高代码清晰度。例如,使用
RxJS 时,将
from函数重命名为observableFrom可能更具可读性。
导出
在所有代码中使用命名导出(Named Export):
// 使用命名导出:
export class Foo { ... }不要使用默认导出(Default Export)。这确保所有导入遵循统一的模式。
// 不要使用默认导出:
export default class Foo { ... } // 不好!为什么?
默认导出不提供规范名称(Canonical Name),这使得集中维护变得困难,同时对代码所有者的好处相对较小,包括可能降低可读性:
import Foo from './bar'; // 合法。
import Bar from './bar'; // 也合法。命名导出的好处是,当导入语句尝试导入未声明的内容时会报错。在 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 {
// 这里只放类相关的内容
}导出可见性
TypeScript 不支持限制导出符号的可见性。只导出模块外部使用的符号。通常应尽量减少模块的导出 API 表面。
可变导出
无论技术上是否支持,可变导出(Mutable
Export)会创建难以理解和调试的代码,特别是在跨多个模块进行重新导出时。换句话说,不允许使用
export let。
export let foo = 3;
// 在纯 ES6 中,foo 是可变的,导入者会在一秒后观察到值的变化。
// 在 TS 中,如果 foo 被第二个文件重新导出,导入者将不会看到值的变化。
window.setTimeout(() => {
foo = 4;
}, 1000 /* ms */);如果需要支持外部可访问且可变的绑定,应该改用显式的 getter 函数。
let foo = 3;
window.setTimeout(() => {
foo = 4;
}, 1000 /* ms */);
// 使用显式 getter 来访问可变导出。
export function getFoo() { return foo; };对于根据条件导出两个值之一的常见模式,先进行条件检查,然后再导出。确保在模块主体执行完毕后所有导出都是最终的。
function pickApi() {
if (useOtherApi()) return OtherApi;
return RegularApi;
}
export const SomeApi = pickApi();容器类
不要为了命名空间化而创建包含静态方法或属性的容器类(Container Class)。
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 编译器可以在两种模式下运行:
- 在开发模式下,我们通常需要快速的迭代循环。编译器在没有完整类型信息的情况下转译为
JavaScript。这要快得多,但在某些情况下需要
import type。 - 在生产模式下,我们需要正确性。编译器会对所有内容进行类型检查,并确保
import type的使用是正确的。
注意:如果你需要强制运行时加载以获取副作用,请使用 import '...';。
导出类型
在重新导出类型时使用 export type,例如:
export type {AnInterface} from './foo';为什么?
export type 可用于在逐文件转译中允许类型重新导出。参见
isolatedModules
文档。
export type 可能看起来也有助于避免为某个 API
导出值符号。然而,它并不能提供保证:下游代码仍然可能通过不同的路径导入该
API。更好的拆分和保证 API
的类型与值用途的方式是实际拆分这些符号,例如拆分为 UserService 和
AjaxUserService。这种方式不容易出错,也能更好地传达意图。
使用模块而非命名空间
TypeScript
支持两种组织代码的方式:命名空间(Namespace)和模块(Module),但命名空间是不允许的。也就是说,你的代码必须使用
import {foo} from 'bar'; 形式的导入和导出来引用其他文件中的代码。
你的代码禁止使用 namespace Foo { ... } 结构。namespace
只能在需要与外部第三方代码交互时使用。要在语义上对代码进行命名空间化,请使用单独的文件。
代码禁止使用 require(如
import x = require('...');)进行导入。使用 ES6 模块语法。
// 不好:不要使用命名空间:
namespace Rocket {
function launch() { ... }
}
// 不好:不要使用 <reference>
/// <reference path="..."/>
// 不好:不要使用 require()
import x = require('mydep');注意:TypeScript 的
namespace以前被称为内部模块(Internal Module),并且曾经使用module关键字,形式为module Foo { ... }。也不要使用这种形式。始终使用 ES6 导入。