This I (also) agree with.
Making unconditional "newtype <-> underlying type" conversions be public may be not a good idea. For validation purposes, we may want to make public APIs for "underlying type to newtype" conversion to be failable.
// Example adapted from: https://dotty.epfl.ch/docs/reference/other-new-features/opaques.html
newtype Logarithm = Double
extension Double {
// We want the public conversion API to be failable.
public var logarithm: Logarithm? { self > 0 : log(self) ? nil }
}
Opaque type aliases (the equivalent newtype feature in Scala) is very nuanced about access control. As others have stated, newtypes often want to provide intentionally designed APIs rather than forwarding all functionality, so limiting access is key.
Oh, you're right. In fact, even in Haskell you could do what's essentially init(_:) throws as in Haskell at Work.
So the would-be newtype construct would need more versatility to support that too, with reasonable defaults.
The more we discuss this the less benefits the one-liner seems to have, compared to a simple struct.
The layout of a Swift struct is a straightforward aggregate (modulo any field ordering concerns, which donât apply for single member structures). Thus, the layout of a struct wrapping a single T is the same as the layout of a T. This is a guarantee in Swift.
(Digression: strictly speaking resilient libraries are entitled to hide this fact from you, but in that case add the word @frozen to the above comment and it becomes true again.)
This is an observable part of Swiftâs ABI and is therefore a guarantee, short of an ABI breakage.
Protocol Forwarding
I firmly believe that tackling generic limitations, such as the ones to be discussed below is better than solving protocol forwarding, which is a very specific problem.
I propose this:
typealias ID = String as Hashable & Comparable
// is equivalent to:
typealias AlsoID = Abstract<String, Hashable & Comparable>
var foo: ID = ""
// provides initialisers for `ExpressibleBy...Literal` types
// (such as String: ExpressibleByStringLiteral)
var bar: CustomType as Hashable & Comparable = .init(CustomType())
// when using custom type the initialiser:
// `.init(_:)` of Abstract will be used
For Abstract to be implemented new language features need to be in place (the following are some features I used my implementation):
- Protocol Generic Requirements
struct Foo<Root, Protocol: AnyProtocol> where Root: Protocol { ... } - Key Paths to PATs (Protocols with Associated Types)
So this isn't allowed:protocol Pat { associatedtype A } let foo: Pat = ... // ERROR!
Of course, this feature would require changes not only toPartialKeyPath<Pat> // ERROR!KeyPaths, but also to Generics.
A possible Abstract implementation:
@dynamicMemberLookup
struct Abstract<Root, Protocol: AnyProtocol> where Root: Protocol {
private let root: Root
init(_ value: Root) { ... }
subscript<Value>(dynamicMember member: KeyPath<Protocol, Value>) -> Value {
...
}
}
New Type Creation
I like this feature. There have been many times when I wanted to do something like this:
func startTimer(for time: Time) { ... }
startTimer(for: .short)
startTimer(for 0.5)
Still, though, I think that the need for such a feature is rare. Additionally, wrapper types can use the following protocol:
protocol Wrapper {
associatedtype WrappedProperty
static var wrappedKeyPath: KeyPath<Self, WrappedProperty> { get }
}
extension Wrapper: ExpressibleBy...Literal
where WrappedProperty: ExpressibleBy...Literal { ... }
// Provides initialisers for literal types.
// This may be rendered useless with Static Dynamic Member Lookup.
which would allow for this:
@dynamicMemberLookup
struct Time: Wrapper {
static var wrappedKeyPath = \Self.seconds
let seconds: Double
static let short: Self = 0.2
}
I don't see considerable improvements between the above and this, though:
newtype Time = Double
extension Time {
static let short: Self = 0.2
}
This is slightly cleaner, I agree. But Time doesn't need to be a completely new type, it just needs to wrap its underlying value of type Double .
The post that this one is a reply to (#15) is the last one I read before going on a research binge. Of course, this thread ran its course in the meantime with a lot of new posts. I still want to express my thoughts.
I looked through my old posts for previous thoughts on newtype/strong type-alias and/or reduced-state types.
- My first versions:
- [early Pitch] retype (or newtype): strong type-alias (typedef) for redoing an interface; also: object aliases
- Trying to grok the ABI documents, w.r.t. strong typedefs
- Invariant checks on strong typedefs?
- [Pitch] strong typedef
- [Pitch] Strong typedef, version 0.2
- [Pitch] Update to Alternative Types (i.e. strong typedef) Proposal
- [Pre-Pitch] Another go at âstrong typedefâ / alternative types / reduced-state types
- A kernel of another newtype idea
I only looked through threads I started, not one proposed by others (whether or not I participated in those). The last post in that list inspired some of my thoughts below.
Generalizing
A general reduced-state type can accommodate newtypes by simply not excluding any states. Everyone before my last post in this thread seemed to want only in same-state-count types, so I'm going to restrict myself to that. Some of the later posts do mention wanting to disallow some states of the original type into the new type. I think we should consider reduced-state types later, although there is the problem to duplicating effort if we have separate means of creating same-count newtypes and reduced-state new types.
Reduced state types also will require us to resolve the co-variance vs. contra-variance for the trampoline members. Technically, same-count new types do have the same issue, but there's more at stake for reduced-state ones.
Declaring Conformance
Unfortunately, I closed the tab, but I read a thread on this forum that copied a response from another thread that stated that the Core Team wants to keep the conformance of a protocol to a type explicit. New ways of declaring/constructing types shouldn't secretly add conformances. I already expressed this wish too, but that other thread lets me emphasize it more. That means the newtype must have a ": MyProtocol" somewhere; even if we let a new type copy all of its source's members, it doesn't copy its conformances implicitly.
Note that the new requested feature for tuples to conform to Equatable, Comparable, and Hashable violates this, but it's more limited and may be rectified in later versions of Swift (if added at all) if general tuple conformance is added.
Acknowledging New-Types
This relates to the last thread in my list above.
- Whether or not a type is a
newtypeis an implementation detail. - This means that a new type that has explicit cases will be declared as an
enum, otherwise as astruct. There won't be anewtypesibling construction. - The declaration needs to specify the access level of the
newtype-ness. For example, if the aliasing is declaredfileprivate, then anything from a different file will only see the new type as an ordinarystruct/enum, and only items within the source file can access the new type'sinit(rawValue:)initializer for forward conversion and useas/as!/as?for backward conversion.
(Despite the init(rawValue:) initializer, a new type doesn't conform to RawRepresentable by default. That conformance has to be explicitly added. If it's added then the new type's newtype-ness has to have the same publicity as the new type.)
Expression of newtype-ness
I'm thinking of a form like:
struct MyType: @newtype(internal) MySourceType, MyProtocol { /*...*/ }
where the access level may be omitted. If it is, the default is the most restrictive of the new type's publicity, the source type's publicity, and internal. Remember that access level determines who can see init(rawValue:) and use as/as!/as?.
The source type may be a struct, enum, tuple, or a single enum case. The last one has the same interaction model as a tuple of the same shape, but the storage is the source's enum type. (Any other structural value types we add, like sum-tuples or fixed-sized arrays, may also be a source type.) Within any members of the new type, access to the internal state as the source type is done through "super". If we specify RawRepresentable conformance without giving a definition, we will have rawValue automatically forward to super.
If the source type is a single enum case, the core initializer is fail-able, where it fails only if the input is of a different case.
For the items that can see the new type's as/as!/as?, their code can reinterpret an instance of the new type as the source type with "as". If the source type is itself a new type, it can convert down the "inheritance" chain. It can cross- or up-convert to a different new type that shares an ancestor. (The last two reinterpretations apply only when the original item's code has enough access to see the destination type's "inheritance" chain, at least to the common ancestor). If the up-cast phase of an reinterpretation involves an enum case, then "as?" or "as!" is required.
If the new type is a struct, then any items that cannot see the init(rawValue:) initializer can only reuse values you give a high-enough access to, or create values through functions you give a high-enough access to. (You can make this an empty list to keep your new type practically private.) If the new type is an enum and the source is an enum too, then you get the same cases, but you can override the names:
enum MyType: @newtype MyOldEnum {
// Prototype syntax
publish case old(hello: Int, Double) as new(Int, world: Double)
}
If a new case name conflicts with a different case of the source, then you need to rename that other old case too. If the new type is an enum and the source is anything else, then you need to specify the name of the sole case:
enum MyType: @newtype MyOldStruct {
publish super as single
}
That sole case will have a payload with one unlabeled item of the source type. (If the source is an enum case, the payload is one item of the equivalent tuple.)
I'll leave specification of trampoline members for a later thread.
Addition: I forgot to write about new-typing a singular enum case. They work the same as if Void was their equivalent tuple.
Well, since you asked, here's an approach I'm experimenting with based on existing features. It's loosely based on Point-Free's Tagged type, but with my approach:
-
I can choose exactly which protocols each new type conforms to.
-
If the wrapped type conforms to a protocol, I can choose to forward the new type's conformance to the raw type's conformance. This requires boilerplate, but I only have to write the boilerplate once for the protocol and then I can use it for every conforming newtype.
-
If the wrapped type doesn't conform, or if I don't want to use its conformance, I can customize how the new type conforms.
In this system, every ânewtypeâ is defined by a type conforming to this BaseTag protocol:
public protocol BaseTag {
associatedtype RawValue
}
For convenience, we can create tags by subclassing this base class:
open class TagBase<RawValue>: BaseTag {
private init() { fatalError() }
}
This class, and its subclasses, should only exist at the type level. There should never be values of a tag type. So we make its init private.
The actual ânewtypeâ is then a typealias for an instantiation of NewType with a tag:
public struct NewType<Tag: BaseTag> {
public typealias RawValue = Tag.RawValue
public init(rawValue: Tag.RawValue) {
self.rawValue = rawValue
}
public var rawValue: Tag.RawValue
}
So, for example, we could create a newtype for string order ids like this:
final class OrderIdTag: TagBase<String> { }
typealias OrderId = NewType<OrderIdTag>
This is not as lightweight as a dedicated syntax like newtype OrderId = String, but I donât think itâs too bad.
However, we want some protocol conformances for the OrderId type. We want it to be Equatable, Hashable, Comparable, and Codable using the raw strings. And we want it to be CustomStringConvertible, but we want to customize the description property.
For Equatable, we create an EquatableTag protocol that inherits BaseTag and mirrors the requirements of Equatable. A NewType conforms to Equatable if its Tag conforms to EquatableTag:
public protocol EquatableTag: BaseTag {
static func areEqual(_ lhs: RawValue, _ rhs: RawValue) -> Bool
}
extension NewType: Equatable where Tag: EquatableTag {
public static func == (lhs: Self, rhs: Self) -> Bool {
return Tag.areEqual(lhs.rawValue, rhs.rawValue)
}
}
We also write this boilerplate for forwarding to the raw valueâs conformance, if the raw value conforms:
extension EquatableTag where RawValue: Equatable {
public static func areEqual(_ lhs: RawValue, _ rhs: RawValue) -> Bool {
return lhs == rhs
}
}
We have similar code for Hashable, Comparable, Codable, and CustomStringConvertible:
public protocol HashableTag: EquatableTag {
static func hash(_ rawValue: RawValue, into hasher: inout Hasher)
}
extension HashableTag where RawValue: Hashable {
public static func hash(_ rawValue: RawValue, into hasher: inout Hasher) {
rawValue.hash(into: &hasher)
}
}
extension NewType: Hashable where Tag: HashableTag {
public func hash(into hasher: inout Hasher) {
Tag.hash(rawValue, into: &hasher)
}
}
public protocol ComparableTag: EquatableTag {
static func `is`(_ lhs: RawValue, lessThan rhs: RawValue) -> Bool
}
extension ComparableTag where RawValue: Comparable {
public static func `is`(_ lhs: RawValue, lessThan rhs: RawValue) -> Bool {
return lhs < rhs
}
}
extension NewType: Comparable where Tag: ComparableTag {
public static func < (lhs: Self, rhs: Self) -> Bool {
return Tag.is(lhs.rawValue, lessThan: rhs.rawValue)
}
}
public protocol EncodableTag: BaseTag {
static func encode(_ rawValue: RawValue, into encoder: Encoder) throws
}
extension EncodableTag where RawValue: Encodable {
public static func encode(_ rawValue: RawValue, into encoder: Encoder) throws {
try rawValue.encode(to: encoder)
}
}
extension NewType: Encodable where Tag: EncodableTag {
public func encode(to encoder: Encoder) throws {
try Tag.encode(rawValue, into: encoder)
}
}
public protocol DecodableTag: BaseTag {
static func decode(from decoder: Decoder) throws -> RawValue
}
extension DecodableTag where RawValue: Decodable {
public static func decode(from decoder: Decoder) throws -> RawValue {
return try RawValue(from: decoder)
}
}
extension NewType: Decodable where Tag: DecodableTag {
public init(from decoder: Decoder) throws {
self.init(rawValue: try Tag.decode(from: decoder))
}
}
public protocol CodableTag: EncodableTag, DecodableTag { }
public protocol CustomStringConvertibleTag: BaseTag {
static func description(of rawValue: RawValue) -> String
}
extension CustomStringConvertibleTag where RawValue: CustomStringConvertible {
public static func description(of rawValue: RawValue) -> String {
return rawValue.description
}
}
extension NewType: CustomStringConvertible where Tag: CustomStringConvertibleTag {
public var description: String { Tag.description(of: rawValue) }
}
It seems like a lot, but we only had to write all that stuff once. Now we can use those tags for any number of NewType instantiations.
Let's add those tag protocols to OrderIdTag to give OrderId the conformances we want. We can add a static method to OrderIdTag to customize how OrderId conforms to CustomStringConvertible:
final class OrderIdTag: TagBase<String>, HashableTag, ComparableTag, CodableTag, CustomStringConvertibleTag {
static func description(of rawValue: String) -> String {
return "#" + rawValue
}
}
typealias OrderId = NewType<OrderIdTag>
Again, this isnât as lightweight as a dedicated syntax could be. But I think itâs not too bad.
In my real project, I want most of my types to be Hashable, Comparable, and Codable, so I have a convenience protocol to shorten my tag declarations:
protocol CommonTags: HashableTag, ComparableTag, CodableTag { }
final class OrderIdTag: TagBase<String>, CommonTags, CustomStringConvertibleTag {
static func description(of rawValue: String) -> String {
return "#" + rawValue
}
}
typealias OrderId = NewType<OrderIdTag>
Now hereâs a more complex example, where thereâs a relationship between two NewType instances.
In my project, we need to deal with timestamps that have nanosecond precision and can uniformly represent timestamps across multiple years. A Double requires 30 bits to the right of the decimal point for nanosecond precision, so any Double-based representation (like Foundationâs Date) has only 24 bits (including the sign bit) for the integer part of the timestamp, giving a lifetime of about 194 days. That isn't sufficient, so we represent a timestamp as the number of nanoseconds since the Unix epoch.
I want to represent these timestamps with newtype, NanoTime, that wraps Int64. I donât want NanoTime to conform to AdditiveArithmetic (what would âMonday at noon + Tuesday at 8amâ mean?) but I do want it to be Strideable. I want a NanoDuration newtype for the Stride of NanoTime. NanoDuration should also wrap Int64.
Any Stride must conform to Comparable and SignedNumeric. I have already covered Comparable with ComparableTag. SignedNumeric inherits three protocols I havenât already covered: Numeric, AdditiveArithmetic, and ExpressibleByIntegerLiteral. I donât really want NanoDuration to have all the features of those protocols, but Iâm going to allow it for now. (Maybe Iâll change my mind later and give up on conforming NanoTime to Strideable.)
So anyway, letâs start with ExpressibleByIntegerLiteral. It has a new complication: the associated type IntegerLiteralType. So we define the tag protocol and the NewType conformance like this:
public protocol ExpressibleByIntegerLiteralTag: BaseTag {
associatedtype IntegerLiteralType: _ExpressibleByBuiltinIntegerLiteral
static func integerLiteral(_ literal: IntegerLiteralType) -> RawValue
}
extension NewType: ExpressibleByIntegerLiteral where Tag: ExpressibleByIntegerLiteralTag {
public typealias IntegerLiteralType = Tag.IntegerLiteralType
public init(integerLiteral: IntegerLiteralType) {
self.init(rawValue: Tag.integerLiteral(integerLiteral))
}
}
For protocol forwarding when RawValue conforms to ExpressibleByIntegerLiteral, we have this extension:
extension ExpressibleByIntegerLiteralTag where RawValue: ExpressibleByIntegerLiteral, RawValue.IntegerLiteralType == IntegerLiteralType {
public static func integerLiteral(_ literal: IntegerLiteralType) -> RawValue {
RawValue(integerLiteral: literal)
}
}
I'd really like a way to set a default typealias IntegerLiteralType = RawValue.IntegerLiteralType, but I couldnât find a way to make that work (as of Xcode 11.5b2).
The code for AdditiveArithmetic, Numeric, SignedNumeric, and Strideable is similar, although they have more requirements:
public protocol AdditiveArithmeticTag: EquatableTag {
static var zero: RawValue { get }
static func addition(_ lhs: RawValue, _ rhs: RawValue) -> RawValue
static func subtraction(_ lhs: RawValue, _ rhs: RawValue) -> RawValue
}
extension AdditiveArithmeticTag where RawValue: AdditiveArithmetic {
public static var zero: RawValue { RawValue.zero }
public static func addition(_ lhs: RawValue, _ rhs: RawValue) -> RawValue { lhs + rhs }
public static func subtraction(_ lhs: RawValue, _ rhs: RawValue) -> RawValue { lhs - rhs }
}
extension NewType: AdditiveArithmetic where Tag: AdditiveArithmeticTag {
public static var zero: NewType<Tag> { .init(rawValue: Tag.zero) }
public static func + (lhs: Self, rhs: Self) -> Self {
return .init(rawValue: Tag.addition(lhs.rawValue, rhs.rawValue))
}
public static func - (lhs: Self, rhs: Self) -> Self {
return .init(rawValue: Tag.subtraction(lhs.rawValue, rhs.rawValue))
}
}
public protocol NumericTag: AdditiveArithmeticTag, ExpressibleByIntegerLiteralTag {
associatedtype Magnitude: Comparable, Numeric
static func exactly<T: BinaryInteger>(_ source: T) -> RawValue?
static func magnitude(of rawValue: RawValue) -> Magnitude
static func multiplication(_ lhs: RawValue, _ rhs: RawValue) -> RawValue
static func inPlaceMultiplication(_ lhs: inout RawValue, _ rhs: RawValue)
}
extension NumericTag where RawValue: Numeric, RawValue.Magnitude == Magnitude {
public static func exactly<T: BinaryInteger>(_ source: T) -> RawValue? { RawValue(exactly: source) }
public static func magnitude(of rawValue: RawValue) -> Magnitude { rawValue.magnitude }
public static func multiplication(_ lhs: RawValue, _ rhs: RawValue) -> RawValue { lhs * rhs }
public static func inPlaceMultiplication(_ lhs: inout RawValue, _ rhs: RawValue) { lhs *= rhs }
}
extension NewType: Numeric where Tag: NumericTag {
public typealias Magnitude = Tag.Magnitude
public init?<T>(exactly source: T) where T : BinaryInteger {
guard let rawValue = Tag.exactly(source) else { return nil }
self.init(rawValue: rawValue)
}
public var magnitude: Tag.Magnitude { Tag.magnitude(of: rawValue) }
public static func * (lhs: NewType<Tag>, rhs: NewType<Tag>) -> NewType<Tag> {
return .init(rawValue: Tag.multiplication(lhs.rawValue, rhs.rawValue))
}
public static func *= (lhs: inout NewType<Tag>, rhs: NewType<Tag>) {
Tag.inPlaceMultiplication(&lhs.rawValue, rhs.rawValue)
}
}
public protocol SignedNumericTag: NumericTag {
static func negate(_ rawValue: inout RawValue)
static func negative(of rawValue: RawValue) -> RawValue
}
extension SignedNumericTag {
public static func negate(_ rawValue: inout RawValue) {
rawValue = Self.subtraction(Self.zero, rawValue)
}
public static func negative(of rawValue: RawValue) -> RawValue {
return Self.subtraction(Self.zero, rawValue)
}
}
extension SignedNumericTag where RawValue: SignedNumeric {
public static func negate(_ rawValue: inout RawValue) { rawValue.negate() }
public static func negative(of rawValue: RawValue) -> RawValue { -rawValue }
}
extension NewType: SignedNumeric where Tag: SignedNumericTag {
public mutating func negate() { Tag.negate(&rawValue) }
public static prefix func - (_ value: Self) -> Self {
return .init(rawValue: Tag.negative(of: value.rawValue))
}
}
public protocol StrideableTag: ComparableTag {
associatedtype Stride: Comparable, SignedNumeric
static func advance(_ rawValue: RawValue, by n: Stride) -> RawValue
static func distance(from start: RawValue, to end: RawValue) -> Stride
}
extension StrideableTag where RawValue: Strideable, RawValue.Stride == Stride {
public static func advance(_ rawValue: RawValue, by n: Stride) -> RawValue {
return rawValue.advanced(by: n)
}
public static func distance(from start: RawValue, to end: RawValue) -> Stride {
return distance(from: start, to: end)
}
}
extension NewType: Strideable where Tag: StrideableTag {
public typealias Stride = Tag.Stride
public func advanced(by n: Tag.Stride) -> NewType<Tag> {
return .init(rawValue: Tag.advance(rawValue, by: n))
}
public func distance(to other: NewType<Tag>) -> Tag.Stride {
return Tag.distance(from: rawValue, to: other.rawValue)
}
}
Now we can define the NanoDuration type:
// Since this will be a Stride, it must be at least Comparable and SignedNumeric.
final class NanoDurationTag: TagBase<Int64>, ComparableTag, SignedNumericTag, HashableTag, CodableTag, CustomStringConvertibleTag {
public typealias IntegerLiteralType = RawValue.IntegerLiteralType
public typealias Magnitude = RawValue.Magnitude
public static func description(of rawValue: Int64) -> String {
rawValue.description + "ns"
}
}
typealias NanoDuration = NewType<NanoDurationTag>
And then we can use it as the Stride for the NanoTime type. Note that we have to define methods in NanoTimeTag to customize the way NanoTime conforms to Strideable, else its Stride will be Int64.Stride, which is Int.
final class NanoTimeTag: TagBase<Int64>, StrideableTag, HashableTag, CodableTag, CustomStringConvertibleTag {
public typealias Stride = NanoDuration
public static func advance(_ rawValue: Int64, by n: Stride) -> Int64 { rawValue + n.rawValue }
public static func distance(from start: Int64, to end: Int64) -> Stride { .init(rawValue: end - start) }
public static func description(of rawValue: Int64) -> String {
rawValue.description + "nse"
}
}
typealias NanoTime = NewType<NanoTimeTag>
That covers all of the protocol conformances I support in my project so far.
I like the concept, it is very useful in addressing the problem of primitive obsession (https://wiki.c2.com/?PrimitiveObsession).
Unfortunately, I have no ideas about the preferred way of language design. But I can provide example and some wishes.
For now, we use the following structure as domain model of Email:
public struct Email: Hashable {
public let wrappedValue: String
public init(rawString: String) throws {
if rawString.isValid() {
wrappedValue = rawString
} else {
throw ValidationError()
}
}
}
Another one is ObjectIdentifier:
public struct ObjectIdentifier: Hashable {
public let wrappedValue: UInt64
public init(rawValue: UInt64) throws {
// I our case we assume that 0 and UInt64.max are invalid values
if rawValue > 0, rawValue != UInt64.max {
wrappedValue = rawValue
} else {
throw ValidationError()
}
}
}
Our needs and requirements for now are:
-
newType doesn't fully behave like underlying type.
It is wrong to do math operations between two ObjectIdentifiers like ObjectIdentifier + ObjectIdentifier or ObjectIdentifier * ObjectIdentifier.
[Email, Email, Email].joined(separator:) is also what we want to avoid. -
newType doesn't inherit initializers form the original type. We want such UInt64's initializers as init(bitPattern x: Int64), init(_source: Float), init() and others where not available by default. The motivation is that domain model has often additional restrictions and provides its own initializer.
-
newType conforms to CustomStringConvertible by default if underlying type conforms to CustomStringConvertible. The same rule is preferred for CustomDebugStringConvertible, Decodable, Encodable, Equatable, Hashable. In our case, we need easily to check if two ObjectIdentifiers are equal, or encode them when sending to backend.
-
newType optionally provides the access to wrappedValue if needed
I guess that we need to aggregate real world examples from different people. Maybe, it is good idea to aggregate those examples on separate page and adding link to it at the top of this one. Thus we separate examples form discussion.
If the tags are never meant to be instantiated, why not make them enums instead of classes?
Excellent question!
You can use an enum for a tag, but you have to write a typealias to define RawValue:
enum OrderIdTag: CommonTags {
typealias RawValue = String
}
If you subclass TagBase, you save a little typing because the type parameter specifies RawValue:
final class OrderIdTag: TagBase<String>, CommonTags { }
Incidentally, I tried using an empty enum with a RawValue as a tag, like this:
enum OrderIdTag: String, CommonTags { }
But Swift (as of Xcode 11.5b2) rejects it:
/Users/mayoff/TestProjects/newtype/newtype/main.swift:259:18: error: an enum with no cases cannot declare a raw type
enum OrderIdTag: String, CommonTags { }
^
Giving it an inhabitant fixes the error, but defeats the point of using an enum.