Skip to content

fix(eslint-plugin): [unified-signatures] exempt this from optional parameter overload check #11005

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 9 commits into from
May 2, 2025
37 changes: 37 additions & 0 deletions packages/eslint-plugin/src/rules/unified-signatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,14 @@ export default createRule<Options, MessageIds>({
types1: readonly TSESTree.Parameter[],
types2: readonly TSESTree.Parameter[],
): Unify | undefined {
const firstParam1 = types1[0];
const firstParam2 = types2[0];

// exempt signatures with `this: void` from the rule
if (isThisVoidParam(firstParam1) || isThisVoidParam(firstParam2)) {
return undefined;
}

const index = getIndexOfFirstDifference(
types1,
types2,
Expand Down Expand Up @@ -294,6 +302,22 @@ export default createRule<Options, MessageIds>({
: undefined;
}

function isThisParam(param: TSESTree.Parameter | undefined): boolean {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI/PSA - I changed this not to be a type predicate since it's only valid in the true branch, not the false branch (the function returning false does not imply that param is not a TSESTree.Identifier).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I didn't think about TS doing a negative refinement when the function returns false. It would be useful to be able to declare an assertion that only refines in the true case

return (
param != null &&
param.type === AST_NODE_TYPES.Identifier &&
param.name === 'this'
);
}

function isThisVoidParam(param: TSESTree.Parameter | undefined) {
return (
isThisParam(param) &&
(param as TSESTree.Identifier).typeAnnotation?.typeAnnotation.type ===
AST_NODE_TYPES.TSVoidKeyword
);
}

/**
* Detect `a(): void` and `a(x: number): void`.
* Returns the parameter declaration (`x: number` in this example) that should be optional/rest, and overload it's a part of.
Expand All @@ -310,6 +334,19 @@ export default createRule<Options, MessageIds>({
const shorter = sig1.length < sig2.length ? sig1 : sig2;
const shorterSig = sig1.length < sig2.length ? a : b;

const firstParam1 = sig1.at(0);
const firstParam2 = sig2.at(0);
// If one signature has explicit this type and another doesn't, they can't
// be unified.
if (isThisParam(firstParam1) !== isThisParam(firstParam2)) {
return undefined;
}

// exempt signatures with `this: void` from the rule
if (isThisVoidParam(firstParam1) || isThisVoidParam(firstParam2)) {
return undefined;
}

// If one is has 2+ parameters more than the other, they must all be optional/rest.
// Differ by optional parameters: f() and f(x), f() and f(x, ?y, ...z)
// Not allowed: f() and f(x, y)
Expand Down
83 changes: 83 additions & 0 deletions packages/eslint-plugin/tests/rules/unified-signatures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,21 @@ declare function f(x: boolean): unknown;
`,
options: [{ ignoreOverloadsWithDifferentJSDoc: true }],
},
`
function f(): void;
function f(this: {}): void;
function f(this: void | {}): void {}
`,
`
function f(a: boolean): void;
function f(this: {}, a: boolean): void;
function f(this: void | {}, a: boolean): void {}
`,
`
function f(this: void, a: boolean): void;
function f(this: {}, a: boolean): void;
function f(this: void | {}, a: boolean): void {}
`,
],
invalid: [
{
Expand Down Expand Up @@ -1136,5 +1151,73 @@ declare function f(x: boolean): unknown;
],
options: [{ ignoreOverloadsWithDifferentJSDoc: true }],
},
{
code: `
function f(this: {}, a: boolean): void;
function f(this: {}, a: string): void;
function f(this: {}, a: boolean | string): void {}
`,
errors: [
{
column: 22,
line: 3,
messageId: 'singleParameterDifference',
},
],
},
{
code: `
function f(this: {}): void;
function f(this: {}, a: string): void;
function f(this: {}, a?: string): void {}
`,
errors: [
{
column: 22,
line: 3,
messageId: 'omittingSingleParameter',
},
],
},
{
code: `
function f(this: string): void;
function f(this: number): void;
function f(this: string | number): void {}
`,
errors: [
{
column: 12,
data: {
failureStringStart:
'These overloads can be combined into one signature',
type1: 'string',
type2: 'number',
},
line: 3,
messageId: 'singleParameterDifference',
},
],
},
{
code: `
function f(this: string, a: boolean): void;
function f(this: number, a: boolean): void;
function f(this: string | number, a: boolean): void {}
`,
errors: [
{
column: 12,
data: {
failureStringStart:
'These overloads can be combined into one signature',
type1: 'string',
type2: 'number',
},
line: 3,
messageId: 'singleParameterDifference',
},
],
},
],
});