top of page

Generics reloaded..

  • Writer: arda doğantemur
    arda doğantemur
  • Feb 18
  • 13 min read


Missed the beginning? Start from Generics in Swift: A Comprehensive Guide to get the full picture!


  1. Generic Subscripts

Subscripts allow us to access elements in a collection-like way (e.g., array[0], dictionary["key"]).

In Swift, we can make subscripts generic to provide flexible and reusable indexing mechanisms.


Problem: Accessing Elements in a Generic Type

Imagine we have a Dictionary that stores values inside a generic wrapper:

struct KeyValueStore {
    private var storage: [String: Any] = [:]
    
    mutating func set<T>(_ value: T, forKey key: String) {
        storage[key] = value
    }
    
    func get<T>(forKey key: String) -> T? {
        return storage[key] as? T
    }
}

This allows us to store and retrieve values of any type:

var store = KeyValueStore()
store.set(42, forKey: "age")
store.set("Swift", forKey: "language")

let age: Int? = store.get(forKey: "age")
let language: String? = store.get(forKey: "language")

print(age ?? 0)       // Output: 42
print(language ?? "") // Output: Swift

But calling get(forKey:) is less intuitive than using store["age"].

Wouldn’t it be better if we could use subscript syntax?


Solution: Using a Generic Subscript

We can replace get(forKey:) with a generic subscript:

struct KeyValueStore {
    private var storage: [String: Any] = [:]
    
    mutating func set<T>(_ value: T, forKey key: String) {
        storage[key] = value
    }

    subscript<T>(key: String) -> T? {
        return storage[key] as? T
    }
}

Now, we can access values like this:

var store = KeyValueStore()
store.set(42, forKey: "age")
store.set("Swift", forKey: "language")

let age: Int? = store["age"]
let language: String? = store["language"]

print(age ?? 0)       // Output: 42
print(language ?? "") // Output: Swift
🏋️‍♂️ Practice Question: Generic Subscripts

We previously created a Queue<T> struct.

👉 Problem: Modify Queue<T> to include a generic subscript that:

  • Allows reading elements at a specific index (queue[0] should return the first item, queue[1] the second, etc.).

  • Returns nil if the index is out of range.

📝 Hint: The subscript should use generics to return an element of type T?.


✅ Solution: Implementing a Generic Subscript in Queue<T>

We’ll modify our Queue<T> struct to include a generic subscript that allows reading elements by index.


Extend Queue<T> to Add a Generic Subscript

extension Queue {
    subscript(index: Int) -> T? {
        return (index >= 0 && index < elements.count) ? elements[index] : nil
    }
}

🛠 How It Works:

• The subscript takes an Int index.

• If the index is valid, it returns the element at that index.

• If the index is out of range, it returns nil instead of crashing.

var numberQueue = Queue<Int>()
numberQueue.enqueue(10)
numberQueue.enqueue(20)
numberQueue.enqueue(30)

print(numberQueue[0] ?? "Out of bounds") // Output: 10
print(numberQueue[1] ?? "Out of bounds") // Output: 20
print(numberQueue[2] ?? "Out of bounds") // Output: 30
print(numberQueue[3] ?? "Out of bounds") // Output: Out of bounds (index is out of range)
  • Generic subscripts allow flexible and intuitive access to elements.

  • We can handle out-of-bounds errors safely by returning nil.

  • This approach makes our Queue<T> behave more like built-in Swift collections.



  1. Generic Type Erasure

Generics in Swift provide flexibility and type safety, but sometimes they can cause type identity issues, especially when working with protocols that have associated types.


This is where type erasure comes in! It allows us to hide generic details and work with a single concrete type instead of exposing generic parameters everywhere.


Problem: Storing Generic Protocols in a Collection

Imagine we have a protocol DataSource with an associated type:

protocol DataSource {
    associatedtype DataType
    func fetchData() -> DataType
}

Now, let’s create two different data sources:

struct IntSource: DataSource {
    func fetchData() -> Int {
        return 42
    }
}

struct StringSource: DataSource {
    func fetchData() -> String {
        return "Swift"
    }
}

Now, let’s try storing them in an array:

let sources: [DataSource] = [IntSource(), StringSource()] // ❌ Error!
Problem Why does this fail?

• DataSource has an associated type, meaning Swift expects each instance to have a specific type, not a mix of types.

• We cannot use protocols with associated types directly in an array.


Solution: Using Type Erasure with AnyDataSource

We need to ensure that AnyDataSource works with any data type by wrapping the fetched value in Any. Here’s the implementation:

struct AnyDataSource: DataSource {
    private let _fetchData: () -> Any

    init<U: DataSource>(_ source: U) {
        self._fetchData = source.fetchData
    }

    func fetchData() -> Any {
        return _fetchData()
    }
}
struct IntSource: DataSource {
    func fetchData() -> Int { return 42 }
}

struct StringSource: DataSource {
    func fetchData() -> String { return "Swift" }
}

let sources: [AnyDataSource] = [AnyDataSource(IntSource()), AnyDataSource(StringSource())]

for source in sources {
    print(source.fetchData()) 
    // Output: 42
    // Output: "Swift"
}

What Changed with Swift 5.7?

Before Swift 5.7, associated types made it impossible to use a protocol with generics as a stored property or in collections.

Now, Primary Associated Types make it easier to work with generics without requiring type erasure.


🚀 Example: Using Primary Associated Types Instead of Type Erasure

Let’s redefine our DataSource protocol with a primary associated type:

protocol DataSource<DataType> {
    func fetchData() -> DataType
}

Now, we can directly use DataSource as a protocol type:

struct IntSource: DataSource {
    func fetchData() -> Int { return 42 }
}

struct StringSource: DataSource {
    func fetchData() -> String { return "Swift" }
}

let sources: [any DataSource] = [IntSource(), StringSource()] // ✅ Works in Swift 5.7+

🎉 No type erasure required! This solves the problem that previously required AnyDataSource.

🧐 When is Type Erasure Still Needed?

Hiding Implementation Details

• If you don’t want to expose the exact generic type, type erasure still helps.

• Example: You might return AnyDataSource from a function to prevent clients from knowing the exact type.


Avoiding any Protocol Existentials Performance Overhead

• Using any DataSource introduces runtime overhead (existential containers).

• Type erasure using AnyDataSource<T> can be more optimized.


Mixing Protocols with Multiple Associated Types

• If a protocol has more than one associated type, primary associated types won’t fully remove the need for type erasure.


🏋️‍♂️ Practice Question: Type Erasure in a Payment System

Imagine we’re building a payment processing system where we have different payment methods. Each payment method conforms to a PaymentProcessor protocol, which has an associated type.

👉 Problem Statement

Define a protocol PaymentProcessor that has an associated type Currency representing the currency it processes.

Implement two different payment processors:

• CreditCardProcessor that processes Double values (e.g., dollars).

• BitcoinProcessor that processes Int values (e.g., BTC).

Challenge: Store multiple PaymentProcessor instances in a collection.


Solution: Type Erasure in a Payment System

Since we cannot store protocols with associated types directly in a collection, we use type erasure to wrap different PaymentProcessor implementations into a common type.


Define the PaymentProcessor Protocol

This protocol includes an associated type Currency representing the currency type it processes.

protocol PaymentProcessor {
    associatedtype Currency
    func process(amount: Currency)
}

Implement Two Different Payment Processors

CreditCardProcessor (Processes Double values in dollars)

struct CreditCardProcessor: PaymentProcessor {
    func process(amount: Double) {
        print("Processing credit card payment of $\(amount)")
    }
}

BitcoinProcessor (Processes Int values in Bitcoin)

struct BitcoinProcessor: PaymentProcessor {
    func process(amount: Int) {
        print("Processing Bitcoin payment of \(amount) BTC")
    }
}

We create a wrapper struct that erases the generic type and allows us to store any PaymentProcessor:

struct AnyPaymentProcessor: PaymentProcessor {
    private let _process: (Any) -> Void

    init<U: PaymentProcessor>(_ processor: U) {
        self._process = { amount in
            if let typedAmount = amount as? U.Currency {
                processor.process(amount: typedAmount)
            } else {
                print("❌ Invalid type provided")
            }
        }
    }

    func process(amount: Any) {
        _process(amount)
    }
}

Now, we can store both CreditCardProcessor and BitcoinProcessor in the same array and process payments dynamically.

let processors: [AnyPaymentProcessor] = [
    AnyPaymentProcessor(CreditCardProcessor()), // Expects `Double`
    AnyPaymentProcessor(BitcoinProcessor())     // Expects `Int`
]

// Process payments correctly
processors[0].process(amount: 100.0) // ✅ Output: Processing credit card payment of $100.0
processors[1].process(amount: 2)    // ✅ Output: Processing Bitcoin payment of 2 BTC
  • AnyPaymentProcessor erases the associated type, allowing different PaymentProcessor types to be stored together.

  • Closures inside AnyPaymentProcessor forward method calls while ensuring type safety.

  • This approach is useful when working with heterogeneous collections of generic types.



  1. Generic Metatypes (T.Type and Self)

In Swift, metatypes represent the type of a type. In other words, a metatype is a reference to the type itself, rather than an instance of the type.


When working with generics, metatypes become powerful because they allow us to:

  • Create instances dynamically at runtime

  • Store types in a collection

  • Use type information as a function parameter


Problem: Creating Objects Dynamically in a Generic Context

Let’s say we have a protocol Animal with different implementations:

protocol Animal {
    init()
    func speak()
}

struct Dog: Animal {
    func speak() { print("🐶 Woof!") }
}

struct Cat: Animal {
    func speak() { print("🐱 Meow!") }
}

Now, let’s say we want to store multiple types in an array and dynamically create instances of them at runtime.

A normal array of [Animal] wouldn’t work because Swift requires explicit types.

We need a way to store the types instead of the instances.


Solution: Using Generic Metatypes (T.Type)

We can store metatypes (T.Type) inside an array, then dynamically create objects at runtime using init():


let animalTypes: [Animal.Type] = [Dog.self, Cat.self]

for animalType in animalTypes {
    let animal = animalType.init()
    animal.speak()
}

🛠 Output:

🐶 Woof!
🐱 Meow!

Explanation:

• Dog.self and Cat.self represent the types themselves, not instances.

• Animal.Type means “a type that conforms to Animal”.

• animalType.init() dynamically creates an instance of Dog or Cat.


🏋️‍♂️ Practice Question: Using Generic Metatypes in Factories

We want to build a Factory Pattern where we can dynamically create instances of different classes based on their type.

👉 Problem:

  • Define a Vehicle protocol with an init() method and a describe() method.

  • Create two types: Car and Bike, both conforming to Vehicle.

  • Create a function createVehicle<T: Vehicle>(ofType type: T.Type) -> T that dynamically creates an instance of T.

  • Use createVehicle to instantiate Car and Bike without explicitly calling init() on them.

📝 Hint: Use T.Type to pass types dynamically.


✅ Solution: Using Generic Metatypes in a Factory Pattern

Since Swift allows us to work with metatypes (T.Type), we can use them to dynamically create instances of different types.


Define the Vehicle Protocol

We create a Vehicle protocol that requires an initializer and a method describe().

protocol Vehicle {
    init()
    func describe()
}

Implement Car and Bike Types

Both Car and Bike conform to Vehicle.

struct Car: Vehicle {
    func describe() {
        print("🚗 This is a car.")
    }
}

struct Bike: Vehicle {
    func describe() {
        print("🚲 This is a bike.")
    }
}

Create a Generic Factory Method

The function takes a metatype (T.Type) as input, then instantiates T dynamically.

func createVehicle<T: Vehicle>(ofType type: T.Type) -> T {
    return type.init()
}

Now, we can create Car and Bike dynamically without calling init() directly.

let myCar = createVehicle(ofType: Car.self)
myCar.describe()  // Output: 🚗 This is a car.

let myBike = createVehicle(ofType: Bike.self)
myBike.describe() // Output: 🚲 This is a bike.

Storing Metatypes in an Array

let vehicleTypes: [Vehicle.Type] = [Car.self, Bike.self]

for vehicleType in vehicleTypes {
    let vehicle = vehicleType.init()
    vehicle.describe()
}

🛠 Output:

🚗 This is a car.
🚲 This is a bike.
  • Metatypes (T.Type) allow dynamic instantiation of generic types.

  • Useful for Factory Patterns where we decide at runtime which object to create.

  • We can store multiple types in an array and initialize them dynamically.



  1. Recursive Generics (Self in Protocols)

Recursive generics are a powerful feature in Swift that allow protocols to reference themselves in a generic way.

This is useful for defining protocols that need to ensure a consistent type across related objects (e.g., Comparable, Builder Patterns, Fluent APIs).


Problem: Defining a Protocol That Returns Self

Let’s say we want to build a builder pattern where an object configures itself and returns an updated version of itself.

If we try defining a protocol like this:

protocol Configurable {
    func configure() -> Configurable
}

This makes configure() return any object conforming to Configurable, not necessarily the same type.

We need a way to ensure configure() returns the same type as the conforming class.


Solution: Using Self in Protocols

By using Self, we enforce that configure() must return the same type as the conforming object.

protocol Configurable {
    func configure() -> Self
}

Now, let’s define a struct User that conforms to Configurable:

struct User: Configurable {
    var name: String
    
    func configure() -> Self {
        return User(name: "Configured \(name)")
    }
}
let user = User(name: "Alice")
let updatedUser = user.configure()
print(updatedUser.name) // Output: Configured Alice

Now, configure() always returns the exact type (User), not just Configurable.


Using Recursive Generics for Comparable Objects

Another common use case for recursive generics is ensuring type-safe comparisons in protocols.

protocol ComparableType {
    func isGreaterThan(_ other: Self) -> Bool
}

Now, let’s implement it for Score:

struct Score: ComparableType {
    var points: Int
    
    func isGreaterThan(_ other: Score) -> Bool {
        return self.points > other.points
    }
}
let score1 = Score(points: 85)
let score2 = Score(points: 90)

print(score1.isGreaterThan(score2)) // Output: false

Now, we can compare only objects of the same type (Score).


🏋️‍♂️ Practice Question: Using Recursive Generics in a Fluent API

We want to build a Fluent API for configuring a UI component dynamically.

👉 Problem:

  • Define a protocol FluentConfigurable that requires a configure() method returning Self.

  • Create a struct Button that conforms to FluentConfigurable.

  • Implement configure() so that we can chain method calls:

let button = Button(title: "Submit")
    .configure()
    .configure()

📝 Hint: Use Self to ensure configure() always returns the same type.


✅ Solution: Using Recursive Generics in a Fluent API

Fluent APIs allow method chaining, making it easier to configure objects step by step.

We use Self in protocols to ensure that the return type always matches the conforming type.


Define the FluentConfigurable Protocol

The protocol requires a configure() method that returns Self, enabling method chaining.

protocol FluentConfigurable {
    func configure() -> Self
}

Implement Button Struct with Fluent API

struct Button: FluentConfigurable {
    var title: String
    
    func configure() -> Self {
        print("Configuring button: \(title)")
        return self
    }
}

Example Usage: Method Chaining

let button = Button(title: "Submit")
    .configure()
    .configure()
Configuring button: Submit
Configuring button: Submit

Now, configure() always returns Button, allowing method chaining.


Adding More Fluent API Methods

Let’s extend Button to allow setting styles dynamically:

struct Button: FluentConfigurable {
    var title: String
    var backgroundColor: String = "Default"

    func configure() -> Self {
        print("Configuring button: \(title)")
        return self
    }

    func setBackgroundColor(_ color: String) -> Self {
        var updatedButton = self
        updatedButton.backgroundColor = color
        return updatedButton
    }
}

Example Usage:

let button = Button(title: "Submit")
    .setBackgroundColor("Blue")
    .configure()
Configuring button: Submit
  • Using Self enforces that the method always returns the exact conforming type.

  • Fluent APIs make object configuration cleaner and more readable.

  • This pattern is commonly used in SwiftUI, Builder Patterns, and DSLs.



  1.  Phantom Types

Phantom types are a powerful but lesser-known feature in Swift’s generics system.

They allow us to add compile-time restrictions without affecting runtime behavior.


This technique is useful for ensuring type safety in scenarios where certain types should be used only in specific ways.


Problem: Preventing Invalid State Using Phantom Types

Imagine we’re building a banking system where users can transfer money between accounts.

Let’s define a Money struct:

struct Money {
    let amount: Double
}

This works, but there’s a problem:

  • We need to differentiate USD from EUR transactions.

  • There’s no type-level enforcement, meaning we could accidentally mix currencies.

For example, this is invalid but still compiles:

let usd = Money(amount: 100)
let eur = Money(amount: 50)

let total = usd.amount + eur.amount // ❌ Mixing currencies, but Swift allows it!

Solution: Using Phantom Types to Enforce Currency Rules

We introduce a generic Money<T> type, where T is a phantom type representing the currency:

struct Money<Currency> {
    let amount: Double
}

Now, let’s create phantom types to distinguish different currencies:

struct USD {}
struct EUR {}

Example usage

let usdMoney = Money<USD>(amount: 100)
let eurMoney = Money<EUR>(amount: 50)

To prevent currency mismatches, we need to ensure operations are only performed between the same generic type.

struct Money<Currency> {
    let amount: Double

    func add(_ other: Money<Currency>) -> Money<Currency> {
        return Money<Currency>(amount: self.amount + other.amount)
    }
}

Now, This Works:

let usd1 = Money<USD>(amount: 100)
let usd2 = Money<USD>(amount: 50)

let totalUsd = usd1.add(usd2) // ✅ Allowed
print(totalUsd.amount) // Output: 150

But This Now Fails at Compile-Time:

let eurMoney = Money<EUR>(amount: 50)

let total = usd1.add(eurMoney) // ❌ Compile-time error!

Error Message (Expected):

Cannot convert value of type 'Money<EUR>' to expected argument type 'Money<USD>'
  • Phantom types provide type safety at compile-time without affecting runtime behavior.

  • Operations must be explicitly restricted to enforce type safety.

  • If operations accept only Money<SameCurrency>, Swift prevents invalid currency mixing at compile-time.


🏋️‍♂️ Practice Question: Using Phantom Types for Safe Database Queries

We want to ensure that database queries are executed only with valid user permissions.

👉 Problem:

  • Define a struct DatabaseQuery<Role> where Role is a phantom type.

  • Create two roles: ReadAccess and WriteAccess.

  • Implement two functions:

    • fetchData(query: DatabaseQuery<ReadAccess>) (only for readers).

    • writeData(query: DatabaseQuery<WriteAccess>) (only for writers).

  • Show that a reader cannot call writeData() at compile-time.

📝 Hint: Use phantom types to prevent invalid operations.


✅ Solution: Using Phantom Types for Safe Database Queries

By using phantom types, we can enforce role-based access control at compile-time.

This ensures that readers can only fetch data and writers can only modify data, preventing accidental misuse.


Define a DatabaseQuery<Role> Struct

We use a generic struct with a phantom type for role enforcement.

struct DatabaseQuery<Role> {
    let query: String
}

Role is a phantom type—it has no runtime effect but ensures type safety.

• Each DatabaseQuery instance must have a role type (ReadAccess or WriteAccess).


Create Role Types (ReadAccess, WriteAccess)

We define empty structs to act as phantom types.

struct ReadAccess {}
struct WriteAccess {}

These do not hold any data, but they enforce type safety at compile-time.


Implement Role-Specific Functions

Now, we create functions that only accept specific roles.

func fetchData(query: DatabaseQuery<ReadAccess>) {
    print("Fetching data for query: \(query.query)")
}

func writeData(query: DatabaseQuery<WriteAccess>) {
    print("Writing data for query: \(query.query)")
}

Now, Swift will only allow the correct role to access each function.


Example Usage


let readQuery = DatabaseQuery<ReadAccess>(query: "SELECT * FROM users")
fetchData(query: readQuery) // ✅ Works

let writeQuery = DatabaseQuery<WriteAccess>(query: "UPDATE users SET name = 'Alice'")
writeData(query: writeQuery) // ✅ Works

Compile-Time Error: Preventing Unauthorized Access

If a reader tries to write data, Swift will prevent it at compile-time.

// fetchData(query: writeQuery) // ❌ Compile-time error: Expected `DatabaseQuery<ReadAccess>`, found `DatabaseQuery<WriteAccess>`

// writeData(query: readQuery) // ❌ Compile-time error: Expected `DatabaseQuery<WriteAccess>`, found `DatabaseQuery<ReadAccess>`

Swift will not allow incorrect role usage, preventing security risks at compile-time!

  • Phantom types enforce access control at compile-time, reducing runtime errors.

  • Using DatabaseQuery<ReadAccess> and DatabaseQuery<WriteAccess> prevents role misuse.

  • No additional runtime checks are needed—everything is enforced by Swift’s type system.

Comentarios


© 2020 by Arda Doğantemur.

bottom of page