Type validation in dart

10 January 2023

â€ĸ

3 min read

â€ĸ 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);
}
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");
}
/// 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();
}
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);
}
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);
  }
}
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 🤝