Understanding existentials and primary associated types in Swift
How existentials and primary associated types enhance flexibility in protocols.
17 Mar 2025 · 5 min read
Existentials are useful when working with protocols that have associated types. In Swift, protocols with associated types cannot be used as standalone types because their concrete type is unknown at compile time. To work around this, Swift allows us to use existentials (any Protocol) to enable type erasure and store different conforming types in a single collection.
Let's explore how they work in practice.

Let's consider an example where we'll create a system to manage different geometric shapes such as circles, rectangles or triangles and calculate their areas. Each shape conforms to a common protocol Shape which defines a method for calculating the area:
protocol Shape {associatedtype Parametersfunc area(with parameters: Parameters) -> Double}
Here, the Parameters associated type will be different for each shape. For example, the parameters for a circle might be the radius, while the parameters for a rectangle could be its width and height.
Implementing concrete shapes
Let's define two shape implementations: Circle and Rectangle.
struct Circle: Shape {struct CircleParameters {let radius: Double}func area(with parameters: CircleParameters) -> Double {return Double.pi * parameters.radius * parameters.radius}}struct Rectangle: Shape {struct RectangleParameters {let width: Doublelet height: Double}func area(with parameters: RectangleParameters) -> Double {return parameters.width * parameters.height}}
Following the same structure, we could define more shapes, such as Triangle.
Storing shapes in a collection
If we try to store different shapes in an array using the Shape protocol directly, we run into a compiler error:
// Compiler error: Protocol 'Shape' can only be used as a generic constraint because it has Self or associated type requirements.var shapes: [Shape] = [Circle(), Rectangle(), Triangle()]
Swift does not allow storing values of a protocol with associated types in a collection unless their concrete types are known at compile time. This is where where existentials come into play:
var shapes: [any Shape] = [Circle(), Rectangle(), Triangle()]
Now, Swift allows different implementations to coexist in the same array, but with the trade-off of losing static type information. This means we don't know the specific type of each shape at compile time. To work around this, we must cast the shapes back to their concrete types before calling their methods.
let circleParameters = Circle.CircleParameters(radius: 10)let rectangleParameters = Rectangle.RectangleParameters(width: 5, height: 8)for shape in shapes {if let circle = shape as? Circle {print("Circle area: \(circle.area(with: circleParameters))")} else if let rectangle = shape as? Rectangle {print("Rectangle area: \(rectangle.area(with: rectangleParameters))")}}
While this works, it introduces runtime type checking and casting, which can be error-prone.
Using primary associated types
To reduce the need for type casting, Swift provides primary associated types, allowing us to specify associated types explicitly when working with existentials:
protocol Shape<Parameters> {associatedtype Parametersfunc area(with parameters: Parameters) -> Double}
Now, we can use Shape<CircleParameters> to define a concrete type when working with the protocol. However, for heterogeneous collections, we still need to use any without a concrete associated type.
While primary associated types help restore lost type information, they do not introduce additional trade-offs when used with any. Instead, they extend the flexibility of existentials, allowing us to specify associated types when needed while still leveraging any for heterogeneous collections.



Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips



