Implement fix for exhaustiveness checking on non-union types

Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-07-23 21:04:58 +00:00
parent 8d3be0e510
commit 5d94666678
5 changed files with 816 additions and 95 deletions

View File

@ -29380,42 +29380,59 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return undefined;
}
function getDiscriminantPropertyAccess(expr: Expression, computedType: Type) {
// As long as the computed type is a subset of the declared type, we use the full declared type to detect
// a discriminant property. In cases where the computed type isn't a subset, e.g because of a preceding type
// predicate narrowing, we use the actual computed type.
if (declaredType.flags & TypeFlags.Union || computedType.flags & TypeFlags.Union) {
const access = getCandidateDiscriminantPropertyAccess(expr);
if (access) {
const name = getAccessedPropertyName(access);
if (name) {
const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType;
if (isDiscriminantProperty(type, name)) {
return access;
}
}
}
}
return undefined;
function getDiscriminantPropertyAccess(expr: Expression, computedType: Type) {
// As long as the computed type is a subset of the declared type, we use the full declared type to detect
// a discriminant property. In cases where the computed type isn't a subset, e.g because of a preceding type
// predicate narrowing, we use the actual computed type.
if (declaredType.flags & TypeFlags.Union || computedType.flags & TypeFlags.Union) {
const access = getCandidateDiscriminantPropertyAccess(expr);
if (access) {
const name = getAccessedPropertyName(access);
if (name) {
const type = declaredType.flags & TypeFlags.Union && isTypeSubsetOf(computedType, declaredType) ? declaredType : computedType;
if (isDiscriminantProperty(type, name)) {
return access;
}
}
}
}
// 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;
const propType = getTypeOfPropertyOfType(type, name);
if (propType && isUnitLikeType(propType)) {
return access;
}
}
}
}
return undefined;
}
function narrowTypeByDiscriminant(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, narrowType: (t: Type) => Type): Type {
const propName = getAccessedPropertyName(access);
if (propName === undefined) {
return type;
}
const optionalChain = isOptionalChain(access);
const removeNullable = strictNullChecks && (optionalChain || isNonNullAccess(access)) && maybeTypeOfKind(type, TypeFlags.Nullable);
let propType = getTypeOfPropertyOfType(removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type, propName);
if (!propType) {
return type;
}
propType = removeNullable && optionalChain ? getOptionalType(propType) : propType;
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);
});
function narrowTypeByDiscriminant(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, narrowType: (t: Type) => Type): Type {
const propName = getAccessedPropertyName(access);
if (propName === undefined) {
return type;
}
const optionalChain = isOptionalChain(access);
const removeNullable = strictNullChecks && (optionalChain || isNonNullAccess(access)) && maybeTypeOfKind(type, TypeFlags.Nullable);
let propType = getTypeOfPropertyOfType(removeNullable ? getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type, propName);
if (!propType) {
return type;
}
propType = removeNullable && optionalChain ? getOptionalType(propType) : propType;
const narrowedPropType = narrowType(propType);
return filterType(type, t => {
const discriminantType = getTypeOfPropertyOrIndexSignatureOfType(t, propName) || unknownType;
const result = !(discriminantType.flags & TypeFlags.Never) && !(narrowedPropType.flags & TypeFlags.Never) && areTypesComparable(narrowedPropType, discriminantType);
return result;
});
}
function narrowTypeByDiscriminantProperty(type: Type, access: AccessExpression | BindingElement | ParameterDeclaration, operator: SyntaxKind, value: Expression, assumeTrue: boolean) {
@ -29618,42 +29635,43 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return removeNullable ? getAdjustedTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type;
}
function narrowTypeByEquality(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type {
if (type.flags & TypeFlags.Any) {
return type;
}
if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) {
assumeTrue = !assumeTrue;
}
const valueType = getTypeOfExpression(value);
const doubleEquals = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken;
if (valueType.flags & TypeFlags.Nullable) {
if (!strictNullChecks) {
return type;
}
const facts = doubleEquals ?
assumeTrue ? TypeFacts.EQUndefinedOrNull : TypeFacts.NEUndefinedOrNull :
valueType.flags & TypeFlags.Null ?
assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull :
assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined;
return getAdjustedTypeWithFacts(type, facts);
}
if (assumeTrue) {
if (!doubleEquals && (type.flags & TypeFlags.Unknown || someType(type, isEmptyAnonymousObjectType))) {
if (valueType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive) || isEmptyAnonymousObjectType(valueType)) {
return valueType;
}
if (valueType.flags & TypeFlags.Object) {
return nonPrimitiveType;
}
}
const filteredType = filterType(type, t => areTypesComparable(t, valueType) || doubleEquals && isCoercibleUnderDoubleEquals(t, valueType));
return replacePrimitivesWithLiterals(filteredType, valueType);
}
if (isUnitType(valueType)) {
return filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType)));
}
return type;
function narrowTypeByEquality(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type {
if (type.flags & TypeFlags.Any) {
return type;
}
if (operator === SyntaxKind.ExclamationEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken) {
assumeTrue = !assumeTrue;
}
const valueType = getTypeOfExpression(value);
const doubleEquals = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken;
if (valueType.flags & TypeFlags.Nullable) {
if (!strictNullChecks) {
return type;
}
const facts = doubleEquals ?
assumeTrue ? TypeFacts.EQUndefinedOrNull : TypeFacts.NEUndefinedOrNull :
valueType.flags & TypeFlags.Null ?
assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull :
assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined;
return getAdjustedTypeWithFacts(type, facts);
}
if (assumeTrue) {
if (!doubleEquals && (type.flags & TypeFlags.Unknown || someType(type, isEmptyAnonymousObjectType))) {
if (valueType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive) || isEmptyAnonymousObjectType(valueType)) {
return valueType;
}
if (valueType.flags & TypeFlags.Object) {
return nonPrimitiveType;
}
}
const filteredType = filterType(type, t => areTypesComparable(t, valueType) || doubleEquals && isCoercibleUnderDoubleEquals(t, valueType));
return replacePrimitivesWithLiterals(filteredType, valueType);
}
if (isUnitType(valueType)) {
const result = filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType)));
return result;
}
return type;
}
function narrowTypeByTypeof(type: Type, typeOfExpr: TypeOfExpression, operator: SyntaxKind, literal: LiteralExpression, assumeTrue: boolean): Type {
@ -39250,31 +39268,33 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return links.isExhaustive;
}
function computeExhaustiveSwitchStatement(node: SwitchStatement): boolean {
if (node.expression.kind === SyntaxKind.TypeOfExpression) {
const witnesses = getSwitchClauseTypeOfWitnesses(node);
if (!witnesses) {
return false;
}
const operandConstraint = getBaseConstraintOrType(checkExpressionCached((node.expression as TypeOfExpression).expression));
// Get the not-equal flags for all handled cases.
const notEqualFacts = getNotEqualFactsFromTypeofSwitch(0, 0, witnesses);
if (operandConstraint.flags & TypeFlags.AnyOrUnknown) {
// We special case the top types to be exhaustive when all cases are handled.
return (TypeFacts.AllTypeofNE & notEqualFacts) === TypeFacts.AllTypeofNE;
}
// A missing not-equal flag indicates that the type wasn't handled by some case.
return !someType(operandConstraint, t => getTypeFacts(t, notEqualFacts) === notEqualFacts);
}
const type = getBaseConstraintOrType(checkExpressionCached(node.expression));
if (!isLiteralType(type)) {
return false;
}
const switchTypes = getSwitchClauseTypes(node);
if (!switchTypes.length || some(switchTypes, isNeitherUnitTypeNorNever)) {
return false;
}
return eachTypeContainedIn(mapType(type, getRegularTypeOfLiteralType), switchTypes);
function computeExhaustiveSwitchStatement(node: SwitchStatement): boolean {
if (node.expression.kind === SyntaxKind.TypeOfExpression) {
const witnesses = getSwitchClauseTypeOfWitnesses(node);
if (!witnesses) {
return false;
}
const operandConstraint = getBaseConstraintOrType(checkExpressionCached((node.expression as TypeOfExpression).expression));
// Get the not-equal flags for all handled cases.
const notEqualFacts = getNotEqualFactsFromTypeofSwitch(0, 0, witnesses);
if (operandConstraint.flags & TypeFlags.AnyOrUnknown) {
// We special case the top types to be exhaustive when all cases are handled.
return (TypeFacts.AllTypeofNE & notEqualFacts) === TypeFacts.AllTypeofNE;
}
// A missing not-equal flag indicates that the type wasn't handled by some case.
return !someType(operandConstraint, t => getTypeFacts(t, notEqualFacts) === notEqualFacts);
}
const type = getBaseConstraintOrType(checkExpressionCached(node.expression));
if (!isLiteralType(type)) {
return false;
}
const switchTypes = getSwitchClauseTypes(node);
if (!switchTypes.length || some(switchTypes, isNeitherUnitTypeNorNever)) {
return false;
}
const mappedType = mapType(type, getRegularTypeOfLiteralType);
const result = eachTypeContainedIn(mappedType, switchTypes);
return result;
}
function functionHasImplicitReturn(func: FunctionLikeDeclaration) {

View File

@ -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;
}

View File

@ -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))
}

View File

@ -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
> : ^^^^^
}

View File

@ -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;
}