Search code examples
dartgenericstypestypeerror

Why do these dart generics compile, even though it throws a `TypeError`? What are some ways to fix it?


I wrote some dart code today, which essentially boils down to this:

extension MapCounting<ItemT> on Map<ItemT, num> {
  num get maxValue => values.reduce((max, value) => (max < value)? value : max);
}

Now, imagine I have a main() method like this:

void main() {
  Map<String, int> myMap = {
    "a": 1,
    "b": 2,
    "c": 3,
  };
  var count = myMap.maxValue; // valid because Map<String, int> is a valid subtype
}

The above code doesn't throw compile-time errors. However, if you run it, you'll get this error:

TypeError: Instance of '(num, num) => num': type '(num, num) => num' is not a subtype of type '(int, int) => int'

I think I understand why it's an error -- really, the crux of it comes down to how my "reducer" function only returns num when it needs to return int. This is because Map<String, int>.reduce() expects a int Function(int, int), and my definition can only guarantee it's a num Function(...).

I have two questions regarding this:

  1. Why does the compiler not catch this as an error? Shouldn't it?
  2. How can I fix my extension (nicely)?

I technically found an answer to my second question, but it basically involves rewriting my code outside the extension. Is it possible to do it with a get maxValue getter inside the extension?


Solution

    1. Why does the compiler not catch this as an error? Shouldn't it?

    It compiles because:

    • The MapCounting<ItemT> extension is legal on its own.
    • As you already noted, Dart treats Map<String, int> as a subtype of Map<String, num>. In general, treating Generic<Derived> as a subtype of Generic<Base> often is convenient, but there are a number of cases where it's not actually safe.

    The compiler can't determine that applying a legal extension to (what it considers to be) a legal subtype won't work at runtime.

    1. How can I fix my extension (nicely)?

    Don't use num. Make your extension work on the actual num subtype:

    extension MapCounting<ItemT, N extends num> on Map<ItemT, N> {
      N get maxValue => values.reduce((max, value) => (max < value)? value : max);
    }