Skip to Content
Course content

45: Associated Types in Protocols

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've already spent time with generics in structs and classes, so the concept of "a type that we'll decide later" isn't new to you. But when you move that logic into a protocol, Swift handles it differently. You can't just put <T> next to a protocol name. Instead, we use associatedtype.

Let's build a simple caching system for a music app. We want a way to store different kinds of data—like Song objects or Artist objects—without writing a completely separate protocol for every single model in our app.

The mistake of using Any

When I first approached this, I tried to keep it "simple" by using Any. It looked like this:

protocol Cache {
    func save(_ item: Any)
    func retrieve() -> Any?
}

On the surface, it works. You can pass anything in. But as soon as I tried to actually use the retrieved item, I had to cast it manually every single time: if let song = cache.retrieve() as? Song { ... }. That's a nightmare. It's fragile, it's verbose, and it completely defeats the purpose of Swift's strong type system. I was basically turning Swift into a dynamically typed language, and that's a path you don't want to go down.

Defining the placeholder with associatedtype

The right way to do this is to tell the protocol: "I don't know exactly what type will be stored here, but whatever the conforming struct or class decides, that's the type we'll use consistently."

protocol Cache {
    associatedtype Item
    
    func save(_ item: Item)
    func retrieve() -> Item?
}

Notice I didn't specify if Item is a String, an Int, or a custom Object. I just gave it a name. Now, the protocol is a blueprint that says: "Whoever implements me must define what Item is."

Locking in the types with concrete implementations

Now we can create specific caches. Let's say we have a Song struct. When we create a SongCache, Swift will infer the associatedtype based on the function signatures we provide.

struct Song {
    let title: String
    let artist: String
}

struct SongCache: Cache {
    // Swift sees 'Song' here and realizes 'Item' must be 'Song'
    var storage: [String: Song] = [:]
    
    func save(_ item: Song) {
        storage[item.title] = item
    }
    
    func retrieve() -> Song? {
        // Simplified for the example
        return storage.values.first
    }
}

I love this because it's clean. If I try to pass an Artist object into SongCache.save(), the compiler will stop me immediately. We get the flexibility of a generic protocol but the safety of concrete types.

The "Protocol with Associated Type" (PAT) quirk

Here is where you might hit a wall. You might try to create an array of different caches like this: let myCaches: [Cache] = [songCache, artistCache]. This will fail.

Swift will complain that Cache has an associated type and cannot be used as a type itself. This is because the compiler needs to know exactly what Item is to ensure memory safety. You can't have a list of "things that are caches" because one might be a Song cache and another an Artist cache, and the compiler can't guarantee what retrieve() will return.

To get around this, you'll usually use generics in the functions that consume these caches, like this:

func clearCache<T: Cache>(cache: T) {
    print("Clearing the cache for \(T.Item.self)")
}

By using <T: Cache>, we're telling Swift: "I don't care which specific cache this is, as long as it follows the Cache protocol."




📋 Practical Task

Build a Type-Safe Configuration Manager

You need to create a system that provides configuration settings for different parts of an app. Some settings are simple strings (like API keys), while others are complex objects (like a DatabaseConfig struct).

Your Requirements:

  • Create a protocol named ConfigurationProvider. It should have an associatedtype called ConfigValue.
  • The protocol must require a method fetchSetting(for key: String) -> ConfigValue?.
  • Create a struct called DatabaseConfig with properties for url and timeout.
  • Implement a NetworkConfigProvider that conforms to ConfigurationProvider and handles String values.
  • Implement a DatabaseConfigProvider that conforms to ConfigurationProvider and handles DatabaseConfig values.
  • Write a generic function printSetting<P: ConfigurationProvider>(provider: P, key: String) that fetches the setting and prints it to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.