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:
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();
}
- Define a validation function
isValidUuid
. This function acts as a guard, blocking all invalid values from becomingUuid
- Create a
Uuid
class and give it a private constructor. In this way, it is not possible to create an instance ofUuid
from outside the class Uuid
contains anid
attribute asString
, which we will be able to assign and access only after proper validation- Define a
static make
function that acts as a validation constructor. This function returnsOption
since the validation may fail (read more aboutOption
here)
We then assign the Uuid
type to User
instead of String
:
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
:
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 π€