Skip to content

Commit 65296fe

Browse files
committed
Make getTypeArgumentConstraint work for type arguments of expressions
Previously, `getTypeArgumentConstraint` could only find constraints for type arguments of generic type instantiations. Now it additionally supports type arguments of the following expression kinds: - function calls - `new` expressions - tagged templates - JSX expressions - decorators - instantiation expressions
1 parent c6a1812 commit 65296fe

9 files changed

+274
-9
lines changed

src/compiler/checker.ts

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42624,6 +42624,48 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4262442624
return undefined;
4262542625
}
4262642626

42627+
function getSignaturesFromCallLike(node: CallLikeExpression): readonly Signature[] {
42628+
switch (node.kind) {
42629+
case SyntaxKind.CallExpression:
42630+
case SyntaxKind.Decorator:
42631+
return getSignaturesOfType(
42632+
getTypeOfExpression(node.expression),
42633+
SignatureKind.Call,
42634+
);
42635+
case SyntaxKind.NewExpression:
42636+
return getSignaturesOfType(
42637+
getTypeOfExpression(node.expression),
42638+
SignatureKind.Construct,
42639+
);
42640+
case SyntaxKind.JsxSelfClosingElement:
42641+
case SyntaxKind.JsxOpeningElement:
42642+
if (isJsxIntrinsicTagName(node.tagName)) return [];
42643+
return getSignaturesOfType(
42644+
getTypeOfExpression(node.tagName),
42645+
SignatureKind.Call,
42646+
);
42647+
case SyntaxKind.TaggedTemplateExpression:
42648+
return getSignaturesOfType(
42649+
getTypeOfExpression(node.tag),
42650+
SignatureKind.Call,
42651+
);
42652+
case SyntaxKind.BinaryExpression:
42653+
case SyntaxKind.JsxOpeningFragment:
42654+
return [];
42655+
}
42656+
}
42657+
42658+
function getTypeParameterConstraintForPositionAcrossSignatures(signatures: readonly Signature[], position: number) {
42659+
const relevantTypeParameterConstraints = flatMap(signatures, signature => {
42660+
const relevantTypeParameter = signature.typeParameters?.[position];
42661+
if (relevantTypeParameter === undefined) return [];
42662+
const relevantConstraint = getConstraintOfTypeParameter(relevantTypeParameter);
42663+
if (relevantConstraint === undefined) return [];
42664+
return [relevantConstraint];
42665+
});
42666+
return getUnionType(relevantTypeParameterConstraints);
42667+
}
42668+
4262742669
function checkTypeReferenceNode(node: TypeReferenceNode | ExpressionWithTypeArguments) {
4262842670
checkGrammarTypeArguments(node, node.typeArguments);
4262942671
if (node.kind === SyntaxKind.TypeReference && !isInJSFile(node) && !isInJSDoc(node) && node.typeArguments && node.typeName.end !== node.typeArguments.pos) {
@@ -42662,12 +42704,62 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4266242704
}
4266342705

4266442706
function getTypeArgumentConstraint(node: TypeNode): Type | undefined {
42665-
const typeReferenceNode = tryCast(node.parent, isTypeReferenceType);
42666-
if (!typeReferenceNode) return undefined;
42667-
const typeParameters = getTypeParametersForTypeReferenceOrImport(typeReferenceNode);
42668-
if (!typeParameters) return undefined;
42669-
const constraint = getConstraintOfTypeParameter(typeParameters[typeReferenceNode.typeArguments!.indexOf(node)]);
42670-
return constraint && instantiateType(constraint, createTypeMapper(typeParameters, getEffectiveTypeArguments(typeReferenceNode, typeParameters)));
42707+
let typeArgumentPosition;
42708+
if (
42709+
"typeArguments" in node.parent && // eslint-disable-line local/no-in-operator
42710+
Array.isArray(node.parent.typeArguments)
42711+
) {
42712+
typeArgumentPosition = node.parent.typeArguments.indexOf(node);
42713+
}
42714+
42715+
if (typeArgumentPosition !== undefined) {
42716+
// The node could be a type argument of a call, a `new` expression, a decorator, an
42717+
// instantiation expression, or a generic type instantiation.
42718+
42719+
if (isCallLikeExpression(node.parent)) {
42720+
return getTypeParameterConstraintForPositionAcrossSignatures(
42721+
getSignaturesFromCallLike(node.parent),
42722+
typeArgumentPosition,
42723+
);
42724+
}
42725+
42726+
if (isDecorator(node.parent.parent)) {
42727+
return getTypeParameterConstraintForPositionAcrossSignatures(
42728+
getSignaturesFromCallLike(node.parent.parent),
42729+
typeArgumentPosition,
42730+
);
42731+
}
42732+
42733+
if (isExpressionWithTypeArguments(node.parent) && isExpressionStatement(node.parent.parent)) {
42734+
const uninstantiatedType = checkExpression(node.parent.expression);
42735+
42736+
const callConstraint = getTypeParameterConstraintForPositionAcrossSignatures(
42737+
getSignaturesOfType(uninstantiatedType, SignatureKind.Call),
42738+
typeArgumentPosition,
42739+
);
42740+
const constructConstraint = getTypeParameterConstraintForPositionAcrossSignatures(
42741+
getSignaturesOfType(uninstantiatedType, SignatureKind.Construct),
42742+
typeArgumentPosition,
42743+
);
42744+
42745+
// An instantiation expression instantiates both call and construct signatures, so
42746+
// if both exist type arguments must be assignable to both constraints.
42747+
if (constructConstraint.flags & TypeFlags.Never) return callConstraint;
42748+
if (callConstraint.flags & TypeFlags.Never) return constructConstraint;
42749+
return getIntersectionType([callConstraint, constructConstraint]);
42750+
}
42751+
42752+
if (isTypeReferenceType(node.parent)) {
42753+
const typeParameters = getTypeParametersForTypeReferenceOrImport(node.parent);
42754+
if (!typeParameters) return undefined;
42755+
const relevantTypeParameter = typeParameters[typeArgumentPosition];
42756+
const constraint = getConstraintOfTypeParameter(relevantTypeParameter);
42757+
return constraint && instantiateType(
42758+
constraint,
42759+
createTypeMapper(typeParameters, getEffectiveTypeArguments(node.parent, typeParameters)),
42760+
);
42761+
}
42762+
}
4267142763
}
4267242764

4267342765
function checkTypeQuery(node: TypeQueryNode) {

src/services/completions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,6 @@ import {
256256
isTypeOnlyImportDeclaration,
257257
isTypeOnlyImportOrExportDeclaration,
258258
isTypeParameterDeclaration,
259-
isTypeReferenceType,
260259
isValidTypeOnlyAliasUseSite,
261260
isVariableDeclaration,
262261
isVariableLike,
@@ -5769,8 +5768,9 @@ function tryGetTypeLiteralNode(node: Node): TypeLiteralNode | undefined {
57695768
function getConstraintOfTypeArgumentProperty(node: Node, checker: TypeChecker): Type | undefined {
57705769
if (!node) return undefined;
57715770

5772-
if (isTypeNode(node) && isTypeReferenceType(node.parent)) {
5773-
return checker.getTypeArgumentConstraint(node);
5771+
if (isTypeNode(node)) {
5772+
const constraint = checker.getTypeArgumentConstraint(node);
5773+
if (constraint) return constraint;
57745774
}
57755775

57765776
const t = getConstraintOfTypeArgumentProperty(node.parent, checker);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// one: string;
5+
//// two: number;
6+
////}
7+
////interface Bar {
8+
//// three: boolean;
9+
//// four: {
10+
//// five: unknown;
11+
//// };
12+
////}
13+
////
14+
////function a<T extends Foo>() {}
15+
////a<{/*0*/}>();
16+
////
17+
////var b = () => <T extends Foo>() => {};
18+
////b()<{/*1*/}>();
19+
////
20+
////declare function c<T extends Foo>(): void
21+
////declare function c<T extends Bar>(): void
22+
////c<{/*2*/}>();
23+
////
24+
////function d<T extends Foo, U extends Bar>() {}
25+
////d<{/*3*/}, {/*4*/}>();
26+
////d<Foo, { four: {/*5*/} }>();
27+
////
28+
////(<T extends Foo>() => {})<{/*6*/}>();
29+
30+
verify.completions(
31+
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
32+
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: true },
33+
{ marker: "2", unsorted: ["one", "two", "three", "four"], isNewIdentifierLocation: true },
34+
{ marker: "3", unsorted: ["one", "two"], isNewIdentifierLocation: true },
35+
{ marker: "4", unsorted: ["three", "four"], isNewIdentifierLocation: true },
36+
{ marker: "5", unsorted: ["five"], isNewIdentifierLocation: true },
37+
{ marker: "6", unsorted: ["one", "two"], isNewIdentifierLocation: true },
38+
);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// one: string;
5+
//// two: number;
6+
////}
7+
////interface Bar {
8+
//// three: boolean;
9+
//// four: symbol;
10+
////}
11+
////
12+
////class A<T extends Foo> {}
13+
////new A<{/*0*/}>();
14+
////
15+
////class B<T extends Foo, U extends Bar> {}
16+
////new B<{/*1*/}, {/*2*/}>();
17+
////
18+
////declare const C: {
19+
//// new <T extends Foo>(): unknown
20+
//// new <T extends Bar>(): unknown
21+
////}
22+
////new C<{/*3*/}>()
23+
////
24+
////new (class <T extends Foo> {})<{/*4*/}>();
25+
26+
verify.completions(
27+
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
28+
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: true },
29+
{ marker: "2", unsorted: ["three", "four"], isNewIdentifierLocation: true },
30+
{ marker: "3", unsorted: ["one", "two", "three", "four"], isNewIdentifierLocation: true },
31+
{ marker: "4", unsorted: ["one", "two"], isNewIdentifierLocation: true },
32+
);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// kind: 'foo';
5+
//// one: string;
6+
////}
7+
////interface Bar {
8+
//// kind: 'bar';
9+
//// two: number;
10+
////}
11+
////
12+
////declare function a<T extends Foo>(): void
13+
////declare function a<T extends Bar>(): void
14+
////a<{ kind: 'bar', /*0*/ }>();
15+
////
16+
////declare function b<T extends Foo>(kind: 'foo'): void
17+
////declare function b<T extends Bar>(kind: 'bar'): void
18+
////b<{/*1*/}>('bar');
19+
20+
// The completion lists are unfortunately not narrowed here (ideally only
21+
// properties of `Bar` would be suggested).
22+
verify.completions(
23+
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
24+
{ marker: "1", unsorted: ["kind", "one", "two"], isNewIdentifierLocation: true },
25+
);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @jsx: preserve
4+
// @filename: a.tsx
5+
////interface Foo {
6+
//// one: string;
7+
//// two: number;
8+
////}
9+
////
10+
////const Component = <T extends Foo>() => <></>;
11+
////
12+
////<Component<{/*0*/}>></Component>;
13+
////<Component<{/*1*/}>/>;
14+
15+
verify.completions(
16+
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
17+
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: true },
18+
);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// one: string;
5+
//// two: number;
6+
////}
7+
////declare function f<T extends Foo>(x: TemplateStringsArray): void;
8+
////f<{/*0*/}>``;
9+
10+
verify.completions({ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true });
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// one: string;
5+
//// two: number;
6+
////}
7+
////
8+
////declare function decorator<T extends Foo>(originalMethod: unknown, _context: unknown): never
9+
////
10+
////class {
11+
//// @decorator<{/*0*/}>
12+
//// method() {}
13+
////}
14+
15+
verify.completions({ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true });
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// one: string;
5+
//// two: number;
6+
////}
7+
////interface Bar {
8+
//// three: boolean;
9+
//// four: {
10+
//// five: unknown;
11+
//// };
12+
////}
13+
////
14+
////(<T extends Foo>() => {})<{/*0*/}>;
15+
////
16+
////(class <T extends Foo>{})<{/*1*/}>;
17+
////
18+
////declare const a: {
19+
//// new <T extends Foo>(): {};
20+
//// <T extends Bar>(): {};
21+
////}
22+
////a<{/*2*/}>;
23+
////
24+
////declare const b: {
25+
//// new <T extends { one: true }>(): {};
26+
//// <T extends { one: false }>(): {};
27+
////}
28+
////b<{/*3*/}>;
29+
30+
verify.completions(
31+
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
32+
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: true },
33+
{ marker: "2", unsorted: ["one", "two", "three", "four"], isNewIdentifierLocation: true },
34+
{ marker: "3", unsorted: [], isNewIdentifierLocation: true },
35+
);

0 commit comments

Comments
 (0)