Skip to content

Commit 4bf65ff

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 65cb4bd commit 4bf65ff

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
@@ -42606,6 +42606,48 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4260642606
return undefined;
4260742607
}
4260842608

42609+
function getSignaturesFromCallLike(node: CallLikeExpression): readonly Signature[] {
42610+
switch (node.kind) {
42611+
case SyntaxKind.CallExpression:
42612+
case SyntaxKind.Decorator:
42613+
return getSignaturesOfType(
42614+
getTypeOfExpression(node.expression),
42615+
SignatureKind.Call,
42616+
);
42617+
case SyntaxKind.NewExpression:
42618+
return getSignaturesOfType(
42619+
getTypeOfExpression(node.expression),
42620+
SignatureKind.Construct,
42621+
);
42622+
case SyntaxKind.JsxSelfClosingElement:
42623+
case SyntaxKind.JsxOpeningElement:
42624+
if (isJsxIntrinsicTagName(node.tagName)) return [];
42625+
return getSignaturesOfType(
42626+
getTypeOfExpression(node.tagName),
42627+
SignatureKind.Call,
42628+
);
42629+
case SyntaxKind.TaggedTemplateExpression:
42630+
return getSignaturesOfType(
42631+
getTypeOfExpression(node.tag),
42632+
SignatureKind.Call,
42633+
);
42634+
case SyntaxKind.BinaryExpression:
42635+
case SyntaxKind.JsxOpeningFragment:
42636+
return [];
42637+
}
42638+
}
42639+
42640+
function getTypeParameterConstraintForPositionAcrossSignatures(signatures: readonly Signature[], position: number) {
42641+
const relevantTypeParameterConstraints = flatMap(signatures, signature => {
42642+
const relevantTypeParameter = signature.typeParameters?.[position];
42643+
if (relevantTypeParameter === undefined) return [];
42644+
const relevantConstraint = getConstraintOfTypeParameter(relevantTypeParameter);
42645+
if (relevantConstraint === undefined) return [];
42646+
return [relevantConstraint];
42647+
});
42648+
return getUnionType(relevantTypeParameterConstraints);
42649+
}
42650+
4260942651
function checkTypeReferenceNode(node: TypeReferenceNode | ExpressionWithTypeArguments) {
4261042652
checkGrammarTypeArguments(node, node.typeArguments);
4261142653
if (node.kind === SyntaxKind.TypeReference && !isInJSFile(node) && !isInJSDoc(node) && node.typeArguments && node.typeName.end !== node.typeArguments.pos) {
@@ -42644,12 +42686,62 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4264442686
}
4264542687

4264642688
function getTypeArgumentConstraint(node: TypeNode): Type | undefined {
42647-
const typeReferenceNode = tryCast(node.parent, isTypeReferenceType);
42648-
if (!typeReferenceNode) return undefined;
42649-
const typeParameters = getTypeParametersForTypeReferenceOrImport(typeReferenceNode);
42650-
if (!typeParameters) return undefined;
42651-
const constraint = getConstraintOfTypeParameter(typeParameters[typeReferenceNode.typeArguments!.indexOf(node)]);
42652-
return constraint && instantiateType(constraint, createTypeMapper(typeParameters, getEffectiveTypeArguments(typeReferenceNode, typeParameters)));
42689+
let typeArgumentPosition;
42690+
if (
42691+
"typeArguments" in node.parent && // eslint-disable-line local/no-in-operator
42692+
Array.isArray(node.parent.typeArguments)
42693+
) {
42694+
typeArgumentPosition = node.parent.typeArguments.indexOf(node);
42695+
}
42696+
42697+
if (typeArgumentPosition !== undefined) {
42698+
// The node could be a type argument of a call, a `new` expression, a decorator, an
42699+
// instantiation expression, or a generic type instantiation.
42700+
42701+
if (isCallLikeExpression(node.parent)) {
42702+
return getTypeParameterConstraintForPositionAcrossSignatures(
42703+
getSignaturesFromCallLike(node.parent),
42704+
typeArgumentPosition,
42705+
);
42706+
}
42707+
42708+
if (isDecorator(node.parent.parent)) {
42709+
return getTypeParameterConstraintForPositionAcrossSignatures(
42710+
getSignaturesFromCallLike(node.parent.parent),
42711+
typeArgumentPosition,
42712+
);
42713+
}
42714+
42715+
if (isExpressionWithTypeArguments(node.parent) && isExpressionStatement(node.parent.parent)) {
42716+
const uninstantiatedType = checkExpression(node.parent.expression);
42717+
42718+
const callConstraint = getTypeParameterConstraintForPositionAcrossSignatures(
42719+
getSignaturesOfType(uninstantiatedType, SignatureKind.Call),
42720+
typeArgumentPosition,
42721+
);
42722+
const constructConstraint = getTypeParameterConstraintForPositionAcrossSignatures(
42723+
getSignaturesOfType(uninstantiatedType, SignatureKind.Construct),
42724+
typeArgumentPosition,
42725+
);
42726+
42727+
// An instantiation expression instantiates both call and construct signatures, so
42728+
// if both exist type arguments must be assignable to both constraints.
42729+
if (constructConstraint.flags & TypeFlags.Never) return callConstraint;
42730+
if (callConstraint.flags & TypeFlags.Never) return constructConstraint;
42731+
return getIntersectionType([callConstraint, constructConstraint]);
42732+
}
42733+
42734+
if (isTypeReferenceType(node.parent)) {
42735+
const typeParameters = getTypeParametersForTypeReferenceOrImport(node.parent);
42736+
if (!typeParameters) return undefined;
42737+
const relevantTypeParameter = typeParameters[typeArgumentPosition];
42738+
const constraint = getConstraintOfTypeParameter(relevantTypeParameter);
42739+
return constraint && instantiateType(
42740+
constraint,
42741+
createTypeMapper(typeParameters, getEffectiveTypeArguments(node.parent, typeParameters)),
42742+
);
42743+
}
42744+
}
4265342745
}
4265442746

4265542747
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)