@typescript-eslint/prefer-optional-chain

Require a?.b instead of a && a.b, !a || !a.b, or (a || {}).b

fixableneeds types

Also called typescript-eslint/prefer-optional-chain, typescript/prefer-optional-chain

user && user.profile && user.profile.city repeats each step of the path to guard the next one. user?.profile?.city says the same thing once.

The rule reports chains of &&, chains of || with negated operands, comparisons with null and undefined such as user != null && user.name, and an empty object as a fallback, like (user || {}).name or (user ?? {}).name.

--fix rewrites a chain only if that doesn't add undefined to the result's type. If user is User | null, user && user.name can be null but user?.name is undefined instead, so it's reported but not fixed. (user || {}).name isn't fixed either.

Configure#

Off by default. This rule needs types, so turning it on makes bun lint type check your code first.

bunfig.toml
[lint.rules]
"@typescript-eslint/prefer-optional-chain" = "error" # or "warn"

Every option, set to its default:

bunfig.toml
[lint.rules]
"@typescript-eslint/prefer-optional-chain" = ["error", {
  checkString = true,
  checkNumber = true,
  checkBoolean = true,
  checkBigInt = true,
  checkAny = true,
  checkUnknown = true,
  requireNullish = false,
  allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing = false,
}]

Examples#

example.ts3 problems
type User = { profile?: { city?: string; refresh?: () => void } };

function show(user: User | undefined) {
  const city = user && user.profile && user.profile.city;
  if (!user || !user.profile) return;
  return (user.profile || {}).refresh;
}
example.tsNo problems
type User = { profile?: { city?: string; refresh?: () => void } };

function show(user: User | undefined) {
  const city = user?.profile?.city;
  if (!user?.profile) return;
  return user.profile?.refresh;
}

function both(user: User | undefined, other: User) {
  return user && other.profile; // two different objects
}

Options#

OptionDefaultWhat it does
checkStringtrueReport x && x.y when x can be a string.
checkNumbertrueThe same for a number.
checkBooleantrueThe same for a boolean.
checkBigInttrueThe same for a bigint.
checkAnytrueThe same for any.
checkUnknowntrueThe same for unknown.
requireNullishfalseOnly report if something that's tested can be null or undefined.
allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoingfalseLet --fix rewrite && and || chains even where that changes the result's type, for example from null to undefined.

For an empty string, name && name.length is "" and name?.length is 0. This is reported by default:

example.ts1 problem
function size(name: string | undefined) {
  return name && name.length;
}

Set checkString: false if your code depends on that difference. Then it isn't reported:

example.tswith { "checkString": false }No problems
function size(name: string | undefined) {
  return name && name.length;
}

Here name is never null or undefined. It's still reported by default:

example.ts1 problem
function size(name: string) {
  return name && name.length;
}

With requireNullish: true it's left alone:

example.tswith { "requireNullish": true }No problems
function size(name: string) {
  return name && name.length;
}