Generics, opaque types and protocol design
Generic functions and constraints, associated types, some versus any, primary associated types, type erasure, and designing protocols that stay useful.
Generic functions and constraints
func firstIndex<T: Collection>(of value: T.Element, in collection: T) -> T.Index?
where T.Element: Equatable {
collection.firstIndex(of: value)
}
// a where clause on the extension, not on every method
extension Sequence where Element: Numeric {
func total() -> Element { reduce(.zero, +) }
}
[1, 2, 3].total() // 6
[1.5, 2.5].total() // 4.0
protocol Repository {
associatedtype Entity: Identifiable
func all() async throws -> [Entity]
func save(_ entity: Entity) async throws
}
struct InMemoryRepository<Entity: Identifiable>: Repository {
private var storage: [Entity] = []
func all() async throws -> [Entity] { storage }
func save(_ entity: Entity) async throws { storage.append(entity) }
}- Prefer the narrowest constraint that works:
Sequenceinstead ofArray,Equatableinstead ofHashable. - An
associatedtypemakes a protocol generic rather than existential, which is why it cannot be used as a plain type without an escape hatch. - Specialisation means a generic function compiles to a version per concrete type. It is usually faster than a protocol witness call, not slower.
someon a parameter (some Collection) is shorthand for an anonymous generic and keeps full type information.
some versus any
| Syntax | Identity | Dispatch | Use when |
|---|---|---|---|
some P | One concrete type, hidden | Static | Returning a type you do not want to name |
any P | Any conforming type | Dynamic via a box | Storing heterogeneous values |
some P parameter | An anonymous generic | Static | Avoiding generics syntax at the call site |
| Primary associated type | any Collection<Int> | Dynamic, type-checked | You need the element type in the signature |
protocol Shape {
func area() -> Double
}
struct Circle: Shape {
let radius: Double
func area() -> Double { .pi * radius * radius }
}
// opaque: the caller knows it is a Shape, the concrete type stays private
func makeDefaultShape() -> some Shape { Circle(radius: 1) }
// existential: different concrete types in one array
let shapes: [any Shape] = [Circle(radius: 1), Circle(radius: 2)]
let total = shapes.reduce(0) { $0 + $1.area() }
// a primary associated type lets the existential be constrained
protocol Store<Value> {
associatedtype Value
func load() -> Value
}
func describe(_ store: any Store<String>) -> String { store.load() }⚠️
Reaching for
any too early costs you static dispatch and lets type errors move from compile time to runtime. Start with concrete types and generics, and use an existential only where you genuinely need a heterogeneous collection.Protocol-oriented design that ages well
// a protocol that is easy to conform to and easy to fake
protocol Clock: Sendable {
var now: Date { get }
}
struct SystemClock: Clock {
var now: Date { .now }
}
struct FixedClock: Clock {
let now: Date
}
protocol ExpiryPolicy {
func isExpired(_ issued: Date, using clock: some Clock) -> Bool
}
struct DurationPolicy: ExpiryPolicy {
let lifetime: TimeInterval
func isExpired(_ issued: Date, using clock: some Clock) -> Bool {
clock.now.timeIntervalSince(issued) > lifetime
}
}
extension ExpiryPolicy {
// a default that every conformer gets for free
func remaining(_ issued: Date, using clock: some Clock) -> TimeInterval? {
nil
}
}Favour protocol composition (Codable & Sendable) over deep inheritance, and inject dependencies as protocol existentials or generics so tests can replace them without a mocking framework.
FAQ
Why can I not use a protocol with an associated type as a property type?
Because the associated type is unknown, so the compiler cannot allocate storage or resolve member signatures. Use
any P with a primary associated type, add a where constraint, or erase the type behind a concrete struct.Is type erasure still needed?
Less often than it used to be, now that
any P exists and primary associated types allow constraints. You still need an erased wrapper when you must store a value whose generic parameter is not expressible in the containing type.Related
Collections, strings and the standard library Macros, property wrappers and result builders
Last refreshed 2026-09-18.