From 8c5ad2429a8a437e5d075c7d8ddde85ab01f0a7e Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Fri, 27 Apr 2018 16:30:43 -0700 Subject: [PATCH] Relax switch-case narrowing restrictions (#23522) * Allow switch case narrowing even when individual clauses are nonunit * And remove unit type restriction * Rename --- src/compiler/checker.ts | 17 ++-- tests/baselines/reference/literalTypes1.types | 6 +- ...stringLiteralsWithSwitchStatements03.types | 2 +- ...gClausesEvenWhenNonMatchingClausesExist.js | 83 +++++++++++++++++++ ...sesEvenWhenNonMatchingClausesExist.symbols | 67 +++++++++++++++ ...ausesEvenWhenNonMatchingClausesExist.types | 79 ++++++++++++++++++ ...gClausesEvenWhenNonMatchingClausesExist.ts | 40 +++++++++ 7 files changed, 280 insertions(+), 14 deletions(-) create mode 100644 tests/baselines/reference/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.js create mode 100644 tests/baselines/reference/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.symbols create mode 100644 tests/baselines/reference/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.types create mode 100644 tests/cases/compiler/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 7d94d64aae7..ccdbadf4252 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11570,6 +11570,10 @@ namespace ts { return !!getPropertyOfType(type, "0" as __String); } + function isNeitherUnitTypeNorNever(type: Type): boolean { + return !(type.flags & (TypeFlags.Unit | TypeFlags.Never)); + } + function isUnitType(type: Type): boolean { return !!(type.flags & TypeFlags.Unit); } @@ -13049,8 +13053,7 @@ namespace ts { function getTypeOfSwitchClause(clause: CaseClause | DefaultClause) { if (clause.kind === SyntaxKind.CaseClause) { - const caseType = getRegularTypeOfLiteralType(getTypeOfExpression(clause.expression)); - return isUnitType(caseType) ? caseType : undefined; + return getRegularTypeOfLiteralType(getTypeOfExpression(clause.expression)); } return neverType; } @@ -13058,15 +13061,9 @@ namespace ts { function getSwitchClauseTypes(switchStatement: SwitchStatement): Type[] { const links = getNodeLinks(switchStatement); if (!links.switchTypes) { - // If all case clauses specify expressions that have unit types, we return an array - // of those unit types. Otherwise we return an empty array. links.switchTypes = []; for (const clause of switchStatement.caseBlock.clauses) { - const type = getTypeOfSwitchClause(clause); - if (type === undefined) { - return links.switchTypes = emptyArray; - } - links.switchTypes.push(type); + links.switchTypes.push(getTypeOfSwitchClause(clause)); } } return links.switchTypes; @@ -19170,7 +19167,7 @@ namespace ts { return false; } const switchTypes = getSwitchClauseTypes(node); - if (!switchTypes.length) { + if (!switchTypes.length || some(switchTypes, isNeitherUnitTypeNorNever)) { return false; } return eachTypeContainedIn(mapType(type, getRegularTypeOfLiteralType), switchTypes); diff --git a/tests/baselines/reference/literalTypes1.types b/tests/baselines/reference/literalTypes1.types index ed2df4ca14c..cb612cab4dd 100644 --- a/tests/baselines/reference/literalTypes1.types +++ b/tests/baselines/reference/literalTypes1.types @@ -61,19 +61,19 @@ function f2(x: 0 | 1 | 2) { >zero : 0 x; ->x : 0 | 1 | 2 +>x : 0 break; case oneOrTwo: >oneOrTwo : 1 | 2 x; ->x : 0 | 1 | 2 +>x : 1 | 2 break; default: x; ->x : 0 | 1 | 2 +>x : 1 | 2 } } diff --git a/tests/baselines/reference/stringLiteralsWithSwitchStatements03.types b/tests/baselines/reference/stringLiteralsWithSwitchStatements03.types index 2b4b3e0fae9..70f6d6cc973 100644 --- a/tests/baselines/reference/stringLiteralsWithSwitchStatements03.types +++ b/tests/baselines/reference/stringLiteralsWithSwitchStatements03.types @@ -51,7 +51,7 @@ switch (x) { >"baz" : "baz" x; ->x : "foo" +>x : never y; >y : "foo" | "bar" diff --git a/tests/baselines/reference/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.js b/tests/baselines/reference/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.js new file mode 100644 index 00000000000..800209fde7a --- /dev/null +++ b/tests/baselines/reference/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.js @@ -0,0 +1,83 @@ +//// [switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts] +export const narrowToLiterals = (str: string) => { + switch (str) { + case 'abc': { + // inferred type as `abc` + return str; + } + default: + return 'defaultValue'; + } + }; + + export const narrowToString = (str: string, someOtherStr: string) => { + switch (str) { + case 'abc': { + // inferred type should be `abc` + return str; + } + case someOtherStr: { + // `string` + return str; + } + default: + return 'defaultValue'; + } + }; + + export const narrowToStringOrNumber = (str: string | number, someNumber: number) => { + switch (str) { + case 'abc': { + // inferred type should be `abc` + return str; + } + case someNumber: { + // inferred type should be `number` + return str; + } + default: + return 'defaultValue'; + } + }; + +//// [switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.js] +"use strict"; +exports.__esModule = true; +exports.narrowToLiterals = function (str) { + switch (str) { + case 'abc': { + // inferred type as `abc` + return str; + } + default: + return 'defaultValue'; + } +}; +exports.narrowToString = function (str, someOtherStr) { + switch (str) { + case 'abc': { + // inferred type should be `abc` + return str; + } + case someOtherStr: { + // `string` + return str; + } + default: + return 'defaultValue'; + } +}; +exports.narrowToStringOrNumber = function (str, someNumber) { + switch (str) { + case 'abc': { + // inferred type should be `abc` + return str; + } + case someNumber: { + // inferred type should be `number` + return str; + } + default: + return 'defaultValue'; + } +}; diff --git a/tests/baselines/reference/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.symbols b/tests/baselines/reference/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.symbols new file mode 100644 index 00000000000..64b8ad44c97 --- /dev/null +++ b/tests/baselines/reference/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.symbols @@ -0,0 +1,67 @@ +=== tests/cases/compiler/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts === +export const narrowToLiterals = (str: string) => { +>narrowToLiterals : Symbol(narrowToLiterals, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 0, 12)) +>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 0, 33)) + + switch (str) { +>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 0, 33)) + + case 'abc': { + // inferred type as `abc` + return str; +>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 0, 33)) + } + default: + return 'defaultValue'; + } + }; + + export const narrowToString = (str: string, someOtherStr: string) => { +>narrowToString : Symbol(narrowToString, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 14)) +>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 33)) +>someOtherStr : Symbol(someOtherStr, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 45)) + + switch (str) { +>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 33)) + + case 'abc': { + // inferred type should be `abc` + return str; +>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 33)) + } + case someOtherStr: { +>someOtherStr : Symbol(someOtherStr, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 45)) + + // `string` + return str; +>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 33)) + } + default: + return 'defaultValue'; + } + }; + + export const narrowToStringOrNumber = (str: string | number, someNumber: number) => { +>narrowToStringOrNumber : Symbol(narrowToStringOrNumber, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 14)) +>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 41)) +>someNumber : Symbol(someNumber, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 62)) + + switch (str) { +>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 41)) + + case 'abc': { + // inferred type should be `abc` + return str; +>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 41)) + } + case someNumber: { +>someNumber : Symbol(someNumber, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 62)) + + // inferred type should be `number` + return str; +>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 41)) + } + default: + return 'defaultValue'; + } + }; diff --git a/tests/baselines/reference/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.types b/tests/baselines/reference/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.types new file mode 100644 index 00000000000..f8c14745075 --- /dev/null +++ b/tests/baselines/reference/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.types @@ -0,0 +1,79 @@ +=== tests/cases/compiler/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts === +export const narrowToLiterals = (str: string) => { +>narrowToLiterals : (str: string) => "abc" | "defaultValue" +>(str: string) => { switch (str) { case 'abc': { // inferred type as `abc` return str; } default: return 'defaultValue'; } } : (str: string) => "abc" | "defaultValue" +>str : string + + switch (str) { +>str : string + + case 'abc': { +>'abc' : "abc" + + // inferred type as `abc` + return str; +>str : "abc" + } + default: + return 'defaultValue'; +>'defaultValue' : "defaultValue" + } + }; + + export const narrowToString = (str: string, someOtherStr: string) => { +>narrowToString : (str: string, someOtherStr: string) => string +>(str: string, someOtherStr: string) => { switch (str) { case 'abc': { // inferred type should be `abc` return str; } case someOtherStr: { // `string` return str; } default: return 'defaultValue'; } } : (str: string, someOtherStr: string) => string +>str : string +>someOtherStr : string + + switch (str) { +>str : string + + case 'abc': { +>'abc' : "abc" + + // inferred type should be `abc` + return str; +>str : "abc" + } + case someOtherStr: { +>someOtherStr : string + + // `string` + return str; +>str : string + } + default: + return 'defaultValue'; +>'defaultValue' : "defaultValue" + } + }; + + export const narrowToStringOrNumber = (str: string | number, someNumber: number) => { +>narrowToStringOrNumber : (str: string | number, someNumber: number) => number | "abc" | "defaultValue" +>(str: string | number, someNumber: number) => { switch (str) { case 'abc': { // inferred type should be `abc` return str; } case someNumber: { // inferred type should be `number` return str; } default: return 'defaultValue'; } } : (str: string | number, someNumber: number) => number | "abc" | "defaultValue" +>str : string | number +>someNumber : number + + switch (str) { +>str : string | number + + case 'abc': { +>'abc' : "abc" + + // inferred type should be `abc` + return str; +>str : "abc" + } + case someNumber: { +>someNumber : number + + // inferred type should be `number` + return str; +>str : number + } + default: + return 'defaultValue'; +>'defaultValue' : "defaultValue" + } + }; diff --git a/tests/cases/compiler/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts b/tests/cases/compiler/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts new file mode 100644 index 00000000000..2d3ebdd1e2a --- /dev/null +++ b/tests/cases/compiler/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts @@ -0,0 +1,40 @@ +export const narrowToLiterals = (str: string) => { + switch (str) { + case 'abc': { + // inferred type as `abc` + return str; + } + default: + return 'defaultValue'; + } + }; + + export const narrowToString = (str: string, someOtherStr: string) => { + switch (str) { + case 'abc': { + // inferred type should be `abc` + return str; + } + case someOtherStr: { + // `string` + return str; + } + default: + return 'defaultValue'; + } + }; + + export const narrowToStringOrNumber = (str: string | number, someNumber: number) => { + switch (str) { + case 'abc': { + // inferred type should be `abc` + return str; + } + case someNumber: { + // inferred type should be `number` + return str; + } + default: + return 'defaultValue'; + } + }; \ No newline at end of file