Skip to content

Proposal: Value assignment via if expressions (block-bodied branches) #4613

@SamuelGadiel

Description

@SamuelGadiel

In Dart, assigning a value based on a condition is commonly done with:

  • The ternary operator (cond ? a : b), which gets hard to read with multiple branches or multi-step computations.
  • An IIFE/closure workaround when you need statements before producing the value:
    final value = condition
        ? (() {
            // multiple statements...
            return compute();
          })()
        : null;

Since Dart already has switch expressions, adding an if expression—especially one that supports statement blocks—would reduce boilerplate and improve readability in many real-world cases (including Flutter widget-building).

Proposal

Allow if to be used as an expression in any expression position (variable initialization, assignment, arguments, etc.), with support for block bodies.

Syntax (sketch)

final x = if (cond) {
  // statements...
  return expr;
};


final y = if (cond) => expr;


final z = if (cond1) {
  // statements...
  return expr1;
} else if (cond2) {
  // statements...
  return expr2;
} else {
  // statements...
  return expr3;
};


final w = if (cond1) => expr1 else if (cond2) => expr2 else => expr3

Semantics

  1. Evaluate the condition.
  2. If true, evaluate the corresponding block and produce a value via return <expr>;.
  3. If false:
    • If there is an else / else if, evaluate the next branch.
    • If there is no else, the expression evaluates to null.

Null-safety and typing rules (suggested)

Implicit null fallback

If an if expression has no else, it has an implicit else null.

  • Therefore, the static type of an if expression without else is nullable.
  • Assigning it to a non-nullable target is a compile-time error unless an else is present.

Type checking

  • Each branch must be value-producing (e.g., all control-flow paths in the block must end in return <expr>;).
  • With an else chain, the type is the least upper bound (LUB) of all branch expression types (similar to existing conditional typing).
  • Context type (downward inference) should work the same way it does for other expressions, helping inference where possible.

Examples

Basic assignment

final someCondition = true;

final someValue = if (someCondition) {
  // ... process something ...
  return 3;
};

print(someValue); // 3

Fallback value (implicit null)

final someCondition = false;

final someValue = if (someCondition) {
  // ... process something ...
  return 3;
};

print(someValue); // null

Arrow returning value

final someCondition = true;

final String? someValue = if (someCondition) => 3;

With typing (type checking applies)

final someCondition = true;

final String? someValue = if (someCondition) {
  return 3; // compile-time error: int is not assignable to String?
};

Reassign

double? someValue = 8;
final someCondition = true;

someValue = if (someCondition) {
  // ... process something ...
  return 1;
};

print(someValue); // 1

if / else if / else (else required if target is non-nullable)

double someValue = 8;
final someCondition = false;
final otherCondition = true;

someValue = if (someCondition) {
  // ... process something ...
  return 42;
} else if (otherCondition) {
  // ... process another thing ...
  return 20;
} else {
  // required because `double` is non-nullable
  return -1;
};

Flutter example: extracting conditional widget segments

Today, we might write:

return Column(
  children: [
    ?_labelWidget,
    SizedBox(height: effectiveGap),
    _selectWidget,
    if (widget.hint != null) ...[
      const SizedBox(height: 4),
      Text(
        widget.hint!,
        style: TextStyle(
          fontSize: 10,
          color: _theme.colors.foregroundMiddle,
        ),
      ),
    ],
  ],
);

Even when we try to separate it, we still have to deal with the condition:

Widget? _hint;
if (widget.hint != null) {
  _hint = [
    const SizedBox(height: 4),
    Text(
      widget.hint!,
      style: TextStyle(
        fontSize: 10,
        color: _theme.colors.foregroundMiddle,
      ),
    ),
  ];
};

return Column(
  children: [
    ?_labelWidget,
    SizedBox(height: effectiveGap),
    _selectWidget,
    ...?_hint,
  ],
);

With an if expression, we could extract the conditional segment into a variable and spread it:

final _hint = if (widget.hint != null) {
  return [
    const SizedBox(height: 4),
    Text(
      widget.hint!,
      style: TextStyle(
        fontSize: 10,
        color: _theme.colors.foregroundMiddle,
      ),
    ),
  ];
};

return Column(
  children: [
    ?_labelWidget,
    SizedBox(height: effectiveGap),
    _selectWidget,
    ...?_hint,
  ],
);

And by using the arrow-returning, we could return the list directly:

final _hint = if (widget.hint != null) => [
  const SizedBox(height: 4),
  Text(
    widget.hint!,
    style: TextStyle(
      fontSize: 10,
      color: _theme.colors.foregroundMiddle,
    ),
  ),
];

return Column(
  children: [
    ?_labelWidget,
    SizedBox(height: effectiveGap),
    _selectWidget,
    ...?_hint,
  ],
);

This can improve readability by:

  • reducing nested inline control-flow in collection literals,
  • allowing reuse of conditional segments,
  • and making complex widget-building logic easier to test/extract.

Open questions / design notes

  • Async interaction: should it allow async blocks (e.g., if (cond) async { ... }) and how should the resulting type be defined?
  • Readability vs. existing constructs: clarify the relationship between this feature, the ternary operator, and collection if / spread usage.

Why this helps

  • Avoids IIFE/closure boilerplate for multi-step computations.
  • Offers a readable alternative to nested ternaries.
  • Complements existing switch expressions.
  • Helps practical code patterns (e.g., Flutter widget-building) by enabling clean extraction of conditional segments.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions