From aeb0a9b235599ba0700e9cae4e7bf7be004979a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 19:42:28 +0000 Subject: [PATCH 1/2] Initial plan From 5809c7f7b45673a19d05e7e1da356bd315c30e5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 20:27:11 +0000 Subject: [PATCH 2/2] Fix type narrowing loss in switch(typeof x) when case "" is present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In getSwitchClauseTypeOfWitnesses, an empty string literal value (case "") was incorrectly treated the same as a non-string-literal expression, causing the function to return nil and disabling all type narrowing. The fix separates the two concerns: 1. Non-string-literal case expressions → return nil (no witnesses) 2. Empty string literal values → treat as empty witness (no narrowing for that clause, but other clauses still narrow correctly) This matches the TypeScript reference implementation behavior. Fixes microsoft/typescript-go#1495 Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/493d3b3e-abfa-47c0-a3d4-e5101635afc7 Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com> --- internal/checker/flow.go | 26 ++++++++--------- .../typeofSwitchEmptyStringCase.errors.txt | 15 ++++++++++ .../compiler/typeofSwitchEmptyStringCase.js | 23 +++++++++++++++ .../typeofSwitchEmptyStringCase.symbols | 21 ++++++++++++++ .../typeofSwitchEmptyStringCase.types | 28 +++++++++++++++++++ .../compiler/typeofSwitchEmptyStringCase.ts | 10 +++++++ 6 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.errors.txt create mode 100644 testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.js create mode 100644 testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.symbols create mode 100644 testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.types create mode 100644 testdata/tests/cases/compiler/typeofSwitchEmptyStringCase.ts diff --git a/internal/checker/flow.go b/internal/checker/flow.go index 84c0d81e8f..ef52e6c347 100644 --- a/internal/checker/flow.go +++ b/internal/checker/flow.go @@ -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. + 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 + } } } } diff --git a/testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.errors.txt b/testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.errors.txt new file mode 100644 index 0000000000..6c047fc3e6 --- /dev/null +++ b/testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.errors.txt @@ -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; + } + } + \ No newline at end of file diff --git a/testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.js b/testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.js new file mode 100644 index 0000000000..41e3b00924 --- /dev/null +++ b/testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.js @@ -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; + } +} diff --git a/testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.symbols b/testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.symbols new file mode 100644 index 0000000000..f6ed238e59 --- /dev/null +++ b/testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.symbols @@ -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; + } +} + diff --git a/testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.types b/testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.types new file mode 100644 index 0000000000..90534606d6 --- /dev/null +++ b/testdata/baselines/reference/compiler/typeofSwitchEmptyStringCase.types @@ -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; + } +} + diff --git a/testdata/tests/cases/compiler/typeofSwitchEmptyStringCase.ts b/testdata/tests/cases/compiler/typeofSwitchEmptyStringCase.ts new file mode 100644 index 0000000000..3b86a94a89 --- /dev/null +++ b/testdata/tests/cases/compiler/typeofSwitchEmptyStringCase.ts @@ -0,0 +1,10 @@ +// @strict: true + +function f(x: string | number) { + switch (typeof x) { + case "": + case "string": + x.charAt(0); + break; + } +}