参考

Google:https://google.github.io/styleguide/tsguide.html

Google官方

一.语法规范

美元符号 $

一般情况下,标识符不应使用 $,除非为了与第三方框架的命名规范保持一致。

_ 前缀与后缀

标识符禁止使用下划线 作为前缀或后缀。这也意味着,禁止使用单个下划线 作为标识符(例如:用来表示未被使用的参数)。

如果需要从数组或元组中取出某个或某几个特定的元素的话,可以在解构语句中插入额外的逗号,忽略掉不需要的元素:

  1. const [a, , b] = [1, 5, 10]; // a <- 1, b <- 10

别名

在为一个已有的标识符创建具有局部作用域的别名时,别名的命名方式应当与现有的标识符和现有的命名规范保持一致。声明别名时,应使用 const (如果它是一个变量)或 readonly (如果它是类里的一个字段)。

  1. const {Foo} = SomeType;
  2. const CAPACITY = 5;
  3. class Teapot {
  4. readonly BrewStateEnum = BrewStateEnum;
  5. readonly CAPACITY = CAPACITY;
  6. }

省略对于 TypeScript 而言多余的注释

例如,不要在 @param 或 @return 注释中声明类型,不要在使用了 implements 、 enum 、 private 等关键字的地方添加 @implements 、 @enum 、 @private 等注释。

不要使用 @override

不要在 TypeScript 代码中使用 @override 注释。 @override 并不会被编译器视为强制性约束,这会导致注释与实现上的不一致性。如果纯粹为了文档添加这一注释,反而令人困惑。

注释必须言之有物

虽然大多数情况下文档对代码十分有益,但对于那些并不用于导出的符号,有时其函数或参数的名称与类型便足以描述自身了。
注释切忌照抄参数类型和参数名,如下面的反面示例:

  1. // 不要这样做!这个注释没有任何有意义的内容。
  2. /** @param fooBarService Foo 应用的 Bar 服务 */

因此,只有当需要添加额外信息时才使用 @param 和 @return 注释,其它情况下直接省略即可。

  1. /**
  2. * 发送 POST 请求,开始煮咖啡
  3. * @param amountLitres 煮咖啡的量,注意和煮锅的尺寸对应!
  4. */
  5. brew(amountLitres: number, logger: Logger) {
  6. // ...
  7. }

参数属性注释

通过为构造函数的参数添加访问限定符,参数属性同时创建了构造函数参数和类成员。例如,如下的构造函数

  1. class Foo {
  2. constructor(private readonly bar: Bar) { }
  3. }

为 Foo 类创建了 Bar 类型的成员 bar 。
如果要为这些成员添加文档,应使用 JSDoc 的 @param 注释,这样编辑器会在调用构造函数和访问属性时显示对应的文档描述信息。

  1. /** 这个类演示了如何为参数属性添加文档 */
  2. class ParamProps {
  3. /**
  4. * @param percolator 煮咖啡所用的咖啡壶。
  5. * @param beans 煮咖啡所用的咖啡豆。
  6. */
  7. constructor(
  8. private readonly percolator: Percolator,
  9. private readonly beans: CoffeeBean[]) {}
  10. }
  1. /** 这个类演示了如何为普通成员添加文档 */
  2. class OrdinaryClass {
  3. /** 下次调用 brew() 时所用的咖啡豆。 */
  4. nextBean: CoffeeBean;
  5. constructor(initialBean: CoffeeBean) {
  6. this.nextBean = initialBean;
  7. }
  8. }

函数调用注释

如果有需要,可以在函数的调用点使用行内的 / 块注释 / 为参数添加文档,或者使用字面量对象为参数添加名称并在函数声明中进行解构。注释的格式和位置没有明确的规定。

  1. // 使用行内块注释为难以理解的参数添加说明:
  2. new Percolator().brew(/* amountLitres= */ 5);
  3. // 或者使用字面量对象为参数命名,并在函数 brew 的声明中将参数解构:
  4. new Percolator().brew({amountLitres: 5});
  1. /** 一个古老的咖啡壶 {@link CoffeeBrewer} */
  2. export class Percolator implements CoffeeBrewer {
  3. /**
  4. * 煮咖啡。
  5. * @param amountLitres 煮咖啡的量,注意必须和煮锅的尺寸对应!
  6. */
  7. brew(amountLitres: number) {
  8. // 这个实现煮出来的咖啡味道差极了,不管了。
  9. // TODO(b/12345): 优化煮咖啡的过程。
  10. }
  11. }

二.语言特性

#private 语法

不要使用 #private 私有字段(又称私有标识符)语法声明私有成员。

  1. // 不要这样做!
  2. class Clazz {
  3. #ident = 1;
  4. }

而应当使用 TypeScript 的访问修饰符。

  1. // 应当这样做!
  2. class Clazz {
  3. private ident = 1;
  4. }

为什么?因为私有字段语法会导致 TypeScipt 在编译为 JavaScript 时出现体积和性能问题。同时,ES2015 之前的标准都不支持私有字段语法,因此它限制了 TypeScript 最低只能被编译至 ES2015。另外,在进行静态类型和可见性检查时,私有字段语法相比访问修饰符并无明显优势。

使用 readonly

对于不会在构造函数以外进行赋值的属性,应使用 readonly 修饰符标记。这些属性并不需要具有深层不可变性。

参数属性

不要在构造函数中显式地对类成员进行初始化。应当使用 TypeScript 的 参数属性 语法。

  1. // 不要这样做!重复的代码太多了!
  2. class Foo {
  3. private readonly barService: BarService;
  4. constructor(barService: BarService) {
  5. this.barService = barService;
  6. }
  7. }
  1. // 应当这样做!简洁明了!
  2. class Foo {
  3. constructor(private readonly barService: BarService) {}
  4. }

字段初始化

如果某个成员并非参数属性,应当在声明时就对其进行初始化,这样有时可以完全省略掉构造函数。

  1. // 不要这样做!没有必要单独把初始化语句放在构造函数里!
  2. class Foo {
  3. private readonly userList: string[];
  4. constructor() {
  5. this.userList = [];
  6. }
  7. }
  1. // 应当这样做!省略了构造函数!
  2. class Foo {
  3. private readonly userList: string[] = [];
  4. }

原始类型与封装类

在 TypeScript 中,不要实例化原始类型的封装类,例如 String 、 Boolean 、 Number 等。封装类有许多不合直觉的行为,例如 new Boolean(false) 在布尔表达式中会被求值为 true。

  1. // 不要这样做!
  2. const s = new String('hello');
  3. const b = new Boolean(false);
  4. const n = new Number(5);
  1. // 应当这样做!
  2. const s = 'hello';
  3. const b = false;
  4. const n = 5;

数组构造函数

在 TypeScript 中,禁止使用 Array() 构造函数(无论是否使用 new 关键字)。它有许多不合直觉又彼此矛盾的行为,例如:

  1. // 不要这样做!同样的构造函数,其构造方式却却完全不同!
  2. const a = new Array(2); // 参数 2 被视作数组的长度,因此返回的结果是 [undefined, undefined]
  3. const b = new Array(2, 3); // 参数 2, 3 被视为数组中的元素,返回的结果此时变成了 [2, 3]


应当使用方括号对数组进行初始化,或者使用 from 构造一个具有确定长度的数组:

  1. const a = [2];
  2. const b = [2, 3];
  3. // 等价于 Array(2):
  4. const c = [];
  5. c.length = 2;
  6. // 生成 [0, 0, 0, 0, 0]
  7. Array.from<number>({length: 5}).fill(0);

强制类型转换

禁止使用一元加法运算符 + 将字符串强制转换为数字。用这种方法进行解析有失败的可能,还有可能出现奇怪的边界情况。而且,这样的写法往往成为代码中的坏味道, + 在代码审核中非常容易被忽略掉。

  1. // 不要这样做!
  2. const x = +y;

同样地,代码中也禁止使用 parseInt 或 parseFloat 进行转换,除非用于解析表示非十进制数字的字符串。因为这两个函数都会忽略字符串中的后缀,这有可能在无意间掩盖了一部分原本会发生错误的情形(例如将 12 dwarves 解析成 12)。

  1. const n = parseInt(someString, 10); // 无论传不传基数,
  2. const f = parseFloat(someString); // 都很容易造成错误。

应当使用 Number() 和 Math.floor 或者 Math.trunc (如果支持的话)解析整数。

  1. let f = Number(someString);
  2. if (isNaN(f)) handleError();
  3. f = Math.floor(f);

不要在 if 、 for 或者 while 的条件语句中显式地将类型转换为 boolean ,因为这里原本就会执行隐式的类型转换。

  1. // 不要这样做!
  2. const foo: MyInterface|null = ...;
  3. if (!!foo) {...}
  4. while (!!foo) {...}

变量

必须使用 const 或 let 声明变量。尽可能地使用 const ,除非这个变量需要被重新赋值。禁止使用 var 。

  1. const foo = otherValue; // 如果 foo 不可变,就使用 const。
  2. let bar = someValue; // 如果 bar 在之后会被重新赋值,就使用 let。

对象迭代

对对象使用 for (… in …) 语法进行迭代很容易出错,因为它同时包括了对象从原型链中继承得来的属性。因此,禁止使用裸的 for (… in …) 语句。

  1. // 不要这样做!
  2. for (const x in someObj) {
  3. // x 可能包括 someObj 从原型中继承得到的属性。
  4. }

在对对象进行迭代时,必须使用 if 语句对对象的属性进行过滤,或者使用 for (… of Object.keys(…)) 。

  1. // 应当这样做!
  2. for (const x in someObj) {
  3. if (!someObj.hasOwnProperty(x)) continue;
  4. // 此时 x 必然是定义在 someObj 上的属性。
  5. }
  6. // 应当这样做!
  7. for (const x of Object.keys(someObj)) { // 注意:这里使用的是 for _of_ 语法!
  8. // 此时 x 必然是定义在 someObj 上的属性。
  9. }
  10. // 应当这样做!
  11. for (const [key, value] of Object.entries(someObj)) { // 注意:这里使用的是 for _of_ 语法!
  12. // 此时 key 必然是定义在 someObj 上的属性。
  13. }

switch 语句

所有的 switch 语句都必须包含一个 default 分支,即使这个分支里没有任何代码。

  1. // 应当这样做!
  2. switch (x) {
  3. case Y:
  4. doSomethingElse();
  5. break;
  6. default:
  7. // 什么也不做。
  8. }

非空语句组( case … )不允许越过分支向下执行(编译器会进行检查):

  1. // 不能这样做!
  2. switch (x) {
  3. case X:
  4. doSomething();
  5. // 不允许向下执行!
  6. case Y:
  7. // ...
  8. }
  9. // 可以这样做!
  10. switch (x) {
  11. case X:
  12. case Y:
  13. doSomething();
  14. break;
  15. default: // 什么也不做。
  16. }

相等性判断

必须使用三等号( === )和对应的不等号( !== )。两等号会在比较的过程中进行类型转换,这非常容易导致难以理解的错误。

函数声明

使用 function foo() { … } 的形式声明具名函数,包括嵌套在其它作用域中,例如其它函数内部的函数。
不要使用将函数表达式赋值给局部变量的写法(例如 const x = function() {…}; )。TypeScript 本身已不允许重新绑定函数,所以在函数声明中使用 const 来阻止重写函数是没有必要的。
例外:如果函数需要访问外层作用域的 this ,则应当使用将箭头函数赋值给变量的形式代替函数声明的形式。

  1. // 应当这样做!
  2. function foo() { ... }
  3. // 不要这样做!
  4. // 在有上一段代码中的函数声明的情况下,下面这段代码无法通过编译:
  5. foo = () => 3; // 错误:赋值表达式的左侧不合法。
  6. // 因此像这样进行函数声明是没有必要的。
  7. const foo = function() { ... }

@ts-ignore

不要使用 @ts-ignore 。表面上看,这是一个“解决”编译错误的简单方法,但实际上,编译错误往往是由其它更大的问题导致的,因此正确的做法是直接解决这些问题本身。

类型断言与非空断言

类型断言( x as SomeType )和非空断言( y! )是不安全的。这两种语法只能够绕过编译器,而并不添加任何运行时断言检查,因此有可能导致程序在运行时崩溃。
因此,除非有明显或确切的理由,否则 不应 使用类型断言和非空断言。

  1. // 不要这样做!
  2. (x as Foo).foo();
  3. y!.bar();
  4. // 应当这样做!
  5. // 这里假定 Foo 是一个类。
  6. if (x instanceof Foo) {
  7. x.foo();
  8. }
  9. if (y) {
  10. y.bar();
  11. }

有时根据代码中的上下文可以确定某个断言必然是安全的。在这种情况下, 应当 添加注释详细地解释为什么这一不安全的行为可以被接受:

  1. // 可以这样做!
  2. // x 是一个 Foo 类型的示例,因为……
  3. (x as Foo).foo();
  4. // y 不可能是 null,因为……
  5. y!.bar();

类型断言语法

类型断言必须使用 as 语法,不要使用尖括号语法,这样能强制保证在断言外必须使用括号。

  1. // 不要这样做!
  2. const x = (<Foo>z).length;
  3. const y = <Foo>z.length;
  4. // 应当这样做!
  5. const x = (z as Foo).length;

枚举

对于枚举类型,必须使用 enum 关键字,但不要使用 const enum 。TypeScript 的枚举类型本身就是不可变的, const enum 的写法是另一种独立的语言特性,其目的是让枚举对 JavaScript 程序员透明。

debugger 语句

不允许在生产环境代码中添加 debugger 语句。

  1. // 不要这样做!
  2. function debugMe() {
  3. debugger;
  4. }

三.代码管理

导入路径

TypeScript 代码必须使用路径进行导入。这里的路径既可以是相对路径,以 . 或 .. 开头,也可以是从项目根目录开始的绝对路径,如 root/path/to/file 。
在引用逻辑上属于同一项目的文件时,应使用相对路径 ./foo ,不要使用绝对路径 path/to/foo 。
应尽可能地限制父层级的数量(避免出现诸如 ../../../ 的路径),过多的层级会导致模块和路径结构难以理解。

  1. import {Symbol1} from 'google3/path/from/root';
  2. import {Symbol2} from '../parent/file';
  3. import {Symbol3} from './sibling';

用 命名空间 还是 模块?

在 TypeScript 有两种组织代码的方式:命名空间(namespace)和模块(module)。
不允许使用命名空间,在 TypeScript 中必须使用模块(即 ES6 模块 )。也就是说,在引用其它文件中的代码时必须以 import {foo} from ‘bar’ 的形式进行导入和导出。
不允许使用 namespace Foo { … } 的形式组织代码。命名空间只能在所用的外部第三方库有要求时才能使用。如果需要在语义上对代码划分命名空间,应当通过分成不同文件的方式实现。
不允许在导入时使用 require 关键字(形如 import x = require(‘…’); )。应当使用 ES6 的模块语法。

  1. // 不要这样做!不要使用命名空间!
  2. namespace Rocket {
  3. function launch() { ... }
  4. }
  5. // 不要这样做!不要使用 <reference> !
  6. /// <reference path="..."/>
  7. // 不要这样做!不要使用 require() !
  8. import x = require('mydep');

可空/未定义类型别名

不允许 为包括 |null 或 |undefined 的联合类型创建类型别名。这种可空的别名通常意味着空值在应用中会被层层传递,并且它掩盖了导致空值出现的源头。另外,这种别名也让类或接口中的某个值何时有可能为空变得不确定。
因此,代码 必须 在使用别名时才允许添加 |null 或者 |undefined 。同时,代码 应当 在空值出现位置的附近对其进行处理。

  1. // 不要这样做!不要在创建别名的时候包含 undefined !
  2. type CoffeeResponse = Latte|Americano|undefined;
  3. class CoffeeService {
  4. getLatte(): CoffeeResponse { ... };
  5. }
  6. // 应当这样做!在使用别名的时候联合 undefined !
  7. type CoffeeResponse = Latte|Americano;
  8. class CoffeeService {
  9. getLatte(): CoffeeResponse|undefined { ... };
  10. }
  11. // 这样做更好!使用断言对可能的空值进行处理!
  12. type CoffeeResponse = Latte|Americano;
  13. class CoffeeService {
  14. getLatte(): CoffeeResponse {
  15. return assert(fetchResponse(), 'Coffee maker is broken, file a ticket');
  16. };
  17. }

any 类型

TypeScript 的 any 类型是所有其它类型的超类,又是所有其它类型的子类,同时还允许解引用一切属性。因此,使用 any 十分危险——它会掩盖严重的程序错误,并且它从根本上破坏了对应的值“具有静态属性”的原则。

尽可能 不要 使用 any 。如果出现了需要使用 any 的场景,可以考虑下列的解决方案:

补充

api类型定义全,至少业务用到的字段类型要覆盖

Antd的Form, React-Router的 withRouter 有类型可以使用.不要重写定义或者是 any, 可以点击进去看定义

不要过度定义类型..能自动推导的.不要在加上类型

Record 用这个来声明对象结构的类型

使用typescript的内置函数来推导出新类型,例如partical,omit,extends等

使用?号代替undefined

  1. // bad
  2. interface A{
  3. b: string | undefined;
  4. }
  5. // good
  6. interface A {
  7. b? : string;
  8. }


常量必须命名, 在做逻辑判断的时候,也不允许直接对比没有命名的常量。

  1. //bad
  2. switch(num){
  3. case 1:
  4. ...
  5. case 3:
  6. ...
  7. case 7:
  8. ...
  9. }
  10. //good
  11. enum DayEnum {
  12. oneDay = 1,
  13. threeDay = 3,
  14. oneWeek = 7,
  15. }
  16. let num = 1;
  17. switch(num){
  18. case DayEnum.oneDay:
  19. ...
  20. case DayEnum.threeDay:
  21. ...
  22. case DayEnum.oneWeek:
  23. ...