Skip to content

Enable exhaustiveness checking and never narrowing for non-union types #62114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
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 @@
}
}
}
// 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 @@
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 @@
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 @@
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