Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes to the recommended sets for 3.0.0 #1423

Closed
bradzacher opened this issue Jan 10, 2020 · 33 comments · Fixed by #2001
Closed

Changes to the recommended sets for 3.0.0 #1423

bradzacher opened this issue Jan 10, 2020 · 33 comments · Fixed by #2001
Labels
breaking change This change will require a new major version to be released package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin recommended-rules Discussion about recommended rule sets
Milestone

Comments

@bradzacher
Copy link
Member

bradzacher commented Jan 10, 2020

Similar to what I did for 2.0.0 (in #651), I'm putting forward the new recommended set ahead of time.
I'm looking for feedback from the community before we go ahead and make the changes.

First up, let me clarify that this set will be somewhat opinionated. It shall contain a small set of stylistic rules that I believe are a best practice based off of what I've seen in my career, and what the community has converged towards in the past.

eslint-recommended

See #1273


recommended (rules without type information)

Comments about the current config
  • "@typescript-eslint/adjacent-overload-signatures": "error",
    • I think this is a best practice.
    • Organising overload signatures next to one another makes code a lot easier to understand.
    • Verdict: in.
  • "@typescript-eslint/ban-ts-ignore": "error",
    • Helps prevent type safety issues.
    • This shall be replaced by the ban-ts-comment comment rule, which also bans ts-nocheck.
  • "@typescript-eslint/ban-types": "error",
    • The default config helps standardise the codebase on the consistently named base types, and warns against unsafe types.
    • We will be updating the default config in 3.0.0 to make it better, and closer to what people use (see discussion in feat(eslint-plugin): [ban-types] rework default options #848)
    • Verdict: in.
  • "@typescript-eslint/camelcase": "error",
    "@typescript-eslint/class-name-casing": "error",
    "@typescript-eslint/interface-name-prefix": "error",
    • These rules are hotly contested by some users that are migrating existing codebases to eslint, or just to ts-eslint after only using eslint:recommended:
      • Some users believe that there's no point to enforcing naming conventions.
      • Some users have legacy code that breaks the rest of the codebase's conventions.
      • Some users don't like to enforce camelCase for properties, as their APIs use snake_case.
    • My response has always been the same; In my opinion, it's better to enforce some standard than no standard, and camelCase is the standard the vast, vast majority of the JS/TS community uses.
      • In particular with new codebases, enforcing a standard from the get-go means that you have a consistent codebase with zero-effort, which reduces diff churn and cognitive load for engineers.
    • I do understand that turning this rule on for existing codebases can be a pain, however; with no rule fixer, it is an entirely manual process to either disable the rule for each non-compliant file, or to fix up violations.
    • That being said, if you find you don't like it for any reason, it is just a one-line change in your eslintrc to turn it off.
    • All of these naming rules are being merged into a single rule, naming-convention (feat(eslint-plugin): add rule naming-conventions #1318)
      • As such, they will all be removed from the recommended config, and replaced with naming-convention.
      • naming-convention is super flexible and powerful, so if it doesn't quite fit, it should be easy to reconfigure it to your liking.
  • "@typescript-eslint/consistent-type-assertions": "error",
    • TODO - want to remove this from recommended, and re-add the (deleted) no-angle-bracket-assertions.
  • "@typescript-eslint/explicit-function-return-type": "warn",
    • There has been some indirect debate about this rule being recommended, as people see it as a stylistic preference.
    • I've always been of the opinion that every function should have a strict contract:
      • It helps you catch places where inferred types don't exactly match up with expected.
      • It makes code reviews in "dumb" web UIs (i.e. github UI) easier.
      • If your project doesn't have noImplicitReturns turned on, it can also help you catch code paths that don't return a value.
    • A lot of people don't like being forced to type every single function return type, which I can understand; for non-exported functions, typing the return may be seen as useless.
    • My current thinking for this one is that we replace it with explicit-module-boundary-types.
      • This rule isn't as strict, but still enforces you create clear contracts at the module boundary, where it really matters.
      • Looking for feedback here.
  • "@typescript-eslint/member-delimiter-style": "error",
    • This is a pure stylistic rule; though nobody has directly complained about this one directly.
    • In the same vein as naming; I think it's good to enforce some standard rather than no standard.
    • Unlike the naming rules, this rule has a fixer which covers all cases, so there isn't a big burden to turning this rule on an existing codebase.
    • Verdict: remove.
  • "@typescript-eslint/no-array-constructor": "error",
    • I don't really have a strong opinion here. I don't think anyone does - we haven't had any real feedback in either direction.
    • Without feedback directly asking for its removal, I'm happy for it to stay in.
    • Verdict: in.
  • "@typescript-eslint/no-empty-function": "error",
    • Stylistic rule that helps ensure you don't have useless code.
    • The argument could be made that this is unnecessary due to bundlers trimming dead code.
    • I don't think we've had an argument for its removal.
      • Without feedback directly asking for its removal, I'm happy for it to stay in.
    • Verdict: in.
  • "@typescript-eslint/no-empty-interface": "error",
    • In the same vein as no-empty-function
    • The argument could be made that this is unnecessary due to interfaces not being runtime code.
    • I don't think we've had an argument for its removal.
      • Without feedback directly asking for its removal, I'm happy for it to stay in.
    • Verdict: in.
  • "@typescript-eslint/no-explicit-any": "warn",
    • Best practice. There is almost no case for using any with the existance of unknown and never.
    • Verdict: in.
  • "@typescript-eslint/no-inferrable-types": "error",
    • Stylistic rule that helps ensure you don't have useless code.
    • The argument could be made that this is unnecessary due to interfaces not being runtime code.
    • I don't think we've had an argument for its removal.
      • Without feedback directly asking for its removal, I'm happy for it to stay in.
    • Verdict: in.
  • "@typescript-eslint/no-misused-new": "error",
    • Helps catch incorrect usage of new/constructor within classes and interfaces respectively.
    • Verdict: in.
  • "@typescript-eslint/no-namespace": "error",
    • Namespaces are, for the most part, considered deprecated syntax. In all new projects, the preferred way is to use import/export syntax to namespace your code and hide internals.
    • Especially when used with global declaration merging, namespaces can make it very hard to reason about where code is defined, and how it is used.
    • Verdict: in.
  • "@typescript-eslint/no-non-null-assertion": "warn",
    • The problem is that this operator is a complete safety hole.
      • e.g. It's very easy to add a non-null assertion, and then have someone refactor code higher up, making the assertion incorrect.
      • Because the operator is tells typescript "shut up I know what I'm doing", there's no way to catch "incorrect" non-null assertions.
    • With the addition of optional chaining and nullish coalescing in TS 3.7, there is almost no need for this operator.
    • There are still some dubiously valid use cases, hence it is recommended as a warning.
    • Verdict: in.
  • "@typescript-eslint/no-this-alias": "error",
    • Aliasing this is an old, old, old, and bad practice. You should just use arrow functions.
    • Verdict: in.
  • "@typescript-eslint/no-unused-vars": "warn",
  • "@typescript-eslint/no-use-before-define": "error",
    • The errors caught by this rule are generally caught by typescript. There's a few edge cases, but by-and-large, it's covered.
    • It also relies upon scope analysis, which is currently in a bad state.
    • Verdict: remove.
  • "@typescript-eslint/no-var-requires": "error",
    • require statements are always typed as any, which is a type safety hole.
    • You should instead use import/export syntax, which will use defined types for the module.
    • Verdict: in.
  • "@typescript-eslint/prefer-namespace-keyword": "error",
    • Already shouldn't be using namespaces due to no-namespace, but this rule just makes sure that if you do have to use it, you don't use the module Foo {} syntax, so it's clear what you're doing.
  • "@typescript-eslint/triple-slash-reference": "error",
    • This rule ensures you use the import syntax when required instead of reference comments
    • Verdict: in.
  • "@typescript-eslint/type-annotation-spacing": "error",
    • This is a pure stylistic rule; though nobody has directly complained about this one directly.
    • In the same vein as naming; I think it's good to enforce some standard rather than no standard.
    • Unlike the naming rules, this rule has a fixer which covers all cases, so there isn't a big burden to turning this rule on an existing codebase.
    • Verdict: remove.
  • "no-var": "error",
    • var declarations are error-prone due and often hard to undersand due to scope hoisting. Should just use let and const.
    • TypeScript will transpile let/const to var for you if you are targetting an old runtime, so there's no reason to use var.
    • Verdict: in.
  • "prefer-const": "error",
    • TypeScript will generally do a better job of inferring types if you use const.
    • The existence of const has recently been debated, but I think that it's better to use it when possible. Though I don't feel strongly in either direction.
    • Unsure if we should leave this in or not. Looking for feedback here.
  • "prefer-rest-params": "error",
    • using arguments is a non-typesafe way of accessing arguments. Using a rest param means you can strictly and clearly declare function inputs and requirements.
    • TypeScript will transpile a rest param to arguments if you are targetting an old runtime, so there's no reason to use arguments.
    • Verdict: in.
  • "prefer-spread": "error"
    • In the same vein, spread arguments is much clearer and safer than using .apply, becuase you don't have to worry about manually specifying the this context.
    • TypeScript will transpile a spread argument to apply if you are targetting an old runtime, so there's no reason to use .apply.
    • Verdict: in.
Comments about the new rules
  • @typescript-eslint/ban-ts-comment
    • A better version of ban-ts-ignore
    • Verdict: add as an error
  • @typescript-eslint/brace-style
    • Stylistic
    • Verdict: do not add
  • @typescript-eslint/class-literal-property-style
    • Stylistic
    • Verdict: do not add
  • @typescript-eslint/default-param-last
    • Stylistic
    • Verdict: do not add
  • @typescript-eslint/explicit-module-boundary-types
    • As mentioned in the previous section, this is a less-strict version of @typescript-eslint/explicit-function-return-type
    • Current thinking is that this should be added.
    • Looking for feedback here.
  • @typescript-eslint/naming-convention
    • As mentioned in the previous section, this unifies several naming rules.
    • However, I don't think it's worthwhile dealing with the fallout from having stylistic rules in the recommended, so this won't be added.
    • Verdict: do not add
  • @typescript-eslint/method-signature-style
    • Stylistic, but it has some value in making types safer.
    • Looking for feedback here.
  • @typescript-eslint/no-extra-non-null-assertion
    • avoids useless code, same as no-extra-semi, which is recommended in the base ruleset.
    • Verdict: add as error
  • @typescript-eslint/no-extra-semi
    • We added this extension to support TS features.
    • It is recommended in the base ruleset.
    • Verdict: add as error
  • @typescript-eslint/no-non-null-asserted-optional-chain
    • Obviously this is an error
    • Verdict: add as error
  • @typescript-eslint/prefer-as-const
    • Stylistic, but just plain better practice
    • Verdict: add as warning
  • @typescript-eslint/quotes
    • Stylistic
    • Verdict: do not add

TL;DR

 {
   "extends": "./configs/base.json",
   "rules": {
     "@typescript-eslint/adjacent-overload-signatures": "error",
+    "@typescript-eslint/ban-ts-comment": "error",
-    "@typescript-eslint/ban-ts-ignore": "error",
     "@typescript-eslint/ban-types": "error",
     "camelcase": "off",
-    "@typescript-eslint/camelcase": "error",
-    "@typescript-eslint/class-name-casing": "error",
-    "@typescript-eslint/consistent-type-assertions": "error",
-    "@typescript-eslint/explicit-function-return-type": "warn",
+    "@typescript-eslint/explicit-module-boundary-types": "warn",
-    "@typescript-eslint/interface-name-prefix": "error",
-    "@typescript-eslint/member-delimiter-style": "error",
     "no-array-constructor": "off",
     "@typescript-eslint/no-array-constructor": "error",
     "no-empty-function": "off",
     "@typescript-eslint/no-empty-function": "error",
     "@typescript-eslint/no-empty-interface": "error",
     "@typescript-eslint/no-explicit-any": "warn",
+    "@typescript-eslint/no-extra-non-null-assertion": "error",
+    "@typescript-eslint/no-extra-semi": "error",
     "@typescript-eslint/no-inferrable-types": "error",
     "@typescript-eslint/no-misused-new": "error",
     "@typescript-eslint/no-namespace": "error",
+    "@typescript-eslint/no-non-null-asserted-optional-chain": "error",
     "@typescript-eslint/no-non-null-assertion": "warn",
     "@typescript-eslint/no-this-alias": "error",
     "no-unused-vars": "off",
     "@typescript-eslint/no-unused-vars": "warn",
-    "no-use-before-define": "off",
-    "@typescript-eslint/no-use-before-define": "error",
     "@typescript-eslint/no-var-requires": "error",
+   "@typescript-eslint/prefer-as-const": "warn",
     "@typescript-eslint/prefer-namespace-keyword": "error",
     "@typescript-eslint/triple-slash-reference": "error",
-    "@typescript-eslint/type-annotation-spacing": "error",
     "no-var": "error",
     "prefer-const": "error",
     "prefer-rest-params": "error",
     "prefer-spread": "error"
   }
 }

recommended-requiring-type-checking (rules with type information)

Comments about the current config
  • "@typescript-eslint/await-thenable": "error",
    • Best practice to avoid unnecessary awaits.
    • Verdict: in.
  • "@typescript-eslint/no-for-in-array": "error",
    • Best practice to avoid accidentally iterating over array properties.
    • Verdict: in.
  • "@typescript-eslint/no-misused-promises": "error",
    • Helps catch bugs due to missing awaits.
    • Verdict: in.
  • "@typescript-eslint/no-unnecessary-type-assertion": "error",
    • Stylistic rule, I don't think this helps catch any bugs.
    • It's certainly a good practice to avoid unnecessary code.
    • Looking for feedback here.
    • Current thinking is to keep it in
  • "@typescript-eslint/prefer-includes": "error",
    "@typescript-eslint/prefer-string-starts-ends-with": "error",
    • Stylistic rules; I don't think they help catch any bugs.
    • These rules are about consistency, and enforcing the use of new APIs, which are all good things.
    • Looking for feedback here.
    • Current thinking is to remove it
  • "@typescript-eslint/prefer-regexp-exec": "error",
    • RegExp#exec is faster than String#match. Why would you ever use the slower version?
    • Verdict: in.
  • "@typescript-eslint/require-await": "error",
    • Async function without an await will cause the runtime to wrap the return value in a promise.
    • This is unnecessary and a bug.
    • Our extension also adds support for explicitly returning promises, which makes the rule easier to use.
    • Verdict: in.
  • "@typescript-eslint/unbound-method": "error",
    • Usage of unbound methods are a consistent source of bugs.
    • Verdict: in.
Comments about the new rules
  • @typescript-eslint/no-base-to-string
    • Stylistic, may catch some errors though.
    • Looking for feedback here.
  • @typescript-eslint/no-dynamic-delete
    • Stylistic, may catch some errors though.
    • Verdict: do not add
  • @typescript-eslint/no-floating-promises
    • Helps catch bugs due to non-awaited function calls
    • Verdict: add as error
  • @typescript-eslint/no-implied-eval
    • using eval-like methods is a security problem, and a type safety hole
    • Verdict: add as error
  • @typescript-eslint/no-throw-literal
    • stylistic, it's a great practice, but it's hard to recommend for all codebases
      • eg react suspense requires you to throw Promises.
    • Verdict: do not add
  • @typescript-eslint/no-unnecessary-boolean-literal-compare
    • Stylistic, I don't think it has enough value to recommend for everyone
    • Verdict: do not add
  • @typescript-eslint/no-unnecessary-condition
    • Unsure if this should be recommended. It's great as it removes unnecessary code, but does it help catch bugs?
    • Two arguments I know of against this rule:
      • It relies upon having 100% correct types, which isn't always the case.
        Case-in-point: the typescript API types themselves, which have a lot of things defined as non-nullable, but in practice are actually nullable in some cases. The types are non-nullable because it's the 0.1% case that it's nullable, so nullable types would pollute the TS codebase with unnecessary checks.
      • There's a lot of people that practice defensive programming as well, purposely guarding against things the types suggest are impossible, just in case something goes wrong at runtime.
    • Looking for feedback here.
  • @typescript-eslint/no-unsafe-assignment (feat(eslint-plugin): add rule no-unsafe-assignment #1694)
    • This should improve the safety of every codebase
    • Verdict: add as error
  • @typescript-eslint/no-unsafe-call
    • This should improve the safety of every codebase
    • Verdict: add as error
  • @typescript-eslint/no-unsafe-member-access
    • This should improve the safety of every codebase
    • Verdict: add as error
  • @typescript-eslint/no-unsafe-return
    • This should improve the safety of every codebase
    • Verdict: add as error
  • @typescript-eslint/prefer-nullish-coalescing
    • This is a good rule on the surface, but it is not easy to recommend due to the changes often being not exactly 1:1 replacements, esp in complex code.
    • Verdict: do not add
  • @typescript-eslint/prefer-optional-chain
    • This is a good rule on the surface, but it is not easy to recommend due to the changes often being not exactly 1:1 replacements, esp in complex code.
    • Verdict: do not add
  • @typescript-eslint/prefer-readonly-parameters
    • Stylistic, many people would hate this due to the deep readonly requirement
    • Verdict: do not add
  • @typescript-eslint/restrict-plus-operands
    • Prevents you from accidentally adding string + undefined, which is definitely a bug.
    • Makes sure you are correctly handling string + number
    • Verdict: add as error
  • @typescript-eslint/restrict-template-expressions
    • Catches accidentally passing objects/arrays/nulls/undefined into template expressions, which may not produce the string you expect.
    • A lot of time these strings are used in user-output positions, so leaking null etc is an obvious bug.
    • Verdict: add as error
  • @typescript-eslint/return-await
    • Stylistic, may catch some errors though.
    • Looking for feedback here.
  • @typescript-eslint/switch-exhaustiveness-check
    • Stylistic, may catch some errors though.
    • Looking for feedback here.

TL;DR

 {
   "extends": "./configs/base.json",
   "rules": {
     "@typescript-eslint/await-thenable": "error",
+    "@typescript-eslint/no-floating-promises": "error",
     "@typescript-eslint/no-for-in-array": "error",
+    "@typescript-eslint/no-implied-eval": "error",
     "@typescript-eslint/no-misused-promises": "error",
     "@typescript-eslint/no-unnecessary-type-assertion": "error",
+    "@typescript-eslint/no-unsafe-assignment": "error",
+    "@typescript-eslint/no-unsafe-call": "error",
+    "@typescript-eslint/no-unsafe-member-access": "error",
+    "@typescript-eslint/no-unsafe-return": "error",
-    "@typescript-eslint/prefer-includes": "error",
     "@typescript-eslint/prefer-regexp-exec": "error",
-    "@typescript-eslint/prefer-string-starts-ends-with": "error",
     "require-await": "off",
     "@typescript-eslint/require-await": "error",
+    "@typescript-eslint/restrict-plus-operands": "error",
+    "@typescript-eslint/restrict-template-expressions": "error",
     "@typescript-eslint/unbound-method": "error",
     "no-var": "error",
     "prefer-const": "error",
     "prefer-rest-params": "error",
     "prefer-spread": "error"
   }
 }
@bradzacher bradzacher added package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin breaking change This change will require a new major version to be released recommended-rules Discussion about recommended rule sets labels Jan 10, 2020
@bradzacher bradzacher added this to the 3.0.0 milestone Jan 10, 2020
@G-Rath
Copy link
Contributor

G-Rath commented Jan 10, 2020

I'm fairly happy with the majority of what you've got here, but I do have three points:

  • "@typescript-eslint/explicit-function-return-type": "warn",

I'm a fan of this on paper, but in practice it is very noisy, especially given that WebStorm now has type hints.

I feel the primary annoyance I have is when I'm returning simple types in simple methods, or void.

"Simple types in simple methods" is pretty arbitrary, and most of time it's when I'm relying on inference to handle an annoying type, so I don't expect there to be much to gain exploring that.

However, I wonder if it's worth exploring allowing methods that return void (or better have an exclusion list option) to not have their type?

Ultimately, I know the primary thing I want it to catch:

const getValue = () => {
  const arr: string[] = [];

  // ...

  if(/* condition */) {
    return arr;
  }

  // ...

  return 'No values found';
}

Effectively: I definitely want complex return types, such as unions, or anything w/ a generic (so Promise, Array, etc) to be explicitly typed.

While this might seem a bit unlikely, it's an easy mistake to make when working w/ highly dynamic languages such as Ruby (which is my works primary stack).

  • "prefer-const": "error",

I'm interested in reading more about this, if you've got links on hand.

I was under the impression that it would make the type more rigid, which can have a poison-well type effect, similar to how if you stick readonly on a type deep in some code you tend to end up having to add it to everything in the types path.

(Not saying it's bad - just interested about the pros vs cons of using it literally everywhere).

  • "@typescript-eslint/require-await": "error",

I'm not a big fan of having this in the recommended set due to it's highly opinionated history (I'm ok w/ having it around as an available rule).


That's my 2cs - do with it as you will 🙂

@bradzacher
Copy link
Member Author

explicit-function-return type .........

This is why I suggested switching it for the upcoming explicit-module-boundary-types (#1020).

I personally love explicit return types on everything for the reasons I stated (clear contracts, and easy to review). But I do understand that people, esp those who use frameworks like react/angular which have many void functions, dislike annotating every single method.

This is why I suggested the new rule which will only require the module boundaries have explicit return types.
This should greatly cut down the number of explicit return types required, and ensure that each module presents a strict, well-defined contract.

However, I wonder if it's worth exploring allowing methods that return void (or better have an exclusion list option) to not have their type?

People have suggested this, but the problem is that it requires type information to do properly, which severely limits the usability of the rule, as many users don't use type information.


I was under the impression that it would make the type more rigid, which can have a poison-well type effect, similar to how if you stick readonly on a type deep in some code you tend to end up having to add it to everything in the types path.

a readonly type is very different to the inferred type of a const variable.
A readonly type is exactly that - it's treated by typescript as immutable (i.e. arrays cannot be pushed to, object properties cannot be assigned to).

OTOH, using const just means typescript can narrow the type significantly for primitive values, because it knows the primitive value cannot change. However, it won't ever break anything.

declare function acceptsSpecific(arg: 1 | 'foo'): void;
enum Foo { a = 1 }

{
    const v1 = 1; // type === 1
    const v2 = 'foo'; // type === 'foo'
    const v3 = true; // type === 'true
    const v4 = Foo.a; // type === Foo.a
}

{
    let v1 = 1; // type === number
    let v2 = 'foo'; // type === string
    let v3 = true; // type === boolean
    let v4 = Foo.a; // type === Foo
}

{
    acceptsSpecific(1); // no error

    const v1 = 1;
    acceptsSpecific(v1); // no error

    let v2 = 1;
    acceptsSpecific(v2); // error - number is not assignable to 1
    if (v2 === 1) {
        acceptsSpecific(v2); // no error now that it's narrowed
    }
}

repl


require-await

Have you used our version of the rule, which uses type information? The base eslint version is limited as it solely relies upon single-file static analysis.

Our extension goes one step further and inspects every return statement to check if it is a promise. I guess the rule name is wrong now, it's probably better to call it async-function-must-have-await-or-return-promise.

With our extension, IMO there's no more opinionated side to it - an async function must actually have code that is async in some form (even if it's a Promise.resolve).

@glen-84
Copy link
Contributor

glen-84 commented Jan 10, 2020

Disclaimer: I'm not actually using the recommended set. 🙃

  1. @typescript-eslint/require-await – I decided not to enable this rule based on the discussion in this issue (there are valid use cases for not using await in an async function). With the addition of TypeScript, I'm not sure if this rule is actually useful in catching bugs?

  2. @typescript-eslint/unbound-method – I'm not yet using this rule because it seems to have quite a lot of bugs:

  3. @typescript-eslint/no-unnecessary-condition – I'm using this. It can be useful in cases like this:

    protected async handleSuccess(message: string, form: HTMLFormElement): Promise<void> {
        this.showSuccess(message);
    
        if (form) { // *** Unnecessary conditional, value is always truthy ***
            this.clearValidationErrors(form);
        }

    The type of form should either be falsy, or the check should be removed. It helps to make the types more accurate – I'm not sure about actually preventing bugs.

@bradzacher
Copy link
Member Author

bradzacher commented Jan 10, 2020

@typescript-eslint/require-await

there are valid use cases for not using await in an async function

One must always look at the eslint core team discussions through the lens of a typescript developer - they do not have types, and they cannot use types, so they rely on lint rules to validate their code.
For us, typescript's type checker can help deal with a lot of their concerns in a cleaner, clearer way, and explicit function return types help ensure you return a promise on every branch etc.

Reading through the linked issue, it seems like there is exactly one use case they say is valid: "I have a function which has no awaits and returns no promise to automatically wrap its return value in a promise".

IMO this is very much an edge case, and it seems like a bit of a code smell; if I ran into an async function with no awaits and no promises, I would assume it is a bug; I wouldn't assume "oh this person wanted to auto-wrap their return in a promise". Unless ofc there is a comment explicitly stating that, but if you're putting in a comment to explicitly state that, then you're purposely circumventing codebase standards, so it would be a case for an eslint-disable comment.

Finally, any modern library should be using async/await itself, which means that more than likely this example is not something to worry about. If you await a non-promise value, the runtime will automatically wrap the non-async value in a promise, giving you the same benefits as if you had marked your function as async.

@typescript-eslint/unbound-method

quite a lot of bugs

I would contest this statement.

#636 can be worked around v easily by using the pretty standard practice of using arrow functions instead of pure methods.
TBH it's been a long while since I saw this style of things.

#743 isn't a bug, it's a feature request. The fact that passing a second argument to those functions isn't a defined language feature, it's an implementation detail of those functions.
Again, there's a simple workaround for it:

[].forEach(this.foo, this);
// instead do
[].forEach(this.foo.bind(this));

Yet to look into #1425, as it's just been raised. #1425 is indeed a bug that's actually always existed in the rule (even in tslint). I have a fix up for it.

@typescript-eslint/no-unnecessary-condition

To be clear, I do like this rule as well! It helps you avoid useless code, and thus is a net perf win.

With the 3.0.0 release I'm just trying to be a bit more conservative with the recommended sets, because I'm sick of having to have discussions about "recommending stylistic rules that don't catch bugs" 🙃.

@G-Rath
Copy link
Contributor

G-Rath commented Jan 10, 2020

Have you used our version of the rule, which uses type information?

That's really interesting - I recently had this flagged on code a coworker of mine had written, as they were using TS for the first time, but having checked it out you (of course) correct: I'm not told I have to use await for async () => Promise.resolve().

Meanwhile I mean to do this:

const fn = async () => {
  return Promise.resolve([1, 2, 3].map(v => v));
};

But then you can remove the async...

I definitely agree the rule is better than the eslint version, so I guess now I'm neutral about it 🤷‍♂
I'll have a play in some of my repos and see what happens.

@glen-84
Copy link
Contributor

glen-84 commented Jan 10, 2020

@typescript-eslint/require-await

I'm going to enable this. I think that it'll do more good than harm in my case.

@typescript-eslint/unbound-method

I thought that I was affected by #636, but I'm actually not. I'm also going to enable this.

@typescript-eslint/no-unnecessary-condition

It's a tricky one. I think that even though it's correct in many/most cases, it's likely going to cause a fair amount of noise in larger codebases that have not had it enabled. I'm interested to hear what other developers have to say about it.

@bradzacher bradzacher pinned this issue Jan 13, 2020
@bradzacher bradzacher changed the title Changes to the recommended sets for 3.0.0 Changes to the recommended sets for 3.0.0 Jan 13, 2020
@Retsam
Copy link
Contributor

Retsam commented Jan 13, 2020

prefer-const

Strong favor of this one. I know const gathered debate because people don't like that the JS semantics of what const means is different than what const means in other languages; but I think there's a big practical advantage to readability - if const is used consistently, then seeing let thing = /*...*/ means that thing will be reassigned at some point.

@typescript-eslint/no-unnecessary-condition

I'm biased, because I wrote the original rule; but I do really like this. A good chunk of the false positives will go away with the breaking change fix in #1163; but there will still be some false positives, (see below).

Good

This found more actual mistakes in our codebase than any linter rule in recent memory. Most of them were of them were dead code removal, not bugs; but we did find some legitimate bugs, too. For example, we had refactored some code from Something | undefined to use a "maybe" type: Maybe<Something>, but we had left code around like:

declare const something: Maybe<Something>;

// Bug: something is a maybe object, and thus always truthy.
if (!something) { }

// It should have been (or further refactored)
if(something.isNothing()) { }

The above is essentially a more general version of the checksConditionals option from no-misused-promises Suggestion: if no-unnecessary-conditionals is added to the ruleset, then no-misused-promises should be configured with { checkConditionals: false } to avoid overlapping rule errors. (Arguably, the checkConditionals option could be removed from the rule altogether in 3.0)

Mixed

Object index types are a mixed bag. It's a common place of false positives; because a real common practice is to use types like type SomethingRecord = Record<string, Something>, (i.e. { [key: string]: Something }). This claims that any possible string will correspond with a Something object, which obviously isn't really true (except with Proxy magic). So you'll get false-positives on reasonable code like:

const somethings: SomethingRecord = {};
function getSomething(key: string) {
    if(somethings[key]) {
        return somethings[key];
    }
}

I consider this a mixed bag, because this does encourage more "honest" types, like type SomethingRecord = Record<string, Something | undefined>, which can help catch bugs; but of course that is still a change that needs to be made to account for this rule; even if it does make for better code overall.

Bad

Array indexing just isn't fun. It's the same issue as object indexing: array types claim that all possible indexes correspond with values, but in practice they don't. There's no good fix because Array<Something | undefined> isn't reasonable in the same way that Record<string, Something | undefined> is: it will create a bunch of false-positive null checks when actually iterating over the array.

I may try to see if we can adjust the rule to alleviate these false-positives. Certainly errors from stuff like if(array[i]) should be easily suppressed; and maybe I can ignore array based errors more generally.

It relies upon having 100% correct types, which isn't always the case.
Case-in-point: the typescript API types themselves, which have a lot of things defined as non-nullable, but in practice are actually nullable in some cases. The types are non-nullable because it's the 0.1% case that it's nullable, so nullable types would pollute the TS codebase with unnecessary checks.

I'd be curious what TS APIs you're thinking of.

But as it says, these are a pretty slim minority case: and even then I think having the explicit lint rule override is a good thing, as it signals that it's an intentional check and not a mistake that someone might "helpfully" "fix" later on. (And hopefully encourages the author to write a comment explaining the circumstances in which the value might be different than the typings claim).

There's a lot of people that practice defensive programming as well, purposely guarding against things the types suggest are impossible, just in case something goes wrong at runtime.

Yeah, codebases with lots of extra type guards for the sake of JS consumers are a good example of a codebase where it might be a good idea to disable this rule. (Should add that to the readme section on "When Not To Use It"...)

Or perhaps we could find some way of avoiding those false positives, too. Maybe an exception for conditions inside "assertion functions". (Which could either be identified via type; or perhaps just by considering any functions with names starting with "assert" as special cases)


Do we have a rough timeline for the 3.0 milestone? I may take a shot at some of these potential enhancements, which might impact the viability of including this as a default rule. (Also some other breaking API changes to make)

@bradzacher
Copy link
Member Author

bradzacher commented Jan 13, 2020

I'd be curious what TS APIs you're thinking of.

microsoft/TypeScript#24706

eg in this codebase, which was added due to a crash encountered by a user (#499):

function isDangerousMethod(symbol: ts.Symbol, ignoreStatic: boolean): boolean {
const { valueDeclaration } = symbol;
if (!valueDeclaration) {
// working around https://github.com/microsoft/TypeScript/issues/31294
return false;
}

Do we have a rough timeline for the 3.0 milestone?

Soon™
We've got our list of things we want to include (https://github.com/typescript-eslint/typescript-eslint/milestone/3), but we don't have an exact timeframe, as it will rely upon us volunteers finding enough time to get it done.

I was hoping by end of this month, but that's a very optimistic goal.


Thanks for the commentary on no-unnecessary-condition. All good points.

For array and object index accessors, we could probably make the rule smarter here to help deal with some of these cases where the types are "correct", but you still want to defensively program, i.e. something like

if (options.allowDefensiveIndexAccessorCheck) {
  if (
    type is arrayType &&
    isPropertyAccessIndexAccessor
  ) { markAsValidCondition(); }
  if (
    type is objectType &&
    typeIncludesIndexSignature &&
    isPropertyAccessIndexAccessor
  ) { markAsValidCondition(); }
}

@Mister-Hope
Copy link

Mister-Hope commented Jan 14, 2020

"@typescript-eslint/explicit-function-return-type": "warn",
There has been some indirect debate about this rule being recommended, as people see it as a stylistic preference.
I've always been of the opinion that every function should have a strict contract:
It helps you catch places where inferred types don't exactly match up with expected.
It makes code reviews in "dumb" web UIs (i.e. github UI) easier.
If your project doesn't have noImplicitReturns turned on, it can also help you catch code paths that don't return a value.
A lot of people don't like being forced to type every single function return type, which I can understand; for non-exported functions, typing the return may be seen as useless.
My current thinking for this one is that we replace it with explicit-module-boundary-types (#1020).
This rule isn't as strict, but still enforces you create clear contracts at the module boundary, where it really matters.
Looking for feedback here.

I have a bad experience on the rule explicit-function-return-type. The problem is if we are using a existing d.ts file, to provide lint and typecheck, this rule can be annoying.

For example, we define an interface here at xxx.d.ts which is inside a package and decalre in tsconfig.json:

interface XXX {
   /** docs and examples writen in JSDocs */
   yyy?: (x: string) =>string
}

Then if we make wrong return like below, TS will show an error:

const x: XXX = {
    yyy: x => Boolean(x)
}

While this is fine, and don't need us to write type info.

const x: XXX = {
    yyy: x => `aaa${x}`
}

So there is no need to write the return type again. Inforce us to write return types in this situation, in my mind, is annoying. Since we build our type declaration files just for additional controling, and reducing type defination work. This rule is obviously doing the opposite job.

I agree that in other situation, writing return types instead of leting typescript infer one itself is a good habit. But disagree the rule in the situation I met. Also, I think that

It makes code reviews in "dumb" web UIs (i.e. github UI) easier.

this makes little advantage, since the expriences on Github and Editors like VSCode is really difference. Most people only use Gihub to review some code only extemporaneous.

@bradzacher
Copy link
Member Author

bradzacher commented Jan 14, 2020

explicit-function-return-type
So there is no need to write the return type again. Inforce us to write return types in this situation, in my mind, is annoying.

Have you tried the allowTypedFunctionExpressions option? This option does exactly what you want, and will not enforce that your example has a return type.

since the expriences on Github and Editors like VSCode is really difference

This is the point though - github doesn't have intellisense, so the more type annotations you put in your code, the less time a reviewer has to spend jumping around codebases..

As a senior engineer, and open source maintainer, I know how important it is to make this process easier; I have had many working days where all I've done is code reviews via github.

The less time I have to spend interrogating types outside the review interface, the faster, and better I can do reviews. I.e. if an engineer spending 1sec writing a return type saves me 30s in jumping outside the UI to a new file to check another signature, then that's a huge win for everyone.

@Retsam
Copy link
Contributor

Retsam commented Jan 15, 2020

explicit-function-return-type

I'm mildly opposed to explicit return type lint rules, too. It's not a huge deal, as I can of course turn the rule off... but I will turn that rule off.

I think restricting explicit type checking to module boundaries is an improvement, but I don't think fundamentally changes the dynamic.

"Boilerplate"

The obvious objection; a lot of type annotations, (especially primitives and : void) don't terribly improve the readability of the code and can be considered boilerplate.

I think my most salient objection is that this rule can be bad for the new-to-Typescript developer's experience. (I imagine this is the class most affected by "recommended" rulesets, because they're the least likely to feel confident in disabling "best practice"). In my experience a lot of JS developers trying TS feel like Typescript is wasting their time, and having a linter complain about not annotating the return of every function adds to this frustration, in a not insignificant way.

In the big picture, a few return type annotations really isn't a meaningful amount of boilerplate in a significant project, but the perception of boilerplate may be more important factor.

Again, though, restricting to module boundaries does help; (though it might introduce some confusion, too, as to why adding export would raise a linter error).

Not that helpful for reviews

It can help with reviewing code outside of a development environment, but I do find that's only true in a rather narrow range of cases. Usually when I'm interested in the return type of a function, I'm interested in what the type is, not what it's called; which is almost always going to mean looking somewhere else in the codebase anyway.

(Tangentially, I've started getting into using VSCode plugins to review PRs from the editor itself, and it's a fairly nice experience)

Annotating return types can be non-trivial

There are various cases in which annotating return types is somewhat annoying. Wrapper functions around APIs that don't export their return type as its own type. Complex generic types can be tedious to annotate.

Functions that return large objects heterogenous can be annoying here, too, like a large config object. You may end up just mirroring the entire shape of the object as the return type, which in some cases is just unnecessary noise.

Dictionary-like objects with a long list of static keys can be annoying here too, a type like Record</* every possible key */, Value> can be tedious. Of course, this is a valid time to just turn this rule off, but sometimes people will instead annotate it as Record<string, Value>, leading to:

Annotating return types can be lossy

Sometimes the typescript compiler does a better job with the return type than the human - specifically, the return types are sometimes looser than necessary. Consider:

const getKey = (isFoo: boolean) => {
    if(isFoo) { return  "foo" }
    return  "bar";
}

A beginner to TS, is likely to annotate this function as : string, but a more precise definition is "foo" | "bar". In most cases, this won't matter, but it can lead to issues like:

const messages = {
    foo: "Foo!",
    bar: "Bar!",
};
const key = getKey(isFoo);
// Element implicitly has an 'any' type because expression of type 'string' can't be used to index type (typeof messages)
//     No index signature with a parameter of type 'string' was found on type (typeof messages)
console.log(messages[key]);

Not a big deal, but something that could be avoided by just letting TS figure out the type.

Lossy type signatures isn't just a beginner mistake, either. For example, we had an issue in our codebase like this:

class Base { /* properties omitted */ };
class Foo extends Base { foo!: string };
class Bar extends Base { bar!: string };

const getStuff = (): Record<"foo" | "bar", Base> => ({
   foo: new Foo(),
   bar: new Bar(),
   // etc.
});

getStuff().foo.foo; // Invalid because `foo` is `Base` not `Foo`; because the return annotation is lossy.

(This turned out tricky because "foo" | "bar" came from elsewhere in the codebase, we wanted the compiler to verify that "stuff" had the right keys, but not to lose the specific shape of the values)

Example: React function components

React function components are a particular case where I find explicit function annotations are a bit annoying. They really don't communicate new information to the reader of the code.

And it's not clear to beginners what the actual annotation should be. The first guess is usually JSX.Element, based on hovering over the JS, but React components are allowed to return other things, (strings, null) and sometimes a mix of all of these. (The best answer is React.ReactNode - but it's really not obvious from inspecting various types)

const Example = (props: ExampleProps): React.ReactNode => {
   return <div></div>
};

EDIT: I'm wrong about ReactNode being the best return type for components - it's the best type for children, but the compiler doesn't really support React elements that return (e.g.) strings, anyway (See this issue for discussion). So the best type really is just JSX.Element or JSX.Element | null (or React.ReactElement) at the worst.

Or to avoid this boilerplate, some will write their components as:

const Example: React.FC<ExampleProps> = (props) => {}

But this another example of a lossy type annotation and has some resulting disadvantages.


Okay... long comment for a rule I'm "mildly" opposed to. I know these examples are all fairly specific and nit-picky, and of course "eslint-disable" exists for a reason. But I guess I'd reiterate the idea that explicit function annotations can be good for bigger teams and bigger projects and might be an "industry best practice", but is more of a wash for an average project, and can actually be a bit detrimental to the onboarding experience for Typescript.

@phaux
Copy link
Contributor

phaux commented Jan 17, 2020

I mostly use the default config with only a few changes and there are a few additional rules that I always enable and I think they'd be a great addition to the default config.

restrict-template-expressions: ["error", {"allowNumber": true}]

Putting something other than a string or number into a template literal expression is basically always a bug or oversight.

It's great for catching these cases where you accidentally concatenate string | undefined into a template and gives you a chance to notice that and maybe handle the nullish case in some way e.g. providing some default string value.

The only slight annoyance I have with it is that it also considers any as invalid type, and triggers on e.g. console.error(`there was an error: ${error}`), but it's easy to fix – just wrap the error variable in String(error). We might also add an allowAny option in the future to mitigate this.

restrict-plus-operands: "error"

Basically the same as above, but with plus operands. A great example of how helpful it can be is when I wanted to concatenate to a string variable in a loop, but the variable could be undefined at the start, so I would end up with "undefinedThe rest of the string..." if not for the warning.

strict-boolean-expressions with all the ignore options

I'm not sure about this one because it could be considered stylistic. If we added additional options to make this rule less strict it would be great to catch some of the possible bugs.

E.g. report only the usage of string|undefined or number|undefined in places where a boolean is expected so that the developer is forced to handle the nullish/zero/empty string cases explicitly.

Right now it always forces you to write numberValue != 0 or stringValue.length > 0 (even if they can't be nullish) which might be annoying to some.

@bradzacher
Copy link
Member Author

strict-boolean-expressions with all the ignore options

I love this rule, because I use flow at work; which has this check built into the compiler (see sketchy-null).

Whilst I think this doesn't take much to get used to, I know a lot of people don't like it being enforced because it means you can never do shorthand checks on strings/numbers/booleans.

I guess the question is how many bugs does it actually catch vs being stylistically explicit.

restrict-plus-operands

How did I forget about this rule? This would be great to add.

restrict-template-expressions

This is probably a good idea to add as well. I don't know why you wouldn't want it.

@Mister-Hope
Copy link

Mister-Hope commented Jan 18, 2020

Have you tried the allowTypedFunctionExpressions option? This option does exactly what you want, and will not enforce that your example has a return type.

Thanks for the advice, I tried, but still catching problems, it can prevent some of the errors, but not all of them. Some function in nested object is not working. I am not sure if it is a bug since you said it will solve my problem. in this case, I don't think I need to declare return type for ready

image

image
@bradzacher

@WayneEllery
Copy link

Is there a reason for @typescript-eslint/no-empty-function is error when in eslint:recommended is off?

@bradzacher
Copy link
Member Author

I commented on that in the OP, if you'd like to expand the sections.

Also note the second paragraph of the OP:

Let me clarify that this set will be somewhat opinionated. It shall contain a small set of stylistic rules that I believe are a best practice based off of what I've seen in my career, and what the community has converged towards in the past.

@armano2
Copy link
Member

armano2 commented Jan 28, 2020

@bradzacher what do you think about splitting recommended from stylistic?
we could determine this base on type property from rules?

@bradzacher
Copy link
Member Author

Stylistic is somewhat opinionated sometimes. Though I've purposely trimmed this list down to only a few "stylistic rules".

IMO; no empty functions isn't stylistic. Empty functions is a code smell, same as no unused vars.
Having required empty functions generally indicates sub optimal api design, or it can indicate paths that have forgotten to be implemented.

@WayneEllery
Copy link

I agree that you shouldn’t have empty functions but I’ve found them useful in unit tests. I never had a need to lint for them because my team would never use them anyway in the non test code.

@glen-84
Copy link
Contributor

glen-84 commented Jan 29, 2020

@WayneEllery,

You could handle that with overrides:

overrides: [
    {
        files: ["*.spec.ts"],
        rules: {
            // Empty functions are used for mocks.
            "@typescript-eslint/no-empty-function": "off"
        }
    }
]

@bradzacher
Copy link
Member Author

Thanks @glen-84! Yeah in that case the recommendation would be to use an override.

There's lots of practices that are smelly, but "okay" to do in tests (like using as any, empty functions, unused variables) because the code isn't run in user land, and doesn't contribute to production safety, bundle size, memory, etc.

The recommended set is setup around what you should to do in production code to ensure your code is correct.

@aggmoulik

This comment has been minimized.

@Retsam
Copy link
Contributor

Retsam commented Apr 24, 2020

Would it be possible/reasonable for each config to extend the "parent" config? It seems like there's a pretty sensible hierarchy of:

@typescript-eslint/recommended-requiring-type-checking
@typescript-eslint/recommended
@typescript-eslint/eslint-recommended
eslint/recommended

It seems like in the majority of cases, someone using the required-type-checking rules would also want the normal recommended rules, and anyone who wants the mildly-opinionated @typescript-eslint/recommended rules is probably going to want to use the pretty un-opinionated base eslint recommended rules. And especially typescript-eslint/eslint-recommended doesn't make much sense without eslint/recommended.

I think it just looks a bit overwhelming for people when they look at the "recommended config for @typescript/eslint and see something like:

{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
  ]
}

It'd be great if this could just be written as:

{
  "extends": [
    "plugin:@typescript-eslint/recommended"
  ]
}

@bradzacher
Copy link
Member Author

It does sound reasonable, but it makes the assumption that everyone wants to use every single config all of the time.

Some people don't like to build on top of eslint:recommended, and instead choose things like airbnb or standard.

Additionally one of the (many) reasons we split out recommended-requiring-type-checking was so that larger codebases can setup multi-stage lints to help control lint times a bit more.

So there are tradeoffs to doing this, and I think the loss of flexibility is probably not worth it.

That being said, I'm not opposed to creating an additional unified configuration to make this use case easier; I don't think providing more options to people is necessarily a bad idea - as long as it's thoroughly documented.

@Retsam
Copy link
Contributor

Retsam commented Apr 24, 2020

These are good points, but I still think there's some value in having one or more of these configurations depend on each other. I suppose I'm really making three separate but related suggestions, which could be accepted or rejected separately, so let me actually split the points up and make my case for each of them. (... which I should have done in the first place)

@t-e/eslint-recommended could extend on eslint:recommended

This is the one I feel most strongly about: given the whole point of @t-e/eslint-recommended is to make eslint:recommended compatible with the @typescript-eslint/parser, it seems quite rare that @t-e/eslint-recommended would be used without eslint:recommended.

@t-e/recommended could extend @t-e/eslint-recommended

My basis for this suggestion is that @t-e/recommended is already a pretty opinionated config, while I believe eslint:recommended is comparatively, much less opinionated. Of course, there's basically no such thing as truly "unopinionated", I'm sure some people disagree with some of the rules in the eslint:recommended (e.g. sparse arrays, condition assignments), but it's as close to "truly unopinionated" as I've ever seen in a linting tool. (Anecdotally, I don't think I've ever overridden a eslint:recommended rule, except to replace it with a better/stricter version)

While, as you've mentioned in this thread, the @t-e/recommended ruleset is deliberately somewhat opinionated, including quite a few moderately controversial rules, as seen by the discussion in this thread. And that's totally fair, but saying "we'll include these somewhat controversial rules, but won't include these largely non-controversial rules because not everyone is going to want them" feels like a very specific level of opinionated to me.

I'd be a little surprised if any of the base eslint:recommended rules would conflict with standard or airbnb, but if they do, they could always specify this ruleset first so that the other config's rules have precedence.

@t-e/recommended-requiring-type-checking could extend @t-e/recommended

This is probably the weakest of the three suggestions, I'll admit I haven't thought about this as much as you have.

It makes sense that some people might configure their project to only run the slower, type-checking based lint rules separately e.g. in a CI job. But at least if I were setting it up, I'd have the CI job also running my base recommended rules, too., because I still care about those base rules, and the performance impact is pretty negligible anyway, compared to the cost of the TS-powered rules.

And it's actually easier to set up that way, anyway, so if I were to guess, that's already how most people have it configured, something like:

// .eslint.ci.json
{
    "extends": [
        "./eslint.json",
        "plugin:@t-e/rrtc"
    ]
}

(In order to only run the ts-powered rules, you'd have to either duplicate all of your base configuration stuff like env and parserOptions or split it out into a third file)

But like I said, this is a lot weaker of a suggestion and I don't have a very strong opinion on this point. TS-powered linting is, unfortunately, still something of an "advanced use case", so I don't have as strong of an objection to its config being a little more verbose for the sake of flexibility. (But I think the other two points could be implemented without this one)


You can look at these changes as a loss of flexibility, but eslint's overall flexibility compensates for it: if adding eslint:recommended causes an unwanted rule to get added, it's easy for them to manually toggle it off, just like any of the other new or changed rules in this PR.

Instead, I'd frame this change as making the more common use-cases more convenient, while making some of the less common use-cases a little bit less convenient.

Again, anecdotal, but I've seen objections to eslint+typescript from people who think it's "too much config" - (I'm thinking specifically of interactions I've had in the official Typescript discord server). Of course, I don't agree with that sentiment overall; but I think seeing stuff like extends: ["eslint:recommended", "plugin:@t-e/recommended:eslint", "plugin:@t-e/recommended"]might be a contributing factor.

And adding another config that includes all of the other configs might just muddy the water more and create more decision fatigue.

Thanks, as always, for your thoughts!

@Dremora
Copy link

Dremora commented May 5, 2020

I typically don't have problems with recommended sets of rules as they tend not to be opinionated. However, two rules seem not to be following this: specifically, @typescript-eslint/explicit-function-return-type and @typescript-eslint/no-empty-function, both specific to React. Enough has been said about the former, but there's at least one good use case for the disabling the latter that hasn't been yet mentioned: passing empty functions to React contexts as default props.

For example, imagine you're creating an authentication context:

export const AuthContext = React.createContext({
  token: '',
  setToken: (t: string) => {},
  logout: () => {}
});

This code has two empty functions.

Perhaps it's just me, and there actually is a better way of doing this with React, in which case I'd like to be corrected. Perhaps one could say that it's a design mistake of React, but here we have to agree that it is a very popular library, and thus the pattern will be rather common.

@bbarry
Copy link
Contributor

bbarry commented May 5, 2020

I would have a utility function in my library that I would use for this:

export const noop: (name: string) => (args: any[]) => void = (name) => (...args) => {
  if (log) { log('no-op call', name, args); }
};

defined someplace. The class file would be replaced by a .release version with an actual noop implementation by a build script.

then use it:

export context AuthContext = React.createContext({
  token: '',
  setToken: noop('setToken'),
  logout: noop('logout'),
});

@Dremora
Copy link

Dremora commented May 5, 2020

This solution has its merit, but I wouldn't want to introduce it just to overcome the linter.

@bradzacher
Copy link
Member Author

bradzacher commented May 5, 2020

explicit-function-return-type

I have conceded that whilst I think this is a good practice, not everybody thinks so due to its verbosity.
It's being replaced with the less strict explicit-module-boundary-types, which only enforces this at the module boundaries. I think that this is the best practice - it enforces clear contracts for consumers of a module without requiring the same for private module functions.


no-empty-function

To start with let me just say that "rather common" is a very relative statement.
Is it "rather common" in the context of defining a react context? Definitely!
Is it "rather common" in the context of an entire react app? Doubtful.
Is it "rather common" in the context of the entire TS ecosystem? Even more doubtful.

Defining a react context itself is a small piece of a react app. I'd expect the ratio of context definitions to component definitions to be around 1:50, if not lower (a quick and naive search of the FB codebase puts it at around 1:40).

For me as an engineer looking at that, I'd question how often you actually want to use a no-op function in a context.
Specifically: how often do you actually want your component to gracefully handle the fact that they're rendered outside of the desired context?

In production, 100% of the time, for sure!
But in development that's generally a code smell / bug vector (there are, ofc, exceptions to this).

For example, your example is called AuthContext. If you render a component outside of an AuthContext.Provider, then it'll never be authed, and likely will never work - ie a definite bug if it got to production (imagine a logout button which never logged you out because of this!).

So for me, I'd be looking to satisfy the linter here in a similar way to @bbarry by doing something like this:

function contextCallingDefaultValue() {
  if (__DEV__) {
    throw new Error('Component was rendered outside a context provider');
  }
}
export const AuthContext = React.createContext({
  token: '',
  setToken: (t: string) => { contextCallingDefaultValue() },
  logout: () => { contextCallingDefaultValue() }
});

@Retsam
Copy link
Contributor

Retsam commented May 5, 2020

A simpler work-around to no-empty-function is to add a comment inside the function:

const noop = () => {/* do nothing */};

The point of this linter rule is to ensure that empty functions are intentionally empty, so adding a comment satisfies the linter that it's intentionally empty.

I do often put const noop = at the top of files that use a lot of empty functions; it avoids a linter exception for each case, and it has the tertiary benefit of using fewer function instances at runtime

@Dremora
Copy link

Dremora commented May 7, 2020

By "rather common" I meant that a lot of applications will have at least one occurrence. The actual frequency of this pattern's usage inside each such application will be, of course, low. But I think even one occurrence matters.

For me the litmus test of a rule in a recommended set would be: the code with no code smell should not have any linter errors/warnings and I shouldn't need to disable any rules to make it work. I can make exception for tests and Storybook files since those can sometimes afford to break certain rules and be imperfect. I also shouldn't have to modify the code just to make the linter pass where it's obvious that the current code doesn't cause any issues otherwise (unless it can be autofixed).

Perhaps my expectations of what constitutes a recommended rule are simply wrong here, in which case I'm curious to learn what drives the selection. But it seems that this rule is the only one I've met so far that doesn't fall into this category (explicit-function-return-type can probably be solved by having a better type for functional component without children, but it's not happening anytime soon). In the last year I've worked on 4 TypeScript codebases in 3 companies, and it's the only one from a recommended set I had to disable.

@WayneEllery had a good point on this rule not actually being in the eslint:recommended config. The rule we are talking about is the same (simply a TS version), so what is the logic behind promoting it to a recommended one?

I like your example above, but note that you are talking about "satisfying the linter", which means the main reason we are doing this is for the linter, not to improve the code. I can also imagine contexts with an implementation that is completely valid and in no need for a fix. For example, an Analytics provider with a track function that is empty is the user has disabled analytics. Or a function that conditionally returns either an empty or non-empty function with the same signature (this could be the track function from above).

While I think empty functions can sometimes be a code smell, they are not necessarily one.


@Retsam: it's an interesting solution, but again, falls under the "changing code only to make the linter work" clause.

Also, if you use this workaround a lot, what is the purpose of having the rule enabled in the first place? Is it any better than having @typescript-eslint/no-empty-function or disabling the rule globally?

@bradzacher
Copy link
Member Author

ESLint core tends towards being as unopinionated as possible, because otherwise they get way too much discussion around things.
They have the problem of dealing with a recommended set that has to work across current and very legacy javascript codebases.

OTOH, we're working with TS codebases - which are much newer and (by and large) adopt much better, and more standardised styles and practices.
This means we can afford to include some rules that aren't included in the base rule.

That being said, we have tended toward being less opinionated with each version. The very first recommended config was based combining on some very all-encompassing configs provided by the community. Though we received a lot of discussion about that, so we have significantly pared it back over 2 major versions.

You can actually see all of my reasoning for every single rule in the spoiler sections in the OP - I've tried to be as transparent as possible about this because of the amount of flack I've copped about the recommended sets in the past.

When reviewing the recommended configs - I look at every rule and more or less go through the following two checks:

  • do I think it's best practice? (i.e. do I code this way, or do I think codebases should be this way)
  • do I think people would contest seeing this rule fire in a codebase? (i.e. do I think it's controversial)

If yes, I put it in, and then the set is put up here for people to discuss it - the community uses these configs, and they should help shape them. I've marked a number of rules as things I'd love feedback on.

If nobody comments, then I'll make an executive decision. By the time this merges it will likely have been up for 6 months, so if someone didn't voice their concern before then, then "tough luck" - it can wait for the next major 😅.


I'll counter your point of "recommended means not having to disable across the codebase" with one example I have run into several times across codebases.
The core no-empty eslint rule is part of ESLint's recommended set.
This rule rightfully bans code smells like if (val) {}.

But by default, this rule also bans empty catch blocks. There have been a number of times where I've purposely swallowed exceptions (for example, mkdir throws an error if the dir already exists), so I would likely have to disable or otherwise satisfy this lint rule when turning it on in most of my codebases.

Usually the way I satisfy it is by changing my code, eg:

try {
  mkdirSync('something');
} catch {
  // mkdir throws if the dir exists but we don't care
}

So for me, that rule isn't much different to no-empty-function - I think that empty functions are going to be a code smell as often as an empty catch is.
"Satisfying the linter" with a comment saying you intentionally left the body empty, or via an explicit exception to make sure it's never called are both correct and valid IMO.

@rfox12
Copy link

rfox12 commented May 24, 2020

@bradzacher I think it was a case of the "loud minority" for explicit-function-return-type... I whole heartedly agree with your statements above, and one of the first things I did after upgrading was figuring out how to turn this back "on" for my projects/teams. "My IDE does it" is a poor excuse.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 24, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
breaking change This change will require a new major version to be released package: eslint-plugin Issues related to @typescript-eslint/eslint-plugin recommended-rules Discussion about recommended rule sets
Projects
None yet
Development

Successfully merging a pull request may close this issue.