Skip to content

Commit e6d472c

Browse files
author
Andy
authored
Merge pull request #15541 from Microsoft/union-completion
For completions of union type, get all possible properties
2 parents 1328cb8 + 5a4be34 commit e6d472c

File tree

4 files changed

+49
-13
lines changed

4 files changed

+49
-13
lines changed

src/compiler/checker.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ namespace ts {
205205
return tryFindAmbientModule(moduleName, /*withAugmentations*/ false);
206206
},
207207
getApparentType,
208+
getAllPossiblePropertiesOfType,
208209
getSuggestionForNonexistentProperty,
209210
getSuggestionForNonexistentSymbol,
210211
getBaseConstraintOfType,
@@ -5695,6 +5696,22 @@ namespace ts {
56955696
getPropertiesOfObjectType(type);
56965697
}
56975698

5699+
function getAllPossiblePropertiesOfType(type: Type): Symbol[] {
5700+
if (type.flags & TypeFlags.Union) {
5701+
const props = createMap<Symbol>();
5702+
for (const memberType of (type as UnionType).types) {
5703+
for (const { name } of getPropertiesOfType(memberType)) {
5704+
if (!props.has(name)) {
5705+
props.set(name, createUnionOrIntersectionProperty(type as UnionType, name));
5706+
}
5707+
}
5708+
}
5709+
return arrayFrom(props.values());
5710+
} else {
5711+
return getPropertiesOfType(type);
5712+
}
5713+
}
5714+
56985715
function getConstraintOfType(type: TypeVariable | UnionOrIntersectionType): Type {
56995716
return type.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter(<TypeParameter>type) :
57005717
type.flags & TypeFlags.IndexedAccess ? getConstraintOfIndexedAccess(<IndexedAccessType>type) :

src/compiler/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2573,6 +2573,12 @@ namespace ts {
25732573
/* @internal */ getIdentifierCount(): number;
25742574
/* @internal */ getSymbolCount(): number;
25752575
/* @internal */ getTypeCount(): number;
2576+
2577+
/**
2578+
* For a union, will include a property if it's defined in *any* of the member types.
2579+
* So for `{ a } | { b }`, this will include both `a` and `b`.
2580+
*/
2581+
/* @internal */ getAllPossiblePropertiesOfType(type: Type): Symbol[];
25762582
}
25772583

25782584
export enum NodeBuilderFlags {

src/services/completions.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -813,19 +813,16 @@ namespace ts.Completions {
813813
// We're looking up possible property names from contextual/inferred/declared type.
814814
isMemberCompletion = true;
815815

816-
let typeForObject: Type;
816+
let typeMembers: Symbol[];
817817
let existingMembers: Declaration[];
818818

819819
if (objectLikeContainer.kind === SyntaxKind.ObjectLiteralExpression) {
820820
// We are completing on contextual types, but may also include properties
821821
// other than those within the declared type.
822822
isNewIdentifierLocation = true;
823-
824-
// If the object literal is being assigned to something of type 'null | { hello: string }',
825-
// it clearly isn't trying to satisfy the 'null' type. So we grab the non-nullable type if possible.
826-
typeForObject = typeChecker.getContextualType(<ObjectLiteralExpression>objectLikeContainer);
827-
typeForObject = typeForObject && typeForObject.getNonNullableType();
828-
823+
const typeForObject = typeChecker.getContextualType(<ObjectLiteralExpression>objectLikeContainer);
824+
if (!typeForObject) return false;
825+
typeMembers = typeChecker.getAllPossiblePropertiesOfType(typeForObject);
829826
existingMembers = (<ObjectLiteralExpression>objectLikeContainer).properties;
830827
}
831828
else if (objectLikeContainer.kind === SyntaxKind.ObjectBindingPattern) {
@@ -849,7 +846,10 @@ namespace ts.Completions {
849846
}
850847
}
851848
if (canGetType) {
852-
typeForObject = typeChecker.getTypeAtLocation(objectLikeContainer);
849+
const typeForObject = typeChecker.getTypeAtLocation(objectLikeContainer);
850+
if (!typeForObject) return false;
851+
// In a binding pattern, get only known properties. Everywhere else we will get all possible properties.
852+
typeMembers = typeChecker.getPropertiesOfType(typeForObject);
853853
existingMembers = (<ObjectBindingPattern>objectLikeContainer).elements;
854854
}
855855
}
@@ -861,11 +861,6 @@ namespace ts.Completions {
861861
Debug.fail("Expected object literal or binding pattern, got " + objectLikeContainer.kind);
862862
}
863863

864-
if (!typeForObject) {
865-
return false;
866-
}
867-
868-
const typeMembers = typeChecker.getPropertiesOfType(typeForObject);
869864
if (typeMembers && typeMembers.length > 0) {
870865
// Add filtered items to the completion list
871866
symbols = filterObjectMembersList(typeMembers, existingMembers);
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+
// @strictNullChecks: true
4+
5+
// Non-objects should be skipped, so `| number | null` should have no effect on completions.
6+
////const x: { a: number, b: number } | { a: string, c: string } | { b: boolean } | number | null = { /*x*/ };
7+
8+
////interface I { a: number; }
9+
////function f(...args: Array<I | I[]>) {}
10+
////f({ /*f*/ });
11+
12+
goTo.marker("x");
13+
verify.completionListContains("a", "(property) a: string | number");
14+
verify.completionListContains("b", "(property) b: number | boolean");
15+
verify.completionListContains("c", "(property) c: string");
16+
17+
goTo.marker("f");
18+
verify.completionListContains("a", "(property) a: number");

0 commit comments

Comments
 (0)