diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index ce0291367b510..b4bc622631ee7 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -776,6 +776,7 @@ import {
isTypeReferenceType,
isTypeUsableAsPropertyName,
isUMDExportSymbol,
+ isUnaryTupleTypeNode,
isValidBigIntString,
isValidESSymbolDeclaration,
isValidTypeOnlyAliasUseSite,
@@ -16536,17 +16537,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return isNoInferType(substitutionType) ? substitutionType.baseType : getIntersectionType([substitutionType.constraint, substitutionType.baseType]);
}
- function isUnaryTupleTypeNode(node: TypeNode) {
- return node.kind === SyntaxKind.TupleType && (node as TupleTypeNode).elements.length === 1;
- }
-
function getImpliedConstraint(type: Type, checkNode: TypeNode, extendsNode: TypeNode): Type | undefined {
return isUnaryTupleTypeNode(checkNode) && isUnaryTupleTypeNode(extendsNode) ? getImpliedConstraint(type, (checkNode as TupleTypeNode).elements[0], (extendsNode as TupleTypeNode).elements[0]) :
getActualTypeVariable(getTypeFromTypeNode(checkNode)) === getActualTypeVariable(type) ? getTypeFromTypeNode(extendsNode) :
undefined;
}
- function getConditionalFlowTypeOfType(type: Type, node: Node) {
+ function getFlowTypeOfType(type: Type, node: Node) {
let constraints: Type[] | undefined;
let covariant = true;
while (node && !isStatement(node) && node.kind !== SyntaxKind.JSDoc) {
@@ -19720,7 +19717,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
function getTypeFromTypeNode(node: TypeNode): Type {
- return getConditionalFlowTypeOfType(getTypeFromTypeNodeWorker(node), node);
+ return getFlowTypeOfType(getTypeFromTypeNodeWorker(node), node);
}
function getTypeFromTypeNodeWorker(node: TypeNode): Type {
diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts
index 75862dde0b9db..6cc0f0b2141d1 100644
--- a/src/compiler/utilities.ts
+++ b/src/compiler/utilities.ts
@@ -12096,3 +12096,8 @@ export function isNewScopeNode(node: Node): node is IntroducesNewScopeNode {
|| isJSDocSignature(node)
|| isMappedTypeNode(node);
}
+
+/** @internal */
+export function isUnaryTupleTypeNode(node: TypeNode): boolean {
+ return node.kind === SyntaxKind.TupleType && (node as TupleTypeNode).elements.length === 1;
+}
diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts
index 63cac4d49cca2..9fc7e5a1a62b6 100644
--- a/src/services/stringCompletions.ts
+++ b/src/services/stringCompletions.ts
@@ -11,6 +11,7 @@ import {
import {
addToSeen,
altDirectorySeparator,
+ append,
arrayFrom,
CallLikeExpression,
CancellationToken,
@@ -28,6 +29,7 @@ import {
CompletionEntryDetails,
CompletionInfo,
concatenate,
+ ConditionalTypeNode,
contains,
containsPath,
ContextFlags,
@@ -43,6 +45,7 @@ import {
endsWith,
ensureTrailingDirectorySeparator,
equateStringsCaseSensitive,
+ escapeLeadingUnderscores,
escapeString,
Extension,
fileExtensionIsOneOf,
@@ -101,9 +104,11 @@ import {
isPatternMatch,
isPrivateIdentifierClassElementDeclaration,
isRootedDiskPath,
+ isStatement,
isString,
isStringLiteral,
isStringLiteralLike,
+ isUnaryTupleTypeNode,
isUrl,
JsxAttribute,
LanguageServiceHost,
@@ -147,6 +152,7 @@ import {
stripQuotes,
supportedTSImplementationExtensions,
Symbol,
+ SymbolFlags,
SyntaxKind,
textPart,
TextSpan,
@@ -159,9 +165,11 @@ import {
tryReadDirectory,
tryRemoveDirectoryPrefix,
tryRemovePrefix,
+ TupleTypeNode,
Type,
TypeChecker,
TypeFlags,
+ TypeNode,
UnionTypeNode,
unmangleScopedPackageName,
UserPreferences,
@@ -513,11 +521,31 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
// bar: string;
// }
// let x: Foo["/*completion position*/"]
- const { indexType, objectType } = grandParent as IndexedAccessTypeNode;
- if (!rangeContainsPosition(indexType, position)) {
+ if (!rangeContainsPosition((grandParent as IndexedAccessTypeNode).indexType, position)) {
return undefined;
}
- return stringLiteralCompletionsFromProperties(typeChecker.getTypeFromTypeNode(objectType));
+ const objectType = typeChecker.getTypeFromTypeNode((grandParent as IndexedAccessTypeNode).objectType);
+ const completions = stringLiteralCompletionsFromProperties(objectType);
+ const flowPropertyNames = getFlowPropertyNamesOfType(typeChecker, objectType, (grandParent as IndexedAccessTypeNode).objectType);
+ if (flowPropertyNames) {
+ const existing = new Set(completions.symbols.map(s => s.escapedName));
+ let extraSymbols: Symbol[] | undefined;
+ for (const name of flowPropertyNames) {
+ const escapedName = escapeLeadingUnderscores(name);
+ if (!existing.has(escapedName)) {
+ existing.add(escapedName);
+ const symbol = typeChecker.createSymbol(SymbolFlags.Property, escapedName);
+ symbol.links.type = typeChecker.getAnyType();
+ extraSymbols = append(extraSymbols, symbol);
+ }
+ }
+ return {
+ kind: StringLiteralCompletionKind.Properties,
+ symbols: concatenate(completions.symbols, extraSymbols),
+ hasIndexSignature: completions.hasIndexSignature,
+ };
+ }
+ return completions;
case SyntaxKind.UnionType: {
const result = fromUnionableLiteralType(walkUpParentheses(grandParent.parent));
if (!result) {
@@ -545,6 +573,48 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
}
}
+// this is a reverse of `getFlowTypeOfType` from the checker
+function getFlowPropertyNamesOfType(typeChecker: TypeChecker, type: Type, node: Node) {
+ const indexType = typeChecker.getIndexType(type);
+
+ if (!indexType.isIndexType()) {
+ // if the index type is not deferred then there is no reason to check for flow property names
+ // as accessing such properties resuls in an error anyway:
+ //
+ // type A = { foo: string };
+ // type B = "bar" extends keyof A ? A["bar"]/*error*/ : never;
+ return;
+ }
+
+ let flowPropertyNames: string[] | undefined;
+
+ while (node && !isStatement(node) && node.kind !== SyntaxKind.JSDoc) {
+ const parent = node.parent;
+ if (parent.kind === SyntaxKind.ConditionalType && node === (parent as ConditionalTypeNode).trueType) {
+ const propertyName = getImpliedPropertyName((parent as ConditionalTypeNode).checkType, (parent as ConditionalTypeNode).extendsType);
+ flowPropertyNames = append(flowPropertyNames, propertyName);
+ }
+ node = parent;
+ }
+
+ return flowPropertyNames;
+
+ function getImpliedPropertyName(checkNode: TypeNode, extendsNode: TypeNode) {
+ if (isUnaryTupleTypeNode(checkNode) && isUnaryTupleTypeNode(extendsNode)) {
+ return getImpliedPropertyName((checkNode as TupleTypeNode).elements[0], (extendsNode as TupleTypeNode).elements[0]);
+ }
+ const checkType = typeChecker.getTypeFromTypeNode(checkNode);
+ if (!checkType.isStringLiteral()) {
+ return;
+ }
+ const extendsType = typeChecker.getTypeFromTypeNode(extendsNode);
+ if (!typeChecker.isTypeAssignableTo(extendsType, indexType)) {
+ return;
+ }
+ return checkType.value;
+ }
+}
+
function walkUpParentheses(node: Node) {
switch (node.kind) {
case SyntaxKind.ParenthesizedType:
@@ -580,8 +650,8 @@ function getStringLiteralCompletionsFromSignature(call: CallLikeExpression, arg:
return length(types) ? { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier } : undefined;
}
-function stringLiteralCompletionsFromProperties(type: Type | undefined): StringLiteralCompletionsFromProperties | undefined {
- return type && {
+function stringLiteralCompletionsFromProperties(type: Type): StringLiteralCompletionsFromProperties {
+ return {
kind: StringLiteralCompletionKind.Properties,
symbols: filter(type.getApparentProperties(), prop => !(prop.valueDeclaration && isPrivateIdentifierClassElementDeclaration(prop.valueDeclaration))),
hasIndexSignature: hasIndexSignature(type),
diff --git a/tests/cases/fourslash/stringLiteralCompletionsFlowPropertiesIndexedAccess1.ts b/tests/cases/fourslash/stringLiteralCompletionsFlowPropertiesIndexedAccess1.ts
new file mode 100644
index 0000000000000..30745a6ce8825
--- /dev/null
+++ b/tests/cases/fourslash/stringLiteralCompletionsFlowPropertiesIndexedAccess1.ts
@@ -0,0 +1,39 @@
+///
+
+//// type Test1 = "foo" extends keyof T ? T["/*1*/"] : never;
+////
+//// type A = { bar: string };
+//// type Test2 = "foo" extends keyof A ? A["/*2*/"] : never;
+////
+//// type Test3 = "foo" extends keyof T ? "bar" extends keyof T ? T["/*3*/"] : never : never;
+////
+//// type Test4 = "foo" extends keyof T ? T["/*4*/"] : never;
+////
+//// type Test5 = "foo" extends keyof T ? "foo" extends keyof T ? T["/*5*/"] : never : never;
+////
+//// type Test6 = ["foo"] extends [keyof T] ? T["/*6*/"] : never;
+////
+//// type Test7 = "bar" extends keyof T ? T["/*7*/"] : never;
+////
+//// type Test8 = "foo" | "bar" extends keyof T ? T["/*8*/"] : never;
+////
+//// type Test9 = "foo" & T2 extends keyof T ? T["/*9*/"] : never;
+////
+//// type Test10 = "foo" extends keyof T & T2 ? T["/*10*/"] : never;
+////
+//// type Test11 = "foo" extends keyof T | T2 ? T["/*11*/"] : never;
+////
+//// type Test12 = "foo" extends keyof T2 ? T["/*12*/"] : never;
+
+verify.completions({ marker: ["1"], exact: ["foo"] });
+verify.completions({ marker: ["2"], excludes: ["foo"] });
+verify.completions({ marker: ["3"], exact: ["bar", "foo"] });
+verify.completions({ marker: ["4"], exact: ["foo"] });
+verify.completions({ marker: ["5"], exact: ["foo"] });
+verify.completions({ marker: ["6"], exact: ["foo"] });
+verify.completions({ marker: ["7"], exact: ["bar", "foo"] });
+verify.completions({ marker: ["8"], excludes: ["bar", "foo"] });
+verify.completions({ marker: ["9"], excludes: ["foo"] });
+verify.completions({ marker: ["10"], exact: ["foo"] });
+verify.completions({ marker: ["11"], excludes: ["foo"] });
+verify.completions({ marker: ["12"], excludes: ["foo"] });