diff --git a/.changeset/use-nullish-coalescing-ternary.md b/.changeset/use-nullish-coalescing-ternary.md
new file mode 100644
index 000000000000..a656152dff61
--- /dev/null
+++ b/.changeset/use-nullish-coalescing-ternary.md
@@ -0,0 +1,5 @@
+---
+"@biomejs/biome": patch
+---
+
+`useNullishCoalescing` now also detects ternary expressions that check for `null` or `undefined` and suggests rewriting them with `??`. A new `ignoreTernaryTests` option allows disabling this behavior.
diff --git a/crates/biome_js_analyze/src/lint/nursery/use_nullish_coalescing.rs b/crates/biome_js_analyze/src/lint/nursery/use_nullish_coalescing.rs
index 1b3b705f1bc9..819cabc02ab5 100644
--- a/crates/biome_js_analyze/src/lint/nursery/use_nullish_coalescing.rs
+++ b/crates/biome_js_analyze/src/lint/nursery/use_nullish_coalescing.rs
@@ -1,4 +1,4 @@
-use crate::{JsRuleAction, services::typed::Typed};
+use crate::{JsRuleAction, services::typed::Typed, utils::is_node_equal};
use biome_analyze::{
FixKind, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
};
@@ -7,13 +7,13 @@ use biome_diagnostics::Severity;
use biome_js_factory::make;
use biome_js_syntax::{
AnyJsAssignmentPattern, AnyJsExpression, JsAssignmentExpression, JsAssignmentOperator,
- JsConditionalExpression, JsDoWhileStatement, JsForStatement, JsIfStatement,
- JsLogicalExpression, JsLogicalOperator, JsParenthesizedExpression, JsSyntaxKind,
- JsWhileStatement, T,
+ JsBinaryOperator, JsConditionalExpression, JsDoWhileStatement, JsForStatement,
+ JsIfStatement, JsLogicalExpression, JsLogicalOperator, JsParenthesizedExpression,
+ JsSyntaxKind, JsWhileStatement, OperatorPrecedence, T,
};
-use biome_js_type_info::ConditionalType;
+use biome_js_type_info::{ConditionalType, TypeData};
use biome_rowan::{
- AstNode, BatchMutationExt, TextRange, declare_node_union,
+ AstNode, BatchMutationExt, TextRange, declare_node_union, trim_leading_trivia_pieces,
};
use biome_rule_options::use_nullish_coalescing::UseNullishCoalescingOptions;
@@ -32,6 +32,10 @@ declare_lint_rule! {
/// For `||=` assignment expressions, the same logic applies: `a ||= b` is
/// flagged when `a` is possibly nullish and can be rewritten as `a ??= b`.
///
+ /// For ternary expressions, this rule detects patterns like `x !== null ? x : y`
+ /// and suggests rewriting them as `x ?? y`. This applies to strict and loose
+ /// equality checks against `null` or `undefined`, including compound checks.
+ ///
/// By default, `||` expressions in conditional test positions (if/while/for/ternary)
/// are ignored, as the falsy-checking behavior is often intentional there. This can
/// be disabled with the `ignoreConditionalTests` option.
@@ -55,6 +59,16 @@ declare_lint_rule! {
/// x ||= 'default'; // should use ??=
/// ```
///
+ /// ```ts,expect_diagnostic
+ /// declare const x: string | null;
+ /// const value = x !== null ? x : 'default'; // should use ??
+ /// ```
+ ///
+ /// ```ts,expect_diagnostic
+ /// declare const x: string | null;
+ /// const value = x == null ? 'default' : x; // should use ??
+ /// ```
+ ///
/// ### Valid
///
/// ```ts
@@ -83,6 +97,53 @@ declare_lint_rule! {
/// y ??= 'default';
/// ```
///
+ /// ## Options
+ ///
+ /// ### ignoreConditionalTests
+ ///
+ /// Ignore `||` expressions inside conditional test positions
+ /// (if/while/for/do-while/ternary conditions), where falsy-checking
+ /// behavior may be intentional.
+ ///
+ /// **Default:** `true`
+ ///
+ /// Setting this to `false` will report `||` in conditional tests:
+ ///
+ /// ```json,options
+ /// {
+ /// "options": {
+ /// "ignoreConditionalTests": false
+ /// }
+ /// }
+ /// ```
+ ///
+ /// ```ts,use_options
+ /// declare const cond: string | null;
+ /// if (cond || 'fallback') {}
+ /// ```
+ ///
+ /// ### ignoreTernaryTests
+ ///
+ /// Ignore ternary expressions that check for `null` or `undefined`
+ /// and could be replaced with `??`.
+ ///
+ /// **Default:** `false`
+ ///
+ /// Setting this to `true` will suppress ternary diagnostics:
+ ///
+ /// ```json,options
+ /// {
+ /// "options": {
+ /// "ignoreTernaryTests": true
+ /// }
+ /// }
+ /// ```
+ ///
+ /// ```ts,use_options
+ /// declare const x: string | null;
+ /// const value = x !== null ? x : 'default'; // no diagnostic
+ /// ```
+ ///
pub UseNullishCoalescing {
version: "2.4.5",
name: "useNullishCoalescing",
@@ -97,7 +158,7 @@ declare_lint_rule! {
}
declare_node_union! {
- pub UseNullishCoalescingQuery = JsLogicalExpression | JsAssignmentExpression
+ pub UseNullishCoalescingQuery = JsLogicalExpression | JsAssignmentExpression | JsConditionalExpression
}
pub enum UseNullishCoalescingState {
@@ -109,6 +170,13 @@ pub enum UseNullishCoalescingState {
operator_range: TextRange,
can_fix: bool,
},
+ Ternary {
+ test_range: TextRange,
+ checked_expr: AnyJsExpression,
+ fallback_expr: AnyJsExpression,
+ is_positive: bool,
+ can_fix: bool,
+ },
}
impl Rule for UseNullishCoalescing {
@@ -125,6 +193,9 @@ impl Rule for UseNullishCoalescing {
UseNullishCoalescingQuery::JsAssignmentExpression(assignment) => {
run_logical_or_assignment(ctx, assignment)
}
+ UseNullishCoalescingQuery::JsConditionalExpression(ternary) => {
+ run_ternary(ctx, ternary)
+ }
}
}
@@ -154,41 +225,120 @@ impl Rule for UseNullishCoalescing {
"The ""||="" operator assigns when the left side is falsy, while ""??="" only assigns when it is null or undefined."
}),
),
+ UseNullishCoalescingState::Ternary { test_range, .. } => Some(
+ RuleDiagnostic::new(
+ rule_category!(),
+ *test_range,
+ markup! {
+ "Prefer ""??"" over a ternary expression checking for nullish."
+ },
+ ),
+ ),
}
}
fn action(ctx: &RuleContext, state: &Self::State) -> Option {
- let (can_fix, replacement_token, message) = match state {
- UseNullishCoalescingState::LogicalOr { can_fix, .. } => (
- *can_fix,
- T![??],
- markup! { "Use ""??"" instead." }.to_owned(),
- ),
- UseNullishCoalescingState::LogicalOrAssignment { can_fix, .. } => (
- *can_fix,
- T![??=],
- markup! { "Use ""??="" instead." }.to_owned(),
- ),
- };
-
- if !can_fix {
- return None;
- }
+ let query = ctx.query();
+ let mut mutation = ctx.root().begin();
- let old_token = match ctx.query() {
- UseNullishCoalescingQuery::JsLogicalExpression(node) => node.operator_token().ok()?,
- UseNullishCoalescingQuery::JsAssignmentExpression(node) => {
- node.operator_token().ok()?
+ let message = match (state, query) {
+ (
+ UseNullishCoalescingState::LogicalOr { can_fix, .. },
+ UseNullishCoalescingQuery::JsLogicalExpression(logical),
+ ) => {
+ if !can_fix {
+ return None;
+ }
+ let old_token = logical.operator_token().ok()?;
+
+ let new_token = make::token(T![??])
+ .with_leading_trivia_pieces(old_token.leading_trivia().pieces())
+ .with_trailing_trivia_pieces(old_token.trailing_trivia().pieces());
+
+ mutation.replace_token(old_token, new_token);
+ markup! { "Use ""??"" instead." }.to_owned()
+ }
+ (
+ UseNullishCoalescingState::LogicalOrAssignment { can_fix, .. },
+ UseNullishCoalescingQuery::JsAssignmentExpression(assignment),
+ ) => {
+ if !can_fix {
+ return None;
+ }
+ let old_token = assignment.operator_token().ok()?;
+
+ let new_token = make::token(T![??=])
+ .with_leading_trivia_pieces(old_token.leading_trivia().pieces())
+ .with_trailing_trivia_pieces(old_token.trailing_trivia().pieces());
+
+ mutation.replace_token(old_token, new_token);
+ markup! { "Use ""??="" instead." }.to_owned()
}
+ (
+ UseNullishCoalescingState::Ternary {
+ checked_expr,
+ fallback_expr,
+ is_positive,
+ can_fix,
+ ..
+ },
+ UseNullishCoalescingQuery::JsConditionalExpression(ternary),
+ ) => {
+ if !can_fix {
+ return None;
+ }
+
+ // Strip trailing whitespace. Ternary layout trivia is not appropriate for `??`.
+ let checked_expr = checked_expr.clone().trim_trailing_trivia()?;
+ let fallback_expr = fallback_expr.clone().trim_trailing_trivia()?;
+
+ // Transfer trivia from the ? and : tokens to the branch expressions they precede.
+ let question = ternary.question_mark_token().ok()?;
+ let colon = ternary.colon_token().ok()?;
+ let question_trivia =
+ trim_leading_trivia_pieces(question.trailing_trivia().pieces());
+ let colon_trivia =
+ trim_leading_trivia_pieces(colon.trailing_trivia().pieces());
+
+ let (checked_expr, fallback_expr) = if *is_positive {
+ (
+ checked_expr.prepend_trivia_pieces(question_trivia)?,
+ fallback_expr.prepend_trivia_pieces(colon_trivia)?,
+ )
+ } else {
+ (
+ checked_expr.prepend_trivia_pieces(colon_trivia)?,
+ fallback_expr.prepend_trivia_pieces(question_trivia)?,
+ )
+ };
+
+ let checked = maybe_parenthesize_for_nullish(checked_expr);
+ let fallback = maybe_parenthesize_for_nullish(fallback_expr);
+
+ let new_expr = make::js_logical_expression(
+ checked,
+ make::token_decorated_with_space(T![??]),
+ fallback,
+ );
+
+ // Transfer leading/trailing trivia from the original ternary
+ let new_expr = AnyJsExpression::from(new_expr)
+ .prepend_trivia_pieces(
+ ternary.syntax().first_leading_trivia()?.pieces(),
+ )?
+ .append_trivia_pieces(
+ ternary.syntax().last_trailing_trivia()?.pieces(),
+ )?;
+
+ mutation.replace_node_discard_trivia(
+ AnyJsExpression::from(ternary.clone()),
+ new_expr,
+ );
+ markup! { "Use ""??"" instead." }.to_owned()
+ }
+ _ => return None,
};
- let new_token = make::token(replacement_token)
- .with_leading_trivia_pieces(old_token.leading_trivia().pieces())
- .with_trailing_trivia_pieces(old_token.trailing_trivia().pieces());
-
- let mut mutation = ctx.root().begin();
- mutation.replace_token(old_token, new_token);
-
Some(JsRuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
ctx.metadata().applicability(),
@@ -285,6 +435,10 @@ fn is_possibly_nullish(ty: &biome_js_type_info::Type) -> bool {
}
fn is_safe_syntax_context_for_replacement(logical: &JsLogicalExpression) -> bool {
+ // Check if the expression is wrapped in parentheses. If it is, the replacement
+ // is safe even inside another logical expression because the parens preserve
+ // grouping. If it's NOT parenthesized and sits inside another logical expression,
+ // the replacement could change precedence, so we skip the fix.
let is_parenthesized = logical
.syntax()
.parent()
@@ -339,21 +493,21 @@ fn is_in_test_position(logical: &JsLogicalExpression) -> bool {
for ancestor in logical.syntax().ancestors() {
let is_in_test = match ancestor.kind() {
- JsSyntaxKind::JS_IF_STATEMENT => {
- test_contains_logical(JsIfStatement::cast_ref(&ancestor).and_then(|s| s.test().ok()))
- }
- JsSyntaxKind::JS_WHILE_STATEMENT => {
- test_contains_logical(JsWhileStatement::cast_ref(&ancestor).and_then(|s| s.test().ok()))
- }
- JsSyntaxKind::JS_FOR_STATEMENT => {
- test_contains_logical(JsForStatement::cast_ref(&ancestor).and_then(|s| s.test()))
- }
- JsSyntaxKind::JS_DO_WHILE_STATEMENT => {
- test_contains_logical(JsDoWhileStatement::cast_ref(&ancestor).and_then(|s| s.test().ok()))
- }
- JsSyntaxKind::JS_CONDITIONAL_EXPRESSION => {
- test_contains_logical(JsConditionalExpression::cast_ref(&ancestor).and_then(|s| s.test().ok()))
- }
+ JsSyntaxKind::JS_IF_STATEMENT => test_contains_logical(
+ JsIfStatement::cast_ref(&ancestor).and_then(|s| s.test().ok()),
+ ),
+ JsSyntaxKind::JS_WHILE_STATEMENT => test_contains_logical(
+ JsWhileStatement::cast_ref(&ancestor).and_then(|s| s.test().ok()),
+ ),
+ JsSyntaxKind::JS_FOR_STATEMENT => test_contains_logical(
+ JsForStatement::cast_ref(&ancestor).and_then(|s| s.test()),
+ ),
+ JsSyntaxKind::JS_DO_WHILE_STATEMENT => test_contains_logical(
+ JsDoWhileStatement::cast_ref(&ancestor).and_then(|s| s.test().ok()),
+ ),
+ JsSyntaxKind::JS_CONDITIONAL_EXPRESSION => test_contains_logical(
+ JsConditionalExpression::cast_ref(&ancestor).and_then(|s| s.test().ok()),
+ ),
_ => false,
};
if is_in_test {
@@ -362,3 +516,241 @@ fn is_in_test_position(logical: &JsLogicalExpression) -> bool {
}
false
}
+
+fn run_ternary(
+ ctx: &RuleContext,
+ ternary: &JsConditionalExpression,
+) -> Option {
+ if ctx.options().ignore_ternary_tests() {
+ return None;
+ }
+
+ let test = ternary.test().ok()?;
+ let consequent = ternary.consequent().ok()?;
+ let alternate = ternary.alternate().ok()?;
+
+ let (checked_expr, fallback_expr, is_positive, check_kind) =
+ check_ternary_nullish_pattern(&test, &consequent, &alternate)?;
+
+ // The fix is unsafe when the checked expression contains calls or `new`, because
+ // the ternary evaluates it twice (test + branch) while `??` evaluates it once.
+ let has_side_effects = contains_call_or_new_expression(&checked_expr);
+
+ let can_fix = !has_side_effects
+ && match check_kind {
+ // Loose equality and compound strict checks cover both null and undefined
+ NullishCheckKind::Loose | NullishCheckKind::Compound => true,
+ // A single strict check only covers one nullish variant. The fix to `??`
+ // is safe only if the type cannot be the opposite variant.
+ NullishCheckKind::StrictSingle(lit) => {
+ let ty = ctx.type_of_expression(&checked_expr);
+ match lit {
+ NullishLiteral::Null => !type_has_undefined(&ty),
+ NullishLiteral::Undefined => !type_has_null(&ty),
+ }
+ }
+ };
+
+ Some(UseNullishCoalescingState::Ternary {
+ test_range: test.syntax().text_trimmed_range(),
+ checked_expr,
+ fallback_expr,
+ is_positive,
+ can_fix,
+ })
+}
+
+/// Returns `(checked, fallback, is_positive, check_kind)` if the ternary can be converted to `??`.
+/// `is_positive` is true when checked = consequent (e.g. `x !== null ? x : y`).
+fn check_ternary_nullish_pattern(
+ test: &AnyJsExpression,
+ consequent: &AnyJsExpression,
+ alternate: &AnyJsExpression,
+) -> Option<(AnyJsExpression, AnyJsExpression, bool, NullishCheckKind)> {
+ // `x !== null ? x : y` or `x != null ? x : y`
+ if let Some((_, kind)) = match_nullish_check(
+ test,
+ consequent,
+ JsBinaryOperator::Inequality,
+ JsBinaryOperator::StrictInequality,
+ JsLogicalOperator::LogicalAnd,
+ ) {
+ return Some((consequent.clone(), alternate.clone(), true, kind));
+ }
+
+ // `x === null ? y : x` or `x == null ? y : x`
+ if let Some((_, kind)) = match_nullish_check(
+ test,
+ alternate,
+ JsBinaryOperator::Equality,
+ JsBinaryOperator::StrictEquality,
+ JsLogicalOperator::LogicalOr,
+ ) {
+ return Some((alternate.clone(), consequent.clone(), false, kind));
+ }
+
+ None
+}
+
+/// Returns the checked expression and the kind of nullish check if the test matches
+/// a nullish comparison pattern (loose, strict, or compound) using the given operator family.
+fn match_nullish_check(
+ test: &AnyJsExpression,
+ value: &AnyJsExpression,
+ loose_op: JsBinaryOperator,
+ strict_op: JsBinaryOperator,
+ compound_logical_op: JsLogicalOperator,
+) -> Option<(AnyJsExpression, NullishCheckKind)> {
+ // Loose: `x != null` or `x == null`
+ if let Some((checked, _)) = extract_nullish_comparison_operand(test, loose_op)
+ && expressions_equivalent(&checked, value)
+ {
+ return Some((checked, NullishCheckKind::Loose));
+ }
+
+ // Strict: `x !== null` or `x === undefined`
+ if let Some((checked, lit)) = extract_nullish_comparison_operand(test, strict_op)
+ && expressions_equivalent(&checked, value)
+ {
+ return Some((checked, NullishCheckKind::StrictSingle(lit)));
+ }
+
+ // Compound: `x !== null && x !== undefined` or `x === null || x === undefined`
+ if let Some(logical) = test.as_js_logical_expression()
+ && logical.operator().ok()? == compound_logical_op
+ {
+ let left = logical.left().ok()?;
+ let right = logical.right().ok()?;
+
+ if let (Some((checked_left, lit_left)), Some((checked_right, lit_right))) = (
+ extract_nullish_comparison_operand(&left, strict_op),
+ extract_nullish_comparison_operand(&right, strict_op),
+ ) && expressions_equivalent(&checked_left, &checked_right)
+ && expressions_equivalent(&checked_left, value)
+ {
+ // Both sides must test different literals (one null, one undefined)
+ // to be a true compound check. If both test the same literal,
+ // treat it as a single strict check.
+ if lit_left != lit_right {
+ return Some((checked_left, NullishCheckKind::Compound));
+ }
+ return Some((checked_left, NullishCheckKind::StrictSingle(lit_left)));
+ }
+ }
+
+ None
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum NullishLiteral {
+ Null,
+ Undefined,
+}
+
+#[derive(Clone, Copy)]
+enum NullishCheckKind {
+ /// Loose equality: `!= null` or `== null` (covers both null and undefined)
+ Loose,
+ /// Single strict equality: `!== null` or `=== undefined` (covers only one)
+ StrictSingle(NullishLiteral),
+ /// Compound strict: `!== null && !== undefined` (covers both)
+ Compound,
+}
+
+/// Returns the non-nullish operand of a binary comparison against `null` or `undefined`.
+///
+/// Checks the right operand first because the conventional form places the literal
+/// on the right (`x !== null`). The left check handles the reversed form (`null !== x`).
+fn extract_nullish_comparison_operand(
+ expr: &AnyJsExpression,
+ expected_op: JsBinaryOperator,
+) -> Option<(AnyJsExpression, NullishLiteral)> {
+ let binary = expr.as_js_binary_expression()?;
+ if binary.operator().ok()? != expected_op {
+ return None;
+ }
+ let left = binary.left().ok()?;
+ let right = binary.right().ok()?;
+ if let Some(lit) = nullish_literal_kind(&right) {
+ return Some((left, lit));
+ }
+ if let Some(lit) = nullish_literal_kind(&left) {
+ return Some((right, lit));
+ }
+ None
+}
+
+fn nullish_literal_kind(expr: &AnyJsExpression) -> Option {
+ use biome_js_syntax::static_value::StaticValue;
+ match expr.as_static_value()? {
+ StaticValue::Null(_) => Some(NullishLiteral::Null),
+ StaticValue::Undefined(_) => Some(NullishLiteral::Undefined),
+ _ => None,
+ }
+}
+
+fn expressions_equivalent(a: &AnyJsExpression, b: &AnyJsExpression) -> bool {
+ is_node_equal(a.syntax(), b.syntax())
+}
+
+/// Returns true if the expression contains a call or `new` expression.
+///
+/// In a ternary like `foo() !== null ? foo() : y`, the subject is evaluated twice
+/// (once in the test, once in the branch). Replacing with `foo() ?? y` evaluates it
+/// once, which is a semantic change if the call has side effects.
+fn contains_call_or_new_expression(expr: &AnyJsExpression) -> bool {
+ expr.syntax().descendants().any(|node| {
+ matches!(
+ node.kind(),
+ JsSyntaxKind::JS_CALL_EXPRESSION | JsSyntaxKind::JS_NEW_EXPRESSION
+ )
+ })
+}
+
+fn type_has_null(ty: &biome_js_type_info::Type) -> bool {
+ let is_null = |t: &biome_js_type_info::Type| {
+ t.resolved_data()
+ .is_some_and(|d| matches!(d.as_raw_data(), TypeData::Null))
+ };
+ if ty.is_union() {
+ ty.flattened_union_variants().any(|v| is_null(&v))
+ } else {
+ is_null(ty)
+ }
+}
+
+fn type_has_undefined(ty: &biome_js_type_info::Type) -> bool {
+ let is_undef = |t: &biome_js_type_info::Type| {
+ t.resolved_data()
+ .is_some_and(|d| matches!(d.as_raw_data(), TypeData::Undefined | TypeData::VoidKeyword))
+ };
+ if ty.is_union() {
+ ty.flattened_union_variants().any(|v| is_undef(&v))
+ } else {
+ is_undef(ty)
+ }
+}
+
+/// Wraps the expression in parentheses if it would be invalid or change meaning
+/// as an operand of `??`. This covers two cases:
+/// - Expressions with lower precedence than `??` (conditional, assignment, yield, comma)
+/// - `||` and `&&` expressions, which cannot be mixed with `??` without parentheses
+fn maybe_parenthesize_for_nullish(expr: AnyJsExpression) -> AnyJsExpression {
+ if expr
+ .precedence()
+ .is_ok_and(|p| p < OperatorPrecedence::Coalesce)
+ {
+ return make::parenthesized(expr).into();
+ }
+ if let Some(logical) = expr.as_js_logical_expression()
+ && logical.operator().is_ok_and(|op| {
+ matches!(
+ op,
+ JsLogicalOperator::LogicalOr | JsLogicalOperator::LogicalAnd
+ )
+ })
+ {
+ return make::parenthesized(expr).into();
+ }
+ expr
+}
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.options.json b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.options.json
new file mode 100644
index 000000000000..109d171a6190
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.options.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json",
+ "linter": {
+ "rules": {
+ "nursery": {
+ "useNullishCoalescing": {
+ "level": "error",
+ "options": {
+ "ignoreTernaryTests": true
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.ts b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.ts
new file mode 100644
index 000000000000..0481bd1f8ba6
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.ts
@@ -0,0 +1,10 @@
+// should not generate diagnostics for ternary (ignoreTernaryTests: true)
+
+declare const a: string | null;
+const r1 = a !== null ? a : 'default';
+
+declare const b: string | undefined;
+const r2 = b === undefined ? 'default' : b;
+
+declare const c: string | null;
+const r3 = c != null ? c : 'default';
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.ts.snap
new file mode 100644
index 000000000000..607144576a7d
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ignoreTernaryTestsEnabled.ts.snap
@@ -0,0 +1,19 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+assertion_line: 155
+expression: ignoreTernaryTestsEnabled.ts
+---
+# Input
+```ts
+// should not generate diagnostics for ternary (ignoreTernaryTests: true)
+
+declare const a: string | null;
+const r1 = a !== null ? a : 'default';
+
+declare const b: string | undefined;
+const r2 = b === undefined ? 'default' : b;
+
+declare const c: string | null;
+const r3 = c != null ? c : 'default';
+
+```
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryCommentTrivia.ts b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryCommentTrivia.ts
new file mode 100644
index 000000000000..c3af745696f9
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryCommentTrivia.ts
@@ -0,0 +1,11 @@
+// Comments around ternary parts should be preserved
+declare const x: string | null;
+const a = x !== null ? /* non-null value */ x : /* fallback */ 'default';
+
+// Comment before test
+declare const y: string | null;
+const b = /* check */ y === null ? 'fallback' : y;
+
+// Negative form with comments on both branches
+declare const z: string | null;
+const c = z === null ? /* fallback */ 'default' : /* value */ z;
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryCommentTrivia.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryCommentTrivia.ts.snap
new file mode 100644
index 000000000000..cfe11350f4d8
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryCommentTrivia.ts.snap
@@ -0,0 +1,103 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+assertion_line: 155
+expression: ternaryCommentTrivia.ts
+---
+# Input
+```ts
+// Comments around ternary parts should be preserved
+declare const x: string | null;
+const a = x !== null ? /* non-null value */ x : /* fallback */ 'default';
+
+// Comment before test
+declare const y: string | null;
+const b = /* check */ y === null ? 'fallback' : y;
+
+// Negative form with comments on both branches
+declare const z: string | null;
+const c = z === null ? /* fallback */ 'default' : /* value */ z;
+
+```
+
+# Diagnostics
+```
+ternaryCommentTrivia.ts:3:11 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 1 │ // Comments around ternary parts should be preserved
+ 2 │ declare const x: string | null;
+ > 3 │ const a = x !== null ? /* non-null value */ x : /* fallback */ 'default';
+ │ ^^^^^^^^^^
+ 4 │
+ 5 │ // Comment before test
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 1 1 │ // Comments around ternary parts should be preserved
+ 2 2 │ declare const x: string | null;
+ 3 │ - const·a·=·x·!==·null·?·/*·non-null·value·*/·x·:·/*·fallback·*/·'default';
+ 3 │ + const·a·=·/*·non-null·value·*/·x·??·/*·fallback·*/·'default';
+ 4 4 │
+ 5 5 │ // Comment before test
+
+
+```
+
+```
+ternaryCommentTrivia.ts:7:23 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 5 │ // Comment before test
+ 6 │ declare const y: string | null;
+ > 7 │ const b = /* check */ y === null ? 'fallback' : y;
+ │ ^^^^^^^^^^
+ 8 │
+ 9 │ // Negative form with comments on both branches
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 5 5 │ // Comment before test
+ 6 6 │ declare const y: string | null;
+ 7 │ - const·b·=·/*·check·*/·y·===·null·?·'fallback'·:·y;
+ 7 │ + const·b·=·/*·check·*/·y·??·'fallback';
+ 8 8 │
+ 9 9 │ // Negative form with comments on both branches
+
+
+```
+
+```
+ternaryCommentTrivia.ts:11:11 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 9 │ // Negative form with comments on both branches
+ 10 │ declare const z: string | null;
+ > 11 │ const c = z === null ? /* fallback */ 'default' : /* value */ z;
+ │ ^^^^^^^^^^
+ 12 │
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 9 9 │ // Negative form with comments on both branches
+ 10 10 │ declare const z: string | null;
+ 11 │ - const·c·=·z·===·null·?·/*·fallback·*/·'default'·:·/*·value·*/·z;
+ 11 │ + const·c·=·/*·value·*/·z·??·/*·fallback·*/·'default';
+ 12 12 │
+
+
+```
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryInvalid.ts b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryInvalid.ts
new file mode 100644
index 000000000000..f8ec5c61c65f
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryInvalid.ts
@@ -0,0 +1,149 @@
+// Strict !== null
+declare const a: string | null;
+const r1 = a !== null ? a : 'default';
+
+// Strict !== undefined
+declare const b: string | undefined;
+const r2 = b !== undefined ? b : 'fallback';
+
+// Loose != null
+declare const c: string | null;
+const r3 = c != null ? c : 'default';
+
+// Inverted === null
+declare const d: string | null;
+const r4 = d === null ? 'default' : d;
+
+// Inverted === undefined
+declare const e: string | undefined;
+const r5 = e === undefined ? 'fallback' : e;
+
+// Inverted == null
+declare const f: string | null;
+const r6 = f == null ? 'default' : f;
+
+// Compound !== null && !== undefined
+declare const g: string | null | undefined;
+const r7 = g !== null && g !== undefined ? g : 'default';
+
+// Compound === null || === undefined
+declare const h: string | null | undefined;
+const r8 = h === null || h === undefined ? 'default' : h;
+
+// Null on left side
+declare const i: string | null;
+const r9 = null !== i ? i : 'default';
+
+// Undefined on left side
+declare const j: string | undefined;
+const r10 = undefined !== j ? j : 'fallback';
+
+// Member access
+declare const obj: { prop: string | null };
+const r11 = obj.prop !== null ? obj.prop : 'default';
+
+// Loose != undefined
+declare const l: string | undefined;
+const r12 = l != undefined ? l : 'default';
+
+// Inverted, null on left
+declare const n: string | null;
+const r13 = null === n ? 'default' : n;
+
+// Strict single with null | undefined (diagnostic only, no fix)
+declare const nu: string | null | undefined;
+const r17 = nu !== null ? nu : 'default';
+
+// Fallback is conditional (needs parens)
+declare const p: string | null;
+const r14 = p !== null ? p : p ? 'a' : 'b';
+
+// Fallback is logical OR (needs parens, ?? cannot mix with ||)
+declare const q: string | null;
+const r15 = q !== null ? q : q || 'default';
+
+// Fallback is logical AND (needs parens, ?? cannot mix with &&)
+declare const s: string | null;
+const r16 = s !== null ? s : s && 'value';
+
+// Call expression in subject (diagnostic only, no fix: side-effect safety)
+declare function foo(): string | null;
+const r18 = foo() !== null ? foo() : 'default';
+
+// New expression in subject (diagnostic only, no fix: side-effect safety)
+declare class Bar { value: string | null }
+const r25 = new Bar().value !== null ? new Bar().value : 'default';
+
+// Undefined literal type
+const undef: undefined = undefined;
+const r19 = undef !== undefined ? undef : 'fallback';
+
+// Number | undefined (non-string union)
+declare const maybeNum: number | undefined;
+const r20 = maybeNum !== undefined ? maybeNum : 42;
+
+// Loose != null with triple union (covers both null and undefined)
+declare const tripleUnion: string | null | undefined;
+const r21 = tripleUnion != null ? tripleUnion : 'default';
+
+// Optional property (a?: string creates string | undefined)
+interface Config { timeout?: number; }
+declare const config: Config;
+const r22 = config.timeout !== undefined ? config.timeout : 3000;
+
+// Array element access
+declare const arr: (number | null)[];
+const r23 = arr[0] !== null ? arr[0] : 0;
+
+// Function return context
+function getVal(x: string | null): string {
+ return x !== null ? x : 'default';
+}
+
+// Nested in parentheses
+declare const paren: string | null;
+const r24 = (paren !== null ? paren : 'default').toUpperCase();
+
+// --- StrictSingle fixability: opposite nullish variant in type ---
+
+// Checks !== null but type has undefined (not null): no fix
+declare const ss1: string | undefined;
+const r26 = ss1 !== null ? ss1 : 'default';
+
+// Checks !== undefined but type has null (not undefined): no fix
+declare const ss2: string | null;
+const r27 = ss2 !== undefined ? ss2 : 'default';
+
+// Inverted: checks === null but type has undefined: no fix
+declare const ss3: string | undefined;
+const r28 = ss3 === null ? 'default' : ss3;
+
+// Inverted: checks === undefined but type has null: no fix
+declare const ss4: string | null;
+const r29 = ss4 === undefined ? 'default' : ss4;
+
+// Checks !== null, type has only null: fix IS safe
+declare const ss5: string | null;
+const r30 = ss5 !== null ? ss5 : 'default';
+
+// Checks !== undefined, type has only undefined: fix IS safe
+declare const ss6: string | undefined;
+const r31 = ss6 !== undefined ? ss6 : 'default';
+
+// Checks !== null, type has void (acts as undefined): no fix
+declare const ss7: string | void;
+const r32 = ss7 !== null ? ss7 : 'default';
+
+// --- Compound fixability: duplicate vs complementary literals ---
+
+// Both sides check null (not complementary): no fix
+declare const cp1: string | null | undefined;
+const r33 = cp1 !== null && cp1 !== null ? cp1 : 'default';
+
+// Complementary: one null, one undefined: fix IS safe
+declare const cp2: string | null | undefined;
+const r34 = cp2 !== null && cp2 !== undefined ? cp2 : 'default';
+
+// Complementary reversed order: fix IS safe
+declare const cp3: string | null | undefined;
+const r35 = cp3 !== undefined && cp3 !== null ? cp3 : 'default';
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryInvalid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryInvalid.ts.snap
new file mode 100644
index 000000000000..aa1233630bea
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryInvalid.ts.snap
@@ -0,0 +1,1095 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: ternaryInvalid.ts
+---
+# Input
+```ts
+// Strict !== null
+declare const a: string | null;
+const r1 = a !== null ? a : 'default';
+
+// Strict !== undefined
+declare const b: string | undefined;
+const r2 = b !== undefined ? b : 'fallback';
+
+// Loose != null
+declare const c: string | null;
+const r3 = c != null ? c : 'default';
+
+// Inverted === null
+declare const d: string | null;
+const r4 = d === null ? 'default' : d;
+
+// Inverted === undefined
+declare const e: string | undefined;
+const r5 = e === undefined ? 'fallback' : e;
+
+// Inverted == null
+declare const f: string | null;
+const r6 = f == null ? 'default' : f;
+
+// Compound !== null && !== undefined
+declare const g: string | null | undefined;
+const r7 = g !== null && g !== undefined ? g : 'default';
+
+// Compound === null || === undefined
+declare const h: string | null | undefined;
+const r8 = h === null || h === undefined ? 'default' : h;
+
+// Null on left side
+declare const i: string | null;
+const r9 = null !== i ? i : 'default';
+
+// Undefined on left side
+declare const j: string | undefined;
+const r10 = undefined !== j ? j : 'fallback';
+
+// Member access
+declare const obj: { prop: string | null };
+const r11 = obj.prop !== null ? obj.prop : 'default';
+
+// Loose != undefined
+declare const l: string | undefined;
+const r12 = l != undefined ? l : 'default';
+
+// Inverted, null on left
+declare const n: string | null;
+const r13 = null === n ? 'default' : n;
+
+// Strict single with null | undefined (diagnostic only, no fix)
+declare const nu: string | null | undefined;
+const r17 = nu !== null ? nu : 'default';
+
+// Fallback is conditional (needs parens)
+declare const p: string | null;
+const r14 = p !== null ? p : p ? 'a' : 'b';
+
+// Fallback is logical OR (needs parens, ?? cannot mix with ||)
+declare const q: string | null;
+const r15 = q !== null ? q : q || 'default';
+
+// Fallback is logical AND (needs parens, ?? cannot mix with &&)
+declare const s: string | null;
+const r16 = s !== null ? s : s && 'value';
+
+// Call expression in subject (diagnostic only, no fix: side-effect safety)
+declare function foo(): string | null;
+const r18 = foo() !== null ? foo() : 'default';
+
+// New expression in subject (diagnostic only, no fix: side-effect safety)
+declare class Bar { value: string | null }
+const r25 = new Bar().value !== null ? new Bar().value : 'default';
+
+// Undefined literal type
+const undef: undefined = undefined;
+const r19 = undef !== undefined ? undef : 'fallback';
+
+// Number | undefined (non-string union)
+declare const maybeNum: number | undefined;
+const r20 = maybeNum !== undefined ? maybeNum : 42;
+
+// Loose != null with triple union (covers both null and undefined)
+declare const tripleUnion: string | null | undefined;
+const r21 = tripleUnion != null ? tripleUnion : 'default';
+
+// Optional property (a?: string creates string | undefined)
+interface Config { timeout?: number; }
+declare const config: Config;
+const r22 = config.timeout !== undefined ? config.timeout : 3000;
+
+// Array element access
+declare const arr: (number | null)[];
+const r23 = arr[0] !== null ? arr[0] : 0;
+
+// Function return context
+function getVal(x: string | null): string {
+ return x !== null ? x : 'default';
+}
+
+// Nested in parentheses
+declare const paren: string | null;
+const r24 = (paren !== null ? paren : 'default').toUpperCase();
+
+// --- StrictSingle fixability: opposite nullish variant in type ---
+
+// Checks !== null but type has undefined (not null): no fix
+declare const ss1: string | undefined;
+const r26 = ss1 !== null ? ss1 : 'default';
+
+// Checks !== undefined but type has null (not undefined): no fix
+declare const ss2: string | null;
+const r27 = ss2 !== undefined ? ss2 : 'default';
+
+// Inverted: checks === null but type has undefined: no fix
+declare const ss3: string | undefined;
+const r28 = ss3 === null ? 'default' : ss3;
+
+// Inverted: checks === undefined but type has null: no fix
+declare const ss4: string | null;
+const r29 = ss4 === undefined ? 'default' : ss4;
+
+// Checks !== null, type has only null: fix IS safe
+declare const ss5: string | null;
+const r30 = ss5 !== null ? ss5 : 'default';
+
+// Checks !== undefined, type has only undefined: fix IS safe
+declare const ss6: string | undefined;
+const r31 = ss6 !== undefined ? ss6 : 'default';
+
+// Checks !== null, type has void (acts as undefined): no fix
+declare const ss7: string | void;
+const r32 = ss7 !== null ? ss7 : 'default';
+
+// --- Compound fixability: duplicate vs complementary literals ---
+
+// Both sides check null (not complementary): no fix
+declare const cp1: string | null | undefined;
+const r33 = cp1 !== null && cp1 !== null ? cp1 : 'default';
+
+// Complementary: one null, one undefined: fix IS safe
+declare const cp2: string | null | undefined;
+const r34 = cp2 !== null && cp2 !== undefined ? cp2 : 'default';
+
+// Complementary reversed order: fix IS safe
+declare const cp3: string | null | undefined;
+const r35 = cp3 !== undefined && cp3 !== null ? cp3 : 'default';
+
+```
+
+# Diagnostics
+```
+ternaryInvalid.ts:3:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 1 │ // Strict !== null
+ 2 │ declare const a: string | null;
+ > 3 │ const r1 = a !== null ? a : 'default';
+ │ ^^^^^^^^^^
+ 4 │
+ 5 │ // Strict !== undefined
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 1 1 │ // Strict !== null
+ 2 2 │ declare const a: string | null;
+ 3 │ - const·r1·=·a·!==·null·?·a·:·'default';
+ 3 │ + const·r1·=·a·??·'default';
+ 4 4 │
+ 5 5 │ // Strict !== undefined
+
+
+```
+
+```
+ternaryInvalid.ts:7:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 5 │ // Strict !== undefined
+ 6 │ declare const b: string | undefined;
+ > 7 │ const r2 = b !== undefined ? b : 'fallback';
+ │ ^^^^^^^^^^^^^^^
+ 8 │
+ 9 │ // Loose != null
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 5 5 │ // Strict !== undefined
+ 6 6 │ declare const b: string | undefined;
+ 7 │ - const·r2·=·b·!==·undefined·?·b·:·'fallback';
+ 7 │ + const·r2·=·b·??·'fallback';
+ 8 8 │
+ 9 9 │ // Loose != null
+
+
+```
+
+```
+ternaryInvalid.ts:11:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 9 │ // Loose != null
+ 10 │ declare const c: string | null;
+ > 11 │ const r3 = c != null ? c : 'default';
+ │ ^^^^^^^^^
+ 12 │
+ 13 │ // Inverted === null
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 9 9 │ // Loose != null
+ 10 10 │ declare const c: string | null;
+ 11 │ - const·r3·=·c·!=·null·?·c·:·'default';
+ 11 │ + const·r3·=·c·??·'default';
+ 12 12 │
+ 13 13 │ // Inverted === null
+
+
+```
+
+```
+ternaryInvalid.ts:15:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 13 │ // Inverted === null
+ 14 │ declare const d: string | null;
+ > 15 │ const r4 = d === null ? 'default' : d;
+ │ ^^^^^^^^^^
+ 16 │
+ 17 │ // Inverted === undefined
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 13 13 │ // Inverted === null
+ 14 14 │ declare const d: string | null;
+ 15 │ - const·r4·=·d·===·null·?·'default'·:·d;
+ 15 │ + const·r4·=·d·??·'default';
+ 16 16 │
+ 17 17 │ // Inverted === undefined
+
+
+```
+
+```
+ternaryInvalid.ts:19:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 17 │ // Inverted === undefined
+ 18 │ declare const e: string | undefined;
+ > 19 │ const r5 = e === undefined ? 'fallback' : e;
+ │ ^^^^^^^^^^^^^^^
+ 20 │
+ 21 │ // Inverted == null
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 17 17 │ // Inverted === undefined
+ 18 18 │ declare const e: string | undefined;
+ 19 │ - const·r5·=·e·===·undefined·?·'fallback'·:·e;
+ 19 │ + const·r5·=·e·??·'fallback';
+ 20 20 │
+ 21 21 │ // Inverted == null
+
+
+```
+
+```
+ternaryInvalid.ts:23:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 21 │ // Inverted == null
+ 22 │ declare const f: string | null;
+ > 23 │ const r6 = f == null ? 'default' : f;
+ │ ^^^^^^^^^
+ 24 │
+ 25 │ // Compound !== null && !== undefined
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 21 21 │ // Inverted == null
+ 22 22 │ declare const f: string | null;
+ 23 │ - const·r6·=·f·==·null·?·'default'·:·f;
+ 23 │ + const·r6·=·f·??·'default';
+ 24 24 │
+ 25 25 │ // Compound !== null && !== undefined
+
+
+```
+
+```
+ternaryInvalid.ts:27:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 25 │ // Compound !== null && !== undefined
+ 26 │ declare const g: string | null | undefined;
+ > 27 │ const r7 = g !== null && g !== undefined ? g : 'default';
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 28 │
+ 29 │ // Compound === null || === undefined
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 25 25 │ // Compound !== null && !== undefined
+ 26 26 │ declare const g: string | null | undefined;
+ 27 │ - const·r7·=·g·!==·null·&&·g·!==·undefined·?·g·:·'default';
+ 27 │ + const·r7·=·g·??·'default';
+ 28 28 │
+ 29 29 │ // Compound === null || === undefined
+
+
+```
+
+```
+ternaryInvalid.ts:31:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 29 │ // Compound === null || === undefined
+ 30 │ declare const h: string | null | undefined;
+ > 31 │ const r8 = h === null || h === undefined ? 'default' : h;
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 32 │
+ 33 │ // Null on left side
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 29 29 │ // Compound === null || === undefined
+ 30 30 │ declare const h: string | null | undefined;
+ 31 │ - const·r8·=·h·===·null·||·h·===·undefined·?·'default'·:·h;
+ 31 │ + const·r8·=·h·??·'default';
+ 32 32 │
+ 33 33 │ // Null on left side
+
+
+```
+
+```
+ternaryInvalid.ts:35:12 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 33 │ // Null on left side
+ 34 │ declare const i: string | null;
+ > 35 │ const r9 = null !== i ? i : 'default';
+ │ ^^^^^^^^^^
+ 36 │
+ 37 │ // Undefined on left side
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 33 33 │ // Null on left side
+ 34 34 │ declare const i: string | null;
+ 35 │ - const·r9·=·null·!==·i·?·i·:·'default';
+ 35 │ + const·r9·=·i·??·'default';
+ 36 36 │
+ 37 37 │ // Undefined on left side
+
+
+```
+
+```
+ternaryInvalid.ts:39:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 37 │ // Undefined on left side
+ 38 │ declare const j: string | undefined;
+ > 39 │ const r10 = undefined !== j ? j : 'fallback';
+ │ ^^^^^^^^^^^^^^^
+ 40 │
+ 41 │ // Member access
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 37 37 │ // Undefined on left side
+ 38 38 │ declare const j: string | undefined;
+ 39 │ - const·r10·=·undefined·!==·j·?·j·:·'fallback';
+ 39 │ + const·r10·=·j·??·'fallback';
+ 40 40 │
+ 41 41 │ // Member access
+
+
+```
+
+```
+ternaryInvalid.ts:43:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 41 │ // Member access
+ 42 │ declare const obj: { prop: string | null };
+ > 43 │ const r11 = obj.prop !== null ? obj.prop : 'default';
+ │ ^^^^^^^^^^^^^^^^^
+ 44 │
+ 45 │ // Loose != undefined
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 41 41 │ // Member access
+ 42 42 │ declare const obj: { prop: string | null };
+ 43 │ - const·r11·=·obj.prop·!==·null·?·obj.prop·:·'default';
+ 43 │ + const·r11·=·obj.prop·??·'default';
+ 44 44 │
+ 45 45 │ // Loose != undefined
+
+
+```
+
+```
+ternaryInvalid.ts:47:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 45 │ // Loose != undefined
+ 46 │ declare const l: string | undefined;
+ > 47 │ const r12 = l != undefined ? l : 'default';
+ │ ^^^^^^^^^^^^^^
+ 48 │
+ 49 │ // Inverted, null on left
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 45 45 │ // Loose != undefined
+ 46 46 │ declare const l: string | undefined;
+ 47 │ - const·r12·=·l·!=·undefined·?·l·:·'default';
+ 47 │ + const·r12·=·l·??·'default';
+ 48 48 │
+ 49 49 │ // Inverted, null on left
+
+
+```
+
+```
+ternaryInvalid.ts:51:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 49 │ // Inverted, null on left
+ 50 │ declare const n: string | null;
+ > 51 │ const r13 = null === n ? 'default' : n;
+ │ ^^^^^^^^^^
+ 52 │
+ 53 │ // Strict single with null | undefined (diagnostic only, no fix)
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 49 49 │ // Inverted, null on left
+ 50 50 │ declare const n: string | null;
+ 51 │ - const·r13·=·null·===·n·?·'default'·:·n;
+ 51 │ + const·r13·=·n·??·'default';
+ 52 52 │
+ 53 53 │ // Strict single with null | undefined (diagnostic only, no fix)
+
+
+```
+
+```
+ternaryInvalid.ts:55:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 53 │ // Strict single with null | undefined (diagnostic only, no fix)
+ 54 │ declare const nu: string | null | undefined;
+ > 55 │ const r17 = nu !== null ? nu : 'default';
+ │ ^^^^^^^^^^^
+ 56 │
+ 57 │ // Fallback is conditional (needs parens)
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+
+```
+
+```
+ternaryInvalid.ts:59:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 57 │ // Fallback is conditional (needs parens)
+ 58 │ declare const p: string | null;
+ > 59 │ const r14 = p !== null ? p : p ? 'a' : 'b';
+ │ ^^^^^^^^^^
+ 60 │
+ 61 │ // Fallback is logical OR (needs parens, ?? cannot mix with ||)
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 57 57 │ // Fallback is conditional (needs parens)
+ 58 58 │ declare const p: string | null;
+ 59 │ - const·r14·=·p·!==·null·?·p·:·p·?·'a'·:·'b';
+ 59 │ + const·r14·=·p·??·(p·?·'a'·:·'b');
+ 60 60 │
+ 61 61 │ // Fallback is logical OR (needs parens, ?? cannot mix with ||)
+
+
+```
+
+```
+ternaryInvalid.ts:63:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 61 │ // Fallback is logical OR (needs parens, ?? cannot mix with ||)
+ 62 │ declare const q: string | null;
+ > 63 │ const r15 = q !== null ? q : q || 'default';
+ │ ^^^^^^^^^^
+ 64 │
+ 65 │ // Fallback is logical AND (needs parens, ?? cannot mix with &&)
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 61 61 │ // Fallback is logical OR (needs parens, ?? cannot mix with ||)
+ 62 62 │ declare const q: string | null;
+ 63 │ - const·r15·=·q·!==·null·?·q·:·q·||·'default';
+ 63 │ + const·r15·=·q·??·(q·||·'default');
+ 64 64 │
+ 65 65 │ // Fallback is logical AND (needs parens, ?? cannot mix with &&)
+
+
+```
+
+```
+ternaryInvalid.ts:63:32 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Use ?? instead of ||.
+
+ 61 │ // Fallback is logical OR (needs parens, ?? cannot mix with ||)
+ 62 │ declare const q: string | null;
+ > 63 │ const r15 = q !== null ? q : q || 'default';
+ │ ^^
+ 64 │
+ 65 │ // Fallback is logical AND (needs parens, ?? cannot mix with &&)
+
+ i The || operator checks for all falsy values (including 0, '', and false), while ?? only checks for null and undefined.
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+
+```
+
+```
+ternaryInvalid.ts:67:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 65 │ // Fallback is logical AND (needs parens, ?? cannot mix with &&)
+ 66 │ declare const s: string | null;
+ > 67 │ const r16 = s !== null ? s : s && 'value';
+ │ ^^^^^^^^^^
+ 68 │
+ 69 │ // Call expression in subject (diagnostic only, no fix: side-effect safety)
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 65 65 │ // Fallback is logical AND (needs parens, ?? cannot mix with &&)
+ 66 66 │ declare const s: string | null;
+ 67 │ - const·r16·=·s·!==·null·?·s·:·s·&&·'value';
+ 67 │ + const·r16·=·s·??·(s·&&·'value');
+ 68 68 │
+ 69 69 │ // Call expression in subject (diagnostic only, no fix: side-effect safety)
+
+
+```
+
+```
+ternaryInvalid.ts:71:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 69 │ // Call expression in subject (diagnostic only, no fix: side-effect safety)
+ 70 │ declare function foo(): string | null;
+ > 71 │ const r18 = foo() !== null ? foo() : 'default';
+ │ ^^^^^^^^^^^^^^
+ 72 │
+ 73 │ // New expression in subject (diagnostic only, no fix: side-effect safety)
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+
+```
+
+```
+ternaryInvalid.ts:75:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 73 │ // New expression in subject (diagnostic only, no fix: side-effect safety)
+ 74 │ declare class Bar { value: string | null }
+ > 75 │ const r25 = new Bar().value !== null ? new Bar().value : 'default';
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^
+ 76 │
+ 77 │ // Undefined literal type
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+
+```
+
+```
+ternaryInvalid.ts:79:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 77 │ // Undefined literal type
+ 78 │ const undef: undefined = undefined;
+ > 79 │ const r19 = undef !== undefined ? undef : 'fallback';
+ │ ^^^^^^^^^^^^^^^^^^^
+ 80 │
+ 81 │ // Number | undefined (non-string union)
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 77 77 │ // Undefined literal type
+ 78 78 │ const undef: undefined = undefined;
+ 79 │ - const·r19·=·undef·!==·undefined·?·undef·:·'fallback';
+ 79 │ + const·r19·=·undef·??·'fallback';
+ 80 80 │
+ 81 81 │ // Number | undefined (non-string union)
+
+
+```
+
+```
+ternaryInvalid.ts:83:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 81 │ // Number | undefined (non-string union)
+ 82 │ declare const maybeNum: number | undefined;
+ > 83 │ const r20 = maybeNum !== undefined ? maybeNum : 42;
+ │ ^^^^^^^^^^^^^^^^^^^^^^
+ 84 │
+ 85 │ // Loose != null with triple union (covers both null and undefined)
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 81 81 │ // Number | undefined (non-string union)
+ 82 82 │ declare const maybeNum: number | undefined;
+ 83 │ - const·r20·=·maybeNum·!==·undefined·?·maybeNum·:·42;
+ 83 │ + const·r20·=·maybeNum·??·42;
+ 84 84 │
+ 85 85 │ // Loose != null with triple union (covers both null and undefined)
+
+
+```
+
+```
+ternaryInvalid.ts:87:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 85 │ // Loose != null with triple union (covers both null and undefined)
+ 86 │ declare const tripleUnion: string | null | undefined;
+ > 87 │ const r21 = tripleUnion != null ? tripleUnion : 'default';
+ │ ^^^^^^^^^^^^^^^^^^^
+ 88 │
+ 89 │ // Optional property (a?: string creates string | undefined)
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 85 85 │ // Loose != null with triple union (covers both null and undefined)
+ 86 86 │ declare const tripleUnion: string | null | undefined;
+ 87 │ - const·r21·=·tripleUnion·!=·null·?·tripleUnion·:·'default';
+ 87 │ + const·r21·=·tripleUnion·??·'default';
+ 88 88 │
+ 89 89 │ // Optional property (a?: string creates string | undefined)
+
+
+```
+
+```
+ternaryInvalid.ts:92:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 90 │ interface Config { timeout?: number; }
+ 91 │ declare const config: Config;
+ > 92 │ const r22 = config.timeout !== undefined ? config.timeout : 3000;
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 93 │
+ 94 │ // Array element access
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 90 90 │ interface Config { timeout?: number; }
+ 91 91 │ declare const config: Config;
+ 92 │ - const·r22·=·config.timeout·!==·undefined·?·config.timeout·:·3000;
+ 92 │ + const·r22·=·config.timeout·??·3000;
+ 93 93 │
+ 94 94 │ // Array element access
+
+
+```
+
+```
+ternaryInvalid.ts:96:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 94 │ // Array element access
+ 95 │ declare const arr: (number | null)[];
+ > 96 │ const r23 = arr[0] !== null ? arr[0] : 0;
+ │ ^^^^^^^^^^^^^^^
+ 97 │
+ 98 │ // Function return context
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+
+```
+
+```
+ternaryInvalid.ts:100:10 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 98 │ // Function return context
+ 99 │ function getVal(x: string | null): string {
+ > 100 │ return x !== null ? x : 'default';
+ │ ^^^^^^^^^^
+ 101 │ }
+ 102 │
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 98 98 │ // Function return context
+ 99 99 │ function getVal(x: string | null): string {
+ 100 │ - ··return·x·!==·null·?·x·:·'default';
+ 100 │ + ··return·x·??·'default';
+ 101 101 │ }
+ 102 102 │
+
+
+```
+
+```
+ternaryInvalid.ts:105:14 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 103 │ // Nested in parentheses
+ 104 │ declare const paren: string | null;
+ > 105 │ const r24 = (paren !== null ? paren : 'default').toUpperCase();
+ │ ^^^^^^^^^^^^^^
+ 106 │
+ 107 │ // --- StrictSingle fixability: opposite nullish variant in type ---
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 103 103 │ // Nested in parentheses
+ 104 104 │ declare const paren: string | null;
+ 105 │ - const·r24·=·(paren·!==·null·?·paren·:·'default').toUpperCase();
+ 105 │ + const·r24·=·(paren·??·'default').toUpperCase();
+ 106 106 │
+ 107 107 │ // --- StrictSingle fixability: opposite nullish variant in type ---
+
+
+```
+
+```
+ternaryInvalid.ts:111:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 109 │ // Checks !== null but type has undefined (not null): no fix
+ 110 │ declare const ss1: string | undefined;
+ > 111 │ const r26 = ss1 !== null ? ss1 : 'default';
+ │ ^^^^^^^^^^^^
+ 112 │
+ 113 │ // Checks !== undefined but type has null (not undefined): no fix
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+
+```
+
+```
+ternaryInvalid.ts:115:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 113 │ // Checks !== undefined but type has null (not undefined): no fix
+ 114 │ declare const ss2: string | null;
+ > 115 │ const r27 = ss2 !== undefined ? ss2 : 'default';
+ │ ^^^^^^^^^^^^^^^^^
+ 116 │
+ 117 │ // Inverted: checks === null but type has undefined: no fix
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+
+```
+
+```
+ternaryInvalid.ts:119:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 117 │ // Inverted: checks === null but type has undefined: no fix
+ 118 │ declare const ss3: string | undefined;
+ > 119 │ const r28 = ss3 === null ? 'default' : ss3;
+ │ ^^^^^^^^^^^^
+ 120 │
+ 121 │ // Inverted: checks === undefined but type has null: no fix
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+
+```
+
+```
+ternaryInvalid.ts:123:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 121 │ // Inverted: checks === undefined but type has null: no fix
+ 122 │ declare const ss4: string | null;
+ > 123 │ const r29 = ss4 === undefined ? 'default' : ss4;
+ │ ^^^^^^^^^^^^^^^^^
+ 124 │
+ 125 │ // Checks !== null, type has only null: fix IS safe
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+
+```
+
+```
+ternaryInvalid.ts:127:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 125 │ // Checks !== null, type has only null: fix IS safe
+ 126 │ declare const ss5: string | null;
+ > 127 │ const r30 = ss5 !== null ? ss5 : 'default';
+ │ ^^^^^^^^^^^^
+ 128 │
+ 129 │ // Checks !== undefined, type has only undefined: fix IS safe
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 125 125 │ // Checks !== null, type has only null: fix IS safe
+ 126 126 │ declare const ss5: string | null;
+ 127 │ - const·r30·=·ss5·!==·null·?·ss5·:·'default';
+ 127 │ + const·r30·=·ss5·??·'default';
+ 128 128 │
+ 129 129 │ // Checks !== undefined, type has only undefined: fix IS safe
+
+
+```
+
+```
+ternaryInvalid.ts:131:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 129 │ // Checks !== undefined, type has only undefined: fix IS safe
+ 130 │ declare const ss6: string | undefined;
+ > 131 │ const r31 = ss6 !== undefined ? ss6 : 'default';
+ │ ^^^^^^^^^^^^^^^^^
+ 132 │
+ 133 │ // Checks !== null, type has void (acts as undefined): no fix
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 129 129 │ // Checks !== undefined, type has only undefined: fix IS safe
+ 130 130 │ declare const ss6: string | undefined;
+ 131 │ - const·r31·=·ss6·!==·undefined·?·ss6·:·'default';
+ 131 │ + const·r31·=·ss6·??·'default';
+ 132 132 │
+ 133 133 │ // Checks !== null, type has void (acts as undefined): no fix
+
+
+```
+
+```
+ternaryInvalid.ts:135:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 133 │ // Checks !== null, type has void (acts as undefined): no fix
+ 134 │ declare const ss7: string | void;
+ > 135 │ const r32 = ss7 !== null ? ss7 : 'default';
+ │ ^^^^^^^^^^^^
+ 136 │
+ 137 │ // --- Compound fixability: duplicate vs complementary literals ---
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+
+```
+
+```
+ternaryInvalid.ts:141:13 lint/nursery/useNullishCoalescing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 139 │ // Both sides check null (not complementary): no fix
+ 140 │ declare const cp1: string | null | undefined;
+ > 141 │ const r33 = cp1 !== null && cp1 !== null ? cp1 : 'default';
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 142 │
+ 143 │ // Complementary: one null, one undefined: fix IS safe
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+
+```
+
+```
+ternaryInvalid.ts:145:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 143 │ // Complementary: one null, one undefined: fix IS safe
+ 144 │ declare const cp2: string | null | undefined;
+ > 145 │ const r34 = cp2 !== null && cp2 !== undefined ? cp2 : 'default';
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 146 │
+ 147 │ // Complementary reversed order: fix IS safe
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 143 143 │ // Complementary: one null, one undefined: fix IS safe
+ 144 144 │ declare const cp2: string | null | undefined;
+ 145 │ - const·r34·=·cp2·!==·null·&&·cp2·!==·undefined·?·cp2·:·'default';
+ 145 │ + const·r34·=·cp2·??·'default';
+ 146 146 │
+ 147 147 │ // Complementary reversed order: fix IS safe
+
+
+```
+
+```
+ternaryInvalid.ts:149:13 lint/nursery/useNullishCoalescing FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Prefer ?? over a ternary expression checking for nullish.
+
+ 147 │ // Complementary reversed order: fix IS safe
+ 148 │ declare const cp3: string | null | undefined;
+ > 149 │ const r35 = cp3 !== undefined && cp3 !== null ? cp3 : 'default';
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 150 │
+
+ i This rule is still being actively worked on, so it may be missing features or have rough edges. Visit https://github.com/biomejs/biome/issues/8043 for more information or to report possible bugs.
+
+ i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
+
+ i Safe fix: Use ?? instead.
+
+ 147 147 │ // Complementary reversed order: fix IS safe
+ 148 148 │ declare const cp3: string | null | undefined;
+ 149 │ - const·r35·=·cp3·!==·undefined·&&·cp3·!==·null·?·cp3·:·'default';
+ 149 │ + const·r35·=·cp3·??·'default';
+ 150 150 │
+
+
+```
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryValid.ts b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryValid.ts
new file mode 100644
index 000000000000..7501ad093ad9
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryValid.ts
@@ -0,0 +1,36 @@
+// should not generate diagnostics
+
+// Already using ??
+declare const x: string | null;
+const a = x ?? 'default';
+
+// Non-nullish comparison
+declare const b: number;
+const c = b > 0 ? b : 1;
+
+// Mismatched consequent
+declare const d: string | null;
+const e = d !== null ? 'something_else' : 'fallback';
+
+// Non-nullish literal comparison
+declare const f: string;
+const g = f === 'foo' ? f : 'bar';
+
+// Typeof check
+declare const t: string | null;
+const h = typeof t === 'string' ? t : 'default';
+
+// Different variables in test and branches
+declare const p: string | null;
+declare const q: string;
+const i = p !== null ? q : 'default';
+
+// Mismatched (loose)
+declare const m: string | null;
+declare const n: string;
+const j = m != null ? n : 'default';
+
+// Compound with different variables
+declare const s1: string | null;
+declare const s2: string | null;
+const k = s1 !== null && s2 !== undefined ? s1 : 'default';
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryValid.ts.snap b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryValid.ts.snap
new file mode 100644
index 000000000000..2968312b4ba5
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/ternaryValid.ts.snap
@@ -0,0 +1,44 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: ternaryValid.ts
+---
+# Input
+```ts
+// should not generate diagnostics
+
+// Already using ??
+declare const x: string | null;
+const a = x ?? 'default';
+
+// Non-nullish comparison
+declare const b: number;
+const c = b > 0 ? b : 1;
+
+// Mismatched consequent
+declare const d: string | null;
+const e = d !== null ? 'something_else' : 'fallback';
+
+// Non-nullish literal comparison
+declare const f: string;
+const g = f === 'foo' ? f : 'bar';
+
+// Typeof check
+declare const t: string | null;
+const h = typeof t === 'string' ? t : 'default';
+
+// Different variables in test and branches
+declare const p: string | null;
+declare const q: string;
+const i = p !== null ? q : 'default';
+
+// Mismatched (loose)
+declare const m: string | null;
+declare const n: string;
+const j = m != null ? n : 'default';
+
+// Compound with different variables
+declare const s1: string | null;
+declare const s2: string | null;
+const k = s1 !== null && s2 !== undefined ? s1 : 'default';
+
+```
diff --git a/crates/biome_rule_options/src/use_nullish_coalescing.rs b/crates/biome_rule_options/src/use_nullish_coalescing.rs
index 841a52040ecb..f90d0764988c 100644
--- a/crates/biome_rule_options/src/use_nullish_coalescing.rs
+++ b/crates/biome_rule_options/src/use_nullish_coalescing.rs
@@ -13,10 +13,20 @@ pub struct UseNullishCoalescingOptions {
///
/// Default: `true`
pub ignore_conditional_tests: Option,
+
+ /// Whether to ignore ternary expressions that could be simplified
+ /// using the nullish coalescing operator.
+ ///
+ /// Default: `false`
+ pub ignore_ternary_tests: Option,
}
impl UseNullishCoalescingOptions {
pub fn ignore_conditional_tests(&self) -> bool {
self.ignore_conditional_tests.unwrap_or(true)
}
+
+ pub fn ignore_ternary_tests(&self) -> bool {
+ self.ignore_ternary_tests.unwrap_or(false)
+ }
}
diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
index 3288cff34b0d..2c436968681f 100644
--- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts
+++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
@@ -7456,6 +7456,13 @@ that appear in places where the falsy-checking behavior may be intentional.
Default: `true`
*/
ignoreConditionalTests?: boolean;
+ /**
+ * Whether to ignore ternary expressions that could be simplified
+using the nullish coalescing operator.
+
+Default: `false`
+ */
+ ignoreTernaryTests?: boolean;
}
export type UsePlaywrightValidDescribeCallbackOptions = {};
export type UseRegexpExecOptions = {};
diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json
index 9674bfb4b581..1652d96df1a9 100644
--- a/packages/@biomejs/biome/configuration_schema.json
+++ b/packages/@biomejs/biome/configuration_schema.json
@@ -13935,6 +13935,11 @@
"description": "Whether to ignore `||` expressions in conditional test positions\n(if/while/for/do-while/ternary conditions).\n\nWhen `true` (the default), the rule will not report `||` expressions\nthat appear in places where the falsy-checking behavior may be intentional.\n\nDefault: `true`",
"type": ["boolean", "null"],
"default": null
+ },
+ "ignoreTernaryTests": {
+ "description": "Whether to ignore ternary expressions that could be simplified\nusing the nullish coalescing operator.\n\nDefault: `false`",
+ "type": ["boolean", "null"],
+ "default": null
}
},
"additionalProperties": false