Post

Swift enums with generic associated values

Swift enums with generic associated values

If you often work with enums, you’ve likely needed cases with associated values that can vary by type. Let’s model a scenario where a generic associated value represents the load state of some data.

A non-generic starting point

1
2
3
4
5
6
enum LoadState {
    case idle
    case loading
    case loaded(String)
    case failed
}

This works if you always load a String. It breaks as soon as you need to load other types (Int, User, etc.).

Make it generic where it matters

We can make the payload generic and keep the same states:

1
2
3
4
5
6
7
8
9
10
11
12
enum LoadState<T> {
    case idle
    case loading
    case loaded(T)
    case failed
}

struct User { let id: Int }

let number: LoadState<Int> = .loaded(42)
let name: LoadState<String> = .loaded("Running")
let user: LoadState<User> = .loaded(User(id: 1))

Here, the generic T is “what is being loaded.” For example, LoadState<User>.loading means “a user is loading.”

Equatable and generic associated values

If we try to make LoadState conform to Equatable directly, the compiler can’t synthesize it because T might not be equatable:

1
2
3
4
5
6
enum LoadState<T>: Equatable {
    case idle
    case loading
    case loaded(T)
    case failed
}

Type LoadState<T> does not conform to protocol Equatable

Option 1: Require T to be Equatable at the type declaration

1
2
3
4
5
6
7
8
9
10
11
12
enum LoadState<T: Equatable>: Equatable {
    case idle
    case loading
    case loaded(T)
    case failed
}

struct User: Equatable { let id: Int }

let a = LoadState<Int>.loaded(1)
let b = LoadState<Int>.loaded(2)
print(a == b) // false

This works, but it forces every LoadState to use an Equatable payload, even when you don’t need equality.

Option 2: Conditional conformance only when needed

Keep LoadState generic without constraints, and add Equatable only when T is Equatable:

1
2
3
4
5
6
7
8
enum LoadState<T> {
    case idle
    case loading
    case loaded(T)
    case failed
}

extension LoadState: Equatable where T: Equatable { }

Now you get equality for free only when it makes sense:

1
2
3
4
struct User: Equatable { let id: Int }
let u1 = LoadState<User>.loaded(User(id: 1))
let u2 = LoadState<User>.loaded(User(id: 2))
print(u1 == u2) // false

And if your payload isn’t equatable, equality isn’t available—which is exactly what we want:

1
2
3
4
struct Session { }
let s1 = LoadState<Session>.idle
let s2 = LoadState<Session>.idle
print(s1 == s2)

Operator function == requires that Session conform to Equatable

This keeps intent clear and avoids extra constraints, while still letting the compiler synthesize Equatable when it can.

I hope this shows how to design enums with generic associated values, and how conditional conformance keeps code simple and clear.


Producing and maintaining these guides takes time and resources. If you found this article helpful and would like to support future content, consider a small contribution via Buy Me a Coffee. Contributions help cover hosting and creation costs and make it possible to keep publishing free, practical material. No pressure — sharing this post with your network or starring the project is equally appreciated. Thank you for reading.

This post is licensed under CC BY 4.0 by the author.