Skip to content
Draft
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
29 changes: 26 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29396,6 +29396,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
}
// Fix for #23572: Allow discriminant property narrowing for non-union types
// This enables narrowing to never when all possibilities are eliminated
else {
const access = getCandidateDiscriminantPropertyAccess(expr);
if (access) {
const name = getAccessedPropertyName(access);
if (name) {
// For non-union types, check if the property exists and has a literal type
const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType;
// Only try to get property type for safe types (avoid EvolvingArray and other special types)
if (type.flags & TypeFlags.Object && !((type as ObjectType).objectFlags & ObjectFlags.EvolvingArray)) {
const propType = getTypeOfPropertyOfType(type, name);
if (propType && isUnitLikeType(propType)) {
return access;
}
}
}
}
}
return undefined;
}

Expand All @@ -29414,7 +29433,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const narrowedPropType = narrowType(propType);
return filterType(type, t => {
const discriminantType = getTypeOfPropertyOrIndexSignatureOfType(t, propName) || unknownType;
return !(discriminantType.flags & TypeFlags.Never) && !(narrowedPropType.flags & TypeFlags.Never) && areTypesComparable(narrowedPropType, discriminantType);
const result = !(discriminantType.flags & TypeFlags.Never) && !(narrowedPropType.flags & TypeFlags.Never) && areTypesComparable(narrowedPropType, discriminantType);
return result;
});
}

Expand Down Expand Up @@ -29651,7 +29671,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return replacePrimitivesWithLiterals(filteredType, valueType);
}
if (isUnitType(valueType)) {
return filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType)));
const result = filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType)));
return result;
}
return type;
}
Expand Down Expand Up @@ -39274,7 +39295,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (!switchTypes.length || some(switchTypes, isNeitherUnitTypeNorNever)) {
return false;
}
return eachTypeContainedIn(mapType(type, getRegularTypeOfLiteralType), switchTypes);
const mappedType = mapType(type, getRegularTypeOfLiteralType);
const result = eachTypeContainedIn(mappedType, switchTypes);
return result;
}

function functionHasImplicitReturn(func: FunctionLikeDeclaration) {
Expand Down
167 changes: 167 additions & 0 deletions tests/baselines/reference/exhaustiveChecksForNonUnionTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//// [tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts] ////

//// [exhaustiveChecksForNonUnionTypes.ts]
// Basic case: narrowing non-union types to never
function testBasicNarrowing(obj: { name: "bob" }) {
if (obj.name === "bob") {
// obj.name is "bob"
} else {
// obj should be narrowed to never since { name: "bob" } with name !== "bob" is impossible
const n: never = obj;
}
}

// Single enum member case
enum SingleAction {
INCREMENT = 'INCREMENT'
}

interface IIncrement {
payload: {};
type: SingleAction.INCREMENT;
}

function testSingleEnumSwitch(action: IIncrement) {
switch (action.type) {
case SingleAction.INCREMENT:
return 1;
}

// action should be narrowed to never since all cases are handled
const n: never = action;
}

// Single literal type case (should already work)
function testSingleLiteral(x: "a") {
if (x === "a") {
// x is "a"
} else {
// x should be never
const n: never = x;
}
}

// Single enum value case
enum Single { A = "a" }

function testSingleEnum(x: Single) {
if (x === Single.A) {
// x is Single.A
} else {
// x should be never
const n: never = x;
}
}

// More complex object with multiple literal properties
function testComplexObject(obj: { type: "user", status: "active" }) {
if (obj.type === "user") {
if (obj.status === "active") {
// Both properties match
} else {
// obj.status !== "active" but obj: { type: "user", status: "active" } - impossible
const n: never = obj;
}
} else {
// obj.type !== "user" but obj: { type: "user", status: "active" } - impossible
const n: never = obj;
}
}

// Switch statement with single case (original issue)
enum ActionTypes {
INCREMENT = 'INCREMENT',
}

interface IAction {
type: ActionTypes.INCREMENT;
}

function testOriginalIssue(action: IAction) {
switch (action.type) {
case ActionTypes.INCREMENT:
return 1;
}

// This was the original issue - action should be never but wasn't
const n: never = action;
}

//// [exhaustiveChecksForNonUnionTypes.js]
"use strict";
// Basic case: narrowing non-union types to never
function testBasicNarrowing(obj) {
if (obj.name === "bob") {
// obj.name is "bob"
}
else {
// obj should be narrowed to never since { name: "bob" } with name !== "bob" is impossible
var n = obj;
}
}
// Single enum member case
var SingleAction;
(function (SingleAction) {
SingleAction["INCREMENT"] = "INCREMENT";
})(SingleAction || (SingleAction = {}));
function testSingleEnumSwitch(action) {
switch (action.type) {
case SingleAction.INCREMENT:
return 1;
}
// action should be narrowed to never since all cases are handled
var n = action;
}
// Single literal type case (should already work)
function testSingleLiteral(x) {
if (x === "a") {
// x is "a"
}
else {
// x should be never
var n = x;
}
}
// Single enum value case
var Single;
(function (Single) {
Single["A"] = "a";
})(Single || (Single = {}));
function testSingleEnum(x) {
if (x === Single.A) {
// x is Single.A
}
else {
// x should be never
var n = x;
}
}
// More complex object with multiple literal properties
function testComplexObject(obj) {
if (obj.type === "user") {
if (obj.status === "active") {
// Both properties match
}
else {
// obj.status !== "active" but obj: { type: "user", status: "active" } - impossible
var n = obj;
}
}
else {
// obj.type !== "user" but obj: { type: "user", status: "active" } - impossible
var n = obj;
}
}
// Switch statement with single case (original issue)
var ActionTypes;
(function (ActionTypes) {
ActionTypes["INCREMENT"] = "INCREMENT";
})(ActionTypes || (ActionTypes = {}));
function testOriginalIssue(action) {
switch (action.type) {
case ActionTypes.INCREMENT:
return 1;
}
// This was the original issue - action should be never but wasn't
var n = action;
}
Loading
Loading