A strongly typed identifier is user-defined 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 if possible. 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 view models 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.
Passing a strongly typed identifier throughout the layers of an example application.
C# have records which provide immutability and equality testing. [1] The record is sealed to prevent inheritance. [2] It overrides the built-in ToString() method. [3]
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>publicsealedrecordUserId(GuidId){/// <summary>/// Initializes a new instance of the <see cref="UserId" /> record./// </summary>/// <returns>A new UserId object.</returns>publicstaticUserIdNew()=>new(Guid.NewGuid());publicoverridestringToString()=>Id.ToString();} C++ has structs but not immutability so here the id field is marked as private with a method named getId() to get the value.
importstd;usingstd::string;classUserId{private:conststringid;public:explicitUserId(conststring&id):id{id}{}[[nodiscard]]stringgetId()constnoexcept{returnid;}[[nodiscard]]booloperator==(constUserId&rhs)constnoexcept{returnid==rhs.getId();}}; Crystal's standard library provides the record macro for creating records which are immutable structs and lets you create override the built-in to_s method. [4]
require"uuid"# Represents a user identifier.recordUserId,id:Stringdodefinitialize()@id=UUID.v4.to_senddefto_s(io)io<<idenddefself.emptyself.new(UUID.empty.to_s)endendimportstd;/** Represents a user identifier. */immutablestructUserId{immutableUUIDid;/** Initializes a new instance of the UserId struct. */this(immutablestringid){this.id=UUID(id);}publicstaticUserIdcreate(){returnUserId(randomUUID.toString());}stringtoString(){returnthis.id.toString();}}Dart have classes with operator overloading.
import'package:meta/meta.dart';/// Represents a user identifier.@immutablefinalclassUserId{finalStringid;/// Initializes a new instance of the UserId struct.constUserId(this.id);@overrideoperator==(other)=>otherisUserId&&other.id==id;@overrideintgethashCode=>id.hashCode;@overrideStringtoString()=>id;} F# lets you create override the Equals, GetHashCode and ToString methods.
openSystem/// <summary>/// Represents a user identifier./// </summary>/// <param name="id">The user identifier.</param>typeUserId(id:Guid)=memberx.id=idstaticmemberNew()=Guid.NewGuid()staticmemberEmpty=Guid.Emptyoverridex.Equals(b)=matchbwith|:?UserIdasp->id=p.id|_->falseoverridex.GetHashCode()=hashidoverridex.ToString()=id.ToString()Go have structs which provide equality testing. Go however does not provide immutability.
// Represents a user identifier.typeUserIdstruct{idstring}// Creates a new user identifier.funcNewUserId(idstring)UserId{returnUserId{id:id}}func(xUserId)String()string{returnx.id}Groovy have record classes which provide immutability and equality testing. [6]
/** * Represents a user identifier. * * @param id The user identifier. */recordUserId(Stringid){StringtoString(){id}} Haskell can create user-defined custom data types using the newtype keyword. [7] It provides equality testing using the Eq standard class and printing using the Read and Show standard classes.
-- Represents a user identifier.newtypeUserId=UserIdStringderiving(Eq,Read,Show) Java have records which provide equality testing. [8] The record is declared using the final modifier keyword to prevent inheritance. It overrides the built-in toString() method.
importjava.util.UUID;/** * Represents a user identifier. * @param id The user identifier. */publicfinalrecordUserId(UUIDid){/** * Initializes a new instance of the UserId record. * @return A new UserId object. */publicstaticUserIdnewId(){returnnewUserId(UUID.randomUUID());}publicStringtoString(){returnid.toString();}}This JavaScript example implementation provides the toJSON method used by the JSON.stringify() [9] function to serialize the class into a simple string instead of a composite data type. It calls Object.freeze() to make the instance immutable. [10] It overrides the built-in toString() method [11] and the valueOf() method. [12]
classUserId{#id;constructor(id){if(id==undefined){thrownewTypeError("Argument is null or undefined.");}this.#id=id;Object.freeze(this);}staticempty=newthis.prototype.constructor("00000000-0000-0000-0000-000000000000");staticnew(){returnnewthis.prototype.constructor(crypto.randomUUID());}equals(id){returnidinstanceofthis.constructor&&this.#id===id.valueOf();}toJSON(){returnthis.#id;}toString(){returnthis.#id;}valueOf(){returnthis.#id;}}Julia have immutable composite data types. [13]
usingUUIDs"Represents a user identifier."structUserIdid::UUIDendBase.string(userId::UserId)=userId.idKotlin have "inline classes". [14]
/** * Represents a user identifier. * * @property id The user identifier. * @constructor Creates a user identifier. */@JvmInlinepublicvalueclassUserId(publicvalid:String){overridefuntoString()=id}Nim have "distinct types". [15] [16]
## Represents a user identifier.typeUserId*=distinctstringThis PHP example implementation implements the __toString() magic method. [17] 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. [18] The class is declared using the final modifier keyword to prevent inheritance. [19] PHP has traits as a way to re-use code. [20]
/** * Represents a user identifier. */finalclassUserIdimplementsJsonSerializable{useStronglyTypedIdentifier;}/** * Provides methods for use with strongly typed identifiers. */traitStronglyTypedIdentifier{/** * Initializes a new instance of the UserId object. * @param string $id The user identifier. */publicfunction__construct(publicreadonlystring$id){}/** * Creates a new user identifier. */publicstaticfunctionnew():self{returnnewself(bin2hex(random_bytes(16)));}publicfunctionjsonSerialize():string{return$this->id;}publicfunction__toString():string{return$this->id;}} Python has data classes which provides equality testing and can be made immutable using the frozen parameter. [21] It overrides the __str__ dunder method. [22]
This example implementation includes a static method which can be used to initialize a new instance with a randomly generated universally unique identifier (UUID).
importuuidfromdataclassesimportdataclassfromuuidimportUUID@dataclass(frozen=True)classUserId:"""Represents a user identifier."""id:UUID@staticmethoddefnew()->Self:"""Create a new user identifier."""return__class__(uuid.uuid4())def__str__(self)->str:returnstr(self.id)Python also has NewType which can be used to create new data types. [23]
fromtypingimportNewTypeUserId:NewType=NewType('UserId',int) Raku have classes which provides equality testing and are immutable. It overrides the built-in Str method.
This example overrides the default gist method. [24] It uses roles which are mixed in into classes to be re-usable. [25]
roleStronglyTypedIdentifier { hasStr$.idisrequired; multimethodnew(Str$id) { self.bless(:$id); } multimethodgist(StronglyTypedIdentifier:U:) { self.^name } multimethodgist(StronglyTypedIdentifier:D:) { self.id } methodStr() { "$!id" } methodempty() { self.new("00000000-0000-0000-0000-000000000000") } } classUserIddoesStronglyTypedIdentifier {} Ruby have data classes which provides equality testing and are immutable. [26] It overrides the built-in to_s method.
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.defself.createself.new(SecureRandom.uuid)enddefself.emptyself.new('00000000-0000-0000-0000-000000000000')enddefto_sidendendIn Rust this can be done using a tuple struct containing a single value. [27] This example implementation implements the Debug [28] and the PartialEq [29] traits. The PartialEq trait provides equality testing.
// Represents a user identifier.#[derive(Debug, PartialEq)]pubstructUserId(String);Scala have case classes which provide immutability and equality testing. [30] The case class is sealed to prevent inheritance.
importjava.util.UUID/** Represents a user identifier. * * @constructor * Create a new user identifier. * @param id * The user identifier. */sealedcaseclassUserId(id:UUID)objectUserId:/** Initializes a new instance of the UserId class. */defcreate():UserId=UserId(UUID.randomUUID()) Swift have the CustomStringConvertible protocol which can be used to provide its own representation to be used when converting an instance to a string, [31] and the Equatable protocol which provides equality testing. [32]
importFoundation/// Represents a user identifier.structUserId:CustomStringConvertible,Equatable{privateletid:UUIDinit(_id:UUID){self.id=id}vardescription:String{returnid.uuidString.lowercased}/// Creates a new user identifier.staticfuncnew()->Self{returnSelf(UUID())}}Zig have structs [33] with constants but by design does not have operator overloading [34] and method overriding.
/// Represents a user identifier.constUserId=struct{value:i32,/// Initializes a new instance of the UserId struct.pubfninit(value:i32)UserId{returnUserId{.value=value};}};