Skip to content

Commit 880ac75

Browse files
author
Josh Goldberg
authored
feat(eslint-plugin): add no-unnecessary-type-constraint rule (typescript-eslint#2516)
* feat(eslint-plugin): add no-unnecessary-type-constraint rule * Inlined report * Improved message and removed type checker * Stop testing with type info
1 parent ef8b5a7 commit 880ac75

File tree

6 files changed

+392
-0
lines changed

6 files changed

+392
-0
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
143143
| [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: |
144144
| [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Enforces that type arguments will not be used if not required | | :wrench: | :thought_balloon: |
145145
| [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: |
146+
| [`@typescript-eslint/no-unnecessary-type-constraint`](./docs/rules/no-unnecessary-type-constraint.md) | Disallows unnecessary constraints on generic types | | :wrench: | |
146147
| [`@typescript-eslint/no-unsafe-assignment`](./docs/rules/no-unsafe-assignment.md) | Disallows assigning any to variables and properties | :heavy_check_mark: | | :thought_balloon: |
147148
| [`@typescript-eslint/no-unsafe-call`](./docs/rules/no-unsafe-call.md) | Disallows calling an any type value | :heavy_check_mark: | | :thought_balloon: |
148149
| [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | :heavy_check_mark: | | :thought_balloon: |
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Disallows unnecessary constraints on generic types (`no-unnecessary-type-constraint`)
2+
3+
## Rule Details
4+
5+
Type parameters (`<T>`) may be "constrained" with an `extends` keyword ([docs](https://www.typescriptlang.org/docs/handbook/generics.html#generic-constraints)).
6+
When not provided, type parameters happen to default to:
7+
8+
- As of TypeScript 3.9: `unknown` ([docs](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-9.html#type-parameters-that-extend-any-no-longer-act-as-any))
9+
- Before that, as of 3.5: `any` ([docs](https://devblogs.microsoft.com/typescript/announcing-typescript-3-5/#breaking-changes))
10+
11+
It is therefore redundant to `extend` from these types in later versions of TypeScript.
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```ts
16+
interface FooAny<T extends any> {}
17+
interface FooUnknown<T extends unknown> {}
18+
19+
type BarAny<T extends any> = {};
20+
type BarUnknown<T extends unknown> = {};
21+
22+
class BazAny<T extends any> {
23+
quxUnknown<U extends unknown>() {}
24+
}
25+
26+
class BazUnknown<T extends unknown> {
27+
quxUnknown<U extends unknown>() {}
28+
}
29+
30+
const QuuxAny = <T extends any>() => {};
31+
const QuuxUnknown = <T extends unknown>() => {};
32+
33+
function QuuzAny<T extends any>() {}
34+
function QuuzUnknown<T extends unknown>() {}
35+
```
36+
37+
Examples of **correct** code for this rule:
38+
39+
```ts
40+
interface Foo<T> {}
41+
42+
type Bar<T> = {};
43+
44+
class Baz<T> {
45+
qux<U> { }
46+
}
47+
48+
const Quux = <T>() => {};
49+
50+
function Quuz<T>() {}
51+
```
52+
53+
## When Not To Use It
54+
55+
If you don't care about the specific styles of your type constraints, or never use them in the first place, then you will not need this rule.

packages/eslint-plugin/src/configs/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export = {
9292
'@typescript-eslint/no-type-alias': 'error',
9393
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error',
9494
'@typescript-eslint/no-unnecessary-condition': 'error',
95+
'@typescript-eslint/no-unnecessary-type-constraint': 'error',
9596
'@typescript-eslint/no-unnecessary-qualifier': 'error',
9697
'@typescript-eslint/no-unnecessary-type-arguments': 'error',
9798
'@typescript-eslint/no-unnecessary-type-assertion': 'error',

packages/eslint-plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import noUnnecessaryCondition from './no-unnecessary-condition';
6767
import noUnnecessaryQualifier from './no-unnecessary-qualifier';
6868
import noUnnecessaryTypeArguments from './no-unnecessary-type-arguments';
6969
import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion';
70+
import noUnnecessaryTypeConstraint from './no-unnecessary-type-constraint';
7071
import noUnsafeAssignment from './no-unsafe-assignment';
7172
import noUnsafeCall from './no-unsafe-call';
7273
import noUnsafeMemberAccess from './no-unsafe-member-access';
@@ -177,6 +178,7 @@ export default {
177178
'no-unnecessary-qualifier': noUnnecessaryQualifier,
178179
'no-unnecessary-type-arguments': noUnnecessaryTypeArguments,
179180
'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion,
181+
'no-unnecessary-type-constraint': noUnnecessaryTypeConstraint,
180182
'no-unsafe-assignment': noUnsafeAssignment,
181183
'no-unsafe-call': noUnsafeCall,
182184
'no-unsafe-member-access': noUnsafeMemberAccess,
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {
2+
AST_NODE_TYPES,
3+
TSESTree,
4+
} from '@typescript-eslint/experimental-utils';
5+
import * as semver from 'semver';
6+
import * as ts from 'typescript';
7+
import * as util from '../util';
8+
9+
type MakeRequired<Base, Key extends keyof Base> = Omit<Base, Key> &
10+
Required<Pick<Base, Key>>;
11+
12+
type TypeParameterWithConstraint = MakeRequired<
13+
TSESTree.TSTypeParameter,
14+
'constraint'
15+
>;
16+
17+
const is3dot5 = semver.satisfies(
18+
ts.version,
19+
`>= 3.5.0 || >= 3.5.1-rc || >= 3.5.0-beta`,
20+
{
21+
includePrerelease: true,
22+
},
23+
);
24+
25+
const is3dot9 =
26+
is3dot5 &&
27+
semver.satisfies(ts.version, `>= 3.9.0 || >= 3.9.1-rc || >= 3.9.0-beta`, {
28+
includePrerelease: true,
29+
});
30+
31+
export default util.createRule({
32+
name: 'no-unnecessary-type-constraint',
33+
meta: {
34+
docs: {
35+
category: 'Best Practices',
36+
description: 'Disallows unnecessary constraints on generic types',
37+
recommended: false,
38+
suggestion: true,
39+
},
40+
fixable: 'code',
41+
messages: {
42+
unnecessaryConstraint:
43+
'Constraining the generic type `{{name}}` to `{{constraint}}` does nothing and is unnecessary.',
44+
},
45+
schema: [],
46+
type: 'suggestion',
47+
},
48+
defaultOptions: [],
49+
create(context) {
50+
if (!is3dot5) {
51+
return {};
52+
}
53+
54+
// In theory, we could use the type checker for more advanced constraint types...
55+
// ...but in practice, these types are rare, and likely not worth requiring type info.
56+
// https://github.com/typescript-eslint/typescript-eslint/pull/2516#discussion_r495731858
57+
const unnecessaryConstraints = is3dot9
58+
? new Map([
59+
[AST_NODE_TYPES.TSAnyKeyword, 'any'],
60+
[AST_NODE_TYPES.TSUnknownKeyword, 'unknown'],
61+
])
62+
: new Map([[AST_NODE_TYPES.TSUnknownKeyword, 'unknown']]);
63+
64+
const inJsx = context.getFilename().toLowerCase().endsWith('tsx');
65+
66+
const checkNode = (
67+
node: TypeParameterWithConstraint,
68+
inArrowFunction: boolean,
69+
): void => {
70+
const constraint = unnecessaryConstraints.get(node.constraint.type);
71+
72+
if (constraint) {
73+
context.report({
74+
data: {
75+
constraint,
76+
name: node.name.name,
77+
},
78+
fix(fixer) {
79+
return fixer.replaceTextRange(
80+
[node.name.range[1], node.constraint.range[1]],
81+
inArrowFunction && inJsx ? ',' : '',
82+
);
83+
},
84+
messageId: 'unnecessaryConstraint',
85+
node,
86+
});
87+
}
88+
};
89+
90+
return {
91+
':not(ArrowFunctionExpression) > TSTypeParameterDeclaration > TSTypeParameter[constraint]'(
92+
node: TypeParameterWithConstraint,
93+
): void {
94+
checkNode(node, false);
95+
},
96+
'ArrowFunctionExpression > TSTypeParameterDeclaration > TSTypeParameter[constraint]'(
97+
node: TypeParameterWithConstraint,
98+
): void {
99+
checkNode(node, true);
100+
},
101+
};
102+
},
103+
});

0 commit comments

Comments
 (0)