preface

ESLint has become a common occurrence in projects. You may be annoyed by the bugs popping out of ESLint, or you may enjoy the neat code that has been uniformly verified. In any case, my advice is that ESLint should be used in more formal projects. Either directly using a simple configuration like extends: [‘ esLint: The Lint tool’s biggest help is to keep the syntax uniform, at least for all JavaScript files in your project (the editor alone can’t guarantee that).

Second, Lint helps you make your code cleaner and more efficient, If unused variables are not allowed, JSX/TSX should use the shorthand true attribute (
instead of
), etc. ESLint does not always try to simplify your code. In many cases it will require you to write more code in exchange for improved readability and security, especially in TypeScript scenarios. The explicit-module-boundary-types rule requires you to explicitly declare the return values of functions and class methods, and the switch-exhaustiveness-check rule requires you to handle all type branches of union variables.

This article is based on what I learned from writing, implementing, and promoting ESLint rule sets within my team (Taobao store). It will briefly introduce some of the rules THAT I think are necessary to share in TypeScript. Through this article, you will get an idea of what we consider when making rules. Think about constraints on TypeScript code and how you can promote this set of rules within your own team.

In addition, the front-end architecture team of Tao Department technology Department is promoting AppLint in Tao Department, and is preparing to promote ESLint to the whole Tao department front-end as one of CI/CD clips. Welcome students from the group to understand and try it out.

P.S. QCon+ projects I participated in: This course covers the transition of our team from JavaScript to TypeScript and the complete experience of TypeScript development. Welcome to listen to ~

Based on constraint

To accommodate different levels of constraint rigor that readers may have, we split the rules into basic and strict sections. The basic constraint rules are syntactically consistent (including the actual code and type sections) and are recommended for use by anyone on any project, even personal projects — they’re actually TypeScript already. Still care about that little Lint rule? The strict constraint section focuses on types and the special syntax of ECMAScript and TypeScript. It is suitable for students who require high code quality. There is no recommended error level here, even if it is all WARN, as long as you open it, you will at least come back later when you are in a good mood, right? (Right?)

array-type

TypeScript supports Array

and T[] declarations of Array types. This rule rules declarations of Array types in projects.

The configurations it supports:

  • Use only theArray<T>T[]One of the
  • For primitive types and type aliasesT[]For object types, function types, and so onArray<T>(recommended)

Why is that? : For such a perfectly consistent syntax, all we need is to define a specification and use that specification everywhere. In fact, this kind of rule (and the type assertion syntax that follows) is similar to the basic rule of single/double quotes, semicolon or no semicolon. If you can’t accept single quotes on the previous line and double quotes on the previous line, there’s no reason to accept an Array

here and a number[] there. Besides, I personally recommend using [].

await-thenable

Await calls are only allowed for asynchronous functions, promises, and PromiseLike

Why: Avoid meaningless await calls.

ban-ts-comment

Disallow the use of the @ts-directive, or allow it to be used if an explanation is provided, such as:

// @ts-expect-error
// @ts-nocheck Unmigrated files
Copy the code

This rule is recommended to be used with prefer-ts-expect-error, as described below.

Why it’s bad: If scribbling any is AnyScript, scribbling @ts-ignore is IgnoreScript.

ban-types

Disallow partial values from being annotated as types. This rule provides specific instructions for each disabled type to give a good indication when an error is triggered. Scenarios such as {}, Function, and object are annotated as types.

Why is that? Using {} can get you in a bind: there is no property ‘foo’ on type {}, so you’ll probably need type assertions or any below. Using object Function makes no sense.

  • For unknown object types, useRecord<string, unknown>
  • For function types, use the concrete type where the input parameter and return value are marked:type SomeFunc = (arg1: string) => void, or in unknown scenariostype SomeFunc = (... args: any[]) => any.

consistent-type-assertions

TypeScript supports type assertion using two different syntaxes, as and <>, for example:

const foo = {} as Foo;
const foo = <Foo>{};
// Similarly, constant assertions
const foo = <const> [1.2];
const foo = [1.2.3] as const;
Copy the code

This rule restricts the use of a uniform type assertion syntax, and I personally tend to use AS in Tsx and <> whenever possible because <> is more concise.

Why: Similar to array-type, it is syntactic, but it is important to note that using <> assertions in Tsx projects can cause errors because unlike generics,

explicitly tells the compiler that this is a generic syntax and not a component.

consistent-type-definitions

TypeScript supports declarations of object types through type and interface, and this rule can tie them down to one or the other.

In most cases, interface is used to declare object types. Type should be used to declare union types, function types, utility types, etc.

interface IFoo {}

type Partial<T> = {
    [P inkeyof T]? : T[P]; };type LiteralBool = "true" | "false";
Copy the code

The main reasons are as follows:

  • Naming -convention (which can be used to check whether the interface is named according to the specification), we can know IFoo is an interface immediately when we see it, Bar is a type alias immediately when we see it, and configure:

    {
      "@typescript-eslint/naming-convention": [
          "error",
          {
            selector: "interface",
            format: ["PascalCase"],
            custom: {
              regex: "^I[A-Z]",
              match: true,},},],}Copy the code
  • Interfaces play a very limited role in type programming, supporting only simple capabilities such as extends and generics, and should only be used to define defined constructs. A Type Alias, on the other hand, can use all the common mapping, conditional, and other types of programming syntax except extends. Also, the meaning of the “type alias” also means that you are actually using it to categorize types (union types), abstract types (function types, class types).

explicit-module-boundary-types

The return values of functions and class methods need to be explicitly specified, rather than relying on type inference, as in:

const foo = (): Foo= > {}
Copy the code

Why: Explicitly specifying functions visually distinguishes functions, such as side effects, while explicitly specifying function return values can improve TypeScript Compiler performance to some extent.

method-signature-style

Method signatures can be declared in method and property modes. The differences are as follows:

// method
interface T1 {
	func(arg: string) :number;
}

// property
interface T2 {
  func: (arg: string) = > number;
}
Copy the code

This rule is declarative and the second property method is recommended.

Method is like defining a method in a Class, whereas property is like defining a normal interface property, except that its value is a function type. The most important reason to recommend using property is that by using property + function value definitions, the types of functions as values can be strictly verified. This configuration uses contravariance instead of covariance to check function parameters. I’ll write a separate article on covariance and contravariance that I won’t expand on here, but you can read about contravariance in TypeScript types if you’re interested.

no-extra-non-null-assertion

Additional repeated non-null assertions are not allowed:

// x
function foo(bar: number | undefined) {
  const bar: number= bar!!! ; }Copy the code

Why: Er, why not?

prefer-for-of

When you iterate through an array using a for loop, if the index is only used to access array members, it should be replaced with for… Of.

Why: If not for compatibility scenarios, there really isn’t a need for a for loop in this scenario.

prefer-nullish-coalescing && prefer-optional-chain

Use?? Rather than | |, use a? B instead of a && a.b.

Why: Will logic or | | 0 and mistakes “” seen as false and the application of the default, and compared with the logic and optional chain && can bring more concise syntax (especially in the property access nested layers, or a value from a function, such as the document. The querySelector), And???? Better collaboration: const foo = a? .b? .c? .d ?? ‘default’; .

consistent-type-imports

Constraint to import types using import type {}, for example:

/ /)
import type { CompilerOptions } from 'typescript';

// x
import { CompilerOptions } from 'typescript';
Copy the code

Why: Import Type helps you better organize the import structure of your project headers (although TypeScript 4.5 supports mixed type and value imports: Import {foo, type foo}, but split value imports and type import statements are recommended for clearer project structure. Value imports and type imports are stored in TypeScript in different heap space, so you don’t have to worry about loop dependencies (so you can import child components from parent components, and child components import types defined in parent components).

A simple, well-organized example of an import statement:

import { useEffect } from 'react';

import { Button, Dialog } from 'ui';
import { ChildComp } from './child';

import { store } from '@/store'
import { useCookie } from '@/hooks/useCookie';
import { SOME_CONSTANTS } from '@/utils/constants';

import type { Foo } from '@/typings/foo';
import type { Shared } from '@/typings/shared';

import styles from './index.module.scss';
Copy the code

no-empty-interface

Empty interfaces are not allowed to be defined, but can be configured to allow empty interfaces under single inheritance:

// x
interface Foo {}

/ /)
interface Foo extends Bar {}
Copy the code

Why: An empty interface without a parent type is actually equal to {}, and while I’m not sure what you’re using it for, I can tell you it’s wrong. However, there are many empty interface scenarios with single inheritance, such as determining the inheritance relationship first and adding members later.

no-explicit-any

Explicit any is not allowed.

In fact, this rule is only set to a WARN level, because it is very expensive to actually do an ANY without or all of the replacement of the form of unknown + type assertions.

It is recommended to work with Tsconfig’s –noImplicitAny (check for implicit any) to ensure as much type integrity and coverage as possible.

no-inferrable-types

Unnecessary type annotations are not allowed, but can be configured to allow additional annotation for attribute members of classes and functions.

const foo: string = "linbudu";

class Foo {
	prop1: string = "linbudu";
}

function foo(a: number = 5, b: boolean = true) {
  // ...
}
Copy the code

Why: For common variables, consistent with the actual assignment type annotation is really meaningless, TypeScript control flow analysis can do it well, for the function parameters and class properties, mainly in order to ensure consistency, namely the function of all the parameters (including overloaded each statement), all of the attributes of the classes have type annotation, Instead of just annotating parameters/attributes that have no initial value.

no-non-null-asserted-nullish-coalescing

Non-null assertions are not allowed in conjunction with nullvalue merging: bar! ?? tmp

Why: Redundancy

no-non-null-asserted-optional-chain

Non-empty assertions are not allowed in conjunction with optional chains: foo? .bar!

Why: As with the previous rule, it’s redundant, and means you’re right! ?? ? There is something improper in the understanding of.

no-throw-literal

Instead of throwing a string like ‘err’, throw Error or an instance of an error-derived class, such as throw new Error(‘Oops! ‘).

Why: : Throwing an Error instance can automatically collect call stack information, and proposal-error-cause proposals can also be used across the call stack to attach context information to the cause of the Error, but does anyone really throw a string?

no-unnecessary-boolean-literal-compare

=== comparison of Boolean variables is not allowed, as in:

declare const someCondition: boolean;
if (someCondition === true) {}Copy the code

Why it works: First, remember that we’re writing TypeScript, so don’t assume that your variable value could be null, so if it does happen, your TS type notation is incorrect. And the rules of configuration items up to allow the Boolean | null values and true/false, so let your type more accurate.

no-unnecessary-type-arguments

Generic parameters consistent with the default values are not allowed, such as:

function foo<T = number> () {}
foo<number> ();Copy the code

Why: For code brevity.

no-unnecessary-type-assertion

Type assertions consistent with actual values are not allowed, such as const foo = ‘foo’ as string.

Why: You get the idea.

no-unnecessary-type-constraint

Generic constraints that conform to the default constraint are not allowed, such as Interface FooAny

{}.

Why: Also to simplify code, unknown is used by default for unspecified generic constraints after TS 3.9, and any before that, so there’s no need to write extends Unknown.

non-nullable-type-assertion-style

The rules require the types of assertions only play a role to null values, such as for string | undefined type assertion of the string, replace it with not empty claims!

const foo:string | undefined = "foo";

/ /)foo! ;// x
foo as string;
Copy the code

Why: Simplified code, of course! The essence of this rule is to check that only null-valued parts of the asserted subset of types are removed, so there is no need to worry about joint type misjudgment for multiple branches of types that have practical meaning.

prefer-as-const

Use as const instead of

for constant assertions, similar to the consistent-type-assertions rule above.

prefer-literal-enum-member

For enumerator values, only ordinary strings, numbers, NULL, and regees are allowed, not variable copy, template strings, and other operations that need to be computed.

Why: Although TypeScript allows various legal expressions to be used as enumerations, the compilation results of enumerations have their own scope and can result in incorrect assignments, such as:

const imOutside = 2;
const b = 2;
enum Foo {
  outer = imOutside,
  a = 1,
  b = a,
  c = b,
}
Copy the code

Where c == foo. b == foo. c == 1, or c == b == 2? Observe the compilation result:

"use strict";
const imOutside = 2;
const b = 2;
var Foo;
(function (Foo) {
    Foo[Foo["outer"] = imOutside] = "outer";
    Foo[Foo["a"] = 1] = "a";
    Foo[Foo["b"] = 1] = "b";
    Foo[Foo["c"] = 1] = "c";
})(Foo || (Foo = {}));
Copy the code

What do you know about my little brother?

prefer-ts-expect-error

Use @ts-expect-error instead of @ts-ignore.

Why: The main difference between @ts-ignore and @ts-expect-error is that the former is ignore, which directly abandons type checking on the next line regardless of whether there is an error on the next line, while the latter expects an error on the next line and throws an error when there is no error on the next line.

This type of intervention code check should be used with great caution, should never be used as an escape hatch under any circumstances (because it really works better than any), and if you do use it, make sure it’s used properly.

promise-function-async

Functions that return promises must be marked async. This rule ensures that the function caller only needs to deal with the try/catch or Rejected Promise cases.

Why: Need an explanation?

restrict-template-expressions

The return value of a computed expression in a template string must be a string. This rule can be configured to allow numbers, booleans, possibly null values, and regular expressions, or you can allow arbitrary values, but that’s not interesting…

Why: Values other than strings and numbers in template expressions can easily cause potential problems, such as:

const arr = [1.2.3];
const obj = { name: 'linbudu' };

/ / 'arr: 1, 2, 3'
const str1 = `arr: ${arr}`;
// 'obj: [object Object]'
const str2 = `obj: ${obj}`;
Copy the code

Either way, you don’t want to see it because it’s literally out of your control. It is recommended that only allowNumber be enabled in the rule configuration to allow numbers and not the other types. All you need to do is perform an actual logical conversion of this variable into the template string.

switch-exhaustiveness-check

When a switch’s criteria are union types, each branch of its type needs to be processed. Such as:

type PossibleTypes = 'linbudu' | 'qiongxin' | 'developer';

let value: PossibleTypes;
let result = 0;

switch (value) {
  case 'linbudu': {
    result = 1;
    break;
  }
  case 'qiongxin': {
    result = 2;
    break;
  }
  case 'developer': {
    result = 3;
    break; }}Copy the code

Why it works: A common problem on engineering projects is that you don’t know what else you’re missing when you look at the code and just pass it on by word of mouth. For example, each type branch in a union type variable may require special processing logic.

You can also check actual code using the never type in TypeScript:

const strOrNumOrBool: string | number | boolean = false;

if (typeof strOrNumOrBool === "string") {
  console.log("str!");
} else if (typeof strOrNumOrBool === "number") {
  console.log("num!");
} else if (typeof strOrNumOrBool === "boolean") {
  console.log("bool!");
} else {
  const _exhaustiveCheck: never = strOrNumOrBool;
  throw new Error(`Unknown input type: ${_exhaustiveCheck}`);
}
Copy the code

There are double safeguards at compile time to ensure that new type branches for union types also need to be handled properly. You can refer to the never article at the beginning for more on the use of never. In addition to union types, you can also use the never type to ensure that each enumerator needs to be handled.

enum PossibleType {
  Foo = "Foo",
  Bar = "Bar",
  Baz = "Baz",}function checker(input: PossibleType) {
  switch (input) {
    case PossibleType.Foo:
      console.log("foo!");
      break;
    case PossibleType.Bar:
      console.log("bar!");
      break;
    case PossibleType.Baz:
      console.log("baz!");
      break;
    default:
      const _exhaustiveCheck: never = input;
      break; }}Copy the code

These are some of the rules we are currently using, but there are a number of rules that are either highly customizable or narrowly applicable, so I won’t list them here. If you have any ideas, please feel free to share them with me, but please note: I’m not trying to indoctrinate you to use any rules, I’m just sharing the rules and considerations we use, so please make sure you don’t fall into this category before leaving a comment. Thank you for reading.