diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index fdc9b72d4be..6e3448ba929 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1727,6 +1727,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { isArrayType, isTupleType, isArrayLikeType, + isEmptyAnonymousObjectType, isTypeInvalidDueToUnionDiscriminant, getExactOptionalProperties, getAllPossiblePropertiesOfTypes, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 87a72ea860f..71cc346bef0 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4946,6 +4946,8 @@ export interface TypeChecker { getPromisedTypeOfPromise(promise: Type, errorNode?: Node): Type | undefined; /** @internal */ getAwaitedType(type: Type): Type | undefined; + /** @internal */ + isEmptyAnonymousObjectType(type: Type): boolean; getReturnTypeOfSignature(signature: Signature): Type; /** * Gets the type of a parameter at a given position in a signature. diff --git a/src/services/completions.ts b/src/services/completions.ts index d9b197c3d27..4526f3901ab 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -218,6 +218,7 @@ import { isStatement, isStatic, isString, + isStringAndEmptyAnonymousObjectIntersection, isStringANonContextualKeyword, isStringLiteralLike, isStringLiteralOrTemplate, @@ -1442,7 +1443,7 @@ function createCompletionEntry( && !(type.flags & TypeFlags.BooleanLike) && !(type.flags & TypeFlags.Union && find((type as UnionType).types, type => !!(type.flags & TypeFlags.BooleanLike))) ) { - if (type.flags & TypeFlags.StringLike || (type.flags & TypeFlags.Union && every((type as UnionType).types, type => !!(type.flags & (TypeFlags.StringLike | TypeFlags.Undefined))))) { + if (type.flags & TypeFlags.StringLike || (type.flags & TypeFlags.Union && every((type as UnionType).types, type => !!(type.flags & (TypeFlags.StringLike | TypeFlags.Undefined) || isStringAndEmptyAnonymousObjectIntersection(type))))) { // If is string like or undefined use quotes insertText = `${escapeSnippetText(name)}=${quote(sourceFile, preferences, "$1")}`; isSnippet = true; diff --git a/src/services/utilities.ts b/src/services/utilities.ts index b6694c1c502..91abf947f1d 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -342,6 +342,7 @@ import { tryCast, Type, TypeChecker, + TypeFlags, TypeFormatFlags, TypeNode, TypeOfExpression, @@ -2154,6 +2155,17 @@ export function isStringOrRegularExpressionOrTemplateLiteral(kind: SyntaxKind): return false; } +/** @internal */ +export function isStringAndEmptyAnonymousObjectIntersection(type: Type) { + if (!type.isIntersection()) { + return false; + } + + const { types, checker } = type; + return types.length === 2 + && (types[0].flags & TypeFlags.String) && checker.isEmptyAnonymousObjectType(types[1]); +} + /** @internal */ export function isPunctuation(kind: SyntaxKind): boolean { return SyntaxKind.FirstPunctuation <= kind && kind <= SyntaxKind.LastPunctuation; diff --git a/tests/cases/fourslash/jsxAttributeCompletionStyleAuto.ts b/tests/cases/fourslash/jsxAttributeCompletionStyleAuto.ts index c3814c5fdc1..f3a5e12ed7c 100644 --- a/tests/cases/fourslash/jsxAttributeCompletionStyleAuto.ts +++ b/tests/cases/fourslash/jsxAttributeCompletionStyleAuto.ts @@ -15,10 +15,11 @@ //// prop_h?: string; //// prop_i?: boolean; //// prop_j?: { p1: string; }; +//// prop_string_literal_union?: 'input' | 'password' | (string & {}) //// } //// } //// } -//// +//// //// verify.completions({ @@ -73,6 +74,12 @@ verify.completions({ insertText: "prop_j={$1}", isSnippet: true, sortText: completion.SortText.OptionalMember, + }, + { + name: "prop_string_literal_union", + insertText: "prop_string_literal_union=\"$1\"", + isSnippet: true, + sortText: completion.SortText.OptionalMember, } ], preferences: { @@ -80,4 +87,4 @@ verify.completions({ includeCompletionsWithSnippetText: true, includeCompletionsWithInsertText: true, } -}); \ No newline at end of file +});