The following is the TS specification that I followed during the development process. It is only my personal opinion and is being updated

Naming conventions

  1. Use camelCase to name attributes or local variables.
  2. Use camelCase to name functions.
  3. Name the type using PascalCase.
  4. Don’t useIAs the prefix of the interface name.
  5. Name enumeration values using PascalCase.
  6. Do not add for private attribute names_Prefix, usingprivateThe rhetoric.
  7. Use full word spellings whenever possible.

variable

Variables are named in camelCase.

// Bad
const Foo = 1

// Good
const foo = 1
Copy the code
// Bad
const UserList:string[] = []

// Good
const userList:string[] = []
Copy the code

function

Functions are named in camelCase.

// Bad
function Foo() {}

// Good
function foo() {}
Copy the code

class

Classes themselves are named PascalCase, and class members are named camelCase.

// Bad
class foo {}

// Good
class Foo {}
Copy the code
// Bad
class Foo {
  Bar: number;
  Baz(): number{}}// Good
class Foo {
  bar: number;
  baz(): number{}}Copy the code

interface

The interface name itself is PascalCase. Do not prefix the interface name with I. Interface members are named in camelCase mode.

// Bad
interface IFoo {
  Bar: number;
  Baz(): number;
}

// Good
interface Foo {
  bar: number;
  baz(): number;
}
Copy the code

Why not name the interface with an I prefix

In TS, classes can implement interfaces, interfaces can inherit interfaces, and interfaces can inherit classes. Both classes and interfaces are abstractions and encapsulation in a sense, and inheritance does not care whether it is an interface or a class. With the I prefix, when the type of a variable changes, such as from an interface to a class, the variable name must be changed simultaneously.

The enumeration

The enumeration object itself and the enumeration members are named PascalCase.

// Bad
enum status {}

// Good
enum Status {}
Copy the code
// Bad
enum Status {
	success = 'success'
}

// Good
enum Status {
	Success = 'success'
}
Copy the code

The file name

Common TS files and React components are distinguished by file names. The React component must use TSX as the suffix.

  • usecamelCaseName files that start with verbs, such as functionsgetName.ts,fetchData.ts
  • File names that start with a noun, such as class, can be named in one of two ways:
    • Use PascalCase names, for exampleUserList.ts.
    • Use dashes to separate descriptive word names, such asuser-list.ts.

Both naming methods are allowed, but only one of them can be used in a project. \

Type declaration specification

The type declaration should rely on the automatic type inference function of TS as far as possible. If the correct type can be inferred, do not manually declare it as far as possible.

variable

Base type variables do not need to be manually typed.

let foo = 'foo'
let bar = 2
let baz = false
Copy the code

References to type variables should be guaranteed to be of the correct type. Incorrect references need to be declared manually.

// Automatic inference
let foo = [1.2] // number[]
Copy the code
// Display the declaration
// Bad
let bar = [] // any[]

// Good
let bar:number[] = [] 
Copy the code

function

We follow Typescript’s official best practices and see the repository for Chinese translations.

Callback function type

The return value type of the callback function

Do not set the return type any for a callback whose return value is ignored:

// Bad
function fn(x: () => any) {
    x();
}
Copy the code

The return type void should be set for callback functions whose return value is ignored:

// Good
function fn(x: () => void) {
    x();
}
Copy the code

Void is relatively safe because it prevents the accidental use of an unchecked return value of x:

function fn(x: () => void) {
    var k = x(); // oops! meant to do something else
    k.doSomething(); // error, but would be OK if the return type had been 'any'
}
Copy the code
Optional arguments in the callback function

Do not use optional arguments in callback functions unless this is what you want:

// Bad
interface Fetcher {
    getObject(done: (data: any, elapsedTime? :number) = > void) :void;
}
Copy the code

Here’s what it means: The done callback can be called with one or two arguments.

The general idea of the code is that this callback does not care about the elapsedTime argument, but it does not need to be defined as an optional argument to do so, because it is always possible to provide a callback that takes fewer arguments.

The callback function should be defined with no optional arguments:

// Good
interface Fetcher {
    getObject(done: (data: any, elapsedTime: number) = > void) :void;
}
Copy the code

Overloading and callback functions

Do not write different overloads depending on the number of arguments to the callback function.

// Bad
declare function beforeAll(action: () => void, timeout? :number) :void;
declare function beforeAll(
    action: (done: DoneFn) => void, timeout? :number
) :void;
Copy the code

An overload should be written only for the maximum number of arguments:

// Good
declare function beforeAll(
    action: (done: DoneFn) => void, timeout? :number
) :void;
Copy the code

The callback function always allows an argument to be ignored, so there is no need to write an overload for the absence of an optional argument.

Providing an overload for cases where optional arguments are missing may result in a typical-error callback function being passed in because it matches the first overload.

Function overloading

The order

Do not put vague overloads in front of concrete overloads:

// Bad
declare function fn(x: any) :any;
declare function fn(x: HTMLElement) :number;
declare function fn(x: HTMLDivElement) :string;

var myElem: HTMLDivElement;
var x = fn(myElem); // x: any, wat?
Copy the code

Overloads should be sorted, with specific ones placed before vague ones:

// Good
declare function fn(x: HTMLDivElement) :string;
declare function fn(x: HTMLElement) :number;
declare function fn(x: any) :any;

var myElem: HTMLDivElement;
var x = fn(myElem); // x: string, :)
Copy the code

When parsing a function call, TypeScript selects the first overload that matches it. When the front overload is more “obscure” than the back overload, the back overload is hidden and not selected.

Use optional parameters

Don’t write different overloads because only the trailing arguments are different:

// Bad
interface Example {
    diff(one: string) :number;
    diff(one: string.two: string) :number;
    diff(one: string.two: string.three: boolean) :number;
}
Copy the code

Optional arguments should be used whenever possible:

// Good
interface Example {
    diff(one: string, two? :string, three? :boolean) :number;
}
Copy the code

Note that this is only fine if the return value type is the same.

There are two important reasons:

  • TypeScript parses signature compatibility to see if a target signature can be called with the original arguments and allows additional arguments.

    The following code exposes a bug only if the signature is defined correctly with optional arguments:

    function fn(x: (a: string, b: number, c: number) = >void) {}
    var x: Example;
    // Function overloading will use the first overload without error, but this is not true for our case
    // There is an error when using optional arguments. The types of callback arguments do not match
    fn(x.diff);
    Copy the code
  • The second reason is when TypeScript’s “strict null checking” feature is used.

    Because an unspecified parameter is represented as undefined in JavaScript, there is usually no problem passing undefined explicitly for an optional parameter. This code works in strict NULL mode:

    var x: Example;
    // Function overloading is an error because (true? Undefined: "hour") is of type string | undefined, overloading and function can be a string, or not fill (and undefined there's a difference).
    // Optional parameters will not report errors
    x.diff("something".true ? undefined : "hour");
    Copy the code

Using union types

Do not define overloading just because the parameter type is different at a particular location:

// Bad
interface Moment {
  utcOffset(): number;
  utcOffset(b: number): Moment;
  utcOffset(b: string): Moment;
}
Copy the code

Union types should be used whenever possible:

// Good
interface Moment {
  utcOffset(): number;
  utcOffset(b: number | string): Moment;
}
Copy the code

Note that we did not make b optional because the return value type of the signature is different.

This is important for users who pass in values for this function:

function fn(x: string) :void;
function fn(x: number) :void;
function fn(x: number | string) {
  // An error is reported when the function is overloaded
  // Use the union type for normal parsing
  return moment().utcOffset(x);
}
Copy the code

class

Class members should be declared with an explicit scope rhetoric except for public members.

// Bad
class Foo {
    foo = 'foo'
    bar = 'bar'
    getFoo() {
        return this.foo
    }
}
const foo = new Foo()
foo.foo
foo.bar

// Good
class Foo {
    private foo = 'foo'
    bar = 'bar'
    getFoo() {
        return this.foo
    }
}
const foo = new Foo()
foo.getFoo()
foo.bar
Copy the code

The React components

The React component recommends using function components as much as possible, embracing hooks, and using class components for a few complex scenarios.

  • Function component declaration:
    • If the component has props, write the corresponding declaration type above the component and export itComponent name + Props.
    • Components useReact.FCDefinition,React.FCThe default children type is provided, and the type of props is passed as a generic parameter.
    import React from 'react'
    // If the component has props, write the declared type above the component and export it. The type name is component name + props
    export interface ExampleProps {
            a:string
    }
    
    // The component is defined using React.FC, which provides the default children type and passes the type of props as a generic parameter
    const Example: React.FC<ExampleProps> = () = > {}
    
    export default Example
    Copy the code
  • Class component declaration:
    • If the component has props, write the corresponding declaration type above the component and export itComponent name + Props.
    • Since state does not need to be exported, it can be named asStateBut you still need to declare it in advance.
    • Try not to run code inside constructor. Omit writing the constructor function and add the correct props type if the constructor function is used.
    import { Component } from 'react'
    
    export interface ExampleProps {
      a: string
    }
    
    interface State {
      value: number
    }
    
    const initialState: State = {
      value: 1
    }
    
    class Example extends Component<ExampleProps.State> {
      readonly state = initialState
    
      // Don't write without constructor; otherwise, add the correct props type
      constructor(props: ExampleProps) {
        super(props)
        this.setState((state) = > ({
          ...state,
          value: state.value + 1}}}))export default Example
    Copy the code

The generic

Do not define a generic type that never uses its type parameters.

Business development specification

Class inheritance methods

When a subclass inherits a parent class and needs to override a parent class method, the override modifier is required.

class Animal {
  eat() {
    console.log('food')}}// Bad
class Dog extends Animal {
  eat() {
    console.log('bone')}}// Good
class Dog extends Animal {
  override eat() {
    console.log('bone')}}Copy the code

Do not use namespaces

Due to the rise of ES6 Module, the original namesapce notation in TS has been gradually abandoned, and module should be used instead in business.

Set a collection of constants using enumerations

An ordinary set of constants defined by an object is modified without error, unless manually as const.

// Bad
const Status = {
    Success: 'success'
}

// Good
enum Status {
    Success = 'success'
}
Copy the code

Definition file (.d.ts) writing specification

Global type/variable definitions

Global types/variables are written in the global.d.ts file.

  • If external modules are introduced, usedeclare global {}Formal definition
import { StateType } from './state'
declare global {
    export const globalState: StateType
    export const foo: string
    export type AsyncFunction = (. args:any[]) = > Promise<any>}Copy the code
  • If no external modules are introduced, use them directlydeclaredefine
interface StateType {}
declare const globalState: StateType
declare const foo: string
declare type AsyncFunction = (. args:any[]) = > Promise<any>
Copy the code

Expand the definition file for third-party libraries

Third party definition files should be named after the [package].d.ts rule and stored in the project’s type directory.

Here’s an example:

// types/references/react-redux.d.ts
// It is better to add this sentence, otherwise the export may be overwritten, only DefaultRootState exists
export * from 'react-redux'
import { FooState } from './foo'

// Extend third-party libraries
declare module "react-redux" {
    // DefaultRootState was defined as {}, we changed it to an index type
    export interface DefaultRootState {
        foo: FooState
        [key: string] :any}}Copy the code

Specification for annotation writing

Use the tsDoc form when writing comments for an interface or SDK, and the IDE can recognize tsDoc when it is used.

/** ** the sum of two numbers */
export function add(a: number, b: number) {
  return a + b
}
Copy the code

Tsdoc specification

Tsdoc’s syntax is stricter than JSDoc’s, and the focus is different. Jsdoc focuses more on providing type annotations to JS, while TS naturally supports types, so TSDoc focuses on document and API management.

Some jSDoc tags are useless in TS, such as:

  • @function: Marks a variable as a function
  • @enum: Marks an object as enum
  • @access: Annotates the access levels of object members (private, public, protected). Typescript supports private, public, readOnly, etc
  • @type: standard variable type

See here for some tsDoc tags

Lint Reference Configuration

npm install @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-import @typescript-eslint/parser eslint-import-resolver-typescript -D
Copy the code

.eslintrc.js

module.exports = {
  env: {
    browser: true.es2021: true.node: true,},root: true.extends: [
    / /... The rest of the configuration
    'plugin:import/recommended',].// rules can be configured according to the conditions
  rules: {
    // Here we suggest these js rules
  },
  settings: {
    'import/parsers': {
      '@typescript-eslint/parser': ['.ts'.'.tsx'],},'import/extensions': ['.tsx'.'.ts'.'.js'.'.jsx'.'.json'].'import/resolver': {
      typescript: {
        // Make sure the file is in the root directory
        project: ['./tsconfig.json'],}}},// the ts rule is overridden separately
  overrides: [{files: ['*.ts'.'*.tsx'].// Use typescript-eslint only for TS
      parser: '@typescript-eslint/parser'.// Enable static check
      parserOptions: {
        tsconfigRootDir: __dirname,
        ecmaFeatures: {
          jsx: true,},sourceType: 'module'.project: ['./tsconfig.json'],},plugins: ['@typescript-eslint'].extends: [
        / / ts support
        'plugin:import/typescript'.'plugin:@typescript-eslint/recommended'.'plugin:@typescript-eslint/recommended-requiring-type-checking',].rules: {
        // close js rules
        'no-shadow': 'off'.// ts
        '@typescript-eslint/no-var-requires': 'warn'.'@typescript-eslint/ban-ts-comment': 'off'.'@typescript-eslint/no-misused-promises': 'off'.'@typescript-eslint/no-floating-promises': 'off'.'@typescript-eslint/ban-types': 'off'.'@typescript-eslint/no-shadow': 'error'.'@typescript-eslint/explicit-module-boundary-types': 'off'.'@typescript-eslint/no-unsafe-member-access': 'off'.'@typescript-eslint/no-loss-of-precision': 'off'.'@typescript-eslint/no-unsafe-argument': 'off'.// no any
        '@typescript-eslint/no-explicit-any': 'off'.'@typescript-eslint/no-unsafe-assignment': 'off'.'@typescript-eslint/no-unsafe-return': 'off'.'@typescript-eslint/no-unsafe-call': 'off'.// ! operator
        '@typescript-eslint/no-non-null-assertion': 'off',},},],}Copy the code

Tsconfig See configuration

The React project

{
  "compilerOptions": {
    "target": "esnext"."module": "esnext"."useDefineForClassFields": true.Emit ECMAScript compliant class fields
    "forceConsistentCasingInFileNames": true.// File import is case-sensitive
    "allowJs": true."checkJs": true."skipLibCheck": true.// Skip checking all.d.ts files, mainly the node_modules directory
    "moduleResolution": "node"."lib": ["ESNext"."DOM"."DOM.Iterable"]."importHelpers": true."jsx": "react"."allowSyntheticDefaultImports": true.// Allow CJS module default to be imported
    "esModuleInterop": false./ / conversion CJS modules, add dedault import, because do not need to TSC compilation, so there is no need to open, only need allowSyntheticDefaultImports do check
    "sourceMap": true."noImplicitOverride": true.// The inherited class override method must write the override keyword
    "strict": true."isolatedModules": true.// Make sure every file has an import or export
    "resolveJsonModule": true./ / parsing json
    "noEmit": true,},"exclude": ["node_modules"]}Copy the code

Library project

{
  "compilerOptions": {
    "target": "es5"."module": "CommonJS"."useDefineForClassFields": true.Emit ECMAScript compliant class fields
    "forceConsistentCasingInFileNames": true.// File import is case-sensitive
    "allowJs": true."checkJs": true."skipLibCheck": true.// Skip checking all.d.ts files, mainly the node_modules directory
    "moduleResolution": "node"."lib": ["ESNext"."DOM"."DOM.Iterable"]."importHelpers": true."allowSyntheticDefaultImports": true.// Allow CJS module default to be imported
    "esModuleInterop": true.// Enable CJS module conversion
    "sourceMap": true."noImplicitOverride": true.// The inherited class override method must write the override keyword
    "strict": true."isolatedModules": true.// Make sure every file has an import or export
    "resolveJsonModule": true./ / parsing json
    "noEmit": false."declaration": true."declarationMap": true."outDir": "./dist"."baseUrl": "."
  },
  "exclude": ["node_modules"]}Copy the code

Some third-party libraries related to TS

@microsoft/api-extractor

Website: api-extractor.com

You can synthesize multiple. D. ts files into one, and have the ability of document generation and API export detection. It is recommended to use it in library development.

typedoc

Website: typedoc.org

Compared to @Microsoft/apI-Extractor, TypeDoc focuses on one thing – documentation. If you only want to convert.ts files to the corresponding API documentation, consider using it in your project.

See this issue for comparison with @Microsoft/api-Extractor.