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

feat(eslint-plugin): add rule use-unknown-in-catch-callback-variables #8383

Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cae9abd
Add rule use-unknown-in-catch-callback-variables
kirkwaiblinger Feb 5, 2024
dff8954
delete PR comment reminders
kirkwaiblinger Feb 5, 2024
2bbf5fc
address code coverage gaps
kirkwaiblinger Feb 6, 2024
955e113
fix spell checker
kirkwaiblinger Feb 6, 2024
ccb352d
fix broken link
kirkwaiblinger Feb 6, 2024
2364a18
little bit of simplifying
kirkwaiblinger Feb 8, 2024
2fcf80c
simplify arrow function parentheses check
kirkwaiblinger Feb 8, 2024
f3cd4d4
Your -> The
kirkwaiblinger Feb 8, 2024
9ddaeb0
Clean up some comments and simplify a bit
kirkwaiblinger Feb 8, 2024
8937352
add to strict config
kirkwaiblinger Feb 8, 2024
f9af052
Add examples to docs
kirkwaiblinger Feb 8, 2024
cfd1403
Improve handling of destructuring parameters
kirkwaiblinger Feb 8, 2024
1112bfb
tweaks
kirkwaiblinger Feb 8, 2024
74b4096
docs changes
kirkwaiblinger Feb 8, 2024
c3db638
Improve wording for "when not to use this"
kirkwaiblinger Feb 8, 2024
2f229ab
improve wording and fix mistake
kirkwaiblinger Feb 8, 2024
88176a3
remove dead return
kirkwaiblinger Feb 8, 2024
2a38257
address istanbul-ignore
kirkwaiblinger Feb 8, 2024
e124d90
add bizarre syntax test cases
kirkwaiblinger Feb 8, 2024
671c44d
Improve main error message wording
kirkwaiblinger Feb 8, 2024
3263652
utility for rest parameter
kirkwaiblinger Feb 9, 2024
1f5f70e
tweaks
kirkwaiblinger Feb 9, 2024
2f175df
fix casing
kirkwaiblinger Feb 23, 2024
b721623
restore older versions of configs
kirkwaiblinger Feb 23, 2024
91a7725
config update
kirkwaiblinger Feb 23, 2024
ef47613
fix internal violations
kirkwaiblinger Feb 23, 2024
4d1860f
Merge branch 'main'
JoshuaKGoldberg Mar 17, 2024
89bed41
Regenerate configs
JoshuaKGoldberg Mar 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
description: 'Enforce typing arguments in `.catch()` callbacks as `unknown`.'
---

> 🛑 This file is source code, not the primary documentation location! 🛑
>
> See **https://typescript-eslint.io/rules/use-unknown-in-catch-callback-variable** for documentation.

This rule enforces that you always use the `unknown` type for the parameter of a `Promise.prototype.catch()` callback.

<!--tabs-->

### ❌ Incorrect

```ts
Promise.reject(new Error('I will reject!')).catch(err => {
console.log(err);
});

Promise.reject(new Error('I will reject!')).catch((err: any) => {
console.log(err);
});

Promise.reject(new Error('I will reject!')).catch((err: Error) => {
console.log(err);
});
```

### ✅ Correct

```ts
Promise.reject(new Error('I will reject!')).catch((err: unknown) => {
console.log(err);
});
```

<!--/tabs-->

The reason for this rule is to enable programmers to impose constraints on `Promise` error handling analogously to what TypeScript provides for ordinary exception handling.

For ordinary exceptions, TypeScript treats the `catch` variable as `any` by default. However, `unknown` would be a more accurate type, so TypeScript [introduced the `useUnknownInCatchVariables` compiler option](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html#defaulting-to-the-unknown-type-in-catch-variables---useunknownincatchvariables) to treat the `catch` variable as `unknown` instead.

```ts
try {
throw x;
} catch (err) {
// err has type 'any' with useUnknownInCatchVariables: false
// err has type 'unknown' with useUnknownInCatchVariables: true
}
```

The Promise analog of the `try-catch` block, [`Promise.prototype.catch()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch), is not affected by the `useUnknownInCatchVariables` compiler option, and its "`catch` variable" will always have the type `any`.

```ts
Promise.reject(x).catch(err => {
// err has type 'any' regardless of `useUnknownInCatchVariables`
});
```

However, you can still provide an explicit type annotation, which lets you achieve the same effect as the `useUnknownInCatchVariables` option does for synchronous `catch` variables.

```ts
Promise.reject(x).catch((err: unknown) => {
// err has type 'unknown'
});
```

:::info
There is actually a way to have the `catch()` callback variable use the `unknown` type _without_ an explicit type annotation at the call sites, but it has the drawback that it involves overriding global type declarations.
For example, the library [better-TypeScript-lib](https://github.com/uhyo/better-typescript-lib) sets this up globally for your project (see [the relevant lines in the better-TypeScript-lib source code](https://github.com/uhyo/better-typescript-lib/blob/c294e177d1cc2b1d1803febf8192a4c83a1fe028/lib/lib.es5.d.ts#L635) for details on how).

For further reading on this, you may also want to look into
[the discussion in the proposal for this rule](https://github.com/typescript-eslint/typescript-eslint/issues/7526#issuecomment-1690600813) and [this TypeScript issue on typing catch callback variables as unknown](https://github.com/microsoft/TypeScript/issues/45602).
:::

## When Not To Use It

If your codebase is not yet able to enable `useUnknownInCatchVariables`, it likely would be similarly difficult to enable this rule.

If you have modified the global type declarations in order to make `catch()` callbacks use the `unknown` type without an explicit type annotation, you do not need this rule.
291 changes: 146 additions & 145 deletions packages/eslint-plugin/src/configs/all.ts
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,153 +5,154 @@
// For developers working in the typescript-eslint monorepo:
// You can regenerate it using `yarn generate:configs`

import type { ClassicConfig } from '@typescript-eslint/utils/ts-eslint';
import type { ClassicConfig } from "@typescript-eslint/utils/ts-eslint";

export = {
extends: ['./configs/base', './configs/eslint-recommended'],
extends: ["./configs/base", "./configs/eslint-recommended"],
rules: {
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/array-type': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/ban-tslint-comment': 'error',
'@typescript-eslint/ban-types': 'error',
'@typescript-eslint/class-literal-property-style': 'error',
'class-methods-use-this': 'off',
'@typescript-eslint/class-methods-use-this': 'error',
'@typescript-eslint/consistent-generic-constructors': 'error',
'@typescript-eslint/consistent-indexed-object-style': 'error',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/consistent-type-definitions': 'error',
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'default-param-last': 'off',
'@typescript-eslint/default-param-last': 'error',
'dot-notation': 'off',
'@typescript-eslint/dot-notation': 'error',
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/explicit-member-accessibility': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'init-declarations': 'off',
'@typescript-eslint/init-declarations': 'error',
'max-params': 'off',
'@typescript-eslint/max-params': 'error',
'@typescript-eslint/member-ordering': 'error',
'@typescript-eslint/method-signature-style': 'error',
'@typescript-eslint/naming-convention': 'error',
'no-array-constructor': 'off',
'@typescript-eslint/no-array-constructor': 'error',
'@typescript-eslint/no-array-delete': 'error',
'@typescript-eslint/no-base-to-string': 'error',
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
'@typescript-eslint/no-confusing-void-expression': 'error',
'no-dupe-class-members': 'off',
'@typescript-eslint/no-dupe-class-members': 'error',
'@typescript-eslint/no-duplicate-enum-values': 'error',
'@typescript-eslint/no-duplicate-type-constituents': 'error',
'@typescript-eslint/no-dynamic-delete': 'error',
'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-extra-non-null-assertion': 'error',
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-for-in-array': 'error',
'no-implied-eval': 'off',
'@typescript-eslint/no-implied-eval': 'error',
'@typescript-eslint/no-import-type-side-effects': 'error',
'@typescript-eslint/no-inferrable-types': 'error',
'no-invalid-this': 'off',
'@typescript-eslint/no-invalid-this': 'error',
'@typescript-eslint/no-invalid-void-type': 'error',
'no-loop-func': 'off',
'@typescript-eslint/no-loop-func': 'error',
'no-loss-of-precision': 'off',
'@typescript-eslint/no-loss-of-precision': 'error',
'no-magic-numbers': 'off',
'@typescript-eslint/no-magic-numbers': 'error',
'@typescript-eslint/no-meaningless-void-operator': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-mixed-enums': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-redundant-type-constituents': 'error',
'@typescript-eslint/no-require-imports': 'error',
'no-restricted-imports': 'off',
'@typescript-eslint/no-restricted-imports': 'error',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/no-this-alias': 'error',
'no-throw-literal': 'off',
'@typescript-eslint/no-throw-literal': 'error',
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error',
'@typescript-eslint/no-unnecessary-condition': 'error',
'@typescript-eslint/no-unnecessary-qualifier': 'error',
'@typescript-eslint/no-unnecessary-type-arguments': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unnecessary-type-constraint': 'error',
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-declaration-merging': 'error',
'@typescript-eslint/no-unsafe-enum-comparison': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/no-unsafe-unary-minus': 'error',
'no-unused-expressions': 'off',
'@typescript-eslint/no-unused-expressions': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'error',
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'error',
'@typescript-eslint/no-useless-empty-export': 'error',
'@typescript-eslint/no-useless-template-literals': 'error',
'@typescript-eslint/no-var-requires': 'error',
'@typescript-eslint/non-nullable-type-assertion-style': 'error',
'@typescript-eslint/parameter-properties': 'error',
'@typescript-eslint/prefer-as-const': 'error',
'prefer-destructuring': 'off',
'@typescript-eslint/prefer-destructuring': 'error',
'@typescript-eslint/prefer-enum-initializers': 'error',
'@typescript-eslint/prefer-find': 'error',
'@typescript-eslint/prefer-for-of': 'error',
'@typescript-eslint/prefer-function-type': 'error',
'@typescript-eslint/prefer-includes': 'error',
'@typescript-eslint/prefer-literal-enum-member': 'error',
'@typescript-eslint/prefer-namespace-keyword': 'error',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'prefer-promise-reject-errors': 'off',
'@typescript-eslint/prefer-promise-reject-errors': 'error',
'@typescript-eslint/prefer-readonly': 'error',
'@typescript-eslint/prefer-readonly-parameter-types': 'error',
'@typescript-eslint/prefer-reduce-type-parameter': 'error',
'@typescript-eslint/prefer-regexp-exec': 'error',
'@typescript-eslint/prefer-return-this-type': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
'@typescript-eslint/prefer-ts-expect-error': 'error',
'@typescript-eslint/promise-function-async': 'error',
'@typescript-eslint/require-array-sort-compare': 'error',
'require-await': 'off',
'@typescript-eslint/require-await': 'error',
'@typescript-eslint/restrict-plus-operands': 'error',
'@typescript-eslint/restrict-template-expressions': 'error',
'no-return-await': 'off',
'@typescript-eslint/return-await': 'error',
'@typescript-eslint/sort-type-constituents': 'error',
'@typescript-eslint/strict-boolean-expressions': 'error',
'@typescript-eslint/switch-exhaustiveness-check': 'error',
'@typescript-eslint/triple-slash-reference': 'error',
'@typescript-eslint/typedef': 'error',
'@typescript-eslint/unbound-method': 'error',
'@typescript-eslint/unified-signatures': 'error',
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/ban-ts-comment": "error",
"@typescript-eslint/ban-tslint-comment": "error",
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/class-literal-property-style": "error",
"class-methods-use-this": "off",
"@typescript-eslint/class-methods-use-this": "error",
"@typescript-eslint/consistent-generic-constructors": "error",
"@typescript-eslint/consistent-indexed-object-style": "error",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/consistent-type-exports": "error",
"@typescript-eslint/consistent-type-imports": "error",
"default-param-last": "off",
"@typescript-eslint/default-param-last": "error",
"dot-notation": "off",
"@typescript-eslint/dot-notation": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/explicit-member-accessibility": "error",
"@typescript-eslint/explicit-module-boundary-types": "error",
"init-declarations": "off",
"@typescript-eslint/init-declarations": "error",
"max-params": "off",
"@typescript-eslint/max-params": "error",
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/method-signature-style": "error",
"@typescript-eslint/naming-convention": "error",
"no-array-constructor": "off",
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-array-delete": "error",
"@typescript-eslint/no-base-to-string": "error",
"@typescript-eslint/no-confusing-non-null-assertion": "error",
"@typescript-eslint/no-confusing-void-expression": "error",
"no-dupe-class-members": "off",
"@typescript-eslint/no-dupe-class-members": "error",
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-duplicate-type-constituents": "error",
"@typescript-eslint/no-dynamic-delete": "error",
"no-empty-function": "off",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error",
"no-implied-eval": "off",
"@typescript-eslint/no-implied-eval": "error",
"@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/no-inferrable-types": "error",
"no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": "error",
"@typescript-eslint/no-invalid-void-type": "error",
"no-loop-func": "off",
"@typescript-eslint/no-loop-func": "error",
"no-loss-of-precision": "off",
"@typescript-eslint/no-loss-of-precision": "error",
"no-magic-numbers": "off",
"@typescript-eslint/no-magic-numbers": "error",
"@typescript-eslint/no-meaningless-void-operator": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-mixed-enums": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-asserted-nullish-coalescing": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"no-redeclare": "off",
"@typescript-eslint/no-redeclare": "error",
"@typescript-eslint/no-redundant-type-constituents": "error",
"@typescript-eslint/no-require-imports": "error",
"no-restricted-imports": "off",
"@typescript-eslint/no-restricted-imports": "error",
"no-shadow": "off",
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/no-this-alias": "error",
"no-throw-literal": "off",
"@typescript-eslint/no-throw-literal": "error",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
"@typescript-eslint/no-unnecessary-condition": "error",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-arguments": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-declaration-merging": "error",
"@typescript-eslint/no-unsafe-enum-comparison": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unsafe-return": "error",
"@typescript-eslint/no-unsafe-unary-minus": "error",
"no-unused-expressions": "off",
"@typescript-eslint/no-unused-expressions": "error",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "error",
"no-useless-constructor": "off",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-useless-empty-export": "error",
"@typescript-eslint/no-useless-template-literals": "error",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/non-nullable-type-assertion-style": "error",
"@typescript-eslint/parameter-properties": "error",
"@typescript-eslint/prefer-as-const": "error",
"prefer-destructuring": "off",
"@typescript-eslint/prefer-destructuring": "error",
"@typescript-eslint/prefer-enum-initializers": "error",
"@typescript-eslint/prefer-find": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-literal-enum-member": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/prefer-optional-chain": "error",
"prefer-promise-reject-errors": "off",
"@typescript-eslint/prefer-promise-reject-errors": "error",
"@typescript-eslint/prefer-readonly": "error",
"@typescript-eslint/prefer-readonly-parameter-types": "error",
"@typescript-eslint/prefer-reduce-type-parameter": "error",
"@typescript-eslint/prefer-regexp-exec": "error",
"@typescript-eslint/prefer-return-this-type": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/prefer-ts-expect-error": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"require-await": "off",
"@typescript-eslint/require-await": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"@typescript-eslint/restrict-template-expressions": "error",
"no-return-await": "off",
"@typescript-eslint/return-await": "error",
"@typescript-eslint/sort-type-constituents": "error",
"@typescript-eslint/strict-boolean-expressions": "error",
"@typescript-eslint/switch-exhaustiveness-check": "error",
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/typedef": "error",
"@typescript-eslint/unbound-method": "error",
"@typescript-eslint/unified-signatures": "error",
"@typescript-eslint/use-unknown-in-catch-callback-variable": "error",
},
} satisfies ClassicConfig.Config;
8 changes: 4 additions & 4 deletions packages/eslint-plugin/src/configs/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
// For developers working in the typescript-eslint monorepo:
// You can regenerate it using `yarn generate:configs`

import type { ClassicConfig } from '@typescript-eslint/utils/ts-eslint';
import type { ClassicConfig } from "@typescript-eslint/utils/ts-eslint";

export = {
parser: '@typescript-eslint/parser',
parserOptions: { sourceType: 'module' },
plugins: ['@typescript-eslint'],
parser: "@typescript-eslint/parser",
parserOptions: { sourceType: "module" },
plugins: ["@typescript-eslint"],
} satisfies ClassicConfig.Config;