In Dart, what does the F-bound T extends Comparable<T> require, and why does maxOf(3, 7) with that bound infer num rather than int?
answer
- comparable to its own type
- int implements Comparable<num>
- Comparable<num> is not Comparable<int>
- inference using bounds, Dart 3.7
- explicit maxOf<int> fails the bound
basics
~10 sT extends Comparable<T> requires T to be comparable with itself. int only implements Comparable<num>, so int fails the bound and Dart 3.7+ infers T as num; maxOf<int>(3, 7) is a compile-time error.
solid answer
~40 sAn **F-bound** mentions the type parameter inside its own bound: `T extends Comparable<T>` means each `T` must have `compareTo(T other)`. `String`, `DateTime` and `Duration` implement `Comparable` of themselves and fit directly. `int` and `double` do not: they inherit `Comparable<num>` from `num`, and `Comparable<num>` is not a subtype of `Comparable<int>`, so `int` fails the bound. For `maxOf(3, 7)`, Dart 3.7's inference using bounds settles on `T = num`, which does satisfy it, and the result has static type `num`; before 3.7 the call failed to infer. Writing `maxOf<int>(3, 7)` is a compile-time error. Options: accept a `num` result, loosen the bound to `T extends Comparable<Object?>`, which `int` satisfies, or take a `Comparator<T>` parameter instead of a bound.
code
dart · 18 linesT maxOf<T extends Comparable<T>>(T a, T b) => a.compareTo(b) >= 0 ? a : b;
T maxLoose<T extends Comparable<Object?>>(T a, T b) =>
a.compareTo(b) >= 0 ? a : b;
T maxBy<T>(T a, T b, Comparator<T> compare) => compare(a, b) >= 0 ? a : b;
void main() {
print(maxOf('ada', 'grace')); // grace: T = String
final n = maxOf(3, 7); // T inferred as num (Dart 3.7+)
print(n); // 7, static type num
// maxOf<int>(3, 7); // error: int does not satisfy Comparable<int>
final int i = maxLoose(3, 7); // T = int satisfies Comparable<Object?>
final int j = maxBy(3, 7, (a, b) => a.compareTo(b));
print(i + j); // 14
}go deeper
Recall that T extends Comparable<T> means T must be comparable with itself, and that String and DateTime fit it.
Explain why int inherits Comparable<num> and therefore fails the bound, and what type maxOf(3, 7) infers.
Choose between an F-bound, a looser Comparable<Object?> bound and a Comparator parameter for shared sorting and ranking helpers.
Keep generic utility APIs simple enough for a team to use correctly, preferring explicit comparators where bounds create type puzzles.
## What an F-bound is A type-parameter bound that mentions the parameter itself is called an **F-bound**, or a self-referential bound. The dart.dev generics page gives the canonical example: ```dart int compareAndOffset<T extends Comparable<T>>(T t1, T t2) => t1.compareTo(t2) + 1; ``` `T extends Comparable<T>` means *`T` must be comparable to itself*: `t1.compareTo(t2)` is statically valid because `t1` is a `Comparable<T>` and `t2` is a `T`. A class `A implements Comparable<A>` satisfies it; so do the core types that implement `Comparable` of themselves. ## Which core types fit | Type | Declared as | Satisfies `T extends Comparable<T>` with `T` = itself? | |---|---|---| | `String` | `implements Comparable<String>` | yes | | `DateTime` | `implements Comparable<DateTime>` | yes | | `Duration` | `implements Comparable<Duration>` | yes | | `num` | `implements Comparable<num>` | yes | | `int`, `double` | `extends num`, so `Comparable<num>` | **no** | The last row is the trap. `int` is a `Comparable<num>`. For it to be a `Comparable<int>`, `Comparable<num>` would have to be a subtype of `Comparable<int>`, which in Dart's covariant generics requires `num` to be a subtype of `int`. It is not, so `int` fails the bound. ## What happens at a call site ```dart T maxOf<T extends Comparable<T>>(T a, T b) => a.compareTo(b) >= 0 ? a : b; ``` 1. `maxOf('a', 'b')` infers `T = String` and returns a `String`. 2. `maxOf(3, 7)` cannot use `T = int`. With **inference using bounds**, added in Dart 3.7, inference takes the bound into account and settles on `T = num`, which satisfies `num extends Comparable<num>`. The call compiles and returns `7`, but its **static type is `num`**, so `int x = maxOf(3, 7);` does not compile without a cast. 3. `maxOf<int>(3, 7)` is a **compile-time error**, because the explicit argument violates the bound. 4. Before Dart 3.7, `maxOf(3, 7)` failed to infer at all and you had to write `maxOf<num>(3, 7)`. ## Ways to handle it - **Accept `num`.** If callers only need ordering, a `num` result is often fine. - **Loosen the bound** to `T extends Comparable<Object?>`. `int` satisfies it, because `Comparable<num>` is a `Comparable<Object?>`, so `maxOf<int>` compiles and returns `int`. The price is a weaker static guarantee: the bound no longer says that `T` compares against `T` specifically. - **Take a comparator** instead of a bound: `T maxBy<T>(T a, T b, Comparator<T> compare)`. Callers pass `(a, b) => a.compareTo(b)` or any ordering, and no bound is needed at all. ## Why Dart's core types look like this `num` is the common supertype of `int` and `double`, and it declares `compareTo(num other)` so that `3.compareTo(2.5)` works across both. Implementing `Comparable<num>` once, on `num`, is what makes mixed comparisons legal. The cost is exactly this bound problem: the subtypes cannot also claim to be `Comparable` of themselves, because a class cannot implement the same generic interface twice with different type arguments. It is a deliberate choice in the core library, not an oversight, and it is why generic numeric helpers in Dart usually bound by `num` or take a comparator rather than using a self-referential bound. ## F-bounds in your own hierarchies F-bounds also appear on class hierarchies, such as `class Node<T extends Node<T>>`. The dart.dev type-system page uses `class A<X extends A<X>>`, `class B extends A<B>` and `class C extends B` to show that `f(C())` for `f<X extends A<X>>` now infers `X = B`, because `C` itself is not an `A<C>`. The same rule is at work as with `int`: the subclass inherits its parent's self-reference, not its own. ## Common mistakes - Assuming `int` is a `Comparable<int>` because it is comparable to other ints. - Adding an F-bound where a `Comparator<T>` parameter would be simpler and more flexible. - Being surprised that a helper's result is `num` rather than `int`, and adding casts instead of revisiting the bound.
- Why does `T extends Comparable<Object?>` accept int when `T extends Comparable<T>` does not?`int` is a `Comparable<num>`, and Dart's generics are covariant, so `Comparable<num>` is a subtype of `Comparable<Object?>`. The loose bound therefore holds for `T = int`. The self-referential bound would need `Comparable<num>` to be a `Comparable<int>`, which is false.
- When would you choose a Comparator<T> parameter over any bound?When callers may want different orderings for the same type, such as by date or by total, or when the type does not implement `Comparable` at all. A comparator parameter needs no bound, works for every `T`, and keeps the result type exactly `T`; it costs one extra argument at each call.
saying these in an interview costs you the question
- int implements Comparable<int>, so it fits T extends Comparable<T>.
- maxOf<int>(3, 7) compiles because 3 and 7 are comparable ints.
- maxOf(3, 7) returns a value whose static type is int.
- An F-bound is checked only at run time.
- Loosening the bound to Comparable<Object?> keeps the same guarantee.