Fix "never nullish" diagnostic missing expressions wrapped in parentheses (#62789)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
Co-authored-by: Ryan Cavanaugh <ryanca@microsoft.com>
Co-authored-by: Ryan Cavanaugh <RyanCavanaugh@users.noreply.github.com>
This commit is contained in:
Copilot 2026-01-09 18:57:02 +00:00 committed by GitHub
parent 632479f28d
commit c574e4090d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 320 additions and 52 deletions

View File

@ -40548,12 +40548,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
checkNullishCoalesceOperandLeft(node);
checkNullishCoalesceOperandRight(node);
}
function checkNullishCoalesceOperandLeft(node: BinaryExpression) {
const leftTarget = skipOuterExpressions(node.left, OuterExpressionKinds.All);
const nullishSemantics = getSyntacticNullishnessSemantics(leftTarget);
if (nullishSemantics !== PredicateSemantics.Sometimes) {
if (nullishSemantics === PredicateSemantics.Always) {
@ -40565,25 +40563,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
function checkNullishCoalesceOperandRight(node: BinaryExpression) {
const rightTarget = skipOuterExpressions(node.right, OuterExpressionKinds.All);
const nullishSemantics = getSyntacticNullishnessSemantics(rightTarget);
if (isNotWithinNullishCoalesceExpression(node)) {
return;
}
if (nullishSemantics === PredicateSemantics.Always) {
error(rightTarget, Diagnostics.This_expression_is_always_nullish);
}
else if (nullishSemantics === PredicateSemantics.Never) {
error(rightTarget, Diagnostics.This_expression_is_never_nullish);
}
}
function isNotWithinNullishCoalesceExpression(node: BinaryExpression) {
return !isBinaryExpression(node.parent) || node.parent.operatorToken.kind !== SyntaxKind.QuestionQuestionToken;
}
function getSyntacticNullishnessSemantics(node: Node): PredicateSemantics {
node = skipOuterExpressions(node);
switch (node.kind) {
@ -40601,15 +40580,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// List of operators that can produce null/undefined:
// = ??= ?? || ||= && &&=
switch ((node as BinaryExpression).operatorToken.kind) {
case SyntaxKind.EqualsToken:
case SyntaxKind.QuestionQuestionToken:
case SyntaxKind.QuestionQuestionEqualsToken:
case SyntaxKind.BarBarToken:
case SyntaxKind.BarBarEqualsToken:
case SyntaxKind.AmpersandAmpersandToken:
case SyntaxKind.AmpersandAmpersandEqualsToken:
return PredicateSemantics.Sometimes;
// For these operator kinds, the right operand is effectively controlling
case SyntaxKind.CommaToken:
case SyntaxKind.EqualsToken:
case SyntaxKind.QuestionQuestionToken:
case SyntaxKind.QuestionQuestionEqualsToken:
return getSyntacticNullishnessSemantics((node as BinaryExpression).right);
}
return PredicateSemantics.Never;

View File

@ -0,0 +1,44 @@
neverNullishThroughParentheses.ts(6,13): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(7,14): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(10,15): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(11,16): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(14,15): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(15,16): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(16,17): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
neverNullishThroughParentheses.ts(16,17): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
==== neverNullishThroughParentheses.ts (8 errors) ====
// Repro for issue where "never nullish" checks miss "never nullish" through parentheses
const x: { y: string | undefined } | undefined = undefined as any;
// Both should error - both expressions are guaranteed to be "oops"
const foo = x?.y ?? `oops` ?? "";
~~~~~~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
const bar = (x?.y ?? `oops`) ?? "";
~~~~~~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
// Additional test cases with various levels of nesting
const baz = ((x?.y ?? `oops`)) ?? "";
~~~~~~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
const qux = (((x?.y ?? `oops`))) ?? "";
~~~~~~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
// Test with different types
const str1 = ("literal") ?? "fallback";
~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
const str2 = (("nested")) ?? "fallback";
~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
const nested = ("a" ?? "b") ?? "c";
~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
~~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.

View File

@ -0,0 +1,36 @@
//// [tests/cases/compiler/neverNullishThroughParentheses.ts] ////
//// [neverNullishThroughParentheses.ts]
// Repro for issue where "never nullish" checks miss "never nullish" through parentheses
const x: { y: string | undefined } | undefined = undefined as any;
// Both should error - both expressions are guaranteed to be "oops"
const foo = x?.y ?? `oops` ?? "";
const bar = (x?.y ?? `oops`) ?? "";
// Additional test cases with various levels of nesting
const baz = ((x?.y ?? `oops`)) ?? "";
const qux = (((x?.y ?? `oops`))) ?? "";
// Test with different types
const str1 = ("literal") ?? "fallback";
const str2 = (("nested")) ?? "fallback";
const nested = ("a" ?? "b") ?? "c";
//// [neverNullishThroughParentheses.js]
"use strict";
// Repro for issue where "never nullish" checks miss "never nullish" through parentheses
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
var x = undefined;
// Both should error - both expressions are guaranteed to be "oops"
var foo = (_b = (_a = x === null || x === void 0 ? void 0 : x.y) !== null && _a !== void 0 ? _a : "oops") !== null && _b !== void 0 ? _b : "";
var bar = (_d = ((_c = x === null || x === void 0 ? void 0 : x.y) !== null && _c !== void 0 ? _c : "oops")) !== null && _d !== void 0 ? _d : "";
// Additional test cases with various levels of nesting
var baz = (_f = (((_e = x === null || x === void 0 ? void 0 : x.y) !== null && _e !== void 0 ? _e : "oops"))) !== null && _f !== void 0 ? _f : "";
var qux = (_h = ((((_g = x === null || x === void 0 ? void 0 : x.y) !== null && _g !== void 0 ? _g : "oops")))) !== null && _h !== void 0 ? _h : "";
// Test with different types
var str1 = (_j = ("literal")) !== null && _j !== void 0 ? _j : "fallback";
var str2 = (_k = (("nested"))) !== null && _k !== void 0 ? _k : "fallback";
var nested = (_l = ("a" !== null && "a" !== void 0 ? "a" : "b")) !== null && _l !== void 0 ? _l : "c";

View File

@ -0,0 +1,46 @@
//// [tests/cases/compiler/neverNullishThroughParentheses.ts] ////
=== neverNullishThroughParentheses.ts ===
// Repro for issue where "never nullish" checks miss "never nullish" through parentheses
const x: { y: string | undefined } | undefined = undefined as any;
>x : Symbol(x, Decl(neverNullishThroughParentheses.ts, 2, 5))
>y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
>undefined : Symbol(undefined)
// Both should error - both expressions are guaranteed to be "oops"
const foo = x?.y ?? `oops` ?? "";
>foo : Symbol(foo, Decl(neverNullishThroughParentheses.ts, 5, 5))
>x?.y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
>x : Symbol(x, Decl(neverNullishThroughParentheses.ts, 2, 5))
>y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
const bar = (x?.y ?? `oops`) ?? "";
>bar : Symbol(bar, Decl(neverNullishThroughParentheses.ts, 6, 5))
>x?.y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
>x : Symbol(x, Decl(neverNullishThroughParentheses.ts, 2, 5))
>y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
// Additional test cases with various levels of nesting
const baz = ((x?.y ?? `oops`)) ?? "";
>baz : Symbol(baz, Decl(neverNullishThroughParentheses.ts, 9, 5))
>x?.y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
>x : Symbol(x, Decl(neverNullishThroughParentheses.ts, 2, 5))
>y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
const qux = (((x?.y ?? `oops`))) ?? "";
>qux : Symbol(qux, Decl(neverNullishThroughParentheses.ts, 10, 5))
>x?.y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
>x : Symbol(x, Decl(neverNullishThroughParentheses.ts, 2, 5))
>y : Symbol(y, Decl(neverNullishThroughParentheses.ts, 2, 10))
// Test with different types
const str1 = ("literal") ?? "fallback";
>str1 : Symbol(str1, Decl(neverNullishThroughParentheses.ts, 13, 5))
const str2 = (("nested")) ?? "fallback";
>str2 : Symbol(str2, Decl(neverNullishThroughParentheses.ts, 14, 5))
const nested = ("a" ?? "b") ?? "c";
>nested : Symbol(nested, Decl(neverNullishThroughParentheses.ts, 15, 5))

View File

@ -0,0 +1,144 @@
//// [tests/cases/compiler/neverNullishThroughParentheses.ts] ////
=== neverNullishThroughParentheses.ts ===
// Repro for issue where "never nullish" checks miss "never nullish" through parentheses
const x: { y: string | undefined } | undefined = undefined as any;
>x : { y: string | undefined; } | undefined
> : ^^^^^ ^^^^^^^^^^^^^^^
>y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>undefined as any : any
> : ^^^
>undefined : undefined
> : ^^^^^^^^^
// Both should error - both expressions are guaranteed to be "oops"
const foo = x?.y ?? `oops` ?? "";
>foo : string
> : ^^^^^^
>x?.y ?? `oops` ?? "" : string
> : ^^^^^^
>x?.y ?? `oops` : string
> : ^^^^^^
>x?.y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>x : { y: string | undefined; } | undefined
> : ^^^^^ ^^^^^^^^^^^^^^^
>y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>`oops` : "oops"
> : ^^^^^^
>"" : ""
> : ^^
const bar = (x?.y ?? `oops`) ?? "";
>bar : string
> : ^^^^^^
>(x?.y ?? `oops`) ?? "" : string
> : ^^^^^^
>(x?.y ?? `oops`) : string
> : ^^^^^^
>x?.y ?? `oops` : string
> : ^^^^^^
>x?.y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>x : { y: string | undefined; } | undefined
> : ^^^^^ ^^^^^^^^^^^^^^^
>y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>`oops` : "oops"
> : ^^^^^^
>"" : ""
> : ^^
// Additional test cases with various levels of nesting
const baz = ((x?.y ?? `oops`)) ?? "";
>baz : string
> : ^^^^^^
>((x?.y ?? `oops`)) ?? "" : string
> : ^^^^^^
>((x?.y ?? `oops`)) : string
> : ^^^^^^
>(x?.y ?? `oops`) : string
> : ^^^^^^
>x?.y ?? `oops` : string
> : ^^^^^^
>x?.y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>x : { y: string | undefined; } | undefined
> : ^^^^^ ^^^^^^^^^^^^^^^
>y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>`oops` : "oops"
> : ^^^^^^
>"" : ""
> : ^^
const qux = (((x?.y ?? `oops`))) ?? "";
>qux : string
> : ^^^^^^
>(((x?.y ?? `oops`))) ?? "" : string
> : ^^^^^^
>(((x?.y ?? `oops`))) : string
> : ^^^^^^
>((x?.y ?? `oops`)) : string
> : ^^^^^^
>(x?.y ?? `oops`) : string
> : ^^^^^^
>x?.y ?? `oops` : string
> : ^^^^^^
>x?.y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>x : { y: string | undefined; } | undefined
> : ^^^^^ ^^^^^^^^^^^^^^^
>y : string | undefined
> : ^^^^^^^^^^^^^^^^^^
>`oops` : "oops"
> : ^^^^^^
>"" : ""
> : ^^
// Test with different types
const str1 = ("literal") ?? "fallback";
>str1 : "literal"
> : ^^^^^^^^^
>("literal") ?? "fallback" : "literal"
> : ^^^^^^^^^
>("literal") : "literal"
> : ^^^^^^^^^
>"literal" : "literal"
> : ^^^^^^^^^
>"fallback" : "fallback"
> : ^^^^^^^^^^
const str2 = (("nested")) ?? "fallback";
>str2 : "nested"
> : ^^^^^^^^
>(("nested")) ?? "fallback" : "nested"
> : ^^^^^^^^
>(("nested")) : "nested"
> : ^^^^^^^^
>("nested") : "nested"
> : ^^^^^^^^
>"nested" : "nested"
> : ^^^^^^^^
>"fallback" : "fallback"
> : ^^^^^^^^^^
const nested = ("a" ?? "b") ?? "c";
>nested : "a"
> : ^^^
>("a" ?? "b") ?? "c" : "a"
> : ^^^
>("a" ?? "b") : "a"
> : ^^^
>"a" ?? "b" : "a"
> : ^^^
>"a" : "a"
> : ^^^
>"b" : "b"
> : ^^^
>"c" : "c"
> : ^^^

View File

@ -7,26 +7,26 @@ predicateSemantics.ts(29,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(30,13): error TS2872: This kind of expression is always truthy.
predicateSemantics.ts(31,13): error TS2872: This kind of expression is always truthy.
predicateSemantics.ts(32,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(32,21): error TS2871: This expression is always nullish.
predicateSemantics.ts(32,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(33,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(34,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(34,22): error TS2871: This expression is always nullish.
predicateSemantics.ts(36,20): error TS2871: This expression is always nullish.
predicateSemantics.ts(37,20): error TS2871: This expression is always nullish.
predicateSemantics.ts(34,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(36,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(37,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(38,21): error TS2871: This expression is always nullish.
predicateSemantics.ts(39,21): error TS2871: This expression is always nullish.
predicateSemantics.ts(40,21): error TS2871: This expression is always nullish.
predicateSemantics.ts(40,29): error TS2871: This expression is always nullish.
predicateSemantics.ts(41,21): error TS2871: This expression is always nullish.
predicateSemantics.ts(42,20): error TS2881: This expression is never nullish.
predicateSemantics.ts(43,21): error TS2881: This expression is never nullish.
predicateSemantics.ts(40,21): error TS2871: This expression is always nullish.
predicateSemantics.ts(41,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(42,13): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
predicateSemantics.ts(43,13): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
predicateSemantics.ts(45,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(45,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(45,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(45,21): error TS2871: This expression is always nullish.
predicateSemantics.ts(45,29): error TS2871: This expression is always nullish.
predicateSemantics.ts(46,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(46,21): error TS2881: This expression is never nullish.
predicateSemantics.ts(46,13): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
predicateSemantics.ts(47,13): error TS2871: This expression is always nullish.
predicateSemantics.ts(47,22): error TS2881: This expression is never nullish.
predicateSemantics.ts(47,13): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
predicateSemantics.ts(50,8): error TS2872: This kind of expression is always truthy.
predicateSemantics.ts(51,11): error TS2872: This kind of expression is always truthy.
predicateSemantics.ts(52,8): error TS2872: This kind of expression is always truthy.
@ -89,7 +89,7 @@ predicateSemantics.ts(90,1): error TS2869: Right operand of ?? is unreachable be
const p07 = null ?? null ?? null;
~~~~
!!! error TS2871: This expression is always nullish.
~~~~
~~~~~~~~~~~~
!!! error TS2871: This expression is always nullish.
const p08 = null ?? opt ?? null;
~~~~
@ -97,14 +97,14 @@ predicateSemantics.ts(90,1): error TS2869: Right operand of ?? is unreachable be
const p09 = null ?? (opt ? null : undefined) ?? null;
~~~~
!!! error TS2871: This expression is always nullish.
~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2871: This expression is always nullish.
const p10 = opt ?? null ?? 1;
~~~~
~~~~~~~~~~~
!!! error TS2871: This expression is always nullish.
const p11 = opt ?? null ?? null;
~~~~
~~~~~~~~~~~
!!! error TS2871: This expression is always nullish.
const p12 = opt ?? (null ?? 1);
~~~~
@ -115,35 +115,35 @@ predicateSemantics.ts(90,1): error TS2869: Right operand of ?? is unreachable be
const p14 = opt ?? (null ?? null ?? null);
~~~~
!!! error TS2871: This expression is always nullish.
~~~~
~~~~~~~~~~~~
!!! error TS2871: This expression is always nullish.
const p15 = opt ?? (opt ? null : undefined) ?? null;
~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2871: This expression is always nullish.
const p16 = opt ?? 1 ?? 2;
~
!!! error TS2881: This expression is never nullish.
~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
const p17 = opt ?? (opt ? 1 : 2) ?? 3;
~~~~~~~~~~~
!!! error TS2881: This expression is never nullish.
~~~~~~~~~~~~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
const p21 = null ?? null ?? null ?? null;
~~~~
!!! error TS2871: This expression is always nullish.
~~~~
~~~~~~~~~~~~
!!! error TS2871: This expression is always nullish.
~~~~
~~~~~~~~~~~~~~~~~~~~
!!! error TS2871: This expression is always nullish.
const p22 = null ?? 1 ?? 1;
~~~~
!!! error TS2871: This expression is always nullish.
~
!!! error TS2881: This expression is never nullish.
~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
const p23 = null ?? (opt ? 1 : 2) ?? 1;
~~~~
!!! error TS2871: This expression is always nullish.
~~~~~~~~~~~
!!! error TS2881: This expression is never nullish.
~~~~~~~~~~~~~~~~~~~~~
!!! error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.
// Outer expression tests
while ({} as any) { }

View File

@ -0,0 +1,18 @@
// @strict: true
// Repro for issue where "never nullish" checks miss "never nullish" through parentheses
const x: { y: string | undefined } | undefined = undefined as any;
// Both should error - both expressions are guaranteed to be "oops"
const foo = x?.y ?? `oops` ?? "";
const bar = (x?.y ?? `oops`) ?? "";
// Additional test cases with various levels of nesting
const baz = ((x?.y ?? `oops`)) ?? "";
const qux = (((x?.y ?? `oops`))) ?? "";
// Test with different types
const str1 = ("literal") ?? "fallback";
const str2 = (("nested")) ?? "fallback";
const nested = ("a" ?? "b") ?? "c";