Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(eslint-plugin): [no-unnecessary-type-parameters] special case tuples and parameter location arrays as single-use #9536

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

Merged
merged 54 commits into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
cd5e9a4
wip 2 usages
jfet97 Jul 10, 2024
baadbe9
move tests
jfet97 Jul 10, 2024
f655fef
add tests
jfet97 Jul 10, 2024
3eeb08a
restore tests
jfet97 Jul 10, 2024
021f12c
point failing tests
jfet97 Jul 10, 2024
6caeb43
fix possible undefined symbol
jfet97 Jul 10, 2024
bd6f08b
left a comment
jfet97 Jul 10, 2024
74b2e17
ok
jfet97 Jul 10, 2024
18f8951
add test
jfet97 Jul 10, 2024
4602ca7
update doc
jfet97 Jul 10, 2024
e00f32f
Merge branch 'main' into main
jfet97 Jul 10, 2024
366158d
chore: consider existing assumeMultipleUses value
jfet97 Jul 10, 2024
6d386c5
add missing test case
jfet97 Jul 10, 2024
9979cf9
doc
jfet97 Jul 10, 2024
8fea88e
no error for factory functions
jfet97 Jul 12, 2024
d1563bd
Merge branch 'main' into main
jfet97 Jul 13, 2024
fe7efb1
add more tests
jfet97 Jul 16, 2024
81a2260
Merge remote-tracking branch 'upstream/main'
jfet97 Jul 16, 2024
9a82fb7
Merge branch 'main' into main
jfet97 Jul 17, 2024
815aebf
Merge branch 'main' into main
jfet97 Aug 7, 2024
2f26941
non methods member not considered as singular
jfet97 Aug 8, 2024
678e6b8
nvm
jfet97 Aug 8, 2024
3d36117
add tests
jfet97 Aug 8, 2024
667e715
fix tests
jfet97 Aug 8, 2024
13733da
add tests
jfet97 Aug 8, 2024
89812f4
renaming
jfet97 Aug 8, 2024
6f2e84d
Merge branch 'main' into main
jfet97 Aug 8, 2024
4232c34
add test
jfet97 Aug 8, 2024
b957878
add test
jfet97 Aug 8, 2024
d7cb27e
enhance tests
jfet97 Aug 9, 2024
38776d1
add comment
jfet97 Aug 9, 2024
8b35cab
fix comment
jfet97 Aug 9, 2024
32ca4c7
better names
jfet97 Aug 9, 2024
544d9c8
Merge branch 'main' into main
jfet97 Aug 13, 2024
34785bf
Merge branch 'main' into main
jfet97 Sep 5, 2024
6cfb31b
Swap test cases to new proposal
JoshuaKGoldberg Dec 2, 2024
c8cee06
Add back fetchJson case + one more
JoshuaKGoldberg Dec 2, 2024
18ba62b
Merge branch 'main'
JoshuaKGoldberg Dec 2, 2024
a9f1684
Some more test cases while we're at it
JoshuaKGoldberg Dec 2, 2024
2e1c31c
wip
jfet97 Dec 2, 2024
f185577
fup
jfet97 Dec 2, 2024
fed012a
Fixed up tests per multi-element tuples
JoshuaKGoldberg Dec 3, 2024
c94a9de
progress
jfet97 Dec 3, 2024
015c68c
comments
jfet97 Dec 3, 2024
0eb9d8c
restore optimization
jfet97 Dec 3, 2024
dd91489
propagate return type context
jfet97 Dec 4, 2024
286e7ae
remove readonly tests
jfet97 Dec 14, 2024
ae4e523
doc
jfet97 Dec 14, 2024
48cfe54
docs again
jfet97 Dec 14, 2024
0d1f8df
Filled in snapshots
JoshuaKGoldberg Jan 7, 2025
27f29e2
implement some suggested changes
jfet97 Jan 28, 2025
21cbd34
Finally commit docs reverts/touchups
JoshuaKGoldberg Feb 24, 2025
fc006d3
last piece of feedback
kirkwaiblinger Feb 26, 2025
8a24eca
chore: fix formatting
JoshuaKGoldberg Mar 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,30 @@ For example, the following `T` is used multiple times by virtue of being in an `
declare function createStateHistory<T>(): T[];
```

This is because the type parameter `T` relates multiple methods in the `T[]` together, making it used more than once.
This is because the type parameter `T` relates multiple methods in `T[]` (`Array<T>`) together, making it used more than once.

Therefore, this rule won't report on type parameters used as a type argument.
That includes type arguments given to global types such as `Array` (including the `T[]` shorthand and in tuples), `Map`, and `Set`.
This includes type arguments provided to global types such as `Array`, `Map`, and `Set` that have multiple methods and properties that can change values based on the type parameter.

On the other hand, readonly and fixed array-likes such as `readonly T[]`, `ReadonlyArray`, and tuples such as `[T]` are special cases that are specifically reported on when used as input types, or as `readonly` output types.
The following example will be reported because `T` is used only once as type argument for the `ReadonlyArray` global type:

<Tabs>
<TabItem value="❌ Incorrect">

```ts
declare function length<T>(array: ReadonlyArray<T>): number;
```

</TabItem>
<TabItem value="✅ Correct">

```ts
declare function length(array: ReadonlyArray<unknown>): number;
```

</TabItem>
</Tabs>

## FAQ

Expand Down
37 changes: 31 additions & 6 deletions packages/eslint-plugin/src/rules/no-unnecessary-type-parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,17 @@ function isTypeParameterRepeatedInAST(
const grandparent = skipConstituentsUpward(
reference.identifier.parent.parent,
);

if (
grandparent.type === AST_NODE_TYPES.TSTypeParameterInstantiation &&
grandparent.params.includes(reference.identifier.parent)
grandparent.params.includes(reference.identifier.parent) &&
// Array and ReadonlyArray must be handled carefully
// let's defer the check to the type-aware phase
!(
grandparent.parent.type === AST_NODE_TYPES.TSTypeReference &&
grandparent.parent.typeName.type === AST_NODE_TYPES.Identifier &&
['Array', 'ReadonlyArray'].includes(grandparent.parent.typeName.name)
)
) {
return true;
}
Expand Down Expand Up @@ -281,13 +289,13 @@ function countTypeParameterUsage(

if (ts.isClassLike(node)) {
for (const typeParameter of node.typeParameters) {
collectTypeParameterUsageCounts(checker, typeParameter, counts);
collectTypeParameterUsageCounts(checker, typeParameter, counts, true);
}
for (const member of node.members) {
collectTypeParameterUsageCounts(checker, member, counts);
collectTypeParameterUsageCounts(checker, member, counts, true);
}
} else {
collectTypeParameterUsageCounts(checker, node, counts);
collectTypeParameterUsageCounts(checker, node, counts, false);
}

return counts;
Expand All @@ -302,6 +310,7 @@ function collectTypeParameterUsageCounts(
checker: ts.TypeChecker,
node: ts.Node,
foundIdentifierUsages: Map<ts.Identifier, number>,
fromClass: boolean, // We are talking about the type parameters of a class or one of its methods
): void {
const visitedSymbolLists = new Set<ts.Symbol[]>();
const type = checker.getTypeAtLocation(node);
Expand All @@ -325,6 +334,7 @@ function collectTypeParameterUsageCounts(
function visitType(
type: ts.Type | undefined,
assumeMultipleUses: boolean,
isReturnType = false,
): void {
// Seeing the same type > (threshold=3 ** 2) times indicates a likely
// recursive type, like `type T = { [P in keyof T]: T }`.
Expand Down Expand Up @@ -380,9 +390,23 @@ function collectTypeParameterUsageCounts(

// Tuple types like `[K, V]`
// Generic type references like `Map<K, V>`
else if (tsutils.isTupleType(type) || tsutils.isTypeReference(type)) {
else if (tsutils.isTypeReference(type)) {
for (const typeArgument of type.typeArguments ?? []) {
visitType(typeArgument, true);
// currently, if we are in a "class context", everything is accepted
let thisAssumeMultipleUses = fromClass || assumeMultipleUses;

// special cases - readonly arrays/tuples are considered only to use the
// type parameter once. Mutable arrays/tuples are considered to use the
// type parameter multiple times if and only if they are returned.
// other kind of type references always count as multiple uses
thisAssumeMultipleUses ||= tsutils.isTupleType(type.target)
? isReturnType && !type.target.readonly
: checker.isArrayType(type.target)
? isReturnType &&
(type.symbol as ts.Symbol | undefined)?.getName() === 'Array'
: true;

visitType(typeArgument, thisAssumeMultipleUses, isReturnType);
}
}

Expand Down Expand Up @@ -472,6 +496,7 @@ function collectTypeParameterUsageCounts(
checker.getTypePredicateOfSignature(signature)?.type ??
signature.getReturnType(),
false,
true,
);
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,6 @@ ruleTester.run('no-unnecessary-type-parameters', rule, {
}
}
`,
`
class Joiner {
join<T extends string | number>(els: T[]) {
return els.map(el => '' + el).join(',');
}
}
`,
`
declare class Foo {
getProp<T>(this: Record<'prop', T>): T;
Expand Down Expand Up @@ -243,8 +236,14 @@ ruleTester.run('no-unnecessary-type-parameters', rule, {
'declare function makeSets<K>(): [Set<K>][];',
'declare function makeMap<K, V>(): Map<K, V>;',
'declare function makeMap<K, V>(): [Map<K, V>];',
'declare function makeArray<T>(): T[];',
'declare function makeArrayNullish<T>(): (T | null)[];',
'declare function makeTupleMulti<T>(): [T | null, T | null];',
'declare function takeTupleMulti<T>(input: [T, T]): void;',
'declare function takeTupleMultiNullish<T>(input: [T | null, T | null]): void;',
'declare function arrayOfPairs<T>(): [T, T][];',
'declare function fetchJson<T>(url: string): Promise<T>;',
'declare function fetchJsonTuple<T>(url: string): Promise<[T]>;',
'declare function fn<T>(input: T): 0 extends 0 ? T : never;',
'declare function useFocus<T extends HTMLOrSVGElement>(): [React.RefObject<T>];',
`
Expand Down Expand Up @@ -447,6 +446,36 @@ const f = <T,>(
},
],
},
{
code: 'const func = <T,>(param: [T]) => null;',
errors: [
{
data: { descriptor: 'function', name: 'T', uses: 'used only once' },
messageId: 'sole',
suggestions: [
{
messageId: 'replaceUsagesWithConstraint',
output: 'const func = (param: [unknown]) => null;',
},
],
},
],
},
{
code: 'const func = <T,>(param: T[]) => null;',
errors: [
{
data: { descriptor: 'function', name: 'T', uses: 'used only once' },
messageId: 'sole',
suggestions: [
{
messageId: 'replaceUsagesWithConstraint',
output: 'const func = (param: unknown[]) => null;',
},
],
},
],
},
{
code: 'const f1 = <T,>(): T => {};',
errors: [
Expand Down Expand Up @@ -1249,6 +1278,139 @@ function foo(_: unknown): <T>(input: T) => T {
},
],
},
{
code: 'declare function makeReadonlyArray<T>(): readonly T[];',
errors: [
{
data: { descriptor: 'function', name: 'T', uses: 'used only once' },
messageId: 'sole',
suggestions: [
{
messageId: 'replaceUsagesWithConstraint',
output:
'declare function makeReadonlyArray(): readonly unknown[];',
},
],
},
],
},
{
code: 'declare function makeReadonlyTuple<T>(): readonly [T];',
errors: [
{
data: { descriptor: 'function', name: 'T', uses: 'used only once' },
messageId: 'sole',
suggestions: [
{
messageId: 'replaceUsagesWithConstraint',
output:
'declare function makeReadonlyTuple(): readonly [unknown];',
},
],
},
],
},
{
code: 'declare function makeReadonlyTupleNullish<T>(): readonly [T | null];',
errors: [
{
data: { descriptor: 'function', name: 'T', uses: 'used only once' },
messageId: 'sole',
suggestions: [
{
messageId: 'replaceUsagesWithConstraint',
output:
'declare function makeReadonlyTupleNullish(): readonly [unknown | null];',
},
],
},
],
},
{
code: 'declare function takeArray<T>(input: T[]): void;',
errors: [
{
data: { descriptor: 'function', name: 'T', uses: 'used only once' },
messageId: 'sole',
suggestions: [
{
messageId: 'replaceUsagesWithConstraint',
output: 'declare function takeArray(input: unknown[]): void;',
},
],
},
],
},
{
code: 'declare function takeArrayNullish<T>(input: (T | null)[]): void;',
errors: [
{
data: { descriptor: 'function', name: 'T', uses: 'used only once' },
messageId: 'sole',
suggestions: [
{
messageId: 'replaceUsagesWithConstraint',
output:
'declare function takeArrayNullish(input: (unknown | null)[]): void;',
},
],
},
],
},
{
code: 'declare function takeTuple<T>(input: [T]): void;',
errors: [
{
data: { descriptor: 'function', name: 'T', uses: 'used only once' },
messageId: 'sole',
suggestions: [
{
messageId: 'replaceUsagesWithConstraint',
output: 'declare function takeTuple(input: [unknown]): void;',
},
],
},
],
},
{
code: 'declare function takeTupleMultiUnrelated<T>(input: [T, number]): void;',
errors: [
{
data: { descriptor: 'function', name: 'T', uses: 'used only once' },
messageId: 'sole',
suggestions: [
{
messageId: 'replaceUsagesWithConstraint',
output:
'declare function takeTupleMultiUnrelated(input: [unknown, number]): void;',
},
],
},
],
},
{
code: `
declare function takeTupleMultiUnrelatedNullish<T>(
input: [T | null, null],
): void;
`,
errors: [
{
data: { descriptor: 'function', name: 'T', uses: 'used only once' },
messageId: 'sole',
suggestions: [
{
messageId: 'replaceUsagesWithConstraint',
output: `
declare function takeTupleMultiUnrelatedNullish(
input: [unknown | null, null],
): void;
`,
},
],
},
],
},
{
code: 'type Fn = <T>() => T;',
errors: [
Expand Down Expand Up @@ -1555,5 +1717,32 @@ function f(x: unknown): void {
},
],
},
{
code: `
class Joiner {
join<T extends number>(els: T[]) {
return els.map(el => '' + el).join(',');
}
}
`,
errors: [
{
data: { descriptor: 'function', name: 'T', uses: 'used only once' },
messageId: 'sole',
suggestions: [
{
messageId: 'replaceUsagesWithConstraint',
output: `
class Joiner {
join(els: number[]) {
return els.map(el => '' + el).join(',');
}
}
`,
},
],
},
],
},
],
});
Loading