diff --git a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx index 52fe0768efe1..e3f047e6e4f0 100644 --- a/packages/eslint-plugin/docs/rules/no-floating-promises.mdx +++ b/packages/eslint-plugin/docs/rules/no-floating-promises.mdx @@ -119,6 +119,63 @@ 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. + +This option takes an array of type specifiers to consider safe. +Each item in the array must have one of the following forms: + +- 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"}]}' +let promise: Promise = Promise.resolve(2); +promise; + +function returnsPromise(): Promise { + return Promise.resolve(42); +} + +returnsPromise(); +``` + + + + +```ts option='{"allowForKnownSafePromises":[{"from":"file","name":"SafePromise"},{"from":"lib","name":"PromiseLike"},{"from":"package","name":"Bar","package":"bar-lib"}]}' +// promises can be marked as safe by using branded types +type SafePromise = Promise & { __linterBrands?: string }; + +let promise: SafePromise = Promise.resolve(2); +promise; + +function returnsSafePromise(): SafePromise { + return Promise.resolve(42); +} + +returnsSafePromise(); +``` + + + + ## 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/docs/rules/prefer-readonly-parameter-types.mdx b/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.mdx index 196b63ba9dab..e01ff6ffbf62 100644 --- a/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.mdx +++ b/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.mdx @@ -142,11 +142,12 @@ interface Foo { Some complex types cannot easily be made readonly, for example the `HTMLElement` type or the `JQueryStatic` type from `@types/jquery`. This option allows you to globally disable reporting of such types. -Each item must be one of: +This option takes an array of type specifiers to ignore. +Each item in the array must have one of the following forms: -- 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: "Foo"}`) -- A type from a package (`{from: "package", name: "Foo", package: "foo-lib"}`, this also works for types defined in a typings package). +- 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: "Foo" }`) +- A type from a package (`{ from: "package", name: "Foo", package: "foo-lib" }`, this also works for types defined in a typings package). Additionally, a type may be defined just as a simple string, which then matches the type independently of its origin. diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index b9fb9b1cc4dd..1ae5e602ae0e 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -3,17 +3,22 @@ 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 +84,7 @@ export default createRule({ 'Whether to ignore async IIFEs (Immediately Invoked Function Expressions).', type: 'boolean', }, + allowForKnownSafePromises: readonlynessOptionsSchema.properties.allow, }, additionalProperties: false, }, @@ -89,12 +95,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,11 +263,11 @@ 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(tsNode)) { return { isUnhandled: true, promiseArray: true }; } - if (!isPromiseLike(checker, tsNode)) { + if (!isPromiseLike(tsNode)) { return { isUnhandled: false }; } @@ -322,63 +332,69 @@ export default createRule({ // we just can't tell. return { isUnhandled: false }; } - }, -}); -function isPromiseArray(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 (isPromiseLike(checker, node, arrayType)) { - return true; - } - } + function isPromiseArray(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 (isPromiseLike(node, arrayType)) { + return true; + } + } - if (checker.isTupleType(ty)) { - for (const tupleElementType of checker.getTypeArguments(ty)) { - if (isPromiseLike(checker, node, tupleElementType)) { - return true; + if (checker.isTupleType(ty)) { + for (const tupleElementType of checker.getTypeArguments(ty)) { + if (isPromiseLike(node, tupleElementType)) { + return true; + } + } } } + return false; } - } - return false; -} -// Modified from tsutils.isThenable() to only consider thenables which can be -// rejected/caught via a second parameter. Original source (MIT licensed): -// -// https://github.com/ajafff/tsutils/blob/49d0d31050b44b81e918eae4fbaf1dfe7b7286af/util/type.ts#L95-L125 -function isPromiseLike( - checker: ts.TypeChecker, - node: ts.Node, - type?: ts.Type, -): boolean { - type ??= checker.getTypeAtLocation(node); - for (const ty of tsutils.unionTypeParts(checker.getApparentType(type))) { - const then = ty.getProperty('then'); - if (then === undefined) { - continue; - } + // Modified from tsutils.isThenable() to only consider thenables which can be + // rejected/caught via a second parameter. Original source (MIT licensed): + // + // https://github.com/ajafff/tsutils/blob/49d0d31050b44b81e918eae4fbaf1dfe7b7286af/util/type.ts#L95-L125 + function isPromiseLike(node: ts.Node, type?: ts.Type): boolean { + type ??= checker.getTypeAtLocation(node); - const thenType = checker.getTypeOfSymbolAtLocation(then, node); - if ( - hasMatchingSignature( - thenType, - signature => - signature.parameters.length >= 2 && - isFunctionParam(checker, signature.parameters[0], node) && - isFunctionParam(checker, signature.parameters[1], node), - ) - ) { - return true; + // Ignore anything specified by `allowForKnownSafePromises` option. + if ( + allowForKnownSafePromises.some(allowedType => + typeMatchesSpecifier(type, allowedType, services.program), + ) + ) { + return false; + } + + for (const ty of tsutils.unionTypeParts(checker.getApparentType(type))) { + const then = ty.getProperty('then'); + if (then === undefined) { + continue; + } + + const thenType = checker.getTypeOfSymbolAtLocation(then, node); + if ( + hasMatchingSignature( + thenType, + signature => + signature.parameters.length >= 2 && + isFunctionParam(checker, signature.parameters[0], node) && + isFunctionParam(checker, signature.parameters[1], node), + ) + ) { + return true; + } + } + return false; } - } - return false; -} + }, +}); function hasMatchingSignature( type: ts.Type, 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 5c6de4ab2e0e..44e303fc3131 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 @@ -73,3 +73,38 @@ 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"}]} + +let promise: Promise = 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. + +function returnsPromise(): Promise { + return Promise.resolve(42); +} + +returnsPromise(); +~~~~~~~~~~~~~~~~~ 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"}]} + +// promises can be marked as safe by using branded types +type SafePromise = Promise & { __linterBrands?: string }; + +let promise: SafePromise = Promise.resolve(2); +promise; + +function returnsSafePromise(): SafePromise { + return Promise.resolve(42); +} + +returnsSafePromise(); +" +`; 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..e6087e512265 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. @@ -506,6 +501,205 @@ void promiseArray; }, { 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' }] }], + }, + { + 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' }] }, + ], + }, + { + code: ` +let promise: () => PromiseLike = () => Promise.resolve(5); +promise(); + `, + options: [ + { allowForKnownSafePromises: [{ from: 'lib', name: 'PromiseLike' }] }, + ], + }, + { + 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; myTag\`abc\`.catch(() => {}); `, @@ -516,6 +710,29 @@ declare const myTag: (strings: TemplateStringsArray) => string; myTag\`abc\`; `, }, + { + 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' }], + }, + ], + }, ], invalid: [ @@ -1855,5 +2072,114 @@ 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: ` +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 containing elements of `Promise` type and a branded Promise type will be treated as just an ordinary `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..33db003f4758 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -202,6 +202,10 @@ describe('TypeOrValueSpecifier', () => { 'type Foo = {prop: string}; type Test = Foo;', { from: 'file', name: 'Foo', path: 'tests/fixtures/file.ts' }, ], + [ + 'type Foo = Promise & {hey?: string}; let foo: Foo = Promise.resolve(5); type Test = typeof foo;', + { from: 'file', name: 'Foo' }, + ], [ 'interface Foo {prop: string}; type Test = Foo;', {