iosNudge

What are associated types in Swift protocols?

MediumSwiftProtocolsGenerics 2 min read

TL;DR

An associated type is a placeholder type in a protocol. You write associatedtype Item. Each conforming type decides what Item really is. Swift can often infer it. It makes protocols generic. Collection.Element and View.Body are associated types.

A simple example

protocol Container {
    associatedtype Item
    var items: [Item] { get }
    mutating func add(_ item: Item)
}

struct IntBox: Container {
    var items: [Int] = []
    mutating func add(_ item: Int) { items.append(item) }   // Item = Int (inferred)
}

struct StringBox: Container {
    typealias Item = String          // you can also say it explicitly
    var items: [String] = []
    mutating func add(_ item: String) { items.append(item) }
}

Without an associated type you would need Any, and lose type safety.

Constraints on associated types

You can require the placeholder to conform to protocols.

protocol Repository {
    associatedtype Model: Identifiable & Codable
    func fetchAll() async throws -> [Model]
    func save(_ model: Model) async throws
}

You can also use where clauses:

protocol Graph {
    associatedtype Node: Hashable
    associatedtype Edges: Sequence where Edges.Element == Node
    func neighbors(of node: Node) -> Edges
}

Using these protocols in functions

The usual way is generics. The compiler knows the real type.

func printAll<C: Container>(_ box: C) where C.Item: CustomStringConvertible {
    for item in box.items { print(item.description) }
}

Or the short form with some:

func countItems(_ box: some Container) -> Int {
    box.items.count
}

Primary associated types

Since Swift 5.7 a protocol can name its main associated type in angle brackets.

protocol Container<Item> {
    associatedtype Item
    var items: [Item] { get }
}

func firstNumber(in box: some Container<Int>) -> Int? {
    box.items.first
}

Standard library examples: Collection<Element>, Sequence<Element>, AsyncSequence<Element, Failure>.

Can you store it as any Container?

Before Swift 5.7, no. You got the famous error: "Protocol can only be used as a generic constraint because it has Self or associated type requirements."

Now you can write any Container. But the associated type is erased. You cannot call add(_:), because Swift does not know what Item is.

let boxes: [any Container] = [IntBox(), StringBox()]
boxes[0].items.count     // OK: count is Int
// boxes[0].add(5)       // error: Item is unknown

With a primary associated type you can keep the element type:

let intBoxes: [any Container<Int>] = [IntBox()]

Type erasure (the older pattern)

Before any, people wrote wrapper types like AnyPublisher or AnySequence. They hide the concrete type but keep the element type.

struct AnyContainer<Item>: Container {
    private let getItems: () -> [Item]
    init<C: Container>(_ base: C) where C.Item == Item {
        getItems = { base.items }
    }
    var items: [Item] { getItems() }
    mutating func add(_ item: Item) { /* forward or ignore */ }
}

You still see this in Combine and older codebases. Interviewers may ask you to explain it.

Associated type vs generic parameter

Associated typeGeneric type parameter
WhereInside a protocolOn a struct, class, enum or function
Who picks itThe conforming type, onceThe caller, each time
ExampleCollection.ElementArray<Element>

A type can conform to a protocol only one way. So IntBox cannot be a Container of both Int and String.

Common follow-up questions

  • Why can't protocols use generic parameters like protocol Container<T>? The angle brackets on a protocol only name primary associated types. The conforming type still chooses them, once.
  • What does Self mean in a protocol? It is the conforming type. Like an associated type, it limits how you can use the protocol as any.
  • How does Swift infer associated types? It looks at the types used in the methods and properties you implement.
  • What is some View? An opaque type. View has an associated type Body, and some hides which exact type it is.
  • When should you use type erasure today? When you need to store mixed types and still call methods that use the associated type. Often any P<T> is enough now.