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