Migrating to Dart null safety: best practice for migrating ternary operator null checks? Is a monadic approach too unconventional?

Issue

I’m migrating a code base to null safety, and it includes lots of code like this:

MyType convert(OtherType value) {
  return MyType(
    field1: value.field1,
    field2: value.field2 != null ? MyWrapper(value.field2) : null,
  );
}

Unfortunately, the ternary operator doesn’t support type promotion with null checks, which means I have to add ! to assert that it’s not null in order to make it compile under null safety:

MyType convert(OtherType value) {
  return MyType(
    field1: value.field1,
    field2: value.field2 != null ? MyWrapper(value.field2!) : null,
  );
}

This makes the code a bit unsafe; one could easily image a scenario where the null check is modified or some code is copied and pasted into a situation where that ! causes a crash.

So my question is whether there is a specific best practice to handle this situation more safely? Rewriting the code to take advantage of flow analysis and type promotion directly is unwieldy:

MyType convert(OtherType value) {
  final rawField2 = value.field2;
  final MyWrapper? field2;
  if (rawField2 != null) {
    field2 = MyWrapper(rawField2);
  } else {
    field2 = null;
  }

  return MyType(
    field1: value.field1,
    field2: field2,
  );
}

As someone who thinks a lot in terms of functional programming, my instinct is to think about about nullable types as a monad, and define map accordingly:

extension NullMap<T> on T? {
  U? map<U>(U Function(T) operation) {
    final value = this;
    if (value == null) {
      return null;
    } else {
      return operation(value);
    }
  }
}

Then this situation could be handled like this:

MyType convert(OtherType value) {
  return MyType(
    field1: value.field1,
    field2: value.field2.map((f) => MyWrapper(f)),
  );
}

This seems like a good approach to maintain both safety and concision. However, I’ve searched long and hard online and I can’t find anyone else using this approach in Dart. There are a few examples of packages that define an Optional monad that seem to predate null safety, but I can’t find any examples of Dart developers defining map directly on nullable types. Is there a major "gotcha" here that I’m missing? Is there another approach this is both ergonomic and more conventional in Dart?

Solution

Unfortunately, the ternary operator doesn’t support type promotion with null checks

This premise is not correct. The ternary operator does do type promotion. However, non-local variables cannot be promoted. Also see:

Therefore you should just introduce a local variable (which you seem to have already realized in your ifelse and NullFlatMap examples):

MyType convert(OtherType value) {
  final field2 = value.field2;
  return MyType(
    field1: value.field1,
    field2: field2 != null ? MyWrapper(field2) : null,
  );
}

Answered By – jamesdlin

Answer Checked By – Gilberto Lyons (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *