Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions internal/checker/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -1967,19 +1967,19 @@ func (c *Checker) getSwitchClauseTypeOfWitnesses(node *ast.Node) []string {
links := c.switchStatementLinks.Get(node)
if !links.witnessesComputed {
clauses := node.AsSwitchStatement().CaseBlock.AsCaseBlock().Clauses.Nodes
witnesses := make([]string, len(clauses))
for i, clause := range clauses {
if clause.Kind == ast.KindCaseClause {
var text string
if ast.IsStringLiteralLike(clause.Expression()) {
text = clause.Expression().Text()
}
if text == "" {
witnesses = nil
break
}
if !slices.Contains(witnesses, text) {
witnesses[i] = text
// Return nil if one or more case clause expressions are not string literals.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot The original code had const witnesses: (string | undefined)[] = [];; I'm not exactly sure that we won't have other errors related to this? Surely empty text will still cause problems as the slice is still of strings

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The []string with "" as the undefined equivalent is the pre-existing design — default clauses and duplicate case values already produced "" entries in the witnesses slice before my change. All three consumers guard against empty strings:

  1. narrowTypeBySwitchOnTypeOf (line 1152): if text != "" → maps empty witnesses to neverType
  2. getNotEqualFactsFromTypeofSwitch (line 1996): witness != "" → skips empty witnesses
  3. computeExhaustiveSwitchStatement → delegates to getNotEqualFactsFromTypeofSwitch

So case "" now produces the same "" witness entry that default clauses and duplicates already do — meaning "no narrowing for this clause" — instead of aborting the entire witness array. The behavior matches the TS reference where undefined witnesses are simply skipped in all consuming code paths.

hasNonStringLiteral := core.Some(clauses, func(clause *ast.Node) bool {
return clause.Kind == ast.KindCaseClause && !ast.IsStringLiteralLike(clause.Expression())
})
var witnesses []string
if !hasNonStringLiteral {
witnesses = make([]string, len(clauses))
for i, clause := range clauses {
if clause.Kind == ast.KindCaseClause {
text := clause.Expression().Text()
if text != "" && !slices.Contains(witnesses, text) {
witnesses[i] = text
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
typeofSwitchEmptyStringCase.ts(3,10): error TS2678: Type '""' is not comparable to type '"bigint" | "boolean" | "function" | "number" | "object" | "string" | "symbol" | "undefined"'.


==== typeofSwitchEmptyStringCase.ts (1 errors) ====
function f(x: string | number) {
switch (typeof x) {
case "":
~~
!!! error TS2678: Type '""' is not comparable to type '"bigint" | "boolean" | "function" | "number" | "object" | "string" | "symbol" | "undefined"'.
case "string":
x.charAt(0);
break;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//// [tests/cases/compiler/typeofSwitchEmptyStringCase.ts] ////

//// [typeofSwitchEmptyStringCase.ts]
function f(x: string | number) {
switch (typeof x) {
case "":
case "string":
x.charAt(0);
break;
}
}


//// [typeofSwitchEmptyStringCase.js]
"use strict";
function f(x) {
switch (typeof x) {
case "":
case "string":
x.charAt(0);
break;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//// [tests/cases/compiler/typeofSwitchEmptyStringCase.ts] ////

=== typeofSwitchEmptyStringCase.ts ===
function f(x: string | number) {
>f : Symbol(f, Decl(typeofSwitchEmptyStringCase.ts, 0, 0))
>x : Symbol(x, Decl(typeofSwitchEmptyStringCase.ts, 0, 11))

switch (typeof x) {
>x : Symbol(x, Decl(typeofSwitchEmptyStringCase.ts, 0, 11))

case "":
case "string":
x.charAt(0);
>x.charAt : Symbol(String.charAt, Decl(lib.es5.d.ts, --, --))
>x : Symbol(x, Decl(typeofSwitchEmptyStringCase.ts, 0, 11))
>charAt : Symbol(String.charAt, Decl(lib.es5.d.ts, --, --))

break;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//// [tests/cases/compiler/typeofSwitchEmptyStringCase.ts] ////

=== typeofSwitchEmptyStringCase.ts ===
function f(x: string | number) {
>f : (x: string | number) => void
>x : string | number

switch (typeof x) {
>typeof x : "bigint" | "boolean" | "function" | "number" | "object" | "string" | "symbol" | "undefined"
>x : string | number

case "":
>"" : ""

case "string":
>"string" : "string"

x.charAt(0);
>x.charAt(0) : string
>x.charAt : (pos: number) => string
>x : string
>charAt : (pos: number) => string
>0 : 0

break;
}
}

10 changes: 10 additions & 0 deletions testdata/tests/cases/compiler/typeofSwitchEmptyStringCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @strict: true

function f(x: string | number) {
switch (typeof x) {
case "":
case "string":
x.charAt(0);
break;
}
}
Loading