← All topics/Swift language features

Practical interview questions

Scenario-style prompts with sample answer outlines. Focus is on how you would design and reason in real codebases.

Question 3

Protocols, generics, and where clauses

How do you use protocol-oriented design with generics and associated types? When do you need a where clause on an extension or generic method?

Answer outline

Protocols, generics, and associated types help describe capabilities and relationships, not just concrete types.

Protocol-oriented design spells out what something can do. Generics keep that description reusable and type-safe across implementations.

Associated types fit when a protocol needs a relationship between types, not a single fixed concrete type.

Examples: a store tied to one model type, or a coordinator that produces one specific output type.

A where clause applies when generic code should only exist under extra constraints. It signals: this extension or method is available only when the generic types meet those conditions.

A common example of a where clause would be when checking that an associated type conforms to Equatable, Hashable, or Sendable.

Principles

  • Use protocols to describe behavior and roles.
  • Use generics to write reusable, type-safe code.
  • Use associated types when a protocol depends on a related type.
  • Keep abstractions meaningful, not overly abstract.
  • Prefer compile-time guarantees over loose runtime checks.
  • Use constrained extensions to add specialized behavior cleanly.

Every Cache works with some Value, but different caches can choose different value types.

Protocol with associated type
protocol Cache {
    associatedtype Value

    func save(_ value: Value, for key: String)
    func load(for key: String) -> Value?
}

A single implementation can be reused for any value type.

Generic type conforming to the protocol
final class MemoryCache<T>: Cache {
    private var storage: [String: T] = [:]

    func save(_ value: T, for key: String) {
        storage[key] = value
    }

    func load(for key: String) -> T? {
        storage[key]
    }
}

This extension only makes sense when Value is Equatable, because == is used—that is when a where clause is needed.

`where` on an extension
extension Cache where Value: Equatable {
    func contains(_ value: Value, for key: String) -> Bool {
        load(for: key) == value
    }
}

The where clause adds the rule that T must conform to Equatable (equivalent to writing <T: Equatable> on the first form).

`where` on a generic method
func areEqual<T: Equatable>(_ lhs: T, _ rhs: T) -> Bool {
    lhs == rhs
}

func compare<T>(_ lhs: T, _ rhs: T) -> Bool where T: Equatable {
    lhs == rhs
}

This extension exists only when the repository’s Model is Sendable.

Constraining an associated type
protocol Repository {
    associatedtype Model
    func save(_ model: Model)
}

extension Repository where Model: Sendable {
    func saveSafely(_ model: Model) {
        save(model)
    }
}