Skip to content

Commit

Permalink
fix(50340): typeof ... === "undefined" check on discriminated union o…
Browse files Browse the repository at this point in the history
…f undefined and object type doesn't narrow correctly (#50344)

* fix(50340): narrow type by discriminant in typeof

* add additional test cases
  • Loading branch information
a-tarasyuk committed Aug 31, 2022
1 parent 43f8ae6 commit a9797d2
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/compiler/checker.ts
Expand Up @@ -25328,11 +25328,19 @@ namespace ts {
}
const target = getReferenceCandidate(typeOfExpr.expression);
if (!isMatchingReference(reference, target)) {
const propertyAccess = getDiscriminantPropertyAccess(typeOfExpr.expression, type);
if (propertyAccess) {
return narrowTypeByDiscriminant(type, propertyAccess, t => narrowTypeByLiteralExpression(t, literal, assumeTrue));
}
if (strictNullChecks && optionalChainContainsReference(target, reference) && assumeTrue === (literal.text !== "undefined")) {
return getAdjustedTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
}
return type;
}
return narrowTypeByLiteralExpression(type, literal, assumeTrue);
}

function narrowTypeByLiteralExpression(type: Type, literal: LiteralExpression, assumeTrue: boolean) {
return assumeTrue ?
narrowTypeByTypeName(type, literal.text) :
getTypeWithFacts(type, typeofNEFacts.get(literal.text) || TypeFacts.TypeofNEHostObject);
Expand Down
31 changes: 31 additions & 0 deletions tests/baselines/reference/narrowingTypeofUndefined.js
@@ -0,0 +1,31 @@
//// [narrowingTypeofUndefined.ts]
declare const a: { error: { prop: string }, result: undefined } | { error: undefined, result: { prop: number } }

if (typeof a.error === 'undefined') {
a.result.prop; // number
}
else {
a.error.prop; // string
}

if (typeof a.error !== 'undefined') {
a.error.prop; // string
}
else {
a.result.prop; // number
}


//// [narrowingTypeofUndefined.js]
if (typeof a.error === 'undefined') {
a.result.prop; // number
}
else {
a.error.prop; // string
}
if (typeof a.error !== 'undefined') {
a.error.prop; // string
}
else {
a.result.prop; // number
}
52 changes: 52 additions & 0 deletions tests/baselines/reference/narrowingTypeofUndefined.symbols
@@ -0,0 +1,52 @@
=== tests/cases/compiler/narrowingTypeofUndefined.ts ===
declare const a: { error: { prop: string }, result: undefined } | { error: undefined, result: { prop: number } }
>a : Symbol(a, Decl(narrowingTypeofUndefined.ts, 0, 13))
>error : Symbol(error, Decl(narrowingTypeofUndefined.ts, 0, 18))
>prop : Symbol(prop, Decl(narrowingTypeofUndefined.ts, 0, 27))
>result : Symbol(result, Decl(narrowingTypeofUndefined.ts, 0, 43))
>error : Symbol(error, Decl(narrowingTypeofUndefined.ts, 0, 67))
>result : Symbol(result, Decl(narrowingTypeofUndefined.ts, 0, 85))
>prop : Symbol(prop, Decl(narrowingTypeofUndefined.ts, 0, 95))

if (typeof a.error === 'undefined') {
>a.error : Symbol(error, Decl(narrowingTypeofUndefined.ts, 0, 18), Decl(narrowingTypeofUndefined.ts, 0, 67))
>a : Symbol(a, Decl(narrowingTypeofUndefined.ts, 0, 13))
>error : Symbol(error, Decl(narrowingTypeofUndefined.ts, 0, 18), Decl(narrowingTypeofUndefined.ts, 0, 67))

a.result.prop; // number
>a.result.prop : Symbol(prop, Decl(narrowingTypeofUndefined.ts, 0, 95))
>a.result : Symbol(result, Decl(narrowingTypeofUndefined.ts, 0, 43), Decl(narrowingTypeofUndefined.ts, 0, 85))
>a : Symbol(a, Decl(narrowingTypeofUndefined.ts, 0, 13))
>result : Symbol(result, Decl(narrowingTypeofUndefined.ts, 0, 43), Decl(narrowingTypeofUndefined.ts, 0, 85))
>prop : Symbol(prop, Decl(narrowingTypeofUndefined.ts, 0, 95))
}
else {
a.error.prop; // string
>a.error.prop : Symbol(prop, Decl(narrowingTypeofUndefined.ts, 0, 27))
>a.error : Symbol(error, Decl(narrowingTypeofUndefined.ts, 0, 18), Decl(narrowingTypeofUndefined.ts, 0, 67))
>a : Symbol(a, Decl(narrowingTypeofUndefined.ts, 0, 13))
>error : Symbol(error, Decl(narrowingTypeofUndefined.ts, 0, 18), Decl(narrowingTypeofUndefined.ts, 0, 67))
>prop : Symbol(prop, Decl(narrowingTypeofUndefined.ts, 0, 27))
}

if (typeof a.error !== 'undefined') {
>a.error : Symbol(error, Decl(narrowingTypeofUndefined.ts, 0, 18), Decl(narrowingTypeofUndefined.ts, 0, 67))
>a : Symbol(a, Decl(narrowingTypeofUndefined.ts, 0, 13))
>error : Symbol(error, Decl(narrowingTypeofUndefined.ts, 0, 18), Decl(narrowingTypeofUndefined.ts, 0, 67))

a.error.prop; // string
>a.error.prop : Symbol(prop, Decl(narrowingTypeofUndefined.ts, 0, 27))
>a.error : Symbol(error, Decl(narrowingTypeofUndefined.ts, 0, 18), Decl(narrowingTypeofUndefined.ts, 0, 67))
>a : Symbol(a, Decl(narrowingTypeofUndefined.ts, 0, 13))
>error : Symbol(error, Decl(narrowingTypeofUndefined.ts, 0, 18), Decl(narrowingTypeofUndefined.ts, 0, 67))
>prop : Symbol(prop, Decl(narrowingTypeofUndefined.ts, 0, 27))
}
else {
a.result.prop; // number
>a.result.prop : Symbol(prop, Decl(narrowingTypeofUndefined.ts, 0, 95))
>a.result : Symbol(result, Decl(narrowingTypeofUndefined.ts, 0, 43), Decl(narrowingTypeofUndefined.ts, 0, 85))
>a : Symbol(a, Decl(narrowingTypeofUndefined.ts, 0, 13))
>result : Symbol(result, Decl(narrowingTypeofUndefined.ts, 0, 43), Decl(narrowingTypeofUndefined.ts, 0, 85))
>prop : Symbol(prop, Decl(narrowingTypeofUndefined.ts, 0, 95))
}

58 changes: 58 additions & 0 deletions tests/baselines/reference/narrowingTypeofUndefined.types
@@ -0,0 +1,58 @@
=== tests/cases/compiler/narrowingTypeofUndefined.ts ===
declare const a: { error: { prop: string }, result: undefined } | { error: undefined, result: { prop: number } }
>a : { error: { prop: string;}; result: undefined; } | { error: undefined; result: { prop: number;}; }
>error : { prop: string; }
>prop : string
>result : undefined
>error : undefined
>result : { prop: number; }
>prop : number

if (typeof a.error === 'undefined') {
>typeof a.error === 'undefined' : boolean
>typeof a.error : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>a.error : { prop: string; }
>a : { error: { prop: string; }; result: undefined; } | { error: undefined; result: { prop: number; }; }
>error : { prop: string; }
>'undefined' : "undefined"

a.result.prop; // number
>a.result.prop : number
>a.result : { prop: number; }
>a : { error: { prop: string; }; result: undefined; } | { error: undefined; result: { prop: number; }; }
>result : { prop: number; }
>prop : number
}
else {
a.error.prop; // string
>a.error.prop : string
>a.error : { prop: string; }
>a : { error: { prop: string; }; result: undefined; } | { error: undefined; result: { prop: number; }; }
>error : { prop: string; }
>prop : string
}

if (typeof a.error !== 'undefined') {
>typeof a.error !== 'undefined' : boolean
>typeof a.error : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
>a.error : { prop: string; }
>a : { error: { prop: string; }; result: undefined; } | { error: undefined; result: { prop: number; }; }
>error : { prop: string; }
>'undefined' : "undefined"

a.error.prop; // string
>a.error.prop : string
>a.error : { prop: string; }
>a : { error: { prop: string; }; result: undefined; } | { error: undefined; result: { prop: number; }; }
>error : { prop: string; }
>prop : string
}
else {
a.result.prop; // number
>a.result.prop : number
>a.result : { prop: number; }
>a : { error: { prop: string; }; result: undefined; } | { error: undefined; result: { prop: number; }; }
>result : { prop: number; }
>prop : number
}

15 changes: 15 additions & 0 deletions tests/cases/compiler/narrowingTypeofUndefined.ts
@@ -0,0 +1,15 @@
declare const a: { error: { prop: string }, result: undefined } | { error: undefined, result: { prop: number } }

if (typeof a.error === 'undefined') {
a.result.prop; // number
}
else {
a.error.prop; // string
}

if (typeof a.error !== 'undefined') {
a.error.prop; // string
}
else {
a.result.prop; // number
}

0 comments on commit a9797d2

Please sign in to comment.