diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx
index 6b42594dae63..8199e11efa92 100644
--- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx
+++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx
@@ -115,6 +115,51 @@ await (async function () {
})();
```
+### `allowForKnownSafePromises`
+
+This option allows marking specific types as "safe" to be floating. For example, you may need to do this in the case of libraries whose APIs return Promises whose rejections are safely handled by the library.
+
+Each item must be one of:
+
+- A type defined in a file (`{from: "file", name: "Foo", path: "src/foo-file.ts"}` with `path` being an optional path relative to the project root directory)
+- A type from the default library (`{from: "lib", name: "PromiseLike"}`)
+- A type from a package (`{from: "package", name: "Foo", package: "foo-lib"}`, this also works for types defined in a typings package).
+
+Examples of code for this rule with:
+
+```json
+{
+ "allowForKnownSafePromises": [
+ { "from": "file", "name": "SafePromise" },
+ { "from": "lib", "name": "PromiseLike" },
+ { "from": "package", "name": "Bar", "package": "bar-lib" }
+ ]
+}
+```
+
+
+
+
+```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}'
+type UnsafePromise = Promise & { __linterBrands?: string };
+let promise: UnsafePromise = Promise.resolve(2);
+promise;
+promise.finally();
+```
+
+
+
+
+```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}'
+type SafePromise = Promise & { __linterBrands?: string }; // promises can be marked as safe by using branded types
+let promise: SafePromise = Promise.resolve(2);
+promise;
+promise.finally();
+```
+
+
+
+
## When Not To Use It
This rule can be difficult to enable on large existing projects that set up many floating Promises.
diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts
index b9fb9b1cc4dd..89558307cef7 100644
--- a/packages/eslint-plugin/src/rules/no-floating-promises.ts
+++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts
@@ -1,19 +1,28 @@
-import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
+import type {
+ ParserServicesWithTypeInformation,
+ TSESLint,
+ TSESTree,
+} from '@typescript-eslint/utils';
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import * as tsutils from 'ts-api-utils';
import * as ts from 'typescript';
+import type { TypeOrValueSpecifier } from '../util';
import {
createRule,
getOperatorPrecedence,
getParserServices,
OperatorPrecedence,
+ readonlynessOptionsDefaults,
+ readonlynessOptionsSchema,
+ typeMatchesSpecifier,
} from '../util';
type Options = [
{
ignoreVoid?: boolean;
ignoreIIFE?: boolean;
+ allowForKnownSafePromises?: TypeOrValueSpecifier[];
},
];
@@ -79,6 +88,7 @@ export default createRule({
'Whether to ignore async IIFEs (Immediately Invoked Function Expressions).',
type: 'boolean',
},
+ allowForKnownSafePromises: readonlynessOptionsSchema.properties.allow,
},
additionalProperties: false,
},
@@ -89,12 +99,16 @@ export default createRule({
{
ignoreVoid: true,
ignoreIIFE: false,
+ allowForKnownSafePromises: readonlynessOptionsDefaults.allow,
},
],
create(context, [options]) {
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
+ // TODO: #5439
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const allowForKnownSafePromises = options.allowForKnownSafePromises!;
return {
ExpressionStatement(node): void {
@@ -253,7 +267,9 @@ export default createRule({
// Check the type. At this point it can't be unhandled if it isn't a promise
// or array thereof.
- if (isPromiseArray(checker, tsNode)) {
+ if (
+ isPromiseArray(services, allowForKnownSafePromises, checker, tsNode)
+ ) {
return { isUnhandled: true, promiseArray: true };
}
@@ -262,6 +278,28 @@ export default createRule({
}
if (node.type === AST_NODE_TYPES.CallExpression) {
+ const member =
+ node.callee.type === AST_NODE_TYPES.MemberExpression
+ ? node.callee.object
+ : node;
+ const calledByThenOrCatch =
+ (node.callee.type === AST_NODE_TYPES.MemberExpression &&
+ node.callee.property.type === AST_NODE_TYPES.Identifier &&
+ node.callee.property.name === 'catch') ||
+ (node.callee.type === AST_NODE_TYPES.MemberExpression &&
+ node.callee.property.type === AST_NODE_TYPES.Identifier &&
+ node.callee.property.name === 'then');
+ if (
+ !calledByThenOrCatch &&
+ doesTypeMatchSpecifier(
+ services,
+ allowForKnownSafePromises,
+ services.getTypeAtLocation(member),
+ )
+ ) {
+ return { isUnhandled: false };
+ }
+
// If the outer expression is a call, a `.catch()` or `.then()` with
// rejection handler handles the promise.
@@ -291,6 +329,15 @@ export default createRule({
// All other cases are unhandled.
return { isUnhandled: true };
} else if (node.type === AST_NODE_TYPES.TaggedTemplateExpression) {
+ if (
+ doesTypeMatchSpecifier(
+ services,
+ allowForKnownSafePromises,
+ services.getTypeAtLocation(node),
+ )
+ ) {
+ return { isUnhandled: false };
+ }
return { isUnhandled: true };
} else if (node.type === AST_NODE_TYPES.ConditionalExpression) {
// We must be getting the promise-like value from one of the branches of the
@@ -305,6 +352,15 @@ export default createRule({
node.type === AST_NODE_TYPES.Identifier ||
node.type === AST_NODE_TYPES.NewExpression
) {
+ if (
+ doesTypeMatchSpecifier(
+ services,
+ allowForKnownSafePromises,
+ services.getTypeAtLocation(node),
+ )
+ ) {
+ return { isUnhandled: false };
+ }
// If it is just a property access chain or a `new` call (e.g. `foo.bar` or
// `new Promise()`), the promise is not handled because it doesn't have the
// necessary then/catch call at the end of the chain.
@@ -325,13 +381,36 @@ export default createRule({
},
});
-function isPromiseArray(checker: ts.TypeChecker, node: ts.Node): boolean {
+function doesTypeMatchSpecifier(
+ services: ParserServicesWithTypeInformation,
+ options: TypeOrValueSpecifier[],
+ type: ts.Type,
+): boolean {
+ return options.some(specifier =>
+ typeMatchesSpecifier(type, specifier, services.program),
+ );
+}
+
+function isPromiseArray(
+ services: ParserServicesWithTypeInformation,
+ options: TypeOrValueSpecifier[],
+ checker: ts.TypeChecker,
+ node: ts.Node,
+): boolean {
const type = checker.getTypeAtLocation(node);
for (const ty of tsutils
.unionTypeParts(type)
.map(t => checker.getApparentType(t))) {
if (checker.isArrayType(ty)) {
const arrayType = checker.getTypeArguments(ty)[0];
+ if (
+ options.length > 0 &&
+ tsutils
+ .unionTypeParts(arrayType)
+ .some(type => doesTypeMatchSpecifier(services, options, type))
+ ) {
+ return false;
+ }
if (isPromiseLike(checker, node, arrayType)) {
return true;
}
@@ -339,6 +418,9 @@ function isPromiseArray(checker: ts.TypeChecker, node: ts.Node): boolean {
if (checker.isTupleType(ty)) {
for (const tupleElementType of checker.getTypeArguments(ty)) {
+ if (doesTypeMatchSpecifier(services, options, tupleElementType)) {
+ return false;
+ }
if (isPromiseLike(checker, node, tupleElementType)) {
return true;
}
diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-floating-promises.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-floating-promises.shot
index 0dc935b52782..00cfbc962882 100644
--- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-floating-promises.shot
+++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-floating-promises.shot
@@ -70,3 +70,27 @@ await (async function () {
})();
"
`;
+
+exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 5`] = `
+"Incorrect
+Options: {"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}
+
+type UnsafePromise = Promise & { __linterBrands?: string };
+let promise: UnsafePromise = Promise.resolve(2);
+promise;
+~~~~~~~~ Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the \`void\` operator.
+promise.finally();
+~~~~~~~~~~~~~~~~~~ Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the \`void\` operator.
+"
+`;
+
+exports[`Validating rule docs no-floating-promises.mdx code examples ESLint output 6`] = `
+"Correct
+Options: {"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}
+
+type SafePromise = Promise & { __linterBrands?: string }; // promises can be marked as safe by using branded types
+let promise: SafePromise = Promise.resolve(2);
+promise;
+promise.finally();
+"
+`;
diff --git a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts
index bd02ed6d5a87..6ec6f4f883d5 100644
--- a/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts
+++ b/packages/eslint-plugin/tests/rules/no-floating-promises.test.ts
@@ -486,11 +486,6 @@ declare const promiseArray: Array>;
void promiseArray;
`,
},
- {
- code: `
-[Promise.reject(), Promise.reject()].then(() => {});
- `,
- },
{
// Expressions aren't checked by this rule, so this just becomes an array
// of number | undefined, which is fine regardless of the ignoreVoid setting.
@@ -504,6 +499,209 @@ void promiseArray;
['I', 'am', 'just', 'an', 'array'];
`,
},
+ // type annotations on variables containing promises
+ {
+ code: `
+interface SafeThenable {
+ then(
+ onfulfilled?:
+ | ((value: T) => TResult1 | SafeThenable)
+ | undefined
+ | null,
+ onrejected?:
+ | ((reason: any) => TResult2 | SafeThenable)
+ | undefined
+ | null,
+ ): SafeThenable;
+}
+let promise: SafeThenable = Promise.resolve(5);
+0, promise;
+ `,
+ options: [
+ {
+ allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }],
+ },
+ ],
+ },
+ {
+ code: `
+interface SafeThenable {
+ then(
+ onfulfilled?:
+ | ((value: T) => TResult1 | SafeThenable)
+ | undefined
+ | null,
+ onrejected?:
+ | ((reason: any) => TResult2 | SafeThenable)
+ | undefined
+ | null,
+ ): SafeThenable;
+}
+let promise: SafeThenable = Promise.resolve(5);
+0 ? promise : 3;
+ `,
+ options: [
+ {
+ allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }],
+ },
+ ],
+ },
+ {
+ code: `
+class SafePromise extends Promise {}
+let promise: { a: SafePromise } = { a: Promise.resolve(5) };
+promise.a;
+ `,
+ options: [
+ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] },
+ ],
+ },
+ {
+ code: `
+class SafePromise extends Promise {}
+let promise: SafePromise = Promise.resolve(5);
+promise;
+ `,
+ options: [
+ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] },
+ ],
+ },
+ {
+ code: `
+type Foo = Promise & { hey?: string };
+let promise: Foo = Promise.resolve(5);
+0 || promise;
+ `,
+ options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }],
+ },
+ {
+ code: `
+type Foo = Promise & { hey?: string };
+let promise: Foo = Promise.resolve(5);
+promise.finally();
+ `,
+ options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }],
+ },
+ // type annotations on promise returning functions (or async functions)
+ {
+ code: `
+interface SafeThenable {
+ then(
+ onfulfilled?:
+ | ((value: T) => TResult1 | SafeThenable)
+ | undefined
+ | null,
+ onrejected?:
+ | ((reason: any) => TResult2 | SafeThenable)
+ | undefined
+ | null,
+ ): SafeThenable;
+}
+let promise: () => SafeThenable = () => Promise.resolve(5);
+0, promise();
+ `,
+ options: [
+ {
+ allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }],
+ },
+ ],
+ },
+ {
+ code: `
+interface SafeThenable {
+ then(
+ onfulfilled?:
+ | ((value: T) => TResult1 | SafeThenable)
+ | undefined
+ | null,
+ onrejected?:
+ | ((reason: any) => TResult2 | SafeThenable)
+ | undefined
+ | null,
+ ): SafeThenable;
+}
+let promise: () => SafeThenable = () => Promise.resolve(5);
+0 ? promise() : 3;
+ `,
+ options: [
+ {
+ allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }],
+ },
+ ],
+ },
+ {
+ code: `
+type Foo = Promise & { hey?: string };
+let promise: () => Foo = () => Promise.resolve(5);
+promise();
+ `,
+ options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }],
+ },
+ {
+ code: `
+type Foo = Promise & { hey?: string };
+let promise: () => Foo = async () => 5;
+promise().finally();
+ `,
+ options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }],
+ },
+ {
+ code: `
+class SafePromise extends Promise {}
+let promise: () => SafePromise = async () => 5;
+0 || promise();
+ `,
+ options: [
+ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] },
+ ],
+ },
+ {
+ code: `
+class SafePromise extends Promise {}
+let promise: () => SafePromise = async () => 5;
+null ?? promise();
+ `,
+ options: [
+ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] },
+ ],
+ },
+ // type from es5.d.ts using `allowForKnownSafePromises`
+ {
+ code: `
+let promise: () => PromiseLike = () => Promise.resolve(5);
+promise();
+ `,
+ options: [
+ { allowForKnownSafePromises: [{ from: 'lib', name: 'PromiseLike' }] },
+ ],
+ },
+ // promises in array using `allowForKnownSafePromises`
+ {
+ code: `
+type Foo = Promise & { hey?: string };
+declare const arrayOrPromiseTuple: Foo[];
+arrayOrPromiseTuple;
+ `,
+ options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }],
+ },
+ {
+ code: `
+type Foo = Promise & { hey?: string };
+declare const arrayOrPromiseTuple: [Foo, 5];
+arrayOrPromiseTuple;
+ `,
+ options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }],
+ },
+ {
+ code: `
+type SafePromise = Promise & { __linterBrands?: string };
+declare const myTag: (strings: TemplateStringsArray) => SafePromise;
+myTag\`abc\`;
+ `,
+ options: [
+ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] },
+ ],
+ },
{
code: `
declare const myTag: (strings: TemplateStringsArray) => Promise;
@@ -1855,5 +2053,137 @@ cursed();
`,
errors: [{ line: 3, messageId: 'floatingPromiseArrayVoid' }],
},
+ {
+ code: `
+interface UnsafeThenable {
+ then(
+ onfulfilled?:
+ | ((value: T) => TResult1 | UnsafeThenable)
+ | undefined
+ | null,
+ onrejected?:
+ | ((reason: any) => TResult2 | UnsafeThenable)
+ | undefined
+ | null,
+ ): UnsafeThenable;
+}
+let promise: UnsafeThenable = Promise.resolve(5);
+promise;
+ `,
+ options: [
+ {
+ allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }],
+ },
+ ],
+ errors: [{ line: 15, messageId: 'floatingVoid' }],
+ },
+ {
+ code: `
+interface SafeThenable {
+ then(
+ onfulfilled?:
+ | ((value: T) => TResult1 | SafeThenable)
+ | undefined
+ | null,
+ onrejected?:
+ | ((reason: any) => TResult2 | SafeThenable)
+ | undefined
+ | null,
+ ): SafeThenable;
+}
+let promise: () => SafeThenable = () => Promise.resolve(5);
+promise().then(() => {});
+ `,
+ options: [
+ {
+ allowForKnownSafePromises: [{ from: 'file', name: 'SafeThenable' }],
+ },
+ ],
+ errors: [{ line: 15, messageId: 'floatingVoid' }],
+ },
+ {
+ code: `
+class SafePromise extends Promise {}
+let promise: SafePromise = Promise.resolve(5);
+promise.catch();
+ `,
+ options: [
+ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] },
+ ],
+ errors: [{ line: 4, messageId: 'floatingVoid' }],
+ },
+ {
+ code: `
+class UnsafePromise extends Promise {}
+let promise: () => UnsafePromise = async () => 5;
+promise().finally();
+ `,
+ options: [
+ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] },
+ ],
+ errors: [{ line: 4, messageId: 'floatingVoid' }],
+ },
+ {
+ code: `
+type UnsafePromise = Promise & { hey?: string };
+let promise: UnsafePromise = Promise.resolve(5);
+0 ? promise.catch() : 2;
+ `,
+ options: [
+ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] },
+ ],
+ errors: [{ line: 4, messageId: 'floatingVoid' }],
+ },
+ {
+ code: `
+type UnsafePromise = Promise & { hey?: string };
+let promise: () => UnsafePromise = async () => 5;
+null ?? promise().catch();
+ `,
+ options: [
+ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] },
+ ],
+ errors: [{ line: 4, messageId: 'floatingVoid' }],
+ },
+ {
+ code: `
+type Foo = Promise & { hey?: string };
+declare const arrayOrPromiseTuple: Foo[];
+arrayOrPromiseTuple;
+ `,
+ options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Bar' }] }],
+ errors: [{ line: 4, messageId: 'floatingPromiseArrayVoid' }],
+ },
+ // an array, which contains elements of `Promise` type and a branded promise type, its type will be reduced to `Promise`, see - https://github.com/typescript-eslint/typescript-eslint/pull/8502#issuecomment-2105734406
+ {
+ code: `
+type SafePromise = Promise & { hey?: string };
+let foo: SafePromise = Promise.resolve(1);
+let bar = [Promise.resolve(2), foo];
+bar;
+ `,
+ options: [
+ { allowForKnownSafePromises: [{ from: 'file', name: 'SafePromise' }] },
+ ],
+ errors: [{ line: 5, messageId: 'floatingPromiseArrayVoid' }],
+ },
+ {
+ code: `
+type Foo = Promise & { hey?: string };
+declare const arrayOrPromiseTuple: [Foo, 5];
+arrayOrPromiseTuple;
+ `,
+ options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Bar' }] }],
+ errors: [{ line: 4, messageId: 'floatingPromiseArrayVoid' }],
+ },
+ {
+ code: `
+type SafePromise = Promise & { __linterBrands?: string };
+declare const myTag: (strings: TemplateStringsArray) => SafePromise;
+myTag\`abc\`;
+ `,
+ options: [{ allowForKnownSafePromises: [{ from: 'file', name: 'Foo' }] }],
+ errors: [{ line: 4, messageId: 'floatingVoid' }],
+ },
],
});
diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot b/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot
index ac6669f651b4..a708c7001d5b 100644
--- a/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot
+++ b/packages/eslint-plugin/tests/schema-snapshots/no-floating-promises.shot
@@ -8,6 +8,100 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos
{
"additionalProperties": false,
"properties": {
+ "allowForKnownSafePromises": {
+ "items": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "enum": ["file"],
+ "type": "string"
+ },
+ "name": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array",
+ "uniqueItems": true
+ }
+ ]
+ },
+ "path": {
+ "type": "string"
+ }
+ },
+ "required": ["from", "name"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "enum": ["lib"],
+ "type": "string"
+ },
+ "name": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array",
+ "uniqueItems": true
+ }
+ ]
+ }
+ },
+ "required": ["from", "name"],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "from": {
+ "enum": ["package"],
+ "type": "string"
+ },
+ "name": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array",
+ "uniqueItems": true
+ }
+ ]
+ },
+ "package": {
+ "type": "string"
+ }
+ },
+ "required": ["from", "name", "package"],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
"ignoreIIFE": {
"description": "Whether to ignore async IIFEs (Immediately Invoked Function Expressions).",
"type": "boolean"
@@ -26,6 +120,23 @@ exports[`Rule schemas should be convertible to TS types for documentation purpos
type Options = [
{
+ allowForKnownSafePromises?: (
+ | {
+ from: 'file';
+ name: [string, ...string[]] | string;
+ path?: string;
+ }
+ | {
+ from: 'lib';
+ name: [string, ...string[]] | string;
+ }
+ | {
+ from: 'package';
+ name: [string, ...string[]] | string;
+ package: string;
+ }
+ | string
+ )[];
/** Whether to ignore async IIFEs (Immediately Invoked Function Expressions). */
ignoreIIFE?: boolean;
/** Whether to ignore \`void\` expressions. */
diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts
index 16958370f57b..cf23b059c802 100644
--- a/packages/type-utils/src/TypeOrValueSpecifier.ts
+++ b/packages/type-utils/src/TypeOrValueSpecifier.ts
@@ -200,9 +200,9 @@ export function typeMatchesSpecifier(
if (!specifierNameMatches(type, specifier.name)) {
return false;
}
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
const declarationFiles =
- type
- .getSymbol()
+ symbol
?.getDeclarations()
?.map(declaration => declaration.getSourceFile()) ?? [];
switch (specifier.from) {
diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts
index 8ddca54d3b34..dad38e22a6cf 100644
--- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts
+++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts
@@ -202,6 +202,11 @@ describe('TypeOrValueSpecifier', () => {
'type Foo = {prop: string}; type Test = Foo;',
{ from: 'file', name: 'Foo', path: 'tests/fixtures/file.ts' },
],
+ [
+ 'type Foo = Promise & {hey?: string}; let Guzz: Foo = Promise.resolve(5); type Test = typeof Guzz;',
+ // type.getSymbol() doesn't work here, .aliasSymbol does
+ { from: 'file', name: 'Foo' },
+ ],
[
'interface Foo {prop: string}; type Test = Foo;',
{