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: Sequence instead of Array, Equatable instead of Hashable.
  • An associatedtype makes 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.
  • some on a parameter (some Collection) is shorthand for an anonymous generic and keeps full type information.

some versus any

SyntaxIdentityDispatchUse when
some POne concrete type, hiddenStaticReturning a type you do not want to name
any PAny conforming typeDynamic via a boxStoring heterogeneous values
some P parameterAn anonymous genericStaticAvoiding generics syntax at the call site
Primary associated typeany Collection<Int>Dynamic, type-checkedYou 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.

Collections, strings and the standard library Macros, property wrappers and result builders

Last refreshed 2026-09-18.