Skip to content

Commit d79c37c

Browse files
authored
Discriminate contextual types (microsoft#19733)
* Discriminate contextual types * Invert conditional * Update findMatchingDiscriminantType and baselines
1 parent d6436f1 commit d79c37c

10 files changed

+262
-26
lines changed

src/compiler/checker.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9269,20 +9269,24 @@ namespace ts {
92699269
return Ternary.False;
92709270
}
92719271

9272+
// Keep this up-to-date with the same logic within `getApparentTypeOfContextualType`, since they should behave similarly
92729273
function findMatchingDiscriminantType(source: Type, target: UnionOrIntersectionType) {
92739274
let match: Type;
92749275
const sourceProperties = getPropertiesOfObjectType(source);
92759276
if (sourceProperties) {
9276-
const sourceProperty = findSingleDiscriminantProperty(sourceProperties, target);
9277-
if (sourceProperty) {
9278-
const sourceType = getTypeOfSymbol(sourceProperty);
9279-
for (const type of target.types) {
9280-
const targetType = getTypeOfPropertyOfType(type, sourceProperty.escapedName);
9281-
if (targetType && isRelatedTo(sourceType, targetType)) {
9282-
if (match) {
9283-
return undefined;
9277+
const sourcePropertiesFiltered = findDiscriminantProperties(sourceProperties, target);
9278+
if (sourcePropertiesFiltered) {
9279+
for (const sourceProperty of sourcePropertiesFiltered) {
9280+
const sourceType = getTypeOfSymbol(sourceProperty);
9281+
for (const type of target.types) {
9282+
const targetType = getTypeOfPropertyOfType(type, sourceProperty.escapedName);
9283+
if (targetType && isRelatedTo(sourceType, targetType)) {
9284+
if (type === match) continue; // Finding multiple fields which discriminate to the same type is fine
9285+
if (match) {
9286+
return undefined;
9287+
}
9288+
match = type;
92849289
}
9285-
match = type;
92869290
}
92879291
}
92889292
}
@@ -11396,14 +11400,15 @@ namespace ts {
1139611400
return false;
1139711401
}
1139811402

11399-
function findSingleDiscriminantProperty(sourceProperties: Symbol[], target: Type): Symbol | undefined {
11400-
let result: Symbol;
11403+
function findDiscriminantProperties(sourceProperties: Symbol[], target: Type): Symbol[] | undefined {
11404+
let result: Symbol[];
1140111405
for (const sourceProperty of sourceProperties) {
1140211406
if (isDiscriminantProperty(target, sourceProperty.escapedName)) {
1140311407
if (result) {
11404-
return undefined;
11408+
result.push(sourceProperty);
11409+
continue;
1140511410
}
11406-
result = sourceProperty;
11411+
result = [sourceProperty];
1140711412
}
1140811413
}
1140911414
return result;
@@ -13691,8 +13696,32 @@ namespace ts {
1369113696
// Return the contextual type for a given expression node. During overload resolution, a contextual type may temporarily
1369213697
// be "pushed" onto a node using the contextualType property.
1369313698
function getApparentTypeOfContextualType(node: Expression): Type {
13694-
const type = getContextualType(node);
13695-
return type && mapType(type, getApparentType);
13699+
let contextualType = getContextualType(node);
13700+
contextualType = contextualType && mapType(contextualType, getApparentType);
13701+
if (!(contextualType && contextualType.flags & TypeFlags.Union && isObjectLiteralExpression(node))) {
13702+
return contextualType;
13703+
}
13704+
// Keep the below up-to-date with the work done within `isRelatedTo` by `findMatchingDiscriminantType`
13705+
let match: Type | undefined;
13706+
propLoop: for (const prop of node.properties) {
13707+
if (!prop.symbol) continue;
13708+
if (prop.kind !== SyntaxKind.PropertyAssignment) continue;
13709+
if (isDiscriminantProperty(contextualType, prop.symbol.escapedName)) {
13710+
const discriminatingType = getTypeOfNode(prop.initializer);
13711+
for (const type of (contextualType as UnionType).types) {
13712+
const targetType = getTypeOfPropertyOfType(type, prop.symbol.escapedName);
13713+
if (targetType && checkTypeAssignableTo(discriminatingType, targetType, /*errorNode*/ undefined)) {
13714+
if (match) {
13715+
if (type === match) continue; // Finding multiple fields which discriminate to the same type is fine
13716+
match = undefined;
13717+
break propLoop;
13718+
}
13719+
match = type;
13720+
}
13721+
}
13722+
}
13723+
}
13724+
return match || contextualType;
1369613725
}
1369713726

1369813727
/**
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//// [contextuallyTypedByDiscriminableUnion.ts]
2+
type ADT = {
3+
kind: "a",
4+
method(x: string): number;
5+
} | {
6+
kind: "b",
7+
method(x: number): string;
8+
};
9+
10+
11+
function invoke(item: ADT) {
12+
if (item.kind === "a") {
13+
item.method("");
14+
}
15+
else {
16+
item.method(42);
17+
}
18+
}
19+
20+
invoke({
21+
kind: "a",
22+
method(a) {
23+
return +a;
24+
}
25+
});
26+
27+
28+
//// [contextuallyTypedByDiscriminableUnion.js]
29+
function invoke(item) {
30+
if (item.kind === "a") {
31+
item.method("");
32+
}
33+
else {
34+
item.method(42);
35+
}
36+
}
37+
invoke({
38+
kind: "a",
39+
method: function (a) {
40+
return +a;
41+
}
42+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
=== tests/cases/compiler/contextuallyTypedByDiscriminableUnion.ts ===
2+
type ADT = {
3+
>ADT : Symbol(ADT, Decl(contextuallyTypedByDiscriminableUnion.ts, 0, 0))
4+
5+
kind: "a",
6+
>kind : Symbol(kind, Decl(contextuallyTypedByDiscriminableUnion.ts, 0, 12))
7+
8+
method(x: string): number;
9+
>method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 1, 14))
10+
>x : Symbol(x, Decl(contextuallyTypedByDiscriminableUnion.ts, 2, 11))
11+
12+
} | {
13+
kind: "b",
14+
>kind : Symbol(kind, Decl(contextuallyTypedByDiscriminableUnion.ts, 3, 5))
15+
16+
method(x: number): string;
17+
>method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 4, 14))
18+
>x : Symbol(x, Decl(contextuallyTypedByDiscriminableUnion.ts, 5, 11))
19+
20+
};
21+
22+
23+
function invoke(item: ADT) {
24+
>invoke : Symbol(invoke, Decl(contextuallyTypedByDiscriminableUnion.ts, 6, 2))
25+
>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion.ts, 9, 16))
26+
>ADT : Symbol(ADT, Decl(contextuallyTypedByDiscriminableUnion.ts, 0, 0))
27+
28+
if (item.kind === "a") {
29+
>item.kind : Symbol(kind, Decl(contextuallyTypedByDiscriminableUnion.ts, 0, 12), Decl(contextuallyTypedByDiscriminableUnion.ts, 3, 5))
30+
>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion.ts, 9, 16))
31+
>kind : Symbol(kind, Decl(contextuallyTypedByDiscriminableUnion.ts, 0, 12), Decl(contextuallyTypedByDiscriminableUnion.ts, 3, 5))
32+
33+
item.method("");
34+
>item.method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 1, 14))
35+
>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion.ts, 9, 16))
36+
>method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 1, 14))
37+
}
38+
else {
39+
item.method(42);
40+
>item.method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 4, 14))
41+
>item : Symbol(item, Decl(contextuallyTypedByDiscriminableUnion.ts, 9, 16))
42+
>method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 4, 14))
43+
}
44+
}
45+
46+
invoke({
47+
>invoke : Symbol(invoke, Decl(contextuallyTypedByDiscriminableUnion.ts, 6, 2))
48+
49+
kind: "a",
50+
>kind : Symbol(kind, Decl(contextuallyTypedByDiscriminableUnion.ts, 18, 8))
51+
52+
method(a) {
53+
>method : Symbol(method, Decl(contextuallyTypedByDiscriminableUnion.ts, 19, 14))
54+
>a : Symbol(a, Decl(contextuallyTypedByDiscriminableUnion.ts, 20, 11))
55+
56+
return +a;
57+
>a : Symbol(a, Decl(contextuallyTypedByDiscriminableUnion.ts, 20, 11))
58+
}
59+
});
60+
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
=== tests/cases/compiler/contextuallyTypedByDiscriminableUnion.ts ===
2+
type ADT = {
3+
>ADT : ADT
4+
5+
kind: "a",
6+
>kind : "a"
7+
8+
method(x: string): number;
9+
>method : (x: string) => number
10+
>x : string
11+
12+
} | {
13+
kind: "b",
14+
>kind : "b"
15+
16+
method(x: number): string;
17+
>method : (x: number) => string
18+
>x : number
19+
20+
};
21+
22+
23+
function invoke(item: ADT) {
24+
>invoke : (item: ADT) => void
25+
>item : ADT
26+
>ADT : ADT
27+
28+
if (item.kind === "a") {
29+
>item.kind === "a" : boolean
30+
>item.kind : "a" | "b"
31+
>item : ADT
32+
>kind : "a" | "b"
33+
>"a" : "a"
34+
35+
item.method("");
36+
>item.method("") : number
37+
>item.method : (x: string) => number
38+
>item : { kind: "a"; method(x: string): number; }
39+
>method : (x: string) => number
40+
>"" : ""
41+
}
42+
else {
43+
item.method(42);
44+
>item.method(42) : string
45+
>item.method : (x: number) => string
46+
>item : { kind: "b"; method(x: number): string; }
47+
>method : (x: number) => string
48+
>42 : 42
49+
}
50+
}
51+
52+
invoke({
53+
>invoke({ kind: "a", method(a) { return +a; }}) : void
54+
>invoke : (item: ADT) => void
55+
>{ kind: "a", method(a) { return +a; }} : { kind: "a"; method(a: string): number; }
56+
57+
kind: "a",
58+
>kind : string
59+
>"a" : "a"
60+
61+
method(a) {
62+
>method : (a: string) => number
63+
>a : string
64+
65+
return +a;
66+
>+a : number
67+
>a : string
68+
}
69+
});
70+

tests/baselines/reference/excessPropertyCheckWithUnions.errors.txt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
tests/cases/compiler/excessPropertyCheckWithUnions.ts(10,30): error TS2322: Type '{ tag: "T"; a1: string; }' is not assignable to type 'ADT'.
22
Object literal may only specify known properties, and 'a1' does not exist in type '{ tag: "T"; }'.
3-
tests/cases/compiler/excessPropertyCheckWithUnions.ts(11,21): error TS2322: Type '{ tag: "A"; d20: 12; }' is not assignable to type 'ADT'.
3+
tests/cases/compiler/excessPropertyCheckWithUnions.ts(11,21): error TS2322: Type '{ tag: "A"; d20: number; }' is not assignable to type 'ADT'.
44
Object literal may only specify known properties, and 'd20' does not exist in type '{ tag: "A"; a1: string; }'.
55
tests/cases/compiler/excessPropertyCheckWithUnions.ts(12,1): error TS2322: Type '{ tag: "D"; }' is not assignable to type 'ADT'.
66
Type '{ tag: "D"; }' is not assignable to type '{ tag: "D"; d20: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20; }'.
@@ -17,9 +17,13 @@ tests/cases/compiler/excessPropertyCheckWithUnions.ts(40,1): error TS2322: Type
1717
Type '{ tag: "A"; z: true; }' is not assignable to type '{ tag: "C"; }'.
1818
Types of property 'tag' are incompatible.
1919
Type '"A"' is not assignable to type '"C"'.
20+
tests/cases/compiler/excessPropertyCheckWithUnions.ts(49,35): error TS2322: Type '{ a: 1; b: 1; first: string; second: string; }' is not assignable to type 'Overlapping'.
21+
Object literal may only specify known properties, and 'second' does not exist in type '{ a: 1; b: 1; first: string; }'.
22+
tests/cases/compiler/excessPropertyCheckWithUnions.ts(50,35): error TS2322: Type '{ a: 1; b: 1; first: string; third: string; }' is not assignable to type 'Overlapping'.
23+
Object literal may only specify known properties, and 'third' does not exist in type '{ a: 1; b: 1; first: string; }'.
2024

2125

22-
==== tests/cases/compiler/excessPropertyCheckWithUnions.ts (7 errors) ====
26+
==== tests/cases/compiler/excessPropertyCheckWithUnions.ts (9 errors) ====
2327
type ADT = {
2428
tag: "A",
2529
a1: string
@@ -35,7 +39,7 @@ tests/cases/compiler/excessPropertyCheckWithUnions.ts(40,1): error TS2322: Type
3539
!!! error TS2322: Object literal may only specify known properties, and 'a1' does not exist in type '{ tag: "T"; }'.
3640
wrong = { tag: "A", d20: 12 }
3741
~~~~~~~
38-
!!! error TS2322: Type '{ tag: "A"; d20: 12; }' is not assignable to type 'ADT'.
42+
!!! error TS2322: Type '{ tag: "A"; d20: number; }' is not assignable to type 'ADT'.
3943
!!! error TS2322: Object literal may only specify known properties, and 'd20' does not exist in type '{ tag: "A"; a1: string; }'.
4044
wrong = { tag: "D" }
4145
~~~~~
@@ -93,9 +97,15 @@ tests/cases/compiler/excessPropertyCheckWithUnions.ts(40,1): error TS2322: Type
9397
| { b: 3, third: string }
9498
let over: Overlapping
9599

96-
// these two are not reported because there are two discriminant properties
100+
// these two are still errors despite their doubled up discriminants
97101
over = { a: 1, b: 1, first: "ok", second: "error" }
102+
~~~~~~~~~~~~~~~
103+
!!! error TS2322: Type '{ a: 1; b: 1; first: string; second: string; }' is not assignable to type 'Overlapping'.
104+
!!! error TS2322: Object literal may only specify known properties, and 'second' does not exist in type '{ a: 1; b: 1; first: string; }'.
98105
over = { a: 1, b: 1, first: "ok", third: "error" }
106+
~~~~~~~~~~~~~~
107+
!!! error TS2322: Type '{ a: 1; b: 1; first: string; third: string; }' is not assignable to type 'Overlapping'.
108+
!!! error TS2322: Object literal may only specify known properties, and 'third' does not exist in type '{ a: 1; b: 1; first: string; }'.
99109

100110
// Freshness disappears after spreading a union
101111
declare let t0: { a: any, b: any } | { d: any, e: any }

tests/baselines/reference/excessPropertyCheckWithUnions.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type Overlapping =
4646
| { b: 3, third: string }
4747
let over: Overlapping
4848

49-
// these two are not reported because there are two discriminant properties
49+
// these two are still errors despite their doubled up discriminants
5050
over = { a: 1, b: 1, first: "ok", second: "error" }
5151
over = { a: 1, b: 1, first: "ok", third: "error" }
5252

@@ -84,7 +84,7 @@ amb = { tag: "A", y: 12, extra: 12 };
8484
amb = { tag: "A" };
8585
amb = { tag: "A", z: true };
8686
var over;
87-
// these two are not reported because there are two discriminant properties
87+
// these two are still errors despite their doubled up discriminants
8888
over = { a: 1, b: 1, first: "ok", second: "error" };
8989
over = { a: 1, b: 1, first: "ok", third: "error" };
9090
var t2 = __assign({}, t1);

tests/baselines/reference/excessPropertyCheckWithUnions.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ let over: Overlapping
127127
>over : Symbol(over, Decl(excessPropertyCheckWithUnions.ts, 45, 3))
128128
>Overlapping : Symbol(Overlapping, Decl(excessPropertyCheckWithUnions.ts, 39, 27))
129129

130-
// these two are not reported because there are two discriminant properties
130+
// these two are still errors despite their doubled up discriminants
131131
over = { a: 1, b: 1, first: "ok", second: "error" }
132132
>over : Symbol(over, Decl(excessPropertyCheckWithUnions.ts, 45, 3))
133133
>a : Symbol(a, Decl(excessPropertyCheckWithUnions.ts, 48, 8))

tests/baselines/reference/excessPropertyCheckWithUnions.types

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ let wrong: ADT = { tag: "T", a1: "extra" }
2929
>"extra" : "extra"
3030

3131
wrong = { tag: "A", d20: 12 }
32-
>wrong = { tag: "A", d20: 12 } : { tag: "A"; d20: 12; }
32+
>wrong = { tag: "A", d20: 12 } : { tag: "A"; d20: number; }
3333
>wrong : ADT
34-
>{ tag: "A", d20: 12 } : { tag: "A"; d20: 12; }
34+
>{ tag: "A", d20: 12 } : { tag: "A"; d20: number; }
3535
>tag : string
3636
>"A" : "A"
3737
>d20 : number
@@ -167,7 +167,7 @@ let over: Overlapping
167167
>over : Overlapping
168168
>Overlapping : Overlapping
169169

170-
// these two are not reported because there are two discriminant properties
170+
// these two are still errors despite their doubled up discriminants
171171
over = { a: 1, b: 1, first: "ok", second: "error" }
172172
>over = { a: 1, b: 1, first: "ok", second: "error" } : { a: 1; b: 1; first: string; second: string; }
173173
>over : Overlapping
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// @noImplicitAny: true
2+
type ADT = {
3+
kind: "a",
4+
method(x: string): number;
5+
} | {
6+
kind: "b",
7+
method(x: number): string;
8+
};
9+
10+
11+
function invoke(item: ADT) {
12+
if (item.kind === "a") {
13+
item.method("");
14+
}
15+
else {
16+
item.method(42);
17+
}
18+
}
19+
20+
invoke({
21+
kind: "a",
22+
method(a) {
23+
return +a;
24+
}
25+
});

tests/cases/compiler/excessPropertyCheckWithUnions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type Overlapping =
4646
| { b: 3, third: string }
4747
let over: Overlapping
4848

49-
// these two are not reported because there are two discriminant properties
49+
// these two are still errors despite their doubled up discriminants
5050
over = { a: 1, b: 1, first: "ok", second: "error" }
5151
over = { a: 1, b: 1, first: "ok", third: "error" }
5252

0 commit comments

Comments
 (0)