Swift’s type system is designed to provide both strong compile-time guarantees and high runtime performance. One of the key features enabling this balance is Swift’s approach to protocol-based abstraction.
Two keywords that often confuse developers especially when working with protocols and generics are:
someanyThese keywords correspond to two different concepts in Swift’s type system:
some)any)Although both are used with protocols, they represent fundamentally different abstraction models.
Understanding when to use each is important when designing APIs, writing performant code, or working with frameworks like SwiftUI where some View appears everywhere.
In this article we will explore:
Protocols define behavioral contracts.
For example:
protocol Animal {
func speak()
}Types conform to the protocol by implementing its requirements:
struct Dog: Animal {
func speak() {
print("Woof")
}
}Now suppose we want to return a protocol type from a function:
func makeAnimal() -> Animal {
Dog()
}At first glance this seems reasonable. However, Swift’s type system historically struggled with certain protocols - especially those with associatedtype or Self requirements.
For example:
protocol Vehicle {
associatedtype Fuel
func refuel(with fuel: Fuel)
}Because this protocol contains an associated type, Swift cannot determine the concrete type of Fuel when the protocol is used directly.
struct Petrol {}
struct ElectricCharge {}
struct Car: Vehicle {
func refuel(with fuel: Petrol) {
print("Car refueled with petrol")
}
}
struct ElectricCar: Vehicle {
func refuel(with fuel: ElectricCharge) {
print("Electric car charging")
}
}Now imagine we want a factory function that returns a vehicle.
A first attempt might look like this:
func makeVehicle() -> Vehicle {
Car()
}However, this returns compile time warning:
Use of protocol 'Vehicle' as a type must be written 'any Vehicle'; this will be an error in a future Swift language modeThis ambiguity created a need for two different abstraction mechanisms:
Swift addresses these needs using:
some → opaque typesany → existential typessome)An opaque type represents a specific concrete type that conforms to a protocol, but whose identity is hidden from the caller.
The key idea is simple:
The compiler knows the concrete type, but the caller does not.
protocol Shape {
func area() -> Double
}
struct Circle: Shape {
let radius: Double
func area() -> Double {
.pi * radius * radius
}
}
struct Square: Shape {
let size: Double
func area() -> Double {
size * size
}
}
func makeShape() -> some Shape {
Circle(radius: 10)
}Here:
makeShape() returns a concrete type (Circle)ShapeUsage:
let shape = makeShape()
print(shape.area())Even though the caller sees Shape, the compiler still knows the returned type is Circle.
Opaque types allow you to hide implementation details while preserving static type information.
This has two major advantages.
If you expose a concrete type:
func makeShape() -> CircleYour API becomes tightly coupled to the implementation.
Instead you can write:
func makeShape() -> some ShapeThis allows you to change the underlying type later without breaking callers.
Because the compiler still knows the exact type, Swift can perform powerful optimizations:
As a result, opaque types have almost zero runtime overhead.
A function returning some Protocol must return the same concrete type in every return path.
This will not compile:
func makeShape(flag: Bool) -> some Shape {
if flag {
return Circle(radius: 10)
} else {
return Square(size: 10) // ❌ compile error
}
}Compile time error:
Function declares an opaque return type 'some Shape', but the return statements in its body do not have matching underlying typesWhy?
Because some Shape means:
There exists one specific concrete type, but its identity is hidden.
Allowing multiple types would violate that guarantee.
SwiftUI relies heavily on opaque types.
You will frequently see this declaration:
var body: some ViewExample:
struct ContentView: View {
var body: some View {
Text("Hello")
}
}Although it appears that body returns View, Swift actually generates a concrete view type behind the scenes.
Opaque types allow SwiftUI to:
any)An existential type represents a container that can hold any value conforming to a protocol.
Unlike opaque types, the underlying type may vary at runtime.
Existential types are written using the any keyword.
protocol Animal {
func speak()
}
struct Dog: Animal {
func speak() {
print("Woof")
}
}
struct Cat: Animal {
func speak() {
print("Meow")
}
}Now consider a function that returns a random animal:
func randomAnimal() -> any Animal {
Bool.random() ? Dog() : Cat()
}Usage:
let animal = randomAnimal()
animal.speak()Here the variable animal might contain:
DogCatThe concrete type is determined at runtime.
To understand the cost of existential types, it helps to look at how Swift represents them internally.
When you write:
let animal: any Animal = Dog()Swift creates an existential container.
Conceptually, this container stores three pieces of information:
Existential Container
├─ Value
├─ Type Metadata
└─ Witness TableThe container stores the concrete value (Dog in this case).
Small values may be stored directly in the container, while larger values may be allocated on the heap.
Swift stores metadata describing the underlying type.
This allows the runtime to know things like:
The witness table is what enables dynamic protocol dispatch.
A witness table contains function pointers that implement the protocol requirements for a specific type.
Conceptually:
Animal Witness Table for Dog
└─ speak → Dog.speak()When Swift executes:
animal.speak()It performs the following steps:
This is known as dynamic dispatch via witness tables.
some vs anySwift supports three different abstraction mechanisms:
some)any)Although they look similar in code, they behave differently at the compiler level.
Example:
func process<T: Shape>(_ shape: T) {
print(shape.area())
}The compiler specializes this function for each concrete type.
Advantages:
Example:
func makeShape() -> some Shape {
Circle(radius: 10)
}Opaque types behave similarly to generics internally because the compiler still knows the underlying type.
Advantages:
Example:
func process(_ shape: any Shape) {
print(shape.area())
}Here the concrete type is unknown at compile time, so Swift must use runtime polymorphism.
This introduces:
| Abstraction | Dispatch | Runtime Cost | Typical Use |
|---|---|---|---|
| Generics | Static | None | Algorithms |
some |
Static | None | Returning protocol types |
any |
Dynamic | Small overhead | Heterogeneous storage |
When writing reusable algorithms.
func process<T: Shape>(_ shape: T)someWhen returning protocol types while hiding implementation details.
func makeView() -> some Viewany When NecessaryWhen storing heterogeneous values or working with runtime polymorphism.
var animals: [any Animal] = [
Dog(),
Cat()
]A useful rule of thumb:
some means
One specific concrete type exists, but it is hidden.
any means
This value may contain any type that conforms to the protocol.
Swift introduced some and any to make protocol abstraction more explicit and more efficient.
They represent two different design trade-offs:
some) hide a concrete type while preserving compile-time optimizations.any) allow dynamic polymorphism by storing values of multiple conforming types.As a practical guideline:
some for return types.any only when dynamic behavior is truly required.Understanding this distinction helps you design better APIs, write more efficient Swift code, and reason more clearly about how Swift’s type system works.
Thank you for reading. If you have any questions, feel free to follow me on X and send me a DM. If you enjoyed this article and would like to support my work, Buy me a coffee ☕️