I was also going to suggest any Hashable & Sendable
.
If you need a "normal" type that can pass through constrained generic code (i.e. <T: Hashable & Sendable>
, which no existential can bind to because existentials never conform to protocols), then you'll need to write your own AnySendableHashable
type eraser, but you can make it easy by reusing AnyHashable
's implementations of the Hashable
requirements.
Another strategy is to avoid existentials (either bare as any ...
or manually type erased as Any...
) completely by using generics. Make Foo
a generic struct with a type parameter <Bar: Hashable & Sendable>
, which will be the type of bar
. In other words don't force the type of bar
to be erased here.
Any code that uses Foo
will then either know what concrete type it uses for bar
and will just bind that to Bar
, or it is similarly erased (getting chosen somewhere else) and becomes generic itself.
It you really need runtime dynamism (i.e. holding an array of Foo
s with different types of Bar
s), which is more rare than you might think, you can handle that with a protocol above Foo
:
protocol FooProtocol<Bar>: Sendable {
associatedtype Bar: Hashable & Sendable
var bar: Bar { get }
}
Then wherever you truly need the type of Bar
to be erased you can just use any FooProtocol
, which automatically erases bar
covariantly to any Hashable & Sendable
.
A problem with this is if you recover the type of Bar
, you get an any FooProtocol<Bar>
but you probably want a Foo<Bar>
. Force downcasting would be unfortunate, and this indicates the problem that anyone can write their own FooProtocol
conformance when really you'd like to lock it down so only Foo
conforms to FooProtocol
. You can use a trick to both avoid the force downcast and make it practically impossible to write an "incorrect" conformance:
protocol FooProtocol<Bar>: Sendable {
associatedtype Bar: Hashable & Sendable
var bar: Bar { get }
var asFoo: Foo<Bar> { get }
}
struct Foo<Bar: Hashable & Sendable>: FooProtocol {
let bar: Bar
var asFoo: Self { self }
}
Now you can turn an any FooProtocol<Bar>
into a Foo<Bar>
without a downcast that might fail, and anyone who tries conforming to FooProtocol
will have to implement asFoo
and be reminded they shouldn't be trying to.
But I would first try to just made code generic between where Foo
s are used and their bar
s are created to avoid existentials altogether (in short make the code strongly typed enough it doesn't need type erasure).