3

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?

jjoelson
  • 5,771
  • 5
  • 31
  • 51

1 Answers1

1

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 if-else and NullFlatMap examples):

MyType convert(OtherType value) {
  final field2 = value.field2;
  return MyType(
    field1: value.field1,
    field2: field2 != null ? MyWrapper(field2) : null,
  );
}
jamesdlin
  • 81,374
  • 13
  • 159
  • 204
  • Thanks for the correction! Would you say this is the idiomatic way to handle this situation? Having to create local variables versus keeping all of the logic in a single expression is a bit of a bummer. Do you see any potential problems with the `flatMap` solution? – jjoelson Mar 24 '22 at 21:19
  • 1
    @jjoelson Using a local variable is the idiomatic approach recommended by https://dart.dev/tools/non-promotion-reasons. I don't see any technical problem with the `flatMap` approach, but it likely would be much less clear to readers. Another approach: https://github.com/dart-lang/language/issues/360#issuecomment-502588017 – jamesdlin Mar 24 '22 at 21:57