mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-04 12:32:08 -06:00
Implement fix for exhaustiveness checking on non-union types
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
This commit is contained in:
parent
8d3be0e510
commit
5d94666678
@ -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) {
|
||||
|
||||
167
tests/baselines/reference/exhaustiveChecksForNonUnionTypes.js
Normal file
167
tests/baselines/reference/exhaustiveChecksForNonUnionTypes.js
Normal 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;
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
266
tests/baselines/reference/exhaustiveChecksForNonUnionTypes.types
Normal file
266
tests/baselines/reference/exhaustiveChecksForNonUnionTypes.types
Normal 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
|
||||
> : ^^^^^
|
||||
}
|
||||
87
tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts
Normal file
87
tests/cases/compiler/exhaustiveChecksForNonUnionTypes.ts
Normal 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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user