From 208ca1df9ecf54f6665be02f117a7ceca55a287c Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sun, 24 Aug 2025 15:16:29 +0900 Subject: [PATCH 1/4] feat: enhance typeMatchesSpecifier to handle union types --- packages/type-utils/src/TypeOrValueSpecifier.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index bbfb99ddcc78..9ef6c91dc9bb 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -169,6 +169,10 @@ export function typeMatchesSpecifier( specifier: TypeOrValueSpecifier, program: ts.Program, ): boolean { + if (tsutils.isUnionType(type)) { + return type.types.some(t => typeMatchesSpecifier(t, specifier, program)); + } + const wholeTypeMatches = ((): boolean => { if (tsutils.isIntrinsicErrorType(type)) { return false; From 61903a8e3178595358a816fddaf182ac280356a3 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sun, 24 Aug 2025 15:39:15 +0900 Subject: [PATCH 2/4] test: add tests --- .../tests/rules/only-throw-error.test.ts | 13 +++++++++++++ .../type-utils/tests/TypeOrValueSpecifier.test.ts | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/only-throw-error.test.ts b/packages/eslint-plugin/tests/rules/only-throw-error.test.ts index 6f60f01b8405..ee9d196be630 100644 --- a/packages/eslint-plugin/tests/rules/only-throw-error.test.ts +++ b/packages/eslint-plugin/tests/rules/only-throw-error.test.ts @@ -191,6 +191,19 @@ throw new Map(); }, { code: ` +function func() { + let err: Promise | Promise; + throw err; +} + `, + options: [ + { + allow: ['Promise'], + }, + ], + }, + { + code: ` try { } catch (e) { throw e; diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index a28d22f36b71..05a4ad1a8f25 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -167,6 +167,7 @@ describe('TypeOrValueSpecifier', () => { it.for([ ['interface Foo {prop: string}; type Test = Foo;', 'Foo'], ['type Test = RegExp;', 'RegExp'], + ['type Test = RegExp | BigInt;', 'RegExp'], ] as const satisfies [string, TypeOrValueSpecifier][])( 'matches a matching universal string specifier: %s\n\t%s', ([code, typeOrValueSpecifier], { expect }) => { @@ -191,6 +192,10 @@ describe('TypeOrValueSpecifier', () => { 'interface Foo {prop: string}; type Test = Foo;', { from: 'file', name: 'Foo' }, ], + [ + 'interface Foo {prop: string}; type Test = Foo | number;', + { from: 'file', name: 'Foo' }, + ], [ 'type Foo = {prop: string}; type Test = Foo;', { from: 'file', name: 'Foo' }, @@ -296,6 +301,7 @@ describe('TypeOrValueSpecifier', () => { it.for([ ['type Test = RegExp;', { from: 'lib', name: 'RegExp' }], + ['type Test = RegExp | BigInt;', { from: 'lib', name: 'RegExp' }], ['type Test = RegExp;', { from: 'lib', name: ['RegExp', 'BigInt'] }], ] as const satisfies [string, TypeOrValueSpecifier][])( 'matches a matching lib specifier: %s\n\t%s', @@ -316,6 +322,7 @@ describe('TypeOrValueSpecifier', () => { it.for([ ['type Test = string;', { from: 'lib', name: 'string' }], + ['type Test = string | number;', { from: 'lib', name: 'string' }], ['type Test = string;', { from: 'lib', name: ['string', 'number'] }], ] as const satisfies [string, TypeOrValueSpecifier][])( 'matches a matching intrinsic type specifier: %s\n\t%s', @@ -339,6 +346,10 @@ describe('TypeOrValueSpecifier', () => { 'import type {Node} from "typescript"; type Test = Node;', { from: 'package', name: 'Node', package: 'typescript' }, ], + [ + 'import type {Node} from "typescript"; type Test = Node | Symbol;', + { from: 'package', name: 'Node', package: 'typescript' }, + ], [ 'import type {Node} from "typescript"; type Test = Node;', { from: 'package', name: ['Node', 'Symbol'], package: 'typescript' }, From c878727f0e3280c3a973b11da6a594d731911e82 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Thu, 28 Aug 2025 18:32:46 +0900 Subject: [PATCH 3/4] fix: update typeMatchesSpecifier to require all union types to match --- packages/type-utils/src/TypeOrValueSpecifier.ts | 2 +- .../type-utils/tests/TypeOrValueSpecifier.test.ts | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index 9ef6c91dc9bb..553c8a4225bf 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -170,7 +170,7 @@ export function typeMatchesSpecifier( program: ts.Program, ): boolean { if (tsutils.isUnionType(type)) { - return type.types.some(t => typeMatchesSpecifier(t, specifier, program)); + return type.types.every(t => typeMatchesSpecifier(t, specifier, program)); } const wholeTypeMatches = ((): boolean => { diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index 05a4ad1a8f25..a28d22f36b71 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -167,7 +167,6 @@ describe('TypeOrValueSpecifier', () => { it.for([ ['interface Foo {prop: string}; type Test = Foo;', 'Foo'], ['type Test = RegExp;', 'RegExp'], - ['type Test = RegExp | BigInt;', 'RegExp'], ] as const satisfies [string, TypeOrValueSpecifier][])( 'matches a matching universal string specifier: %s\n\t%s', ([code, typeOrValueSpecifier], { expect }) => { @@ -192,10 +191,6 @@ describe('TypeOrValueSpecifier', () => { 'interface Foo {prop: string}; type Test = Foo;', { from: 'file', name: 'Foo' }, ], - [ - 'interface Foo {prop: string}; type Test = Foo | number;', - { from: 'file', name: 'Foo' }, - ], [ 'type Foo = {prop: string}; type Test = Foo;', { from: 'file', name: 'Foo' }, @@ -301,7 +296,6 @@ describe('TypeOrValueSpecifier', () => { it.for([ ['type Test = RegExp;', { from: 'lib', name: 'RegExp' }], - ['type Test = RegExp | BigInt;', { from: 'lib', name: 'RegExp' }], ['type Test = RegExp;', { from: 'lib', name: ['RegExp', 'BigInt'] }], ] as const satisfies [string, TypeOrValueSpecifier][])( 'matches a matching lib specifier: %s\n\t%s', @@ -322,7 +316,6 @@ describe('TypeOrValueSpecifier', () => { it.for([ ['type Test = string;', { from: 'lib', name: 'string' }], - ['type Test = string | number;', { from: 'lib', name: 'string' }], ['type Test = string;', { from: 'lib', name: ['string', 'number'] }], ] as const satisfies [string, TypeOrValueSpecifier][])( 'matches a matching intrinsic type specifier: %s\n\t%s', @@ -346,10 +339,6 @@ describe('TypeOrValueSpecifier', () => { 'import type {Node} from "typescript"; type Test = Node;', { from: 'package', name: 'Node', package: 'typescript' }, ], - [ - 'import type {Node} from "typescript"; type Test = Node | Symbol;', - { from: 'package', name: 'Node', package: 'typescript' }, - ], [ 'import type {Node} from "typescript"; type Test = Node;', { from: 'package', name: ['Node', 'Symbol'], package: 'typescript' }, From 0320f33926c39fbcbadac2aaa566eb1841b2a6aa Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Thu, 28 Aug 2025 20:42:13 +0900 Subject: [PATCH 4/4] test: add tests --- .../tests/rules/only-throw-error.test.ts | 18 ++++++++++++++++++ .../tests/TypeOrValueSpecifier.test.ts | 11 +++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/only-throw-error.test.ts b/packages/eslint-plugin/tests/rules/only-throw-error.test.ts index ee9d196be630..500454d0a9cb 100644 --- a/packages/eslint-plugin/tests/rules/only-throw-error.test.ts +++ b/packages/eslint-plugin/tests/rules/only-throw-error.test.ts @@ -628,6 +628,24 @@ function fun(t: T): void { }, { code: ` +function func() { + let err: Promise | Promise | void; + throw err; +} + `, + errors: [ + { + messageId: 'object', + }, + ], + options: [ + { + allow: ['Promise'], + }, + ], + }, + { + code: ` class UnknownError implements Error {} throw new UnknownError(); `, diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index a28d22f36b71..3d3429fbb3d1 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -179,6 +179,7 @@ describe('TypeOrValueSpecifier', () => { ['interface Foo {prop: string}; type Test = Foo;', 'RegExp'], ['type Test = RegExp;', 'Foo'], ['type Test = RegExp;', 'BigInt'], + ['type Test = RegExp | BigInt;', 'BigInt'], ] as const satisfies [string, TypeOrValueSpecifier][])( "doesn't match a mismatched universal string specifier: %s\n\t%s", ([code, typeOrValueSpecifier], { expect }) => { @@ -267,6 +268,10 @@ describe('TypeOrValueSpecifier', () => { 'interface Foo {prop: string}; type Test = Foo;', { from: 'file', name: 'Bar' }, ], + [ + 'interface Foo {prop: string}; type Test = Foo | string;', + { from: 'file', name: 'Foo' }, + ], [ 'interface Foo {prop: string}; type Test = Foo;', { from: 'file', name: ['Bar', 'Baz'] }, @@ -306,6 +311,7 @@ describe('TypeOrValueSpecifier', () => { it.for([ ['type Test = RegExp;', { from: 'lib', name: 'BigInt' }], + ['type Test = RegExp | BigInt;', { from: 'lib', name: 'BigInt' }], ['type Test = RegExp;', { from: 'lib', name: ['BigInt', 'Date'] }], ] as const satisfies [string, TypeOrValueSpecifier][])( "doesn't match a mismatched lib specifier: %s\n\t%s", @@ -326,6 +332,7 @@ describe('TypeOrValueSpecifier', () => { it.for([ ['type Test = string;', { from: 'lib', name: 'number' }], + ['type Test = string | number;', { from: 'lib', name: 'number' }], ['type Test = string;', { from: 'lib', name: ['number', 'boolean'] }], ] as const satisfies [string, TypeOrValueSpecifier][])( "doesn't match a mismatched intrinsic type specifier: %s\n\t%s", @@ -545,6 +552,10 @@ describe('TypeOrValueSpecifier', () => { 'import type {Node} from "typescript"; type Test = Node;', { from: 'package', name: 'Symbol', package: 'typescript' }, ], + [ + 'import type {Node} from "typescript"; type Test = Node | Symbol;', + { from: 'package', name: 'Node', package: 'typescript' }, + ], [ 'import type {Node} from "typescript"; type Test = Node;', { from: 'package', name: ['Symbol', 'Checker'], package: 'typescript' },