Tiny types wrapping primitive types

Tiny types (also called micro types) can make code easier to read and navigate. Also validation can be done at a single place.

What is the problem to solve?

Please take a look to the code this method:


private void SendMessage(int portNumber, int timeout)
{
    // Implementation
}

When calling this message, the parameters can be unintentionally swapped and it still compiles. This is one of the problems with this anti-pattern called primitive obsession.

Imagine that a new port number must be used everywhere in the solution and you need to check all places where a port number is used. 'Find all references' of int will not help you.

  • You can no longer accidentally swap the parameters, without compiling errors.
  • 'Find al references' to PortNumber is useful to find other usages of PortNumber.
  • The timeout is in milliseconds. That is clear to the reader of the code.
  • The type can have a build in validation for illegal values (like a port number cannot be negative).

The solution: build your own class for it or use tiny types:


private void SendMessage(PortNumber portNumber, Miliseconds timeout)
{
    // Implementation
}

This solution is more useful:

  • You can no longer accidentally swap the parameters, without compiling errors.
  • 'Find al references' to PortNumber is useful to find other usages of PortNumber.
  • The timeout is in milliseconds. That is clear to the reader of the code.
  • The type can have a build in validation for illegal values (like a port number cannot be negative).

Create tiny types by wrapping primitive types

When using a base class that do most of the stuff vor you, this is how to create a tiny type:


public class PortNumber : TinyType<int>
{
    protected PortNumber(int value) : base(value)
    {
        if (value < 0)
        {
            throw new ArgumentException($"A {nameof(PortNumber)} must have a value of at least 0.", nameof(value));
        }
    }

    public static implicit operator PortNumber(int value) => new(value);
}

Note that the int is a struct and it is wrapped by a class. A more performant solution would be to use a struct as a wrapper but then you can not use inheritance. In this case a lot of functionality is implemented in the base class.

Tine types that are simular can use a common base class

The code for Miliseconds is almost the same as the PortNumber class so you can even create an abstract class to save duplicate code. This makes the code of the 2 classes even shorter.


public abstract class NonNegativeNumber : TinyType<int>
{
    protected NonNegativeNumber(int value) : base(value)
    {
        if (value < 0)
        {
            throw new ArgumentException($"A {nameof(NonNegativeNumber)} must have a value of at least 0.", nameof(value));
        }
    }
}

public class PortNumber : NonNegativeNumber
{
    protected PortNumber(int value) : base(value)
    {
    }

    public static implicit operator PortNumber(int value) => new(value);
}

public class Miliseconds : NonNegativeNumber
{
    protected Miliseconds(int value) : base(value)
    {
    }

    public static implicit operator Miliseconds(int value) => new(value);
}

Works just like primitive types

The TinyType base class has support for IComparable, Equals, GetHasCode() and the basic operators


Miliseconds m1 = 200; // Assign like an int
Miliseconds m2 = 400;
Miliseconds m3 = 200;

var isSame = m1 == m3; // true
var isGreaterThan = m2 > m1; // true
var isComparedToInt = m2 > 5; // true, compares to int

// Support for ordering / IComparable<T>
var orderedItems = new[] { m1, m2, m3 }.OrderBy(m => m); 

// Support for GetHashCode for quick lookup in Dictionary
var dictionary = new Dictionary<Miliseconds, string> 
{
    [m1] = "1",
    [m2] = "2"
};

Support for classes like strings

The examples above works for structs, for classes like strings there is a second base class that works very simular:


EmailAddress emailAddress = "alex@siepman.nl";
Name firstName = "Alex";

Examples of the type implementations:


public class Name : TinyTypeClass<string>
{
    public Name(string value) : base(value.Trim())
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentException($"A {nameof(Name)} must have a length of at least 1.", nameof(value));
        }
    }

    public static implicit operator Name(string value) => new(value);
}

public class EmailAddress : TinyTypeClass<string>
{
    public EmailAddress(string value) : base(value.Trim())
    {
        if (!IsValidEmail(value))
        {
            throw new ArgumentException($"The format of the {nameof(EmailAddress)} is invalid.", nameof(value));
        }
    }

    public static implicit operator EmailAddress(string value) => new(value);

    private static bool IsValidEmail(string email)
    {
        if (email.Trim().EndsWith("."))
        {
            return false;
        }

        try
        {
            var address = new System.Net.Mail.MailAddress(email);
            return address.Address == email;
        }
        catch
        {
            return false;
        }
    }
}

The used base classes


public abstract class TinyType<T> : IComparable<TinyType<T>> 
    where T : struct, IComparable<T>
{
    public T Value { get; }

    protected TinyType(T value)
    {
        Value = value;
    }

    public override bool Equals(object? obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != GetType()) return false;
        return Equals((TinyType<T>)obj);
    }

    protected bool Equals(TinyType<T> value)
    {
        return Value.Equals(value.Value);
    }

    public bool Equals(T value)
    {
        return Value.Equals(value);
    }

    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }

    public override string? ToString()
    {
        return Value.ToString();
    }

    public static bool operator <(TinyType<T> left, TinyType<T> right)
    {
        return Comparer<TinyType<T>>.Default.Compare(left, right) < 0;
    }

    public static bool operator >(TinyType<T> left, TinyType<T> right)
    {
        return Comparer<TinyType<T>>.Default.Compare(left, right) > 0;
    }

    public static bool operator <=(TinyType<T> left, TinyType<T> right)
    {
        return Comparer<TinyType<T>>.Default.Compare(left, right) <= 0;
    }

    public static bool operator >=(TinyType<T> left, TinyType<T> right)
    {
        return Comparer<TinyType<T>>.Default.Compare(left, right) >= 0;
    }

    public static bool operator ==(TinyType<T> left, TinyType<T> right)
    {
        return Comparer<TinyType<T>>.Default.Compare(left, right) == 0;
    }

    public static bool operator !=(TinyType<T> left, TinyType<T> right)
    {
        return Comparer<TinyType<T>>.Default.Compare(left, right) != 0;
    }

    public static bool operator <(TinyType<T> left, T right)
    {
        return Comparer<T>.Default.Compare(left.Value, right) < 0;
    }

    public static bool operator >(TinyType<T> left, T right)
    {
        return Comparer<T>.Default.Compare(left.Value, right) > 0;
    }

    public static bool operator <(T left, TinyType<T> right)
    {
        return Comparer<T>.Default.Compare(left, right.Value) < 0;
    }

    public static bool operator >=(T left, TinyType<T> right)
    {
        return Comparer<T>.Default.Compare(left, right.Value) >= 0;
    }

    public static bool operator <=(TinyType<T> left, T right)
    {
        return Comparer<T>.Default.Compare(left.Value, right) <= 0;
    }

    public static bool operator >=(TinyType<T> left, T right)
    {
        return Comparer<T>.Default.Compare(left.Value, right) >= 0;
    }

    public static bool operator <=(T left, TinyType<T> right)
    {
        return Comparer<T>.Default.Compare(left, right.Value) <= 0;
    }

    public static bool operator >(T left, TinyType<T> right)
    {
        return Comparer<T>.Default.Compare(left, right.Value) > 0;
    }

    public static bool operator ==(TinyType<T> left, T right)
    {
        return Comparer<T>.Default.Compare(left.Value, right) == 0;
    }

    public static bool operator !=(TinyType<T> left, T right)
    {
        return Comparer<T>.Default.Compare(left.Value, right) != 0;
    }

    public static bool operator ==(T left, TinyType<T> right)
    {
        return Comparer<T>.Default.Compare(left, right.Value) == 0;
    }

    public static bool operator !=(T left, TinyType<T> right)
    {
        return Comparer<T>.Default.Compare(left, right.Value) != 0;
    }

    public int CompareTo(TinyType<T>? other)
    {
        if (ReferenceEquals(this, other)) return 0;
        if (ReferenceEquals(null, other)) return 1;
        return Value.CompareTo(other.Value);
    }
}

public abstract class TinyTypeClass<T> : IComparable<T>, IComparable<TinyTypeClass<T>>
    where T : IComparable<T>
{
    public T Value { get; }

    protected TinyTypeClass(T value)
    {
        Value = value ?? throw new ArgumentNullException($"If you want the value to be null, make the {nameof(TinyTypeClass<T>)} null.",nameof(value));
    }

    public override bool Equals(object? obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != GetType()) return false;
        return Equals((TinyTypeClass<T>)obj);
    }

    protected bool Equals(TinyTypeClass<T> value)
    {
        return EqualityComparer<T>.Default.Equals(Value, value.Value);
    }

    public bool Equals(T value)
    {
        return Value.Equals(value);
    }

    public override int GetHashCode()
    {
        return EqualityComparer<T>.Default.GetHashCode(Value);
    }

    public override string ToString()
    {
        return Value.ToString() ?? "";
    }
    public int CompareTo(T? other)
    {
        if (ReferenceEquals(this, other)) return 0;
        if (ReferenceEquals(null, other)) return 1;
        return Value.CompareTo(other);
    }

    public int CompareTo(TinyTypeClass<T>? other)
    {
        if (ReferenceEquals(this, other)) return 0;
        if (ReferenceEquals(null, other)) return 1;
        return Value.CompareTo(other.Value);
    }

    public static bool operator <(TinyTypeClass<T>? left, TinyTypeClass<T>? right)
    {
        return Comparer<TinyTypeClass<T>>.Default.Compare(left, right) < 0;
    }

    public static bool operator >(TinyTypeClass<T>? left, TinyTypeClass<T>? right)
    {
        return Comparer<TinyTypeClass<T>>.Default.Compare(left, right) > 0;
    }

    public static bool operator <=(TinyTypeClass<T>? left, TinyTypeClass<T>? right)
    {
        return Comparer<TinyTypeClass<T>>.Default.Compare(left, right) <= 0;
    }

    public static bool operator >=(TinyTypeClass<T>? left, TinyTypeClass<T>? right)
    {
        return Comparer<TinyTypeClass<T>>.Default.Compare(left, right) >= 0;
    }

    public static bool operator ==(TinyTypeClass<T>? left, TinyTypeClass<T>? right)
    {
        return Comparer<TinyTypeClass<T>>.Default.Compare(left, right) == 0;
    }

    public static bool operator !=(TinyTypeClass<T>? left, TinyTypeClass<T>? right)
    {
        return Comparer<TinyTypeClass<T>>.Default.Compare(left, right) != 0;
    }

    public static bool operator <(TinyTypeClass<T>? left, T right)
    {
        if (left == null) return false;
        return Comparer<T>.Default.Compare(left.Value, right) < 0;
    }

    public static bool operator >(TinyTypeClass<T>? left, T right)
    {
        if (left == null) return true;
        return Comparer<T>.Default.Compare(left.Value, right) > 0;
    }

    public static bool operator <(T left, TinyTypeClass<T>? right)
    {
        if (right == null) return true;
        return Comparer<T>.Default.Compare(left, right.Value) < 0;
    }

    public static bool operator >=(T left, TinyTypeClass<T>? right)
    {
        if (right == null) return false;
        return Comparer<T>.Default.Compare(left, right.Value) >= 0;
    }

    public static bool operator <=(TinyTypeClass<T>? left, T right)
    {
        if (left == null) return false;
        return Comparer<T>.Default.Compare(left.Value, right) <= 0;
    }

    public static bool operator >=(TinyTypeClass<T>? left, T right)
    {
        if (left == null) return true;
        return Comparer<T>.Default.Compare(left.Value, right) >= 0;
    }

    public static bool operator <=(T left, TinyTypeClass<T>? right)
    {
        if (right == null) return true;
        return Comparer<T>.Default.Compare(left, right.Value) <= 0;
    }

    public static bool operator >(T left, TinyTypeClass<T>? right)
    {
        if (right == null) return false;
        return Comparer<T>.Default.Compare(left, right.Value) > 0;
    }

    public static bool operator ==(TinyTypeClass<T>? left, T right)
    {
        if (left == null) return false;
        return Comparer<T>.Default.Compare(left.Value, right) == 0;
    }

    public static bool operator !=(TinyTypeClass<T>? left, T right)
    {
        if (left == null) return true;
        return Comparer<T>.Default.Compare(left.Value, right) != 0;
    }

    public static bool operator ==(T left, TinyTypeClass<T>? right)
    {
        if (right == null) return true;
        return Comparer<T>.Default.Compare(left, right.Value) == 0;
    }

    public static bool operator !=(T left, TinyTypeClass<T>? right)
    {
        if (right == null) return false;
        return Comparer<T>.Default.Compare(left, right.Value) != 0;
    }
}

Leave a Comment

Comment

Comments

C# CSharp Blog Comment

Dan 27-09-2023 / Reply

This is incredibly useful. Thank you for posting it.


C# CSharp Blog Comment

geld ophalen voor vereniging 06-12-2023 / Reply

"Fantastisch initiatief! Het is geweldig om te zien dat jullie actief bezig zijn met geld ophalen voor de vereniging. Het is cruciaal om de betrokkenheid van de gemeenschap te vergroten en zo de ondersteuning te versterken. Samen kunnen we bijdragen aan het succes van de vereniging en haar doelen realiseren.