Strongly-typed identifier

From HandWiki

A strongly-typed identifier is data type which serves as an identifier or key that is strongly typed. This is a solution to the "primitive obsession" code smell as mentioned by Martin Fowler. The data type should preferably be immutable. It is common for implementations to handle equality testing, serialization and model binding.

The strongly-typed identifier commonly wraps the data type used as the primary key in the database, such as a string, an integer or universally unique identifier (UUID).

Web frameworks can often be configured to model bind properties on them model that are strongly-typed identifiers. Object–relational mappers can often be configured with value converters to map data between the properties on a model using strongly-typed identifier data types and database columns.

Examples

C#

C# have records which provide immutability and equality testing.[1] The record is sealed to prevent inheritance.[2]

This example implementation includes a static method which can be used to initialize a new instance with a randomly generated globally unique identifier (GUID).

/// <summary>
/// Represents a user identifier.
/// </summary>
/// <param name="Id">The user identifier.</param>
public sealed record UserId(Guid Id)
{
    /// <summary>
    /// Initializes a new instance of the <see cref="UserId" /> record.
    /// </summary>
    /// <returns>A new UserId object.</returns>
    public static UserId New() => new(Guid.NewGuid());
}

Go

Go have structs which provide equality testing. Go however does not provide immutability.

// Represents a user identifier.
type UserId struct{ id string }

// Creates a new user identifier.
func NewUserId(id string) UserId {
	return UserId{id: id}
}

Java

Java have records which provide equality testing.[3] The record is declared using the final modifier keyword to prevent inheritance.

/**
 * Represents a user identifier.
 * @param id The user identifier.
 */
public final record UserId(UUID id) {
    /**
     * Initializes a new instance of the UserId record.
     * @return A new UserId object.
     */
    public static UserId new() {
        return new UserId(UUID.randomUUID());
    }
}

JavaScript

This JavaScript example implementation provides the toJSON method used by the JSON.stringify()[4] function to serialize the class into a simple string instead of a composite data type. It calls Object.freeze() to make the instance immutable.[5] It overrides the toString() method[6] and the valueOf() method.[7]

class UserId {
  #id;

  constructor(id) {
    if (id == undefined) {
      throw new TypeError("Argument is null or undefined.");
    }
    this.#id = id;
    Object.freeze(this);
  }

  static empty = new this.prototype.constructor("00000000-0000-0000-0000-000000000000");

  static new() {
    return new this.prototype.constructor(crypto.randomUUID());
  }

  equals(id) {
    return id instanceof this.constructor && this.#id === id.valueOf();
  }

  toJSON() {
  	return this.#id;
  }

  toString() {
    return this.#id;
  }

  valueOf() {
  	return this.#id;
  }
}

Kotlin

Kotlin have "inline classes".[8]

/**
 * Represents a user identifier.
 */
public value class UserId(private val id: String)

PHP

This PHP example implementation implements the __toString() magic method.[9] Furthermore, it implements the JsonSerializable interface which is used by the built-in json_encode function to serialize the class into a simple string instead of a composite data type.[10] The class is declared using the final modifier keyword to prevent inheritance.[11]

/**
 * Represents a user identifier.
 */
final class UserId implements JsonSerializable
{
    /**
     * Initializes a new instance of the UserId object.
     * @param string $id The user identifier.
     */
    public function __construct(public readonly string $id) {}

    /**
     * Creates a new user identifier.
     */
    public static function new(): self
    {
        return new UserId(uniqid());
    }

    public function jsonSerialize(): string
    {
        return $this->id;
    }

    public function __toString(): string
    {
        return $this->id;
    }
}

Python

Python have data classes which provides equality testing and can be made immutable using the frozen parameter.[12]

This example implementation includes a static method which can be used to initialize a new instance with a randomly generated universally unique identifier (UUID).

from dataclasses import dataclass
import uuid

@dataclass(frozen=True)
class UserId:
    """Represents a user identifier."""

    id: str

    @staticmethod
    def new() -> Self:
        """Create a new user identifier."""
        return UserId(uuid.uuid4())

Ruby

Ruby have data classes which provides equality testing and are immutable.[13]

This example implementation includes a static method which can be used to initialize a new instance with a randomly generated universally unique identifier (UUID).

require 'securerandom'

# Represents a user identifier.
UserId = Data.define(:id) do
  # Create a new user identifier.
  def self.create
    UserId.new(SecureRandom.uuid)
  end

  def self.empty
    UserId.new('00000000-0000-0000-0000-000000000000')
  end
end

Rust

In Rust this can be done using a tuple struct containing a single value. This example implementation implements the Debug[14] and the PartialEq[15] traits. The PartialEq trait provides equality testing.

// Represents a user identifier.
#[derive(Debug, PartialEq)]
pub struct UserId(String);

See also

References

  1. "Records - C# reference" (in en-us). https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record. 
  2. "sealed modifier - C# Reference" (in en-us). https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/sealed. 
  3. "Record Classes". https://docs.oracle.com/en/java/javase/19/language/records.html. 
  4. "JSON.stringify() - JavaScript | MDN". https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify. 
  5. "Object.freeze() - JavaScript | MDN". https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze. 
  6. "Object.prototype.toString() - JavaScript | MDN". https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString. 
  7. "Object.prototype.valueOf() - JavaScript | MDN". https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf. 
  8. "Inline classes | Kotlin". https://kotlinlang.org/docs/inline-classes.html. 
  9. "PHP: Magic Methods - Manual". https://www.php.net/manual/en/language.oop5.magic.php#object.tostring. 
  10. "PHP: JsonSerializable::jsonSerialize - Manual". https://www.php.net/manual/en/jsonserializable.jsonserialize.php. 
  11. "PHP: Final Keyword - Manual". https://www.php.net/manual/en/language.oop5.final.php. 
  12. "dataclasses — Data Classes". https://docs.python.org/3/library/dataclasses.html. 
  13. "class Data - Documentation for Ruby 3.3". https://docs.ruby-lang.org/en/master/Data.html. 
  14. "Debug in std::fmt - Rust". https://doc.rust-lang.org/std/fmt/trait.Debug.html. 
  15. "PartialEq in std::cmp - Rust". https://doc.rust-lang.org/std/cmp/trait.PartialEq.html. 

External links