Skip to content

Commit 37e96b3

Browse files
committed
Stricter check for discriminant properties in type guards
1 parent b54aec1 commit 37e96b3

File tree

2 files changed

+54
-10
lines changed

2 files changed

+54
-10
lines changed

src/compiler/checker.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4416,10 +4416,19 @@ namespace ts {
44164416
}
44174417
const propTypes: Type[] = [];
44184418
const declarations: Declaration[] = [];
4419+
let commonType: Type = undefined;
4420+
let hasCommonType = true;
44194421
for (const prop of props) {
44204422
if (prop.declarations) {
44214423
addRange(declarations, prop.declarations);
44224424
}
4425+
const type = getTypeOfSymbol(prop);
4426+
if (!commonType) {
4427+
commonType = type;
4428+
}
4429+
else if (type !== commonType) {
4430+
hasCommonType = false;
4431+
}
44234432
propTypes.push(getTypeOfSymbol(prop));
44244433
}
44254434
const result = <TransientSymbol>createSymbol(
@@ -4429,6 +4438,7 @@ namespace ts {
44294438
commonFlags,
44304439
name);
44314440
result.containingType = containingType;
4441+
result.hasCommonType = hasCommonType;
44324442
result.declarations = declarations;
44334443
result.isReadonly = isReadonly;
44344444
result.type = containingType.flags & TypeFlags.Union ? getUnionType(propTypes) : getIntersectionType(propTypes);
@@ -7793,8 +7803,39 @@ namespace ts {
77937803
return false;
77947804
}
77957805

7796-
function rootContainsMatchingReference(source: Node, target: Node) {
7797-
return target.kind === SyntaxKind.PropertyAccessExpression && containsMatchingReference(source, (<PropertyAccessExpression>target).expression);
7806+
// Return true if target is a property access xxx.yyy, source is a property access xxx.zzz, the declared
7807+
// type of xxx is a union type, and yyy is a property that is possibly a discriminant. We consider a property
7808+
// a possible discriminant if its type differs in the constituents of containing union type, and if every
7809+
// choice is a unit type or a union of unit types.
7810+
function containsMatchingReferenceDiscriminant(source: Node, target: Node) {
7811+
return target.kind === SyntaxKind.PropertyAccessExpression &&
7812+
containsMatchingReference(source, (<PropertyAccessExpression>target).expression) &&
7813+
isDiscriminantProperty(getDeclaredTypeOfReference((<PropertyAccessExpression>target).expression), (<PropertyAccessExpression>target).name.text);
7814+
}
7815+
7816+
function getDeclaredTypeOfReference(expr: Node): Type {
7817+
if (expr.kind === SyntaxKind.Identifier) {
7818+
return getTypeOfSymbol(getResolvedSymbol(<Identifier>expr));
7819+
}
7820+
if (expr.kind === SyntaxKind.PropertyAccessExpression) {
7821+
const type = getDeclaredTypeOfReference((<PropertyAccessExpression>expr).expression);
7822+
return type && getTypeOfPropertyOfType(type, (<PropertyAccessExpression>expr).name.text);
7823+
}
7824+
return undefined;
7825+
}
7826+
7827+
function isDiscriminantProperty(type: Type, name: string) {
7828+
if (type && type.flags & TypeFlags.Union) {
7829+
const prop = getPropertyOfType(type, name);
7830+
if (prop && prop.flags & SymbolFlags.SyntheticProperty) {
7831+
if ((<TransientSymbol>prop).isDiscriminantProperty === undefined) {
7832+
(<TransientSymbol>prop).isDiscriminantProperty = !(<TransientSymbol>prop).hasCommonType &&
7833+
isUnitUnionType(getTypeOfSymbol(prop));
7834+
}
7835+
return (<TransientSymbol>prop).isDiscriminantProperty;
7836+
}
7837+
}
7838+
return false;
77987839
}
77997840

78007841
function isOrContainsMatchingReference(source: Node, target: Node) {
@@ -8222,7 +8263,7 @@ namespace ts {
82228263
if (isMatchingReference(reference, expr)) {
82238264
type = narrowTypeBySwitchOnDiscriminant(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
82248265
}
8225-
else if (isMatchingPropertyAccess(expr)) {
8266+
else if (isMatchingReferenceDiscriminant(expr)) {
82268267
type = narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd));
82278268
}
82288269
return createFlowType(type, isIncomplete(flowType));
@@ -8300,10 +8341,11 @@ namespace ts {
83008341
return cache[key] = getUnionType(antecedentTypes);
83018342
}
83028343

8303-
function isMatchingPropertyAccess(expr: Expression) {
8344+
function isMatchingReferenceDiscriminant(expr: Expression) {
83048345
return expr.kind === SyntaxKind.PropertyAccessExpression &&
8346+
declaredType.flags & TypeFlags.Union &&
83058347
isMatchingReference(reference, (<PropertyAccessExpression>expr).expression) &&
8306-
(declaredType.flags & TypeFlags.Union) !== 0;
8348+
isDiscriminantProperty(declaredType, (<PropertyAccessExpression>expr).name.text);
83078349
}
83088350

83098351
function narrowTypeByDiscriminant(type: Type, propAccess: PropertyAccessExpression, narrowType: (t: Type) => Type): Type {
@@ -8317,10 +8359,10 @@ namespace ts {
83178359
if (isMatchingReference(reference, expr)) {
83188360
return getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy);
83198361
}
8320-
if (isMatchingPropertyAccess(expr)) {
8362+
if (isMatchingReferenceDiscriminant(expr)) {
83218363
return narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy));
83228364
}
8323-
if (rootContainsMatchingReference(reference, expr)) {
8365+
if (containsMatchingReferenceDiscriminant(reference, expr)) {
83248366
return declaredType;
83258367
}
83268368
return type;
@@ -8349,13 +8391,13 @@ namespace ts {
83498391
if (isMatchingReference(reference, right)) {
83508392
return narrowTypeByEquality(type, operator, left, assumeTrue);
83518393
}
8352-
if (isMatchingPropertyAccess(left)) {
8394+
if (isMatchingReferenceDiscriminant(left)) {
83538395
return narrowTypeByDiscriminant(type, <PropertyAccessExpression>left, t => narrowTypeByEquality(t, operator, right, assumeTrue));
83548396
}
8355-
if (isMatchingPropertyAccess(right)) {
8397+
if (isMatchingReferenceDiscriminant(right)) {
83568398
return narrowTypeByDiscriminant(type, <PropertyAccessExpression>right, t => narrowTypeByEquality(t, operator, left, assumeTrue));
83578399
}
8358-
if (rootContainsMatchingReference(reference, left) || rootContainsMatchingReference(reference, right)) {
8400+
if (containsMatchingReferenceDiscriminant(reference, left) || containsMatchingReferenceDiscriminant(reference, right)) {
83598401
return declaredType;
83608402
}
83618403
break;

src/compiler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2160,6 +2160,8 @@ namespace ts {
21602160
mapper?: TypeMapper; // Type mapper for instantiation alias
21612161
referenced?: boolean; // True if alias symbol has been referenced as a value
21622162
containingType?: UnionOrIntersectionType; // Containing union or intersection type for synthetic property
2163+
hasCommonType?: boolean; // True if constituents of synthetic property all have same type
2164+
isDiscriminantProperty?: boolean; // True if discriminant synthetic property
21632165
resolvedExports?: SymbolTable; // Resolved exports of module
21642166
exportsChecked?: boolean; // True if exports of external module have been checked
21652167
isDeclarationWithCollidingName?: boolean; // True if symbol is block scoped redeclaration

0 commit comments

Comments
 (0)