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 protocolEquatable
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 thatSessionconform toEquatable
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.