Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 52 additions & 10 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4416,10 +4416,19 @@ namespace ts {
}
const propTypes: Type[] = [];
const declarations: Declaration[] = [];
let commonType: Type = undefined;
let hasCommonType = true;
for (const prop of props) {
if (prop.declarations) {
addRange(declarations, prop.declarations);
}
const type = getTypeOfSymbol(prop);
if (!commonType) {
commonType = type;
}
else if (type !== commonType) {
hasCommonType = false;
}
propTypes.push(getTypeOfSymbol(prop));
}
const result = <TransientSymbol>createSymbol(
Expand All @@ -4429,6 +4438,7 @@ namespace ts {
commonFlags,
name);
result.containingType = containingType;
result.hasCommonType = hasCommonType;
result.declarations = declarations;
result.isReadonly = isReadonly;
result.type = containingType.flags & TypeFlags.Union ? getUnionType(propTypes) : getIntersectionType(propTypes);
Expand Down Expand Up @@ -7793,8 +7803,39 @@ namespace ts {
return false;
}

function rootContainsMatchingReference(source: Node, target: Node) {
return target.kind === SyntaxKind.PropertyAccessExpression && containsMatchingReference(source, (<PropertyAccessExpression>target).expression);
// Return true if target is a property access xxx.yyy, source is a property access xxx.zzz, the declared
// type of xxx is a union type, and yyy is a property that is possibly a discriminant. We consider a property
// a possible discriminant if its type differs in the constituents of containing union type, and if every
// choice is a unit type or a union of unit types.
function containsMatchingReferenceDiscriminant(source: Node, target: Node) {
return target.kind === SyntaxKind.PropertyAccessExpression &&
containsMatchingReference(source, (<PropertyAccessExpression>target).expression) &&
isDiscriminantProperty(getDeclaredTypeOfReference((<PropertyAccessExpression>target).expression), (<PropertyAccessExpression>target).name.text);
}

function getDeclaredTypeOfReference(expr: Node): Type {
if (expr.kind === SyntaxKind.Identifier) {
return getTypeOfSymbol(getResolvedSymbol(<Identifier>expr));
}
if (expr.kind === SyntaxKind.PropertyAccessExpression) {
const type = getDeclaredTypeOfReference((<PropertyAccessExpression>expr).expression);
return type && getTypeOfPropertyOfType(type, (<PropertyAccessExpression>expr).name.text);
}
return undefined;
}

function isDiscriminantProperty(type: Type, name: string) {
if (type && type.flags & TypeFlags.Union) {
const prop = getPropertyOfType(type, name);
if (prop && prop.flags & SymbolFlags.SyntheticProperty) {
if ((<TransientSymbol>prop).isDiscriminantProperty === undefined) {
(<TransientSymbol>prop).isDiscriminantProperty = !(<TransientSymbol>prop).hasCommonType &&
isUnitUnionType(getTypeOfSymbol(prop));
}
return (<TransientSymbol>prop).isDiscriminantProperty;
}
}
return false;
}

function isOrContainsMatchingReference(source: Node, target: Node) {
Expand Down Expand Up @@ -8222,7 +8263,7 @@ namespace ts {
if (isMatchingReference(reference, expr)) {
type = narrowTypeBySwitchOnDiscriminant(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
}
else if (isMatchingPropertyAccess(expr)) {
else if (isMatchingReferenceDiscriminant(expr)) {
type = narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd));
}
return createFlowType(type, isIncomplete(flowType));
Expand Down Expand Up @@ -8300,10 +8341,11 @@ namespace ts {
return cache[key] = getUnionType(antecedentTypes);
}

function isMatchingPropertyAccess(expr: Expression) {
function isMatchingReferenceDiscriminant(expr: Expression) {
return expr.kind === SyntaxKind.PropertyAccessExpression &&
declaredType.flags & TypeFlags.Union &&
isMatchingReference(reference, (<PropertyAccessExpression>expr).expression) &&
(declaredType.flags & TypeFlags.Union) !== 0;
isDiscriminantProperty(declaredType, (<PropertyAccessExpression>expr).name.text);
}

function narrowTypeByDiscriminant(type: Type, propAccess: PropertyAccessExpression, narrowType: (t: Type) => Type): Type {
Expand All @@ -8317,10 +8359,10 @@ namespace ts {
if (isMatchingReference(reference, expr)) {
return getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy);
}
if (isMatchingPropertyAccess(expr)) {
if (isMatchingReferenceDiscriminant(expr)) {
return narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy));
}
if (rootContainsMatchingReference(reference, expr)) {
if (containsMatchingReferenceDiscriminant(reference, expr)) {
return declaredType;
}
return type;
Expand Down Expand Up @@ -8349,13 +8391,13 @@ namespace ts {
if (isMatchingReference(reference, right)) {
return narrowTypeByEquality(type, operator, left, assumeTrue);
}
if (isMatchingPropertyAccess(left)) {
if (isMatchingReferenceDiscriminant(left)) {
return narrowTypeByDiscriminant(type, <PropertyAccessExpression>left, t => narrowTypeByEquality(t, operator, right, assumeTrue));
}
if (isMatchingPropertyAccess(right)) {
if (isMatchingReferenceDiscriminant(right)) {
return narrowTypeByDiscriminant(type, <PropertyAccessExpression>right, t => narrowTypeByEquality(t, operator, left, assumeTrue));
}
if (rootContainsMatchingReference(reference, left) || rootContainsMatchingReference(reference, right)) {
if (containsMatchingReferenceDiscriminant(reference, left) || containsMatchingReferenceDiscriminant(reference, right)) {
return declaredType;
}
break;
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2160,6 +2160,8 @@ namespace ts {
mapper?: TypeMapper; // Type mapper for instantiation alias
referenced?: boolean; // True if alias symbol has been referenced as a value
containingType?: UnionOrIntersectionType; // Containing union or intersection type for synthetic property
hasCommonType?: boolean; // True if constituents of synthetic property all have same type
isDiscriminantProperty?: boolean; // True if discriminant synthetic property
resolvedExports?: SymbolTable; // Resolved exports of module
exportsChecked?: boolean; // True if exports of external module have been checked
isDeclarationWithCollidingName?: boolean; // True if symbol is block scoped redeclaration
Expand Down
77 changes: 77 additions & 0 deletions tests/baselines/reference/discriminantPropertyCheck.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
tests/cases/compiler/discriminantPropertyCheck.ts(30,9): error TS2532: Object is possibly 'undefined'.
tests/cases/compiler/discriminantPropertyCheck.ts(66,9): error TS2532: Object is possibly 'undefined'.


==== tests/cases/compiler/discriminantPropertyCheck.ts (2 errors) ====

type Item = Item1 | Item2;

interface Base {
bar: boolean;
}

interface Item1 extends Base {
kind: "A";
foo: string | undefined;
baz: boolean;
qux: true;
}

interface Item2 extends Base {
kind: "B";
foo: string | undefined;
baz: boolean;
qux: false;
}

function goo1(x: Item) {
if (x.kind === "A" && x.foo !== undefined) {
x.foo.length;
}
}

function goo2(x: Item) {
if (x.foo !== undefined && x.kind === "A") {
x.foo.length; // Error, intervening discriminant guard
~~~~~
!!! error TS2532: Object is possibly 'undefined'.
}
}

function foo1(x: Item) {
if (x.bar && x.foo !== undefined) {
x.foo.length;
}
}

function foo2(x: Item) {
if (x.foo !== undefined && x.bar) {
x.foo.length;
}
}

function foo3(x: Item) {
if (x.baz && x.foo !== undefined) {
x.foo.length;
}
}

function foo4(x: Item) {
if (x.foo !== undefined && x.baz) {
x.foo.length;
}
}

function foo5(x: Item) {
if (x.qux && x.foo !== undefined) {
x.foo.length;
}
}

function foo6(x: Item) {
if (x.foo !== undefined && x.qux) {
x.foo.length; // Error, intervening discriminant guard
~~~~~
!!! error TS2532: Object is possibly 'undefined'.
}
}
111 changes: 111 additions & 0 deletions tests/baselines/reference/discriminantPropertyCheck.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//// [discriminantPropertyCheck.ts]

type Item = Item1 | Item2;

interface Base {
bar: boolean;
}

interface Item1 extends Base {
kind: "A";
foo: string | undefined;
baz: boolean;
qux: true;
}

interface Item2 extends Base {
kind: "B";
foo: string | undefined;
baz: boolean;
qux: false;
}

function goo1(x: Item) {
if (x.kind === "A" && x.foo !== undefined) {
x.foo.length;
}
}

function goo2(x: Item) {
if (x.foo !== undefined && x.kind === "A") {
x.foo.length; // Error, intervening discriminant guard
}
}

function foo1(x: Item) {
if (x.bar && x.foo !== undefined) {
x.foo.length;
}
}

function foo2(x: Item) {
if (x.foo !== undefined && x.bar) {
x.foo.length;
}
}

function foo3(x: Item) {
if (x.baz && x.foo !== undefined) {
x.foo.length;
}
}

function foo4(x: Item) {
if (x.foo !== undefined && x.baz) {
x.foo.length;
}
}

function foo5(x: Item) {
if (x.qux && x.foo !== undefined) {
x.foo.length;
}
}

function foo6(x: Item) {
if (x.foo !== undefined && x.qux) {
x.foo.length; // Error, intervening discriminant guard
}
}

//// [discriminantPropertyCheck.js]
function goo1(x) {
if (x.kind === "A" && x.foo !== undefined) {
x.foo.length;
}
}
function goo2(x) {
if (x.foo !== undefined && x.kind === "A") {
x.foo.length; // Error, intervening discriminant guard
}
}
function foo1(x) {
if (x.bar && x.foo !== undefined) {
x.foo.length;
}
}
function foo2(x) {
if (x.foo !== undefined && x.bar) {
x.foo.length;
}
}
function foo3(x) {
if (x.baz && x.foo !== undefined) {
x.foo.length;
}
}
function foo4(x) {
if (x.foo !== undefined && x.baz) {
x.foo.length;
}
}
function foo5(x) {
if (x.qux && x.foo !== undefined) {
x.foo.length;
}
}
function foo6(x) {
if (x.foo !== undefined && x.qux) {
x.foo.length; // Error, intervening discriminant guard
}
}
69 changes: 69 additions & 0 deletions tests/cases/compiler/discriminantPropertyCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// @strictNullChecks: true

type Item = Item1 | Item2;

interface Base {
bar: boolean;
}

interface Item1 extends Base {
kind: "A";
foo: string | undefined;
baz: boolean;
qux: true;
}

interface Item2 extends Base {
kind: "B";
foo: string | undefined;
baz: boolean;
qux: false;
}

function goo1(x: Item) {
if (x.kind === "A" && x.foo !== undefined) {
x.foo.length;
}
}

function goo2(x: Item) {
if (x.foo !== undefined && x.kind === "A") {
x.foo.length; // Error, intervening discriminant guard
}
}

function foo1(x: Item) {
if (x.bar && x.foo !== undefined) {
x.foo.length;
}
}

function foo2(x: Item) {
if (x.foo !== undefined && x.bar) {
x.foo.length;
}
}

function foo3(x: Item) {
if (x.baz && x.foo !== undefined) {
x.foo.length;
}
}

function foo4(x: Item) {
if (x.foo !== undefined && x.baz) {
x.foo.length;
}
}

function foo5(x: Item) {
if (x.qux && x.foo !== undefined) {
x.foo.length;
}
}

function foo6(x: Item) {
if (x.foo !== undefined && x.qux) {
x.foo.length; // Error, intervening discriminant guard
}
}