TypeScript 编码规约

前言

TypeScript 是微软开发的一款开源编程语言,它是 JavaScript 的超集,因此其编码规约和配套 Lint 工具也与《JavaScript 编码规约》一脉相承。

本文未包含的编码风格说明均默认遵循《JavaScript 编码规约》。

编码风格

示例代码标注图

自然相关的项组合在一起将提高代码可读性和组织性。

ts
// bad
declare namespace Foo {
  export function foo(s: string): void
  export function foo(n: number): void
  export function bar(): void
  export function foo(sn: string | number): void
}

// good
declare namespace Foo {
  export function foo(s: string): void
  export function foo(n: number): void
  export function foo(sn: string | number): void
  export function bar(): void
}
ts
// bad
type Foo = {
  foo(s: string): void
  foo(n: number): void
  bar(): void
  foo(sn: string | number): void
}

interface Foo {
  foo(s: string): void
  foo(n: number): void
  bar(): void
  foo(sn: string | number): void
}

class Foo {
  foo(s: string): void
  foo(n: number): void
  bar(): void {}
  foo(sn: string | number): void {}
}

// good
type Foo = {
  foo(s: string): void
  foo(n: number): void
  foo(sn: string | number): void
  bar(): void
}

interface Foo {
  foo(s: string): void
  foo(n: number): void
  foo(sn: string | number): void
  bar(): void
}

class Foo {
  foo(s: string): void
  foo(n: number): void
  foo(sn: string | number): void {}
  bar(): void {}
}
ts
// bad
export function foo(s: string): void
export function foo(n: number): void
export function bar(): void
export function foo(sn: string | number): void

// good
export function bar(): void
export function foo(s: string): void
export function foo(n: number): void
export function foo(sn: string | number): void

对数组类型的定义使用相同的规范将帮助开发者更快地理解和阅读类型。

简单类型(数字、字符串、布尔等)请使用 T[]readonly T[] ,其他复杂类型(联合、交叉、对象、函数等)请使用 Array<T>ReadonlyArray<T>

ts
// bad
const a: (string | number)[] = ['a', 1]
const b: { prop: string }[] = [{ prop: 'a' }]
const c: (() => void)[] = [() => {}]
const d: Array<MyType> = ['a', 'b']
const e: Array<string> = ['a', 'b']
const f: ReadonlyArray<string> = ['a', 'b']

// good
const a: Array<string | number> = ['a', 1]
const b: Array<{ prop: string }> = [{ prop: 'a' }]
const c: Array<() => void> = [() => {}]
const d: MyType[] = ['a', 'b']
const e: string[] = ['a', 'b']
const f: readonly string[] = ['a', 'b']

TS 提供了一些指令注释,可用于忽略 TypeScript 编译器在编译阶段的错误,如下:

ts
// @ts-expect-error
// @ts-ignore
// @ts-nocheck
// @ts-check

我们允许在代码中使用指令注释,但需要跟随一定长度的描述说明。

ts
// bad
// @ts-expect-error
console.log('my code')

// @ts-ignore
console.log('my code')

// good
// @ts-expect-error: Unreachable code here
console.log('my code')

// @ts-ignore: It's ok to ignore this compile error
console.log('my code')

tslint 已经被废弃,对应的指令注释也不应再出现。

ts
// bad
/* tslint:disable */
/* tslint:enable */
/* tslint:disable:rule1 rule2 rule3... */
/* tslint:enable:rule1 rule2 rule3... */
// tslint:disable-next-line
someCode() // tslint:disable-line
// tslint:disable-next-line:rule1 rule2 rule3...

类上所有返回「字面量」的 getter 方法,都推荐使用 readonly 修饰符来代替,字面量包含字符串、模板字符串、数字、bigint、正则和 null。

说明:在一些特殊场景,编写代码的最终用户是 JavaScript 开发者时,可以使用 getter 来保证字段无法被重新定义和覆盖,因为 readonly 修饰符只作用于 TypeScript 编译阶段。

ts
// bad
class Mx {
  public static get myField1() {
    return 1
  }
  private get ['myField2']() {
    return 'hello world'
  }
}

// good
class Mx {
  public readonly myField1 = 1
  public readonly myField2 = [1, 2, 3] // 非字面量
  private readonly ['myField3'] = 'hello world'
  public get myField4() {
    return `hello from ${window.location.href}`
  }
}

类型断言(type assertiions)也可称作类型转换(type casting),本质上是对 TypeScript 类型系统的人为干预:

  1. 强制对类型的断言统一使用 as Type 风格而非 <Type> ,后者容易与 JSX 产生混淆。
  2. 对象字面量禁止类型断言,断言成 any 除外,对象字面量应该直接声明。

TypeScript 3.4 中引入的 const 断言在本规则中不受约束,let x = 'hello' as const;let x = <const>'hello'; 都是允许的。

ts
// bad
const foo = <string>'bar'

// good
const foo = 'bar' as string
ts
// bad
const x = { ... } as T;
const y = { ... } as object;

// good
const x: T = { ... };
const y = { ... } as any;
const z = { ... } as unknown;

interface 支持 extend/implement/union 等等类型能力,同时也可以用于描述普通对象。

ts
// bad
type T = { x: number }

// good
type T = string
type Foo = string | {}
interface T {
  x: number
}

将非公开成员的可访问性设置为「私有」,可以增强代码可理解性,同时也能避免一些非法调用,公开的成员可省略 public 修饰符。

ts
// bad
class Foo {
  static foo = 'foo'
  static getFoo() {
    return Foo.foo
  }
  constructor() {}
  bar = 'bar'
  getBar() {}
  get baz() {
    return 'baz'
  }
  set baz(value) {
    console.log(value)
  }
}

// good
class Foo {
  private static foo = 'foo'
  public static getFoo() {
    return Foo.foo
  }
  public constructor() {}
  protected bar = 'bar'
  public getBar() {}
  public get baz() {
    return 'baz'
  }
  public set baz(value) {
    console.log(value)
  }
}
ts
// bad: comma style(JSON style)
interface Foo {
  name: string
  greet(): void
}

type Bar = {
  name: string
  greet(): void
}

// bad: line break style
interface Foo {
  name: string
  greet(): void
}

type Bar = {
  name: string
  greet(): void
}

// good
interface Foo {
  name: string
  greet(): void
}

type Bar = {
  name: string
  greet(): void
}
  1. 类的静态方法 / 属性(static)优先于实例的方法 / 属性(instance)
  2. 属性(field)优先于构造函数(constructor),优先于方法(method)
  3. 公开的成员(public)优先于受保护的成员(protected),优先于私有的成员(private)
ts
// good
class Foo {
  public static foo1 = 'foo1'
  protected static foo2 = 'foo2'
  private static foo3 = 'foo3'
  public static getFoo1() {}
  protected static getFoo2() {}
  private static getFoo3() {
    return Foo.foo3
  }
  public bar1 = 'bar1'
  protected bar2 = 'bar2'
  private bar3 = 'bar3'
  public constructor() {
    console.log(Foo.getFoo3())
    console.log(this.getBar3())
  }
  public getBar1() {}
  protected getBar2() {}
  private getBar3() {
    return this.bar3
  }
}

使用属性去定义接口中的方法,可以获得更严格的检查。

ts
// bad
interface T1 {
  func(arg: string): number
}
type T2 = {
  func(arg: boolean): void
}
interface T3 {
  func(arg: number): void
  func(arg: string): void
  func(arg: boolean): void
}

// good
interface T1 {
  func: (arg: string) => number
}
type T2 = {
  func: (arg: boolean) => void
}
// 属性方法实现重载
interface T3 {
  func: ((arg: number) => void) &
    ((arg: string) => void) &
    ((arg: boolean) => void)
}

在相等比较运算符(=====)前使用非空断言(!)很容易和不等运算符(!=!==)混淆,不建议使用。

ts
interface Foo {
  bar?: string
  num?: number
}

// bad
const foo: Foo = getFoo()
const isEqualsBar = foo.bar! == 'hello'
const isEqualsNum = 1 + foo.num! == 2

// good
const foo: Foo = getFoo()
const isEqualsBar = foo.bar == 'hello'
const isEqualsNum = 1 + foo.num! == 2

空的接口类型等效于空对象,若它只继承另一个接口类型,那么该类型与被继承的类型等效。在代码中应减少定义无意义的接口类型。

ts
// bad
// an empty interface
interface Foo {}

// an interface with only one supertype (Bar === Foo)
interface Bar extends Foo {}

// an interface with an empty list of supertypes
interface Baz {}

// good
// an interface with any number of members
interface Foo {
  name: string
}

// same as above
interface Bar {
  age: number
}

// an interface with more than one supertype
// in this case the interface can be used as a replacement of a union type.
interface Baz extends Foo, Bar {}

对于容易类型推倒出的变量、参数,再次的显式声明类型会带来代码冗余。

ts
// bad
const foo: number = 1
const bar: string = ''
class Foo {
  prop: number = 5
}
function fn(a: number = 5, b: boolean = true) {}

// good
const foo = 1
const bar = ''
class Foo {
  prop = 5
}
function fn(a = 5, b = true) {}

禁止在返回类型或泛型类型参数之外使用 void 类型,而且在返回类型中不应再与其他类型做联合或交叉。

void 类型代表「无」或函数「不返回任何值」,隐式未定义类型代表函数返回「未定义的值 undefined」,所以 void 类型无法与除了 never 外的其他类型做联合、交叉。

ts
// bad
type PossibleValues = string | number | void
type MorePossibleValues = string | ((number & any) | (string | void))
function logSomething(thing: void) {}
function printArg<T = void>(arg: T) {}
logAndReturn<void>(undefined)
interface Interface {
  lambda: () => void
  prop: void
}
class MyClass {
  private readonly propName: void
}

// good
type NoOp = () => void
function noop(): void {}
const trulyUndefined = void 0
async function promiseMeSomething(): Promise<void> {}
type stillVoid = void | never

自定义 TypeScript 模块(module)和命名空间(namespace)已经不再推荐使用,首选 ES2015 的模块语法来导入导出。此规则仍然允许定义外部的模块或命名空间。

ts
// bad
namespace foo {}
namespace foo {}

// good
declare module 'foo' {}
declare namespace foo {}
declare namespace foo {}
declare global {
  namespace foo {}
}
declare namespace foo {
  namespace foo {}
}

optional chaining 被设计为返回 undefined,在之后使用非空断言是错误的,会引入严重的类型安全问题。

ts
// bad
foo?.bar!
foo?.bar!.baz
foo?.bar()!
foo?.bar!()
foo?.bar!().baz

// good
foo?.bar
;(foo?.bar).baz
foo?.bar()
foo?.bar()
foo?.bar().baz

// 如果您使用的是 TS3.9 或更高版本,则以下代码也是正确的
foo?.bar!.baz
foo?.bar!()
foo?.bar!().baz
ts
// bad
// good
import * as fs from 'fs'

const fs = require('fs')

通过变量赋值为 this 的方式来管理函数作用域不是我们推荐的最佳实践,应使用箭头函数保留函数作用域。此规则中允许对 this 的解构赋值。

ts
// bad
function foo() {
  const self = this
  setTimeout(() => {
    self.doWork()
  })
}

// good
function foo() {
  setTimeout(() => {
    this.doWork()
  })
}
ts
// bad
let bar: 2 = 2
let foo = <const>'bar'
let foo = { bar: 'baz' as const }

// good
let foo = 'bar'
let foo = 'bar' as const
let foo: 'bar' = 'bar' as const
let bar = 'bar' as string
let foo = <string>'bar'
let foo = { bar: 'baz' }

module 已经成为 JS 语言的关键字,应避免 TypeScript 模块与 ES2015 模块混淆。declare module 不做限制。

ts
// bad
namespace Foo {}

// good
declare namespace Foo {}
declare namespace Foo {}
ts
// bad
const foo = 'bar'

// good
const foo = 'bar'

数字与字符串的连接往往会导致一些预期外的问题。

ts
// bad
var foo = `5.5${5}`
var foo = 1n + 1

// good
var foo = Number.parseInt('5.5', 10) + 10
var foo = 1n + 1n

三斜杠语法已经被废弃,声明文件(d.ts)以外禁止使用。

ts
// bad
/// <reference path="./my-module" />

// good
import myModule from './my-module'

TypeScript 类型声明周围添加合适的间距可以有效的提升代码可读性,我们约定:

  1. 冒号前无空格,冒号后保留一个空格
  2. 箭头前后都保留一个空格
ts
// bad
let foo: string = 'bar'
let foo: string = 'bar'
let foo: string = 'bar'

function foo(): string {}
function foo(): string {}
function foo(): string {}

class Foo {
  name: string
}

class Foo {
  name: string
}

class Foo {
  name: string
}

type Foo = () => {}

// good
let foo: string = 'bar'

function foo(): string {}

class Foo {
  name: string
}

type Foo = () => {}
ts
// bad
type Members = {
  member
  otherMember
}

// good
type Members = {
  member: boolean
  otherMember: string
}
ts
// bad
function f(x: number): void;
function f(x: string): void;
f(): void;
f(...x: number[]): void;

// good
function f(x: number | string): void;
function f(x?: ...number[]): void;

配套工具

  • eslint-config:本规约配套的 ESLint 规则包,可使用 eslint-config/typescripteslint-config/typescript/xx 引入本文介绍的规则