Dart lint: omit_local_variable_types


The omit_local_variable_types lint is one of my favourites.

It forces you to rely on type inference to determine the static type of a local variable, instead of writing the type you want it to be.

// You're not allowed to write this.
List<int> list = listOfNumbers();

// You have to omit `List<int>`, like this.
final list = listOfNumbers();

At first glance that looks less precise: you aren’t saying quite so clearly what you want it to mean.

But in fact writing the type can be misleading, because …

Implicit upcasts on assignment

Dart has implicit upcasts, which means you can just freely assign a narrower-typed value to a wider-typed variable.

For example, a double is one kind of num. So you can take a double and assign to a variable that has static type num.

num x;
x = 3.0; // It's a `double`, but that's fine!

That’s all well and good and not at all surprising—it would be awkward indeed if you had to write a cast.

num x;
x = 3.0 as num;

Covariant generics

Dart has covariant generics.

That’s not exactly a widely used and well understood term, but it’s easy enough to explain by example.

It means that you can take a List<int> and assign it to a variable of type List<num>.

List<num> list;
list = <int>[1, 2, 3];

“What’s the problem,” I hear you shout—an int is a num!

And indeed it is. As long as you only read from list, you’ll be just fine.

List<num> list;
list = <int>[1, 2, 3];
print(list[0] is num); // true
print(list[1] is num); // true
print(list[2] is num); // true

But what if you try to write to it? A double is also a num.

List<num> list;
list = <int>[1, 2, 3];
list.add(3.5); // Runtime error!

A List<int> is allowed to masquerade as a List<num>, which it does just fine, until you hand it a double. Then it’s revealed for what it is: a narrower type that can happily provide num but can’t accept arbitrary num.

Type annotations as surprise upcasts

What we get to is that if you write the type of a local variable as you initialize it, you might think you’re writing the runtime type of the variable, but actually be writing an upcast. And if there are generics, that upcast can lead quite easily to a runtime error.

void main() {
  // Prohibited by `omit_local_variable_types`.
  List<num> numbers = listOfNumbers();
  numbers.add(3.5); // Throws at runtime.

  // Write this instead.
  final moreNumbers = listOfNumbers();
  moreNumbers.add(3.5); // Fails at compile time.
}

List<int> listOfNumbers() => [1, 2, 3];
🔗 dartpad.dev

Type annotations as hidden arguments

The other effect that type annotations on local variable declarations have is that they sometimes end up as a type argument passed at runtime. For example:

void main() {
  List<num> numbers = createList();
}

List<T> createList<T>() => <T>[];

I much prefer that if we’re going to write <num> then it’s clear that it’s being passed as an argument to createList:

void main() {
  final numbers = createList<num>();
}

List<T> createList<T>() => <T>[];

 —because that matches what actually happens. It is the case that <num> is an argument, and a very important one, since it determines the type that is instantiated.

I’ve worked on code that uses generics in substantially complicated ways, and I’m reasonably sure that the “generics on the right” style handles that complexity better: it’s simple and clear.

In conclusion

I like omit_local_variable_types.

If you’re not already using it, perhaps give it a try!



📄 Dart
📄 Dart lint: omit_local_variable_types