Skip to main content

prefer-optional-chain

Enforce using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty objects.

🔧

Some problems reported by this rule are automatically fixable by the --fix ESLint command line option.

💡

Some problems reported by this rule are manually fixable by editor suggestions.

💭

This rule requires type information to run.

?. optional chain expressions provide undefined if an object is null or undefined. Because the optional chain operator only chains when the property value is null or undefined, it is much safer than relying upon logical AND operator chaining &&; which chains on any truthy value. It is also often less code to use ?. optional chaining than && truthiness checks.

This rule reports on code where an && operator can be safely replaced with ?. optional chaining.

eslint.config.mjs
export default tseslint.config({
rules: {
"@typescript-eslint/prefer-optional-chain": "error"
}
});

Try this rule in the playground ↗

Examples

foo && foo.a && foo.a.b && foo.a.b.c;
foo && foo['a'] && foo['a'].b && foo['a'].b.c;
foo && foo.a && foo.a.b && foo.a.b.method && foo.a.b.method();

// With empty objects
(((foo || {}).a || {}).b || {}).c;
(((foo || {})['a'] || {}).b || {}).c;

// With negated `or`s
!foo || !foo.bar;
!foo || !foo[bar];
!foo || !foo.bar || !foo.bar.baz || !foo.bar.baz();

// this rule also supports converting chained strict nullish checks:
foo &&
foo.a != null &&
foo.a.b !== null &&
foo.a.b.c != undefined &&
foo.a.b.c.d !== undefined &&
foo.a.b.c.d.e;
Open in Playground

Options

This rule accepts the following options:

type Options = [
{
/** Allow autofixers that will change the return type of the expression. This option is considered unsafe as it may break the build. */
allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing?: boolean;
/** Check operands that are typed as `any` when inspecting "loose boolean" operands. */
checkAny?: boolean;
/** Check operands that are typed as `bigint` when inspecting "loose boolean" operands. */
checkBigInt?: boolean;
/** Check operands that are typed as `boolean` when inspecting "loose boolean" operands. */
checkBoolean?: boolean;
/** Check operands that are typed as `number` when inspecting "loose boolean" operands. */
checkNumber?: boolean;
/** Check operands that are typed as `string` when inspecting "loose boolean" operands. */
checkString?: boolean;
/** Check operands that are typed as `unknown` when inspecting "loose boolean" operands. */
checkUnknown?: boolean;
/** Skip operands that are not typed with `null` and/or `undefined` when inspecting "loose boolean" operands. */
requireNullish?: boolean;
},
];

const defaultOptions: Options = [
{
allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing: false,
checkAny: true,
checkBigInt: true,
checkBoolean: true,
checkNumber: true,
checkString: true,
checkUnknown: true,
requireNullish: false,
},
];

In the context of the descriptions below a "loose boolean" operand is any operand that implicitly coerces the value to a boolean. Specifically the argument of the not operator (!loose) or a bare value in a logical expression (loose && looser).

allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing

Allow autofixers that will change the return type of the expression. This option is considered unsafe as it may break the build. Default: false.

When this option is true, the rule will provide an auto-fixer for cases where the return type of the expression would change. For example for the expression !foo || foo.bar the return type of the expression is true | T, however for the equivalent optional chain foo?.bar the return type of the expression is undefined | T. Thus changing the code from a logical expression to an optional chain expression has altered the type of the expression.

In some cases this distinction may matter - which is why these fixers are considered unsafe - they may break the build! For example in the following code:

declare const foo: { bar: boolean } | null | undefined;
declare function acceptsBoolean(arg: boolean): void;

// ✅ typechecks succesfully as the expression only returns `boolean`
acceptsBoolean(foo != null && foo.bar);

// ❌ typechecks UNSUCCESSFULLY as the expression returns `boolean | undefined`
acceptsBoolean(foo?.bar);
Open in Playground

This style of code isn't super common - which means having this option set to true should be safe in most codebases. However we default it to false due to its unsafe nature. We have provided this option for convenience because it increases the autofix cases covered by the rule. If you set option to true the onus is entirely on you and your team to ensure that each fix is correct and safe and that it does not break the build.

When this option is false unsafe cases will have suggestion fixers provided instead of auto-fixers - meaning you can manually apply the fix using your IDE tooling.

checkAny

Check operands that are typed as any when inspecting "loose boolean" operands. Default: true.

Examples of code for this rule with { checkAny: false }:

declare const thing: any;

thing && thing.toString();
Open in Playground

checkUnknown

Check operands that are typed as unknown when inspecting "loose boolean" operands. Default: true.

Examples of code for this rule with { checkUnknown: false }:

declare const thing: unknown;

thing && thing.toString();
Open in Playground

checkString

Check operands that are typed as string when inspecting "loose boolean" operands. Default: true.

Examples of code for this rule with { checkString: false }:

declare const thing: string;

thing && thing.toString();
Open in Playground

checkNumber

Check operands that are typed as number when inspecting "loose boolean" operands. Default: true.

Examples of code for this rule with { checkNumber: false }:

declare const thing: number;

thing && thing.toString();
Open in Playground

checkBoolean

Check operands that are typed as boolean when inspecting "loose boolean" operands. Default: true.

note

This rule intentionally ignores the following case:

declare const x: false | { a: string };
x && x.a;
!x || x.a;

The boolean expression narrows out the non-nullish falsy cases - so converting the chain to x?.a would introduce a type error.

Examples of code for this rule with { checkBoolean: false }:

declare const thing: true;

thing && thing.toString();
Open in Playground

checkBigInt

Check operands that are typed as bigint when inspecting "loose boolean" operands. Default: true.

Examples of code for this rule with { checkBigInt: false }:

declare const thing: bigint;

thing && thing.toString();
Open in Playground

requireNullish

Skip operands that are not typed with null and/or undefined when inspecting "loose boolean" operands. Default: false.

Examples of code for this rule with { requireNullish: false }:

declare const thing1: string | null;
thing1 && thing1.toString();
Open in Playground

When Not To Use It

If your project is not accurately typed, such as if it's in the process of being converted to TypeScript or is susceptible to trade-offs in control flow analysis, it may be difficult to enable this rule for particularly non-type-safe areas of code. You might consider using ESLint disable comments for those specific situations instead of completely disabling this rule.

Further Reading


Type checked lint rules are more powerful than traditional lint rules, but also require configuring type checked linting.

See Troubleshooting > Linting with Type Information > Performance if you experience performance degradations after enabling type checked rules.

Resources