Take advantage of type aliases in Dart

mvolpato

Michele Volpato

Posted on May 24, 2021

Take advantage of type aliases in Dart

Dart 2.13 introduced type aliases for all types, not only function types. Like in the example provided in the official announcement, typedef Json = Map<String, dynamic>;, type aliases provide syntactic sugar that helps the developing process.
Let's see how we can exploit this new feature of Dart, and how not to use it.

Mask generics

As for the Json example, we can specify a generic type the is used in the same way in our code. If everywhere in our code, when we handle json data, we expect it to be of type Map<String, dynamic>, then we can just type-define it as Json and just use it as a new type everywhere.

In a more complicated case, let's say we have an app where we sell our products and services. We created a PaymentService that triggers a payment event, based on the cart and a given payment method, like credit card, or bank transfer:

abstract class PaymentMethod {}
class CreditCard implements PaymentMethod {}
class BankTransfer implements PaymentMethod {}

class PaymentService<ItemType, Method extends PaymentMethod> {
  ...
}
Enter fullscreen mode Exit fullscreen mode

We did this so that we can isolate credit card payment specific code in its specific class, and the same for bank transfer specific code. When we want to use PaymentService in our code, we specialize the generic type. For instance, a ShippingService might need to access payment services specialized on both credit card and bank transfer:

class ShippingService {
  final PaymentService<Product, CreditCard> creditCardPayment;
  final PaymentService<Product, BankTransfer> bankTransferPayment;
  ...
}
Enter fullscreen mode Exit fullscreen mode

Those specialized generic types are always the same inside ShippingService, and they make the code harder to read. In this case we can define some easier-to-read types, one for CreditCardPayment and one for BankTransferPayment.

typedef CreditCardPayment = PaymentService<Product, CreditCard>;
typedef BankTransferPayment = PaymentService<Product, BankTransfer>;
Enter fullscreen mode Exit fullscreen mode

We can now refactor ShippingService to a cleaner version:

class ShippingService {
  final CreditCardPayment creditCardPayment;
  final BankTransferPayment bankTransferPayment;
  ...
}
Enter fullscreen mode Exit fullscreen mode

The detail implementation is now hidden in the new type name, but it can still be accessed by reviewing the typedef statements. We do not need to use the long specialized types anymore.

Better semantic

Another situation where type aliases can help us write better code is by giving the proper semantic to variables and class properties.

Let's say we define the dimensions of a product as

class Product {
  /// Height in centimetres.
  final double height;

  /// Length in centimetres.
  final double length;

  /// Width in centimetres.
  final double width;

  ...
}
Enter fullscreen mode Exit fullscreen mode

There is nothing wrong here, but we can make it easier to read by writing some self-documenting code by using type aliases. We could define a Centimetres type alias:

typedef Centimetres = double;

class Product {
  final Centimetres height;
  final Centimetres length;
  final Centimetres width;

  ...
}
Enter fullscreen mode Exit fullscreen mode

Now not only the type specifies the unit of measure, making the comments unnecessary, but if we find out that those dimensions should be expressed with an integer instead, we only need to change the alias.

Generic function type

The possibility to create an alias for a generic function is available in Dart since version 1.24.

A generic function type allows us to treat functions as first-class citizens, even from a readability point of view, using them everywhere a type is expected, for instance in type annotations, return types, and actual type arguments. It also helps by improving readability, which, as for long generic types, is a problem for complex function signatures.

Continuing the payment example above, let's say that we have a function that calculates the tax owed, given the products. We can define the type of such function as:

typedef CalculateTaxes<Product> = int Function(List<Product>);

void processPayment(List<Product> products, CalculateTaxes<Product> taxHandler) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

This change does not seem to improve a lot, but with more complicated function definitions, it could make a bigger change for code readability.

When not to use type aliases

Not all cases where we could use type aliases are good cases. If a type alias does not improve readability, and does not provide value, it should not be used.

For instance, if we use a list of products in our app, while the user is adding and removing products from this list, we can see it as the cart for that user.

typedef Cart = List<Product>;
Enter fullscreen mode Exit fullscreen mode

But this Cart, as defined above, does not improve readability and does not add value. I would expect to be able to get the total of a cart by calling cart.total, but total is not available for List.

If we want to have a Cart type, we should consider creating a new class that contains a list of products, instead.

Conclusions

Type aliases are a very simple language feature that can be exploited to improve the readability of our codebase, but we should not over-use them, and we should consider carefully where a type alias is a good idea or just an over-complication.

💖 💪 🙅 🚩
mvolpato
Michele Volpato

Posted on May 24, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related