Skip to content

Commit 94a0daf

Browse files
committed
Properly handle partially discriminated unions
1 parent 08eafd3 commit 94a0daf

File tree

2 files changed

+39
-30
lines changed

2 files changed

+39
-30
lines changed

src/compiler/checker.ts

+37-29
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

+2-1
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

0 commit comments

Comments
 (0)