preface

Dart is a relatively young language with many of the features of modern programming languages, such as JavaScript ES6 features… Expansion operators, arrow functions, etc. At the same time, the type definition has absorbed the type inference feature, so that we can use the var keyword to define variables directly. Dart also supports dynamic typing at runtime. You can use the dynamic keyword to declare an unknown type and then make specific type judgments at runtime. So, how exactly do you use types properly? In this article, we describe the type guidelines for the Dart.

Summary of the type

When we write down a type in code, it means that the declared value will follow the type convention in the rest of the code. Types usually appear in two places: variable (member) type annotations or generic types specified. Type annotations are often thought of as so-called static types. We can type variables, function parameters, class member attributes, or return values. For example, bool and String are type annotations in the following example. This statically declared structure means that the type does not change while the code is running, and in fact the compiler will report an error if the wrong type is used.

bool isEmpty(String parameter) {
  bool result = parameter.isEmpty;
  return result;
}
Copy the code

Generics are a syntax for a set of types that can handle different types in the same code to improve coding efficiency. This feature has been widely used in Java and TypeScript as well. Generics include generic classes or generic methods, such as List

, Map

, and Set

. Something like ValueChanged

is a generic function. A generic class specifies a type in two forms, either when the type is declared or when the value is initialized.


,v>

/ / a generic class
var list = <int> [1.2.3];
List<int> anotherList = [1.2.3];

// Generic function :ValueChanged
typedef ValueChanged = void Function<T>(T item);
Copy the code

Type inference

In Dart, type annotation is optional. If the type annotation is omitted, Dart will infer the specific type based on the most recent context. But sometimes there is not enough information to infer the exact type (for example, when declaring a value, it might infer an integer or a floating point number). Dart sometimes reports an error when it cannot be accurately inferred, and sometimes assigns dynamic implicitly. Assigning dynamic implicitly causes the code to look like the type is fine, but in fact disables type checking. Dart’s support for both type inference and dynamic typing raises the question of what does “untyped” mean? Is the code dynamically typed, or is it written without typing? To avoid this confusion, you should actually avoid saying “no type.” In fact, we don’t have to worry about this concept, the code is either doing type annotation or type exact inference, or dynamic. And in the case of dynamic, you actually want to avoid it, because you introduce uncertainty. The benefit of type inference is that it saves us time writing type code and reading code without focusing on type information and on the business code itself. Declaring types explicitly increases the robustness and maintainability of code, in this case defining static types for the API and thus limiting the types available in different parts of the code in the program. While type inference is powerful, there’s nothing magical about it. Sometimes it fails, as in the following example:

void main() {
  var aNumber = 1;
  inferError(aNumber);
}

void inferError(double floatValue) {
  print('value: $floatValue');
}
Copy the code

In fact, type inference would putaNumberInference forintType, causing the compiler to report an error.The solution to this problem is to be as precise as possible when initializing the value of a variable. For example, the above example should be changed to, at this timeaNumberWould be inferred to fit the billdoubleType. It’s also good programming practice to make the type clear to the code reader by assigning it precisely.

void main() {
  var aNumber = 1.0;
  inferError(aNumber);
}
Copy the code

Here are three practical guidelines for striking the right balance between simplicity, control, flexibility, and extensibility.

  • Declare the type, even if the type is Dynamic, when there is not enough context to infer.
  • There is no need to annotate local variables or generic types if necessary.
  • It is recommended to use type annotations for global variables or member attributes unless the initialization value clearly represents the corresponding type.

Here are some specific coding suggestions.

For variables that do not have an initialized value, be sure to annotate the type

For global variables, local variables, static attributes, or member attributes, it is usually possible to infer their types from their initial values, but failure to do so will result in type inference failure. Therefore, in cases where there is no initialization value, it is important to specify the type.

// Correct example
List<AstNode> parameters;
if (node is Constructor) {
  parameters = node.signature;
} else if (node is Method) {
  parameters = node.parameters;
}

// Error example
var parameters;
if (node is Constructor) {
  parameters = node.signature;
} else if (node is Method) {
  parameters = node.parameters;
}
Copy the code

If fields and global variables are semantically difficult to infer the type, then the type should be annotated

Type annotations can function as documentation to a certain extent, and you can use boundary constraints to avoid type usage errors. Consider the following error example.

// Error example
install(id, destination) => ...
Copy the code

We can’t tell from the context what type id is, it could be int, it could be String, it could be anything else. Destination then has no idea what type of object it is. At this point, callers will have to read the source code to know how to call the install method. It’s very clear if you change it to something like this.

// Correct example
Future<bool> install(PackageId id, String destination) => ...
Copy the code

There is no exact definition of how to ensure semantic clarity, but the following are good examples:

  • The literal meaning is clear, enter the variable namename.emailThese are the kinds of things that we usually know as strings.
  • The constructor initializes the variable;
  • Initialize by referring to constants of explicit type;
  • A simple assignment expression for a number or string;
  • Factory methods, such as int.parse(), futrue.wait () and other common factory methods that know the return type.

The principle is simple: if you feel that there is anything that might confuse the type, then you should add a type annotation. Similarly, in cases where type inference depends on a return value from another library, it is recommended to include a type annotation so that if the return value type of another library changes, we can find specific solutions through compiler errors.

Do not type local variables repeatedly

Local variables have very little scope in a short function, and omitting type annotations allows code readers to focus on more important variable names and initial values, thus improving code reading efficiency. For example:

// Correct example
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
  var desserts = <List<Ingredient>>[];
  for (final recipe in cookbook) {
    if(pantry.containsAll(recipe)) { desserts.add(recipe); }}return desserts;
}

// Error example
List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
  List<List<Ingredient>> desserts = <List<Ingredient>>[];
  for (final List<Ingredient> recipe in cookbook) {
    if(pantry.containsAll(recipe)) { desserts.add(recipe); }}return desserts;
}
Copy the code

Another case is if the inferred type is not what we want, then we can redeclare it with another compatible type (usually a parent class) so that the type can be changed later.

// Correct example
Widget build(BuildContext context) {
  Widget result = Text('You won! ');
  if (applyPadding) {
    result = Padding(padding: EdgeInsets.all(8.0), child: result);
  }
  return result;
}
Copy the code

Be sure to annotate the return value type of the function

Dart does not infer a function’s return value type from the function body, so we need to annotate the function’s return value type ourselves. In fact, dynamic is returned by default if not annotated.

// Correct example
String makeGreeting(String who) {
  return 'Hello, $who! ';
}

// Error example
makeGreeting(String who) {
  return 'Hello, $who! ';
}
Copy the code

Of course, this is not necessary for anonymous functions, which infer the return value type from the function body.

Parameter types must be specified in function declarations

This is actually covered in the Dart Function Parameters Best Practice guide. A function defines the interface for external calls. Specifying parameter types can restrict the parameter types passed in by the caller, effectively avoiding running exceptions caused by incorrect parameter types. At the same time, specifying the parameter types also helps to improve the readability of the code. Note that even if the default value of a function parameter looks like initialization, no type inference is actually performed. If the type is not specified, it is considered dynamic, so the type needs to be annotated as well.

// Correct example
void sayRepeatedly(String message, {int count = 2{})for (var i = 0; i < count; i++) {
    print(message); }}// Error example
void sayRepeatedly(message, {count = 2{})for (var i = 0; i < count; i++) {
    print(message); }}Copy the code

You do not annotate the types of functions that can infer the parameter types

Note that functions here are usually callback-only, not declarative. We have seen this in many cases, such as the map operation of the collection class.

// Correct example
var names = people.map((person) => person.name);

// Error example
var names = people.map((Person person) => person.name);
Copy the code

Although the parameter names of callback functions can be customized, it is recommended to use variable names that are consistent with the object type names to improve readability.

For constructor initialization parameters, there is no need to annotate the type

In Dart, we typically use this.xx in constructors to initialize member attributes. In this case, there is no need to use type annotations for the parameters declared by the constructor.

// Correct example
class Point {
  double x, y;
  Point(this.x, this.y);
}

// Error example
class Point {
  double x, y;
  Point(double this.x, double this.y);
}
Copy the code

When using generics, you need to explicitly annotate the type if you cannot infer the type

While Dart is effective at inferring the type of a generic, there are cases where there is not enough context information to infer the type directly, and you need to explicitly annotate the type.

// Correct example
var playerScores = <String.int> {};final events = StreamController<Event>();

// Error example
var playerScores = {};
final events = StreamController()
Copy the code

Other times, assignment to a generic type is done through an expression, and if the initial value of the generic type is not a local variable, using type annotations makes our code more robust and readable.

// Correct example
class Downloader {
  final Completer<String> response = Completer();
}

// Error example
class Downloader {
  final response = Completer();
}
Copy the code

A counterpoint to this is that if the type of a generic can be inferred, there is no need to annotate the type. For example, in the example above, the Completer can already infer that it is Completer

(), so there is no need to add an additional

annotation.

// Error example
class Downloader {
  final Completer<String> response = Completer<String> (); }Copy the code

Avoid using incomplete generic types

This is often the case with collections, such as omitting type parameters by assuming that the List is already capable of inferred types, or not specifying specific K and V types when using a Map. In this case, Dart will assume type Dynamic.

// Correct example
List<num> numbers = [1.2.3];
var completer = Completer<Map<String.int> > ();// Error example
List numbers = [1.2.3];
var completer = Completer<Map> ();Copy the code

Rather than let type inference fail, annotate as Dynamic

Typically, dynamic is defaulted to when type inference does not match a type. However, if we already need a Dynamic type, it is better to actively label it as Dynamic, because this lets the code reader know that it needs a Dynamic type, which is often used when receiving data from the back end.

// Correct example
dynamic mergeJson(dynamic original, dynamic changes) => ...
  
Map<String.dynamic> readJson() => ...

void printUsers() {
  var json = readJson();
  var users = json['users'];
  print(users);
}

// Error example
mergeJson(original, changes) => ...
Copy the code

When using functions as arguments, it is best to do type annotation

Dart Function is a special Function identifier. In theory, we can use Function to match any Function argument, but this can lead to problems such as misuse or misuse that make programs less readable and maintainable. Therefore, in the case of Function as a Function parameter, it is best to specify the type, both the return value and the parameter type. In addition, if the function argument is too long, you can use a typedef to define the function alias to improve readability.

// Correct example
bool isValid(String value, bool Function(String) test) => ...
  
// Error example
bool isValid(String value, Function test) => ...
Copy the code

There are exceptions to this rule. If the Function argument needs to handle multiple types, you can pass Function directly, such as the following error handler,

// Correct example
void handleError(void Function() operation, Function errorHandler) {
  try {
    operation();
  } catch (err, stack) {
    if (errorHandler is Function(Object)) {
      errorHandler(err);
    } else if (errorHandler is Function(Object, StackTrace)) {
      errorHandler(err, stack);
    } else {
      throw ArgumentError('errorHandler has wrong signature.'); }}}Copy the code

Do not use the deprecated typedef syntax

Earlier Versions of Dart supported the following typedef syntax:

/ / has been abandoned
typedef int Comparison<T>(T a, T b);
typedef bool TestNumber(num);
Copy the code

This looks like a generic type, but actually uses dynamic. The above two functions are equivalent to:

int Comparison(dynamic a, dynamic b);
bool TestNumber(dynamic);
Copy the code

The correct way to use this is to assign:

// Correct example
typedef Comparison<T> = int Function(T, T);
typedef Comparison<T> = int Function(T a, T b);
Copy the code

For asynchronous functions with no return value, use the Future as the return type

For asynchronous functions with no return value, we might declare void directly. However, it is not excluded that the caller will call with await, in which case the return value will be Future

. Therefore, it is a good practice to use Future

as the return type for asynchronous functions that have no return value. Of course, if it is certain that no caller will need to await the asynchronous function to complete (such as reporting an error log for non-critical operations), it can be declared void.

Avoid using FutureOr as the return type

Using FutureOr

as the return type means that the caller needs to check the return value type before doing any further business processing, whereas using Future directly makes the caller’s code more consistent.

// Correct example
Future<int> triple(FutureOr<int> value) async= > (await value) * 3;

// Error example
FutureOr<int> triple(FutureOr<int> value) {
  if (value is int) return value * 3;
  return value.then((v) => v * 3);
}
Copy the code

There is only one situation in which FutureOr is used, and that is contravariant. In this case, you are essentially converting one type to another through an asynchronous operation. For example:

Stream<S> asyncMap<T, S>(
    可迭代<T> iterable, FutureOr<S> Function(T) callback) async* {
  for (final element in iterable) {
    yield awaitcallback(element); }}Copy the code

conclusion

As you can see, Dart is getting more and more focused on the importance of engagement. A good convention can reduce the pitfalls of many programs, such as the argument type annotation mentioned in this article, and the explicit argument type and return value type of functions. In fact, the type annotation itself is a convention — it tells code what an object is and how it should be used. Conventions should also be followed in our actual development process, which is important in team collaboration or writing base class libraries.

I am dao Code Farmer with the same name as my wechat official account. This is a column about the introduction and practice of Flutter, providing systematic learning articles about Flutter. See the corresponding source code here: The source code of Flutter Introduction and Practical column. If you have any questions, please add me to the wechat account: island-coder.

πŸ‘πŸ» : feel the harvest please point a praise to encourage!

🌟 : Collect articles, easy to look back!

πŸ’¬ : Comment exchange, mutual progress!