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 @@ +/// <reference path="fourslash.ts" /> + +////interface CustomElements { +//// 'component-one': { +//// foo?: string; +//// }, +//// 'component-two': { +//// bar?: string; +//// } +////} +//// +////interface Options<T extends keyof CustomElements> { +//// props: CustomElements[T]; +////} +//// +////declare function create<T extends keyof CustomElements>(name: T, options: Options<T>): 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 @@ +/// <reference path="fourslash.ts" /> + +////interface CustomElements { +//// 'component-one': { +//// foo?: string; +//// }, +//// 'component-two': { +//// bar?: string; +//// } +////} +//// +////interface Options<T extends keyof CustomElements> { +//// props: CustomElements[T]; +////} +//// +////declare function create<T extends 'hello' | 'goodbye'>(name: T, options: Options<T extends 'hello' ? 'component-one' : 'component-two'>): void; +////declare function create<T extends keyof CustomElements>(name: T, options: Options<T>): 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<T extends keyof CustomElements> { +//// props?: {} & { x: CustomElements[(T extends string ? T : never) & string][] }['x']; +////} +//// +////declare function f<T extends keyof CustomElements>(k: T, options: Options<T>): 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<T extends keyof CustomElements> = { kind: T } & Required<{ x: CustomElements[(T extends string ? T : never) & string] }['x']>; +//// +////declare function Component<T extends keyof CustomElements>(props: Options<T>): void; +//// +////const c = <Component /**/ kind="component-one" /> + +verify.completions({ + marker: "", + exact: [{ + name: "foo" + }] +})