Any time you ever read a string, its type is always just going to be "string" (modulo whatever passes for a "string" in your programming language of choice). To get an actual non-string type, you'd need to parse that string, and presumably your parsing function would read the prefix and reject the string if it was passed an ID whose type doesn't match. So it's dynamically type-safe, if not statically type-safe.
I don't know go, but in C# I'd probably do something like the code below. The object really only needs to carry the uuid/guid, let the language type system worry about the difference between a user and post id. We just need a generic mechanism to ensure that UserId object can only be constructed from a valid type id string with type = user. For production use you'd obviously need more methods to construct it from a database tuple (mentioned elsewhere in the comments), etc.
interface ITypeIdPrefix
{
static abstract string Prefix { get; }
}
abstract class TypeId<T>
where T : TypeId<T>, ITypeIdPrefix, new()
{
public Guid Id { get; private init; }
public override string ToString() => $"{T.Prefix}_{Id.ToBase32String()}";
// Override GetHashcode(), Equals(), etc.
public static bool TryParse(string s, out T? result)
{
if (!s.StartsWith(T.Prefix) || !TrySplitStringAndParseBase32ToGuid(s, out var id))
{
result = default;
return false;
}
result = new T { Id = id };
return true;
}
}
class UserId : TypeId<UserId>, ITypeIdPrefix
{
public static string Prefix => "user";
}
class PostId : TypeId<PostId>, ITypeIdPrefix
{
public static string Prefix => "post";
}
Let's say I have structs:
type User struct { ID TypeID }
type Post struct { ID TypeID }
How can I ensure the correct type is used in each of the structs?