diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index aabb6dd9301fc..945ae034b25e5 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -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; } @@ -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; }); } @@ -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; } @@ -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) { diff --git a/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.js b/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.js new file mode 100644 index 0000000000000..3d64fb5f1c955 --- /dev/null +++ b/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.js @@ -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; +} diff --git a/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.symbols b/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.symbols new file mode 100644 index 0000000000000..bff1ca23ec84b --- /dev/null +++ b/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.symbols @@ -0,0 +1,181 @@ +//// [tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts] //// + +=== exhaustiveChecksForNonUnionTypes.ts === +// Basic case: narrowing non-union types to never +function testBasicNarrowing(obj: { name: "bob" }) { +>testBasicNarrowing : Symbol(testBasicNarrowing, Decl(exhaustiveChecksForNonUnionTypes.ts, 0, 0)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 1, 28)) +>name : Symbol(name, Decl(exhaustiveChecksForNonUnionTypes.ts, 1, 34)) + + if (obj.name === "bob") { +>obj.name : Symbol(name, Decl(exhaustiveChecksForNonUnionTypes.ts, 1, 34)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 1, 28)) +>name : Symbol(name, Decl(exhaustiveChecksForNonUnionTypes.ts, 1, 34)) + + // obj.name is "bob" + } else { + // obj should be narrowed to never since { name: "bob" } with name !== "bob" is impossible + const n: never = obj; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 6, 9)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 1, 28)) + } +} + +// Single enum member case +enum SingleAction { +>SingleAction : Symbol(SingleAction, Decl(exhaustiveChecksForNonUnionTypes.ts, 8, 1)) + + INCREMENT = 'INCREMENT' +>INCREMENT : Symbol(SingleAction.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 11, 19)) +} + +interface IIncrement { +>IIncrement : Symbol(IIncrement, Decl(exhaustiveChecksForNonUnionTypes.ts, 13, 1)) + + payload: {}; +>payload : Symbol(IIncrement.payload, Decl(exhaustiveChecksForNonUnionTypes.ts, 15, 22)) + + type: SingleAction.INCREMENT; +>type : Symbol(IIncrement.type, Decl(exhaustiveChecksForNonUnionTypes.ts, 16, 14)) +>SingleAction : Symbol(SingleAction, Decl(exhaustiveChecksForNonUnionTypes.ts, 8, 1)) +>INCREMENT : Symbol(SingleAction.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 11, 19)) +} + +function testSingleEnumSwitch(action: IIncrement) { +>testSingleEnumSwitch : Symbol(testSingleEnumSwitch, Decl(exhaustiveChecksForNonUnionTypes.ts, 18, 1)) +>action : Symbol(action, Decl(exhaustiveChecksForNonUnionTypes.ts, 20, 30)) +>IIncrement : Symbol(IIncrement, Decl(exhaustiveChecksForNonUnionTypes.ts, 13, 1)) + + switch (action.type) { +>action.type : Symbol(IIncrement.type, Decl(exhaustiveChecksForNonUnionTypes.ts, 16, 14)) +>action : Symbol(action, Decl(exhaustiveChecksForNonUnionTypes.ts, 20, 30)) +>type : Symbol(IIncrement.type, Decl(exhaustiveChecksForNonUnionTypes.ts, 16, 14)) + + case SingleAction.INCREMENT: +>SingleAction.INCREMENT : Symbol(SingleAction.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 11, 19)) +>SingleAction : Symbol(SingleAction, Decl(exhaustiveChecksForNonUnionTypes.ts, 8, 1)) +>INCREMENT : Symbol(SingleAction.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 11, 19)) + + return 1; + } + + // action should be narrowed to never since all cases are handled + const n: never = action; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 27, 7)) +>action : Symbol(action, Decl(exhaustiveChecksForNonUnionTypes.ts, 20, 30)) +} + +// Single literal type case (should already work) +function testSingleLiteral(x: "a") { +>testSingleLiteral : Symbol(testSingleLiteral, Decl(exhaustiveChecksForNonUnionTypes.ts, 28, 1)) +>x : Symbol(x, Decl(exhaustiveChecksForNonUnionTypes.ts, 31, 27)) + + if (x === "a") { +>x : Symbol(x, Decl(exhaustiveChecksForNonUnionTypes.ts, 31, 27)) + + // x is "a" + } else { + // x should be never + const n: never = x; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 36, 9)) +>x : Symbol(x, Decl(exhaustiveChecksForNonUnionTypes.ts, 31, 27)) + } +} + +// Single enum value case +enum Single { A = "a" } +>Single : Symbol(Single, Decl(exhaustiveChecksForNonUnionTypes.ts, 38, 1)) +>A : Symbol(Single.A, Decl(exhaustiveChecksForNonUnionTypes.ts, 41, 13)) + +function testSingleEnum(x: Single) { +>testSingleEnum : Symbol(testSingleEnum, Decl(exhaustiveChecksForNonUnionTypes.ts, 41, 23)) +>x : Symbol(x, Decl(exhaustiveChecksForNonUnionTypes.ts, 43, 24)) +>Single : Symbol(Single, Decl(exhaustiveChecksForNonUnionTypes.ts, 38, 1)) + + if (x === Single.A) { +>x : Symbol(x, Decl(exhaustiveChecksForNonUnionTypes.ts, 43, 24)) +>Single.A : Symbol(Single.A, Decl(exhaustiveChecksForNonUnionTypes.ts, 41, 13)) +>Single : Symbol(Single, Decl(exhaustiveChecksForNonUnionTypes.ts, 38, 1)) +>A : Symbol(Single.A, Decl(exhaustiveChecksForNonUnionTypes.ts, 41, 13)) + + // x is Single.A + } else { + // x should be never + const n: never = x; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 48, 9)) +>x : Symbol(x, Decl(exhaustiveChecksForNonUnionTypes.ts, 43, 24)) + } +} + +// More complex object with multiple literal properties +function testComplexObject(obj: { type: "user", status: "active" }) { +>testComplexObject : Symbol(testComplexObject, Decl(exhaustiveChecksForNonUnionTypes.ts, 50, 1)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 27)) +>type : Symbol(type, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 33)) +>status : Symbol(status, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 47)) + + if (obj.type === "user") { +>obj.type : Symbol(type, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 33)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 27)) +>type : Symbol(type, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 33)) + + if (obj.status === "active") { +>obj.status : Symbol(status, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 47)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 27)) +>status : Symbol(status, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 47)) + + // Both properties match + } else { + // obj.status !== "active" but obj: { type: "user", status: "active" } - impossible + const n: never = obj; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 59, 11)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 27)) + } + } else { + // obj.type !== "user" but obj: { type: "user", status: "active" } - impossible + const n: never = obj; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 63, 9)) +>obj : Symbol(obj, Decl(exhaustiveChecksForNonUnionTypes.ts, 53, 27)) + } +} + +// Switch statement with single case (original issue) +enum ActionTypes { +>ActionTypes : Symbol(ActionTypes, Decl(exhaustiveChecksForNonUnionTypes.ts, 65, 1)) + + INCREMENT = 'INCREMENT', +>INCREMENT : Symbol(ActionTypes.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 68, 18)) +} + +interface IAction { +>IAction : Symbol(IAction, Decl(exhaustiveChecksForNonUnionTypes.ts, 70, 1)) + + type: ActionTypes.INCREMENT; +>type : Symbol(IAction.type, Decl(exhaustiveChecksForNonUnionTypes.ts, 72, 19)) +>ActionTypes : Symbol(ActionTypes, Decl(exhaustiveChecksForNonUnionTypes.ts, 65, 1)) +>INCREMENT : Symbol(ActionTypes.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 68, 18)) +} + +function testOriginalIssue(action: IAction) { +>testOriginalIssue : Symbol(testOriginalIssue, Decl(exhaustiveChecksForNonUnionTypes.ts, 74, 1)) +>action : Symbol(action, Decl(exhaustiveChecksForNonUnionTypes.ts, 76, 27)) +>IAction : Symbol(IAction, Decl(exhaustiveChecksForNonUnionTypes.ts, 70, 1)) + + switch (action.type) { +>action.type : Symbol(IAction.type, Decl(exhaustiveChecksForNonUnionTypes.ts, 72, 19)) +>action : Symbol(action, Decl(exhaustiveChecksForNonUnionTypes.ts, 76, 27)) +>type : Symbol(IAction.type, Decl(exhaustiveChecksForNonUnionTypes.ts, 72, 19)) + + case ActionTypes.INCREMENT: +>ActionTypes.INCREMENT : Symbol(ActionTypes.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 68, 18)) +>ActionTypes : Symbol(ActionTypes, Decl(exhaustiveChecksForNonUnionTypes.ts, 65, 1)) +>INCREMENT : Symbol(ActionTypes.INCREMENT, Decl(exhaustiveChecksForNonUnionTypes.ts, 68, 18)) + + return 1; + } + + // This was the original issue - action should be never but wasn't + const n: never = action; +>n : Symbol(n, Decl(exhaustiveChecksForNonUnionTypes.ts, 83, 7)) +>action : Symbol(action, Decl(exhaustiveChecksForNonUnionTypes.ts, 76, 27)) +} diff --git a/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.types b/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.types new file mode 100644 index 0000000000000..573841765fb5f --- /dev/null +++ b/tests/baselines/reference/exhaustiveChecksForNonUnionTypes.types @@ -0,0 +1,266 @@ +//// [tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts] //// + +=== exhaustiveChecksForNonUnionTypes.ts === +// Basic case: narrowing non-union types to never +function testBasicNarrowing(obj: { name: "bob" }) { +>testBasicNarrowing : (obj: { name: "bob"; }) => void +> : ^ ^^ ^^^^^^^^^ +>obj : { name: "bob"; } +> : ^^^^^^^^ ^^^ +>name : "bob" +> : ^^^^^ + + if (obj.name === "bob") { +>obj.name === "bob" : boolean +> : ^^^^^^^ +>obj.name : "bob" +> : ^^^^^ +>obj : { name: "bob"; } +> : ^^^^^^^^ ^^^ +>name : "bob" +> : ^^^^^ +>"bob" : "bob" +> : ^^^^^ + + // obj.name is "bob" + } else { + // obj should be narrowed to never since { name: "bob" } with name !== "bob" is impossible + const n: never = obj; +>n : never +> : ^^^^^ +>obj : never +> : ^^^^^ + } +} + +// Single enum member case +enum SingleAction { +>SingleAction : SingleAction +> : ^^^^^^^^^^^^ + + INCREMENT = 'INCREMENT' +>INCREMENT : SingleAction.INCREMENT +> : ^^^^^^^^^^^^^^^^^^^^^^ +>'INCREMENT' : "INCREMENT" +> : ^^^^^^^^^^^ +} + +interface IIncrement { + payload: {}; +>payload : {} +> : ^^ + + type: SingleAction.INCREMENT; +>type : SingleAction +> : ^^^^^^^^^^^^ +>SingleAction : any +> : ^^^ +} + +function testSingleEnumSwitch(action: IIncrement) { +>testSingleEnumSwitch : (action: IIncrement) => number +> : ^ ^^ ^^^^^^^^^^^ +>action : IIncrement +> : ^^^^^^^^^^ + + switch (action.type) { +>action.type : SingleAction +> : ^^^^^^^^^^^^ +>action : IIncrement +> : ^^^^^^^^^^ +>type : SingleAction +> : ^^^^^^^^^^^^ + + case SingleAction.INCREMENT: +>SingleAction.INCREMENT : SingleAction +> : ^^^^^^^^^^^^ +>SingleAction : typeof SingleAction +> : ^^^^^^^^^^^^^^^^^^^ +>INCREMENT : SingleAction +> : ^^^^^^^^^^^^ + + return 1; +>1 : 1 +> : ^ + } + + // action should be narrowed to never since all cases are handled + const n: never = action; +>n : never +> : ^^^^^ +>action : never +> : ^^^^^ +} + +// Single literal type case (should already work) +function testSingleLiteral(x: "a") { +>testSingleLiteral : (x: "a") => void +> : ^ ^^ ^^^^^^^^^ +>x : "a" +> : ^^^ + + if (x === "a") { +>x === "a" : boolean +> : ^^^^^^^ +>x : "a" +> : ^^^ +>"a" : "a" +> : ^^^ + + // x is "a" + } else { + // x should be never + const n: never = x; +>n : never +> : ^^^^^ +>x : never +> : ^^^^^ + } +} + +// Single enum value case +enum Single { A = "a" } +>Single : Single +> : ^^^^^^ +>A : Single.A +> : ^^^^^^^^ +>"a" : "a" +> : ^^^ + +function testSingleEnum(x: Single) { +>testSingleEnum : (x: Single) => void +> : ^ ^^ ^^^^^^^^^ +>x : Single +> : ^^^^^^ + + if (x === Single.A) { +>x === Single.A : boolean +> : ^^^^^^^ +>x : Single +> : ^^^^^^ +>Single.A : Single +> : ^^^^^^ +>Single : typeof Single +> : ^^^^^^^^^^^^^ +>A : Single +> : ^^^^^^ + + // x is Single.A + } else { + // x should be never + const n: never = x; +>n : never +> : ^^^^^ +>x : never +> : ^^^^^ + } +} + +// More complex object with multiple literal properties +function testComplexObject(obj: { type: "user", status: "active" }) { +>testComplexObject : (obj: { type: "user"; status: "active"; }) => void +> : ^ ^^ ^^^^^^^^^ +>obj : { type: "user"; status: "active"; } +> : ^^^^^^^^ ^^^^^^^^^^ ^^^ +>type : "user" +> : ^^^^^^ +>status : "active" +> : ^^^^^^^^ + + if (obj.type === "user") { +>obj.type === "user" : boolean +> : ^^^^^^^ +>obj.type : "user" +> : ^^^^^^ +>obj : { type: "user"; status: "active"; } +> : ^^^^^^^^ ^^^^^^^^^^ ^^^ +>type : "user" +> : ^^^^^^ +>"user" : "user" +> : ^^^^^^ + + if (obj.status === "active") { +>obj.status === "active" : boolean +> : ^^^^^^^ +>obj.status : "active" +> : ^^^^^^^^ +>obj : { type: "user"; status: "active"; } +> : ^^^^^^^^ ^^^^^^^^^^ ^^^ +>status : "active" +> : ^^^^^^^^ +>"active" : "active" +> : ^^^^^^^^ + + // Both properties match + } else { + // obj.status !== "active" but obj: { type: "user", status: "active" } - impossible + const n: never = obj; +>n : never +> : ^^^^^ +>obj : never +> : ^^^^^ + } + } else { + // obj.type !== "user" but obj: { type: "user", status: "active" } - impossible + const n: never = obj; +>n : never +> : ^^^^^ +>obj : never +> : ^^^^^ + } +} + +// Switch statement with single case (original issue) +enum ActionTypes { +>ActionTypes : ActionTypes +> : ^^^^^^^^^^^ + + INCREMENT = 'INCREMENT', +>INCREMENT : ActionTypes.INCREMENT +> : ^^^^^^^^^^^^^^^^^^^^^ +>'INCREMENT' : "INCREMENT" +> : ^^^^^^^^^^^ +} + +interface IAction { + type: ActionTypes.INCREMENT; +>type : ActionTypes +> : ^^^^^^^^^^^ +>ActionTypes : any +> : ^^^ +} + +function testOriginalIssue(action: IAction) { +>testOriginalIssue : (action: IAction) => number +> : ^ ^^ ^^^^^^^^^^^ +>action : IAction +> : ^^^^^^^ + + switch (action.type) { +>action.type : ActionTypes +> : ^^^^^^^^^^^ +>action : IAction +> : ^^^^^^^ +>type : ActionTypes +> : ^^^^^^^^^^^ + + case ActionTypes.INCREMENT: +>ActionTypes.INCREMENT : ActionTypes +> : ^^^^^^^^^^^ +>ActionTypes : typeof ActionTypes +> : ^^^^^^^^^^^^^^^^^^ +>INCREMENT : ActionTypes +> : ^^^^^^^^^^^ + + return 1; +>1 : 1 +> : ^ + } + + // This was the original issue - action should be never but wasn't + const n: never = action; +>n : never +> : ^^^^^ +>action : never +> : ^^^^^ +} diff --git a/tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts b/tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts new file mode 100644 index 0000000000000..1b2f551a5cd51 --- /dev/null +++ b/tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts @@ -0,0 +1,87 @@ +// @strict: true + +// 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; +} \ No newline at end of file