Dart's flow analysis narrows a nullable type after a real null check. Inside an if (x != null) { ... } branch the variable is treated as the non-nullable type T, so member access like x.toUpperCase() is a normal call, no ! and no ?. needed. Outside that branch the type goes back to T?. This is the preferred way to handle a value that might actually be missing.

Program

Play the program to check a nullable String? and let flow analysis promote it to String inside the if branch.

null_check_promote.dart
void main() {
  String? maybeCity = 'Oslo';
  String result;
  if (maybeCity != null) {
    result = maybeCity.toUpperCase();
  } else {
    result = 'UNKNOWN';
  }
  print('city=$result');
}
flow promotion Inside `if (maybeCity != null)`, Dart treats `maybeCity` as `String`, not `String?`. Member access like `.toUpperCase()` is a plain call with no null guard.
else branch Outside the `if`, `maybeCity` is back to `String?`. The `else` arm handles the missing case explicitly so `result` is always assigned before being read.
preferred form Prefer `if (x != null)` over `x!` when the value can actually be null. The compiler tracks the narrowing so you do not need to write `!` or `?.` inside the branch.