Skip to content

Commit e4bac38

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 53d3f29 commit e4bac38

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
@@ -42449,6 +42449,48 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4244942449
return undefined;
4245042450
}
4245142451

42452+
function getSignaturesFromCallLike(node: CallLikeExpression): readonly Signature[] {
42453+
switch (node.kind) {
42454+
case SyntaxKind.CallExpression:
42455+
case SyntaxKind.Decorator:
42456+
return getSignaturesOfType(
42457+
getTypeOfExpression(node.expression),
42458+
SignatureKind.Call,
42459+
);
42460+
case SyntaxKind.NewExpression:
42461+
return getSignaturesOfType(
42462+
getTypeOfExpression(node.expression),
42463+
SignatureKind.Construct,
42464+
);
42465+
case SyntaxKind.JsxSelfClosingElement:
42466+
case SyntaxKind.JsxOpeningElement:
42467+
if (isJsxIntrinsicTagName(node.tagName)) return [];
42468+
return getSignaturesOfType(
42469+
getTypeOfExpression(node.tagName),
42470+
SignatureKind.Call,
42471+
);
42472+
case SyntaxKind.TaggedTemplateExpression:
42473+
return getSignaturesOfType(
42474+
getTypeOfExpression(node.tag),
42475+
SignatureKind.Call,
42476+
);
42477+
case SyntaxKind.BinaryExpression:
42478+
case SyntaxKind.JsxOpeningFragment:
42479+
return [];
42480+
}
42481+
}
42482+
42483+
function getTypeParameterConstraintForPositionAcrossSignatures(signatures: readonly Signature[], position: number) {
42484+
const relevantTypeParameterConstraints = flatMap(signatures, signature => {
42485+
const relevantTypeParameter = signature.typeParameters?.[position];
42486+
if (relevantTypeParameter === undefined) return [];
42487+
const relevantConstraint = getConstraintOfTypeParameter(relevantTypeParameter);
42488+
if (relevantConstraint === undefined) return [];
42489+
return [relevantConstraint];
42490+
});
42491+
return getUnionType(relevantTypeParameterConstraints);
42492+
}
42493+
4245242494
function checkTypeReferenceNode(node: TypeReferenceNode | ExpressionWithTypeArguments) {
4245342495
checkGrammarTypeArguments(node, node.typeArguments);
4245442496
if (node.kind === SyntaxKind.TypeReference && !isInJSFile(node) && !isInJSDoc(node) && node.typeArguments && node.typeName.end !== node.typeArguments.pos) {
@@ -42487,12 +42529,62 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4248742529
}
4248842530

4248942531
function getTypeArgumentConstraint(node: TypeNode): Type | undefined {
42490-
const typeReferenceNode = tryCast(node.parent, isTypeReferenceType);
42491-
if (!typeReferenceNode) return undefined;
42492-
const typeParameters = getTypeParametersForTypeReferenceOrImport(typeReferenceNode);
42493-
if (!typeParameters) return undefined;
42494-
const constraint = getConstraintOfTypeParameter(typeParameters[typeReferenceNode.typeArguments!.indexOf(node)]);
42495-
return constraint && instantiateType(constraint, createTypeMapper(typeParameters, getEffectiveTypeArguments(typeReferenceNode, typeParameters)));
42532+
let typeArgumentPosition;
42533+
if (
42534+
"typeArguments" in node.parent && // eslint-disable-line local/no-in-operator
42535+
Array.isArray(node.parent.typeArguments)
42536+
) {
42537+
typeArgumentPosition = node.parent.typeArguments.indexOf(node);
42538+
}
42539+
42540+
if (typeArgumentPosition !== undefined) {
42541+
// The node could be a type argument of a call, a `new` expression, a decorator, an
42542+
// instantiation expression, or a generic type instantiation.
42543+
42544+
if (isCallLikeExpression(node.parent)) {
42545+
return getTypeParameterConstraintForPositionAcrossSignatures(
42546+
getSignaturesFromCallLike(node.parent),
42547+
typeArgumentPosition,
42548+
);
42549+
}
42550+
42551+
if (isDecorator(node.parent.parent)) {
42552+
return getTypeParameterConstraintForPositionAcrossSignatures(
42553+
getSignaturesFromCallLike(node.parent.parent),
42554+
typeArgumentPosition,
42555+
);
42556+
}
42557+
42558+
if (isExpressionWithTypeArguments(node.parent) && isExpressionStatement(node.parent.parent)) {
42559+
const uninstantiatedType = checkExpression(node.parent.expression);
42560+
42561+
const callConstraint = getTypeParameterConstraintForPositionAcrossSignatures(
42562+
getSignaturesOfType(uninstantiatedType, SignatureKind.Call),
42563+
typeArgumentPosition,
42564+
);
42565+
const constructConstraint = getTypeParameterConstraintForPositionAcrossSignatures(
42566+
getSignaturesOfType(uninstantiatedType, SignatureKind.Construct),
42567+
typeArgumentPosition,
42568+
);
42569+
42570+
// An instantiation expression instantiates both call and construct signatures, so
42571+
// if both exist type arguments must be assignable to both constraints.
42572+
if (constructConstraint.flags & TypeFlags.Never) return callConstraint;
42573+
if (callConstraint.flags & TypeFlags.Never) return constructConstraint;
42574+
return getIntersectionType([callConstraint, constructConstraint]);
42575+
}
42576+
42577+
if (isTypeReferenceType(node.parent)) {
42578+
const typeParameters = getTypeParametersForTypeReferenceOrImport(node.parent);
42579+
if (!typeParameters) return undefined;
42580+
const relevantTypeParameter = typeParameters[typeArgumentPosition];
42581+
const constraint = getConstraintOfTypeParameter(relevantTypeParameter);
42582+
return constraint && instantiateType(
42583+
constraint,
42584+
createTypeMapper(typeParameters, getEffectiveTypeArguments(node.parent, typeParameters)),
42585+
);
42586+
}
42587+
}
4249642588
}
4249742589

4249842590
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,
@@ -5767,8 +5766,9 @@ function tryGetTypeLiteralNode(node: Node): TypeLiteralNode | undefined {
57675766
function getConstraintOfTypeArgumentProperty(node: Node, checker: TypeChecker): Type | undefined {
57685767
if (!node) return undefined;
57695768

5770-
if (isTypeNode(node) && isTypeReferenceType(node.parent)) {
5771-
return checker.getTypeArgumentConstraint(node);
5769+
if (isTypeNode(node)) {
5770+
const constraint = checker.getTypeArgumentConstraint(node);
5771+
if (constraint) return constraint;
57725772
}
57735773

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