Skip to content

Fix #10991: Add improved error messages for comparison with narrowed types #22319

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
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
16 changes: 16 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
@@ -19260,6 +19260,22 @@ namespace ts {
}
if (!isTypeEqualityComparableTo(leftType, rightType) && !isTypeEqualityComparableTo(rightType, leftType)) {
reportOperatorError();
const leftDeclaredType = getDeclaredTypeOfReference(left) || leftType;
const rightDeclaredType = getDeclaredTypeOfReference(right) || rightType;
if (isTypeEqualityComparableTo(leftDeclaredType, rightDeclaredType) || isTypeEqualityComparableTo(rightDeclaredType, leftDeclaredType)) {
if (leftDeclaredType === leftType) {
error(errorNode || operatorToken,
Diagnostics.Earlier_code_in_this_block_narrowed_type_0_to_1_making_this_operation_invalid,
typeToString(rightDeclaredType),
typeToString(rightType));
}
else {
error(errorNode || operatorToken,
Diagnostics.Earlier_code_in_this_block_narrowed_type_0_to_1_making_this_operation_invalid,
typeToString(leftDeclaredType),
typeToString(leftType));
}
}
}
return booleanType;
case SyntaxKind.InstanceOfKeyword:
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
@@ -2321,6 +2321,10 @@
"category": "Error",
"code": 2724
},
"Earlier code in this block narrowed type '{0}' to '{1}' making this operation invalid.": {
"category": "Error",
"code": 2725
},
"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
"code": 4000
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts(13,9): error TS2365: Operator '===' cannot be applied to types 'Keys.Shift' and 'Keys.Tab'.
tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts(13,9): error TS2725: Earlier code in this block narrowed type 'Keys' to 'Keys.Shift' making this operation invalid.
tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts(28,12): error TS2365: Operator '===' cannot be applied to types 'string' and 'number'.
tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts(28,12): error TS2725: Earlier code in this block narrowed type 'string | number' to 'string' making this operation invalid.
tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts(47,12): error TS2365: Operator '===' cannot be applied to types 'number' and 'string'.
tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts(47,12): error TS2725: Earlier code in this block narrowed type 'string | number' to 'string' making this operation invalid.
tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts(55,9): error TS2365: Operator '===' cannot be applied to types 'false' and 'true'.
tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts(55,9): error TS2725: Earlier code in this block narrowed type 'boolean' to 'false' making this operation invalid.
tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts(60,9): error TS2365: Operator '===' cannot be applied to types 'false' and 'true'.
tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts(60,9): error TS2725: Earlier code in this block narrowed type 'boolean' to 'false' making this operation invalid.


==== tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts (10 errors) ====
const enum Keys {
Tab = 10,
Shift = 13
}

function enumType() {
let key: Keys;

if (key === Keys.Tab) {
return;
}

if (key === Keys.Tab || key === Keys.Shift) {
~~~~~~~~~~~~~~~~
!!! error TS2365: Operator '===' cannot be applied to types 'Keys.Shift' and 'Keys.Tab'.
~~~~~~~~~~~~~~~~
!!! error TS2725: Earlier code in this block narrowed type 'Keys' to 'Keys.Shift' making this operation invalid.
return;
}
}


function unionType() {
let x!: string | number;
let y!: string | number;
if (typeof x === 'number') {
return;
}
if (typeof y === 'string') {
return;
}
return x === y;
~~~~~~~
!!! error TS2365: Operator '===' cannot be applied to types 'string' and 'number'.
~~~~~~~
!!! error TS2725: Earlier code in this block narrowed type 'string | number' to 'string' making this operation invalid.
}

interface Left {
kind: 'left'
data: number;
}

interface Right {
kind: 'right'
data: string;
}

function discriminatedType() {
let x!: Left;
let y!: Left | Right;
if (y.kind === 'left') {
return;
}
return x.data === y.data;
~~~~~~~~~~~~~~~~~
!!! error TS2365: Operator '===' cannot be applied to types 'number' and 'string'.
~~~~~~~~~~~~~~~~~
!!! error TS2725: Earlier code in this block narrowed type 'string | number' to 'string' making this operation invalid.
}


function booleanType(bar: boolean) {
if (bar === true) {
return "true";
}
if (bar === true) {
~~~~~~~~~~~~
!!! error TS2365: Operator '===' cannot be applied to types 'false' and 'true'.
~~~~~~~~~~~~
!!! error TS2725: Earlier code in this block narrowed type 'boolean' to 'false' making this operation invalid.
return "false";
}

const f: boolean = false;
if (f === true) {
~~~~~~~~~~
!!! error TS2365: Operator '===' cannot be applied to types 'false' and 'true'.
~~~~~~~~~~
!!! error TS2725: Earlier code in this block narrowed type 'boolean' to 'false' making this operation invalid.
return "false";
}
}

107 changes: 107 additions & 0 deletions tests/baselines/reference/operatorErrorsDetectedByNarrowingMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//// [operatorErrorsDetectedByNarrowingMessage.ts]
const enum Keys {
Tab = 10,
Shift = 13
}

function enumType() {
let key: Keys;

if (key === Keys.Tab) {
return;
}

if (key === Keys.Tab || key === Keys.Shift) {
return;
}
}


function unionType() {
let x!: string | number;
let y!: string | number;
if (typeof x === 'number') {
return;
}
if (typeof y === 'string') {
return;
}
return x === y;
}

interface Left {
kind: 'left'
data: number;
}

interface Right {
kind: 'right'
data: string;
}

function discriminatedType() {
let x!: Left;
let y!: Left | Right;
if (y.kind === 'left') {
return;
}
return x.data === y.data;
}


function booleanType(bar: boolean) {
if (bar === true) {
return "true";
}
if (bar === true) {
return "false";
}

const f: boolean = false;
if (f === true) {
return "false";
}
}


//// [operatorErrorsDetectedByNarrowingMessage.js]
function enumType() {
var key;
if (key === 10 /* Tab */) {
return;
}
if (key === 10 /* Tab */ || key === 13 /* Shift */) {
return;
}
}
function unionType() {
var x;
var y;
if (typeof x === 'number') {
return;
}
if (typeof y === 'string') {
return;
}
return x === y;
}
function discriminatedType() {
var x;
var y;
if (y.kind === 'left') {
return;
}
return x.data === y.data;
}
function booleanType(bar) {
if (bar === true) {
return "true";
}
if (bar === true) {
return "false";
}
var f = false;
if (f === true) {
return "false";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
=== tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts ===
const enum Keys {
>Keys : Symbol(Keys, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 0, 0))

Tab = 10,
>Tab : Symbol(Keys.Tab, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 0, 17))

Shift = 13
>Shift : Symbol(Keys.Shift, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 1, 13))
}

function enumType() {
>enumType : Symbol(enumType, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 3, 1))

let key: Keys;
>key : Symbol(key, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 6, 7))
>Keys : Symbol(Keys, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 0, 0))

if (key === Keys.Tab) {
>key : Symbol(key, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 6, 7))
>Keys.Tab : Symbol(Keys.Tab, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 0, 17))
>Keys : Symbol(Keys, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 0, 0))
>Tab : Symbol(Keys.Tab, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 0, 17))

return;
}

if (key === Keys.Tab || key === Keys.Shift) {
>key : Symbol(key, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 6, 7))
>Keys.Tab : Symbol(Keys.Tab, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 0, 17))
>Keys : Symbol(Keys, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 0, 0))
>Tab : Symbol(Keys.Tab, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 0, 17))
>key : Symbol(key, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 6, 7))
>Keys.Shift : Symbol(Keys.Shift, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 1, 13))
>Keys : Symbol(Keys, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 0, 0))
>Shift : Symbol(Keys.Shift, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 1, 13))

return;
}
}


function unionType() {
>unionType : Symbol(unionType, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 15, 1))

let x!: string | number;
>x : Symbol(x, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 19, 7))

let y!: string | number;
>y : Symbol(y, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 20, 7))

if (typeof x === 'number') {
>x : Symbol(x, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 19, 7))

return;
}
if (typeof y === 'string') {
>y : Symbol(y, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 20, 7))

return;
}
return x === y;
>x : Symbol(x, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 19, 7))
>y : Symbol(y, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 20, 7))
}

interface Left {
>Left : Symbol(Left, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 28, 1))

kind: 'left'
>kind : Symbol(Left.kind, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 30, 16))

data: number;
>data : Symbol(Left.data, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 31, 16))
}

interface Right {
>Right : Symbol(Right, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 33, 1))

kind: 'right'
>kind : Symbol(Right.kind, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 35, 17))

data: string;
>data : Symbol(Right.data, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 36, 17))
}

function discriminatedType() {
>discriminatedType : Symbol(discriminatedType, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 38, 1))

let x!: Left;
>x : Symbol(x, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 41, 7))
>Left : Symbol(Left, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 28, 1))

let y!: Left | Right;
>y : Symbol(y, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 42, 7))
>Left : Symbol(Left, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 28, 1))
>Right : Symbol(Right, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 33, 1))

if (y.kind === 'left') {
>y.kind : Symbol(kind, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 30, 16), Decl(operatorErrorsDetectedByNarrowingMessage.ts, 35, 17))
>y : Symbol(y, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 42, 7))
>kind : Symbol(kind, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 30, 16), Decl(operatorErrorsDetectedByNarrowingMessage.ts, 35, 17))

return;
}
return x.data === y.data;
>x.data : Symbol(Left.data, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 31, 16))
>x : Symbol(x, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 41, 7))
>data : Symbol(Left.data, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 31, 16))
>y.data : Symbol(Right.data, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 36, 17))
>y : Symbol(y, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 42, 7))
>data : Symbol(Right.data, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 36, 17))
}


function booleanType(bar: boolean) {
>booleanType : Symbol(booleanType, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 47, 1))
>bar : Symbol(bar, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 50, 21))

if (bar === true) {
>bar : Symbol(bar, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 50, 21))

return "true";
}
if (bar === true) {
>bar : Symbol(bar, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 50, 21))

return "false";
}

const f: boolean = false;
>f : Symbol(f, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 58, 9))

if (f === true) {
>f : Symbol(f, Decl(operatorErrorsDetectedByNarrowingMessage.ts, 58, 9))

return "false";
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
=== tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts ===
const enum Keys {
>Keys : Keys

Tab = 10,
>Tab : Keys.Tab
>10 : 10

Shift = 13
>Shift : Keys.Shift
>13 : 13
}

function enumType() {
>enumType : () => void

let key: Keys;
>key : Keys
>Keys : Keys

if (key === Keys.Tab) {
>key === Keys.Tab : boolean
>key : Keys
>Keys.Tab : Keys.Tab
>Keys : typeof Keys
>Tab : Keys.Tab

return;
}

if (key === Keys.Tab || key === Keys.Shift) {
>key === Keys.Tab || key === Keys.Shift : boolean
>key === Keys.Tab : boolean
>key : Keys.Shift
>Keys.Tab : Keys.Tab
>Keys : typeof Keys
>Tab : Keys.Tab
>key === Keys.Shift : boolean
>key : Keys.Shift
>Keys.Shift : Keys.Shift
>Keys : typeof Keys
>Shift : Keys.Shift

return;
}
}


function unionType() {
>unionType : () => boolean

let x!: string | number;
>x : string | number

let y!: string | number;
>y : string | number

if (typeof x === 'number') {
>typeof x === 'number' : boolean
>typeof x : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
>x : string | number
>'number' : "number"

return;
}
if (typeof y === 'string') {
>typeof y === 'string' : boolean
>typeof y : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
>y : string | number
>'string' : "string"

return;
}
return x === y;
>x === y : boolean
>x : string
>y : number
}

interface Left {
>Left : Left

kind: 'left'
>kind : "left"

data: number;
>data : number
}

interface Right {
>Right : Right

kind: 'right'
>kind : "right"

data: string;
>data : string
}

function discriminatedType() {
>discriminatedType : () => boolean

let x!: Left;
>x : Left
>Left : Left

let y!: Left | Right;
>y : Left | Right
>Left : Left
>Right : Right

if (y.kind === 'left') {
>y.kind === 'left' : boolean
>y.kind : "left" | "right"
>y : Left | Right
>kind : "left" | "right"
>'left' : "left"

return;
}
return x.data === y.data;
>x.data === y.data : boolean
>x.data : number
>x : Left
>data : number
>y.data : string
>y : Right
>data : string
}


function booleanType(bar: boolean) {
>booleanType : (bar: boolean) => "true" | "false"
>bar : boolean

if (bar === true) {
>bar === true : boolean
>bar : boolean
>true : true

return "true";
>"true" : "true"
}
if (bar === true) {
>bar === true : boolean
>bar : false
>true : true

return "false";
>"false" : "false"
}

const f: boolean = false;
>f : boolean
>false : false

if (f === true) {
>f === true : boolean
>f : false
>true : true

return "false";
>"false" : "false"
}
}

63 changes: 63 additions & 0 deletions tests/cases/compiler/operatorErrorsDetectedByNarrowingMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const enum Keys {
Tab = 10,
Shift = 13
}

function enumType() {
let key: Keys;

if (key === Keys.Tab) {
return;
}

if (key === Keys.Tab || key === Keys.Shift) {
return;
}
}


function unionType() {
let x!: string | number;
let y!: string | number;
if (typeof x === 'number') {
return;
}
if (typeof y === 'string') {
return;
}
return x === y;
}

interface Left {
kind: 'left'
data: number;
}

interface Right {
kind: 'right'
data: string;
}

function discriminatedType() {
let x!: Left;
let y!: Left | Right;
if (y.kind === 'left') {
return;
}
return x.data === y.data;
}


function booleanType(bar: boolean) {
if (bar === true) {
return "true";
}
if (bar === true) {
return "false";
}

const f: boolean = false;
if (f === true) {
return "false";
}
}