Skip to content

Commit 98f3f68

Browse files
authored
Merge pull request #11198 from Microsoft/partiallyDiscriminatedUnions
Properly handle partially discriminated unions
2 parents f7c7c00 + 8b26ced commit 98f3f68

File tree

6 files changed

+407
-30
lines changed

6 files changed

+407
-30
lines changed

src/compiler/checker.ts

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4366,15 +4366,27 @@ namespace ts {
43664366
function getPropertiesOfUnionOrIntersectionType(type: UnionOrIntersectionType): Symbol[] {
43674367
for (const current of type.types) {
43684368
for (const prop of getPropertiesOfType(current)) {
4369-
getPropertyOfUnionOrIntersectionType(type, prop.name);
4369+
getUnionOrIntersectionProperty(type, prop.name);
43704370
}
43714371
// The properties of a union type are those that are present in all constituent types, so
43724372
// we only need to check the properties of the first type
43734373
if (type.flags & TypeFlags.Union) {
43744374
break;
43754375
}
43764376
}
4377-
return type.resolvedProperties ? symbolsToArray(type.resolvedProperties) : emptyArray;
4377+
const props = type.resolvedProperties;
4378+
if (props) {
4379+
const result: Symbol[] = [];
4380+
for (const key in props) {
4381+
const prop = props[key];
4382+
// We need to filter out partial properties in union types
4383+
if (!(prop.flags & SymbolFlags.SyntheticProperty && (<TransientSymbol>prop).isPartial)) {
4384+
result.push(prop);
4385+
}
4386+
}
4387+
return result;
4388+
}
4389+
return emptyArray;
43784390
}
43794391

43804392
function getPropertiesOfType(type: Type): Symbol[] {
@@ -4427,6 +4439,7 @@ namespace ts {
44274439
// Flags we want to propagate to the result if they exist in all source symbols
44284440
let commonFlags = (containingType.flags & TypeFlags.Intersection) ? SymbolFlags.Optional : SymbolFlags.None;
44294441
let isReadonly = false;
4442+
let isPartial = false;
44304443
for (const current of types) {
44314444
const type = getApparentType(current);
44324445
if (type !== unknownType) {
@@ -4444,21 +4457,20 @@ namespace ts {
44444457
}
44454458
}
44464459
else if (containingType.flags & TypeFlags.Union) {
4447-
// A union type requires the property to be present in all constituent types
4448-
return undefined;
4460+
isPartial = true;
44494461
}
44504462
}
44514463
}
44524464
if (!props) {
44534465
return undefined;
44544466
}
4455-
if (props.length === 1) {
4467+
if (props.length === 1 && !isPartial) {
44564468
return props[0];
44574469
}
44584470
const propTypes: Type[] = [];
44594471
const declarations: Declaration[] = [];
44604472
let commonType: Type = undefined;
4461-
let hasCommonType = true;
4473+
let hasNonUniformType = false;
44624474
for (const prop of props) {
44634475
if (prop.declarations) {
44644476
addRange(declarations, prop.declarations);
@@ -4468,25 +4480,26 @@ namespace ts {
44684480
commonType = type;
44694481
}
44704482
else if (type !== commonType) {
4471-
hasCommonType = false;
4483+
hasNonUniformType = true;
44724484
}
4473-
propTypes.push(getTypeOfSymbol(prop));
4485+
propTypes.push(type);
44744486
}
4475-
const result = <TransientSymbol>createSymbol(
4476-
SymbolFlags.Property |
4477-
SymbolFlags.Transient |
4478-
SymbolFlags.SyntheticProperty |
4479-
commonFlags,
4480-
name);
4487+
const result = <TransientSymbol>createSymbol(SymbolFlags.Property | SymbolFlags.Transient | SymbolFlags.SyntheticProperty | commonFlags, name);
44814488
result.containingType = containingType;
4482-
result.hasCommonType = hasCommonType;
4489+
result.hasNonUniformType = hasNonUniformType;
4490+
result.isPartial = isPartial;
44834491
result.declarations = declarations;
44844492
result.isReadonly = isReadonly;
44854493
result.type = containingType.flags & TypeFlags.Union ? getUnionType(propTypes) : getIntersectionType(propTypes);
44864494
return result;
44874495
}
44884496

4489-
function getPropertyOfUnionOrIntersectionType(type: UnionOrIntersectionType, name: string): Symbol {
4497+
// Return the symbol for a given property in a union or intersection type, or undefined if the property
4498+
// does not exist in any constituent type. Note that the returned property may only be present in some
4499+
// constituents, in which case the isPartial flag is set when the containing type is union type. We need
4500+
// these partial properties when identifying discriminant properties, but otherwise they are filtered out
4501+
// and do not appear to be present in the union type.
4502+
function getUnionOrIntersectionProperty(type: UnionOrIntersectionType, name: string): Symbol {
44904503
const properties = type.resolvedProperties || (type.resolvedProperties = createMap<Symbol>());
44914504
let property = properties[name];
44924505
if (!property) {
@@ -4498,6 +4511,12 @@ namespace ts {
44984511
return property;
44994512
}
45004513

4514+
function getPropertyOfUnionOrIntersectionType(type: UnionOrIntersectionType, name: string): Symbol {
4515+
const property = getUnionOrIntersectionProperty(type, name);
4516+
// We need to filter out partial properties in union types
4517+
return property && !(property.flags & SymbolFlags.SyntheticProperty && (<TransientSymbol>property).isPartial) ? property : undefined;
4518+
}
4519+
45014520
/**
45024521
* Return the symbol for the property with the given name in the given type. Creates synthetic union properties when
45034522
* necessary, maps primitive types and type parameters are to their apparent types, and augments with properties from
@@ -8078,21 +8097,10 @@ namespace ts {
80788097

80798098
function isDiscriminantProperty(type: Type, name: string) {
80808099
if (type && type.flags & TypeFlags.Union) {
8081-
let prop = getPropertyOfType(type, name);
8082-
if (!prop) {
8083-
// The type may be a union that includes nullable or primitive types. If filtering
8084-
// those out produces a different type, get the property from that type instead.
8085-
// Effectively, we're checking if this *could* be a discriminant property once nullable
8086-
// and primitive types are removed by other type guards.
8087-
const filteredType = getTypeWithFacts(type, TypeFacts.Discriminatable);
8088-
if (filteredType !== type && filteredType.flags & TypeFlags.Union) {
8089-
prop = getPropertyOfType(filteredType, name);
8090-
}
8091-
}
8100+
const prop = getUnionOrIntersectionProperty(<UnionType>type, name);
80928101
if (prop && prop.flags & SymbolFlags.SyntheticProperty) {
80938102
if ((<TransientSymbol>prop).isDiscriminantProperty === undefined) {
8094-
(<TransientSymbol>prop).isDiscriminantProperty = !(<TransientSymbol>prop).hasCommonType &&
8095-
isLiteralType(getTypeOfSymbol(prop));
8103+
(<TransientSymbol>prop).isDiscriminantProperty = (<TransientSymbol>prop).hasNonUniformType && isLiteralType(getTypeOfSymbol(prop));
80968104
}
80978105
return (<TransientSymbol>prop).isDiscriminantProperty;
80988106
}

src/compiler/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2289,7 +2289,8 @@ namespace ts {
22892289
mapper?: TypeMapper; // Type mapper for instantiation alias
22902290
referenced?: boolean; // True if alias symbol has been referenced as a value
22912291
containingType?: UnionOrIntersectionType; // Containing union or intersection type for synthetic property
2292-
hasCommonType?: boolean; // True if constituents of synthetic property all have same type
2292+
hasNonUniformType?: boolean; // True if constituents have non-uniform types
2293+
isPartial?: boolean; // True if syntheric property of union type occurs in some but not all constituents
22932294
isDiscriminantProperty?: boolean; // True if discriminant synthetic property
22942295
resolvedExports?: SymbolTable; // Resolved exports of module
22952296
exportsChecked?: boolean; // True if exports of external module have been checked
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//// [partiallyDiscriminantedUnions.ts]
2+
// Repro from #10586
3+
4+
interface A1 {
5+
type: 'a';
6+
subtype: 1;
7+
}
8+
9+
interface A2 {
10+
type: 'a';
11+
subtype: 2;
12+
foo: number;
13+
}
14+
15+
interface B {
16+
type: 'b';
17+
}
18+
19+
type AB = A1 | A2 | B;
20+
21+
const ab: AB = <AB>{};
22+
23+
if (ab.type === 'a') {
24+
if (ab.subtype === 2) {
25+
ab.foo;
26+
}
27+
}
28+
29+
// Repro from #11185
30+
31+
class Square { kind: "square"; }
32+
class Circle { kind: "circle"; }
33+
34+
type Shape = Circle | Square;
35+
type Shapes = Shape | Array<Shape>;
36+
37+
function isShape(s : Shapes): s is Shape {
38+
return !Array.isArray(s);
39+
}
40+
41+
function fail(s: Shapes) {
42+
if (isShape(s)) {
43+
if (s.kind === "circle") {
44+
let c: Circle = s;
45+
}
46+
}
47+
}
48+
49+
//// [partiallyDiscriminantedUnions.js]
50+
// Repro from #10586
51+
var ab = {};
52+
if (ab.type === 'a') {
53+
if (ab.subtype === 2) {
54+
ab.foo;
55+
}
56+
}
57+
// Repro from #11185
58+
var Square = (function () {
59+
function Square() {
60+
}
61+
return Square;
62+
}());
63+
var Circle = (function () {
64+
function Circle() {
65+
}
66+
return Circle;
67+
}());
68+
function isShape(s) {
69+
return !Array.isArray(s);
70+
}
71+
function fail(s) {
72+
if (isShape(s)) {
73+
if (s.kind === "circle") {
74+
var c = s;
75+
}
76+
}
77+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
=== tests/cases/compiler/partiallyDiscriminantedUnions.ts ===
2+
// Repro from #10586
3+
4+
interface A1 {
5+
>A1 : Symbol(A1, Decl(partiallyDiscriminantedUnions.ts, 0, 0))
6+
7+
type: 'a';
8+
>type : Symbol(A1.type, Decl(partiallyDiscriminantedUnions.ts, 2, 14))
9+
10+
subtype: 1;
11+
>subtype : Symbol(A1.subtype, Decl(partiallyDiscriminantedUnions.ts, 3, 14))
12+
}
13+
14+
interface A2 {
15+
>A2 : Symbol(A2, Decl(partiallyDiscriminantedUnions.ts, 5, 1))
16+
17+
type: 'a';
18+
>type : Symbol(A2.type, Decl(partiallyDiscriminantedUnions.ts, 7, 14))
19+
20+
subtype: 2;
21+
>subtype : Symbol(A2.subtype, Decl(partiallyDiscriminantedUnions.ts, 8, 14))
22+
23+
foo: number;
24+
>foo : Symbol(A2.foo, Decl(partiallyDiscriminantedUnions.ts, 9, 15))
25+
}
26+
27+
interface B {
28+
>B : Symbol(B, Decl(partiallyDiscriminantedUnions.ts, 11, 1))
29+
30+
type: 'b';
31+
>type : Symbol(B.type, Decl(partiallyDiscriminantedUnions.ts, 13, 13))
32+
}
33+
34+
type AB = A1 | A2 | B;
35+
>AB : Symbol(AB, Decl(partiallyDiscriminantedUnions.ts, 15, 1))
36+
>A1 : Symbol(A1, Decl(partiallyDiscriminantedUnions.ts, 0, 0))
37+
>A2 : Symbol(A2, Decl(partiallyDiscriminantedUnions.ts, 5, 1))
38+
>B : Symbol(B, Decl(partiallyDiscriminantedUnions.ts, 11, 1))
39+
40+
const ab: AB = <AB>{};
41+
>ab : Symbol(ab, Decl(partiallyDiscriminantedUnions.ts, 19, 5))
42+
>AB : Symbol(AB, Decl(partiallyDiscriminantedUnions.ts, 15, 1))
43+
>AB : Symbol(AB, Decl(partiallyDiscriminantedUnions.ts, 15, 1))
44+
45+
if (ab.type === 'a') {
46+
>ab.type : Symbol(type, Decl(partiallyDiscriminantedUnions.ts, 2, 14), Decl(partiallyDiscriminantedUnions.ts, 7, 14), Decl(partiallyDiscriminantedUnions.ts, 13, 13))
47+
>ab : Symbol(ab, Decl(partiallyDiscriminantedUnions.ts, 19, 5))
48+
>type : Symbol(type, Decl(partiallyDiscriminantedUnions.ts, 2, 14), Decl(partiallyDiscriminantedUnions.ts, 7, 14), Decl(partiallyDiscriminantedUnions.ts, 13, 13))
49+
50+
if (ab.subtype === 2) {
51+
>ab.subtype : Symbol(subtype, Decl(partiallyDiscriminantedUnions.ts, 3, 14), Decl(partiallyDiscriminantedUnions.ts, 8, 14))
52+
>ab : Symbol(ab, Decl(partiallyDiscriminantedUnions.ts, 19, 5))
53+
>subtype : Symbol(subtype, Decl(partiallyDiscriminantedUnions.ts, 3, 14), Decl(partiallyDiscriminantedUnions.ts, 8, 14))
54+
55+
ab.foo;
56+
>ab.foo : Symbol(A2.foo, Decl(partiallyDiscriminantedUnions.ts, 9, 15))
57+
>ab : Symbol(ab, Decl(partiallyDiscriminantedUnions.ts, 19, 5))
58+
>foo : Symbol(A2.foo, Decl(partiallyDiscriminantedUnions.ts, 9, 15))
59+
}
60+
}
61+
62+
// Repro from #11185
63+
64+
class Square { kind: "square"; }
65+
>Square : Symbol(Square, Decl(partiallyDiscriminantedUnions.ts, 25, 1))
66+
>kind : Symbol(Square.kind, Decl(partiallyDiscriminantedUnions.ts, 29, 14))
67+
68+
class Circle { kind: "circle"; }
69+
>Circle : Symbol(Circle, Decl(partiallyDiscriminantedUnions.ts, 29, 32))
70+
>kind : Symbol(Circle.kind, Decl(partiallyDiscriminantedUnions.ts, 30, 14))
71+
72+
type Shape = Circle | Square;
73+
>Shape : Symbol(Shape, Decl(partiallyDiscriminantedUnions.ts, 30, 32))
74+
>Circle : Symbol(Circle, Decl(partiallyDiscriminantedUnions.ts, 29, 32))
75+
>Square : Symbol(Square, Decl(partiallyDiscriminantedUnions.ts, 25, 1))
76+
77+
type Shapes = Shape | Array<Shape>;
78+
>Shapes : Symbol(Shapes, Decl(partiallyDiscriminantedUnions.ts, 32, 29))
79+
>Shape : Symbol(Shape, Decl(partiallyDiscriminantedUnions.ts, 30, 32))
80+
>Array : Symbol(Array, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
81+
>Shape : Symbol(Shape, Decl(partiallyDiscriminantedUnions.ts, 30, 32))
82+
83+
function isShape(s : Shapes): s is Shape {
84+
>isShape : Symbol(isShape, Decl(partiallyDiscriminantedUnions.ts, 33, 35))
85+
>s : Symbol(s, Decl(partiallyDiscriminantedUnions.ts, 35, 17))
86+
>Shapes : Symbol(Shapes, Decl(partiallyDiscriminantedUnions.ts, 32, 29))
87+
>s : Symbol(s, Decl(partiallyDiscriminantedUnions.ts, 35, 17))
88+
>Shape : Symbol(Shape, Decl(partiallyDiscriminantedUnions.ts, 30, 32))
89+
90+
return !Array.isArray(s);
91+
>Array.isArray : Symbol(ArrayConstructor.isArray, Decl(lib.d.ts, --, --))
92+
>Array : Symbol(Array, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
93+
>isArray : Symbol(ArrayConstructor.isArray, Decl(lib.d.ts, --, --))
94+
>s : Symbol(s, Decl(partiallyDiscriminantedUnions.ts, 35, 17))
95+
}
96+
97+
function fail(s: Shapes) {
98+
>fail : Symbol(fail, Decl(partiallyDiscriminantedUnions.ts, 37, 1))
99+
>s : Symbol(s, Decl(partiallyDiscriminantedUnions.ts, 39, 14))
100+
>Shapes : Symbol(Shapes, Decl(partiallyDiscriminantedUnions.ts, 32, 29))
101+
102+
if (isShape(s)) {
103+
>isShape : Symbol(isShape, Decl(partiallyDiscriminantedUnions.ts, 33, 35))
104+
>s : Symbol(s, Decl(partiallyDiscriminantedUnions.ts, 39, 14))
105+
106+
if (s.kind === "circle") {
107+
>s.kind : Symbol(kind, Decl(partiallyDiscriminantedUnions.ts, 29, 14), Decl(partiallyDiscriminantedUnions.ts, 30, 14))
108+
>s : Symbol(s, Decl(partiallyDiscriminantedUnions.ts, 39, 14))
109+
>kind : Symbol(kind, Decl(partiallyDiscriminantedUnions.ts, 29, 14), Decl(partiallyDiscriminantedUnions.ts, 30, 14))
110+
111+
let c: Circle = s;
112+
>c : Symbol(c, Decl(partiallyDiscriminantedUnions.ts, 42, 15))
113+
>Circle : Symbol(Circle, Decl(partiallyDiscriminantedUnions.ts, 29, 32))
114+
>s : Symbol(s, Decl(partiallyDiscriminantedUnions.ts, 39, 14))
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)