diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index b1f516dad69d9..65e43c1278944 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -21337,11 +21337,19 @@ namespace ts {
return argIndex === -1 ? undefined : getContextualTypeForArgumentAtIndex(callTarget, argIndex, contextFlags);
}
- function getContextualTypeForArgumentAtIndex(callTarget: CallLikeExpression, argIndex: number, contextFlags?: ContextFlags): Type {
+ function getContextualTypeForArgumentAtIndex(callTarget: CallLikeExpression, argIndex: number, contextFlags?: ContextFlags): Type | undefined {
// If we're already in the process of resolving the given signature, don't resolve again as
// that could cause infinite recursion. Instead, return anySignature.
let signature = getNodeLinks(callTarget).resolvedSignature === resolvingSignature ? resolvingSignature : getResolvedSignature(callTarget);
- if (contextFlags && contextFlags & ContextFlags.BaseConstraint && signature.target && !hasTypeArguments(callTarget)) {
+
+ if (contextFlags && contextFlags & ContextFlags.Uninstantiated) {
+ return signature.target ? getTypeAtPosition(signature.target, argIndex) : undefined;
+ }
+
+ if (contextFlags && contextFlags & ContextFlags.BaseConstraint) {
+ if (!signature.target || hasTypeArguments(callTarget)) {
+ return undefined;
+ }
signature = getBaseSignature(signature.target);
}
diff --git a/src/compiler/types.ts b/src/compiler/types.ts
index 98f2531b6c6d1..17f080385b58b 100644
--- a/src/compiler/types.ts
+++ b/src/compiler/types.ts
@@ -3605,6 +3605,7 @@ namespace ts {
Signature = 1 << 0, // Obtaining contextual signature
NoConstraints = 1 << 1, // Don't obtain type variable constraints
BaseConstraint = 1 << 2, // Use base constraint type for completions
+ Uninstantiated = 1 << 3, // Attempt to get the type from an uninstantiated signature
}
// NOTE: If modifying this enum, must modify `TypeFormatFlags` too!
diff --git a/src/services/completions.ts b/src/services/completions.ts
index 471d8e4c142c6..ef445a5a79d24 100644
--- a/src/services/completions.ts
+++ b/src/services/completions.ts
@@ -1279,7 +1279,14 @@ namespace ts.Completions {
// Cursor is inside a JSX self-closing element or opening element
const attrsType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes);
if (!attrsType) return GlobalsSearch.Continue;
- const baseType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes, ContextFlags.BaseConstraint);
+ const uninstantiatedType = typeChecker.getContextualType(jsxContainer!.attributes, ContextFlags.Uninstantiated);
+ let baseType;
+ if (uninstantiatedType) {
+ const signature = tryGetContextualTypeProvidingSignature(jsxContainer!, typeChecker)?.target;
+ if (signature && !isIndexedAccessTypeWithTypeParameterIndex(uninstantiatedType, signature)) {
+ baseType = typeChecker.getContextualType(jsxContainer!.attributes, ContextFlags.BaseConstraint);
+ }
+ }
symbols = filterJsxAttributes(getPropertiesForObjectExpression(attrsType, baseType, jsxContainer!.attributes, typeChecker), jsxContainer!.attributes.properties);
setSortTextToOptionalMember();
completionKind = CompletionKind.MemberLike;
@@ -1800,9 +1807,17 @@ namespace ts.Completions {
if (objectLikeContainer.kind === SyntaxKind.ObjectLiteralExpression) {
const instantiatedType = typeChecker.getContextualType(objectLikeContainer);
- const baseType = instantiatedType && typeChecker.getContextualType(objectLikeContainer, ContextFlags.BaseConstraint);
- if (!instantiatedType || !baseType) return GlobalsSearch.Fail;
- isNewIdentifierLocation = hasIndexSignature(instantiatedType || baseType);
+ if (!instantiatedType) return GlobalsSearch.Fail;
+ const uninstantiatedType = typeChecker.getContextualType(objectLikeContainer, ContextFlags.Uninstantiated);
+ let baseType;
+ if (uninstantiatedType) {
+ const signature = tryGetContextualTypeProvidingSignature(objectLikeContainer, typeChecker)?.target;
+ if (signature && !isIndexedAccessTypeWithTypeParameterIndex(uninstantiatedType, signature)) {
+ baseType = typeChecker.getContextualType(objectLikeContainer, ContextFlags.BaseConstraint);
+ }
+ }
+
+ isNewIdentifierLocation = hasIndexSignature(instantiatedType);
typeMembers = getPropertiesForObjectExpression(instantiatedType, baseType, objectLikeContainer, typeChecker);
existingMembers = objectLikeContainer.properties;
}
@@ -1852,6 +1867,68 @@ namespace ts.Completions {
return GlobalsSearch.Success;
}
+ function tryGetContextualTypeProvidingSignature(node: Node, checker: TypeChecker): Signature | undefined {
+ loop: while (true) {
+ switch (node.kind) {
+ case SyntaxKind.SpreadAssignment:
+ case SyntaxKind.ArrayLiteralExpression:
+ case SyntaxKind.ParenthesizedExpression:
+ case SyntaxKind.ConditionalExpression:
+ case SyntaxKind.PropertyAssignment:
+ case SyntaxKind.ShorthandPropertyAssignment:
+ case SyntaxKind.ObjectLiteralExpression:
+ case SyntaxKind.JsxAttribute:
+ case SyntaxKind.JsxAttributes:
+ node = node.parent;
+ break;
+ default:
+ break loop;
+ }
+ }
+ if (!isCallLikeExpression(node)) {
+ return;
+ }
+ return checker.getResolvedSignature(node);
+ }
+
+ function isIndexedAccessTypeWithTypeParameterIndex(type: Type, signature: Signature): boolean {
+ if (type.isUnionOrIntersection()) {
+ return some(type.types, t => isIndexedAccessTypeWithTypeParameterIndex(t, signature));
+ }
+ if (type.flags & TypeFlags.IndexedAccess) {
+ return typeIsTypeParameterFromSignature((type as IndexedAccessType).indexType, signature);
+ }
+ if (getObjectFlags(type) & ObjectFlags.Mapped) {
+ const { constraintType } = (type as MappedType);
+ if (constraintType && constraintType.flags & TypeFlags.Index) {
+ return isIndexedAccessTypeWithTypeParameterIndex((constraintType as IndexType).type, signature);
+ }
+ }
+ return false;
+ }
+
+ function typeIsTypeParameterFromSignature(type: Type, signature: Signature): boolean {
+ if (!signature.typeParameters) {
+ return false;
+ }
+ if (type.isUnionOrIntersection()) {
+ return some(type.types, t => typeIsTypeParameterFromSignature(t, signature));
+ }
+ if (type.flags & TypeFlags.Conditional) {
+ return typeIsTypeParameterFromSignature((type as ConditionalType).checkType, signature)
+ || typeIsTypeParameterFromSignature((type as ConditionalType).extendsType, signature)
+ || typeIsTypeParameterFromSignature((type as ConditionalType).resolvedTrueType, signature)
+ || typeIsTypeParameterFromSignature((type as ConditionalType).resolvedFalseType, signature);
+ }
+ if (type.flags & TypeFlags.Index) {
+ return typeIsTypeParameterFromSignature((type as IndexType).type, signature);
+ }
+ if (type.flags & TypeFlags.TypeParameter) {
+ return some(signature.typeParameters, p => p.symbol === type.symbol);
+ }
+ return false;
+ }
+
/**
* Aggregates relevant symbols for completion in import clauses and export clauses
* whose declarations have a module specifier; for instance, symbols will be aggregated for
diff --git a/tests/cases/fourslash/completionsGenericIndexedAccess3.ts b/tests/cases/fourslash/completionsGenericIndexedAccess3.ts
new file mode 100644
index 0000000000000..730cdd8f631dd
--- /dev/null
+++ b/tests/cases/fourslash/completionsGenericIndexedAccess3.ts
@@ -0,0 +1,35 @@
+///
+
+////interface CustomElements {
+//// 'component-one': {
+//// foo?: string;
+//// },
+//// 'component-two': {
+//// bar?: string;
+//// }
+////}
+////
+////interface Options {
+//// props: CustomElements[T];
+////}
+////
+////declare function create(name: T, options: Options): void;
+////
+////create('component-one', { props: { /*1*/ } });
+////create('component-two', { props: { /*2*/ } });
+
+verify.completions({
+ marker: "1",
+ exact: [{
+ name: "foo",
+ sortText: completion.SortText.OptionalMember
+ }]
+});
+
+verify.completions({
+ marker: "2",
+ exact: [{
+ name: "bar",
+ sortText: completion.SortText.OptionalMember
+ }]
+});
diff --git a/tests/cases/fourslash/completionsGenericIndexedAccess4.ts b/tests/cases/fourslash/completionsGenericIndexedAccess4.ts
new file mode 100644
index 0000000000000..0edeaedf9821f
--- /dev/null
+++ b/tests/cases/fourslash/completionsGenericIndexedAccess4.ts
@@ -0,0 +1,45 @@
+///
+
+////interface CustomElements {
+//// 'component-one': {
+//// foo?: string;
+//// },
+//// 'component-two': {
+//// bar?: string;
+//// }
+////}
+////
+////interface Options {
+//// props: CustomElements[T];
+////}
+////
+////declare function create(name: T, options: Options): void;
+////declare function create(name: T, options: Options): void;
+////
+////create('hello', { props: { /*1*/ } })
+////create('goodbye', { props: { /*2*/ } })
+////create('component-one', { props: { /*3*/ } });
+
+verify.completions({
+ marker: "1",
+ exact: [{
+ name: "foo",
+ sortText: completion.SortText.OptionalMember
+ }]
+});
+
+verify.completions({
+ marker: "2",
+ exact: [{
+ name: "bar",
+ sortText: completion.SortText.OptionalMember
+ }]
+});
+
+verify.completions({
+ marker: "3",
+ exact: [{
+ name: "foo",
+ sortText: completion.SortText.OptionalMember
+ }]
+});
diff --git a/tests/cases/fourslash/completionsGenericIndexedAccess5.ts b/tests/cases/fourslash/completionsGenericIndexedAccess5.ts
new file mode 100644
index 0000000000000..7f3a0aa369094
--- /dev/null
+++ b/tests/cases/fourslash/completionsGenericIndexedAccess5.ts
@@ -0,0 +1,28 @@
+////interface CustomElements {
+//// 'component-one': {
+//// foo?: string;
+//// },
+//// 'component-two': {
+//// bar?: string;
+//// }
+////}
+////
+////interface Options {
+//// props?: {} & { x: CustomElements[(T extends string ? T : never) & string][] }['x'];
+////}
+////
+////declare function f(k: T, options: Options): void;
+////
+////f("component-one", {
+//// props: [{
+//// /**/
+//// }]
+////})
+
+verify.completions({
+ marker: "",
+ exact: [{
+ name: "foo",
+ sortText: completion.SortText.OptionalMember
+ }]
+});
diff --git a/tests/cases/fourslash/completionsGenericIndexedAccess6.ts b/tests/cases/fourslash/completionsGenericIndexedAccess6.ts
new file mode 100644
index 0000000000000..496ebf7b50ecf
--- /dev/null
+++ b/tests/cases/fourslash/completionsGenericIndexedAccess6.ts
@@ -0,0 +1,23 @@
+// @Filename: component.tsx
+
+////interface CustomElements {
+//// 'component-one': {
+//// foo?: string;
+//// },
+//// 'component-two': {
+//// bar?: string;
+//// }
+////}
+////
+////type Options = { kind: T } & Required<{ x: CustomElements[(T extends string ? T : never) & string] }['x']>;
+////
+////declare function Component(props: Options): void;
+////
+////const c =
+
+verify.completions({
+ marker: "",
+ exact: [{
+ name: "foo"
+ }]
+})