-
Notifications
You must be signed in to change notification settings - Fork 227
Description
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 => expr3Semantics
- Evaluate the condition.
- If
true, evaluate the corresponding block and produce a value viareturn <expr>;. - If
false:- If there is an
else/else if, evaluate the next branch. - If there is no
else, the expression evaluates tonull.
- If there is an
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
ifexpression withoutelseis nullable. - Assigning it to a non-nullable target is a compile-time error unless an
elseis 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
elsechain, 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); // 3Fallback value (implicit null)
final someCondition = false;
final someValue = if (someCondition) {
// ... process something ...
return 3;
};
print(someValue); // nullArrow 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); // 1if / 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.