Type validation in dart

Sandro Maglione

Sandro Maglione

Mobile development

Most of the times primitive types are too generic, and this can cause problems.

Think about a generic app which has a User class. Every User generally has an id associated with it. You could implement this as follows:

class User {
  final String id;
  const User(this.id);
}

This works, but it may cause problems. In fact, not every String is a valid id. Nonetheless, this model allows any String, without validation.

You could implement validation before creating a User:

/// Getting `id` (`String`) from an API or somewhere else ๐Ÿ‘‡
if (isValidUuid(id)) {
  return User(id);
} else {
  throw Exception("Not a valid id");
}

Nonetheless, also this solution is not ideal. In fact, we still have a User containing a String as id.

We cannot be sure that the id will still be valid throughout the lifecycle of our application. Someone may accidentally change this value or use it improperly.


Type-safe models in dart: Uuid

The ideal type-safe solution is to introduce a new type specific for id.

This type cannot be accessed or created unless the provided String is valid.

We can call this type Uuid. The implementation is as follows:

uuid.dart
import 'package:fpdart/fpdart.dart';
 
bool isValidUuid(String str) => true; // Validation here โ˜๏ธ
 
class Uuid {
  final String id;
 
  /// Private constructor, not accessible from outside ๐Ÿ”‘
  const Uuid._(this.id);
 
  /// The only way to get a [Uuid] is to use `make`
  ///
  /// This ensures that every [Uuid] is in the correct format โ˜‘๏ธ
  static Option<Uuid> make(String id) =>
      isValidUuid(id) ? some(Uuid._(id)) : none();
}
  1. Define a validation function isValidUuid. This function acts as a guard, blocking all invalid values from becoming Uuid
  2. Create a Uuid class and give it a private constructor. In this way, it is not possible to create an instance of Uuid from outside the class
  3. Uuid contains an id attribute as String, which we will be able to assign and access only after proper validation
  4. Define a static make function that acts as a validation constructor. This function returns Option since the validation may fail (read more about Option here)

We then assign the Uuid type to User instead of String:

user.dart
class User {
  final Uuid uuid;
  const User(this.uuid);
}

Now we are required to validate every id before we are allowed to create a new User:

main.dart
void main() {
  final optionUuid = Uuid.make("");
 
  /// Cannot create [User] unless [Uuid] is verified ๐Ÿ”
  if (optionUuid is Some<Uuid>) {
    final user = User(optionUuid.value);
  }
}

Now we are safe to use User everywhere in the app. We can access the valid String from Uuid as follows: user.uuid.id.

We are also sure that no one will be allowed to change the id unless they properly validate it again.

That's because the only way of creating a Uuid is to use make, which validates every time any String.

We are safe again now. Happy coding ๐Ÿค

๐Ÿ‘‹ใƒปInterested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.