`some` vs `any` in Swift: Opaque Types and Existential Types Demystified
You swapped some Renderable for any Renderable in a protocol return type, the compiler stopped complaining, and you
shipped it — but now Instruments shows an unexpected heap allocation per frame in your rendering loop. These two
keywords look nearly identical on the surface but represent fundamentally different runtime strategies.
In this post you’ll learn what opaque types and existential types are under the hood, when each is appropriate, and how
to make the call confidently. We won’t cover @objc protocol existentials or Objective-C interop — those have their own
constraints that go beyond Swift’s type system.
Contents
- The Problem
- Opaque Types:
some Protocol - Existential Types:
any Protocol - Advanced Usage
- Performance Considerations
- When to Use (and When Not To)
- Summary
The Problem
Imagine you’re building a rendering pipeline for a Pixar-style studio app. You define a Renderable protocol and a
handful of conforming types. You then write a factory function that hands back whatever renderer is appropriate for the
current asset:
protocol Renderable {
func render(frame: Int) -> CGImage
var resolution: CGSize { get }
}
struct PixarFilm: Renderable {
let title: String
let resolution: CGSize
func render(frame: Int) -> CGImage {
// ... heavy compositing work
fatalError("Simplified for clarity")
}
}
struct PreviewRenderer: Renderable {
let resolution: CGSize
func render(frame: Int) -> CGImage {
fatalError("Simplified for clarity")
}
}
// What return type should this function have?
func makeRenderer(forProduction: Bool) -> ??? {
forProduction ? PixarFilm(title: "Elemental", resolution: .init(width: 4096, height: 2160))
: PreviewRenderer(resolution: .init(width: 1280, height: 720))
}
You have three plausible options: return PixarFilm directly, return some Renderable, or return any Renderable.
Each compiles under different conditions and each has meaningfully different runtime behaviour. Picking the wrong one
either locks your API to a concrete type, causes dynamic dispatch overhead, or — in the some case — generates a
compiler error you may not immediately understand.
Opaque Types: some Protocol
An opaque return type, written as some Protocol, tells the compiler: “this function returns a single concrete type
that conforms to Protocol, but I’m not telling the caller what that type is.” The concrete type is fixed and known at
compile time — the compiler knows it even though the caller doesn’t.
// Works only when there is ONE possible concrete return type
func makeProductionRenderer() -> some Renderable {
// The compiler knows this always returns PixarFilm — that's the
// "opaque" part: the type is hidden from callers, not from the compiler.
PixarFilm(
title: "Inside Out 2",
resolution: .init(width: 4096, height: 2160)
)
}
// ❌ This does NOT compile — `some` requires a single underlying type
func makeRenderer(forProduction: Bool) -> some Renderable {
if forProduction {
return PixarFilm(title: "Inside Out 2", resolution: .init(width: 4096, height: 2160))
} else {
return PreviewRenderer(resolution: .init(width: 1280, height: 720)) // Error!
}
}
The compiler error on that second version reads: “Function declares an opaque return type, but the return statements in
its body do not have matching underlying types.” This is not a limitation to work around — it’s the invariant that
makes some useful. Because the underlying type is fixed, the compiler can specialise the callee and inline through the
abstraction layer entirely.
some in SwiftUI
You already use some every day. SwiftUI’s body property is declared as some View:
struct FilmPosterView: View {
let film: PixarFilm
var body: some View {
// The compiler knows this is always a VStack<TupleView<(Text, Text)>>
// even though you only see `some View`
VStack {
Text(film.title)
Text("\(Int(film.resolution.width))p")
}
}
}
SwiftUI uses some View rather than any View deliberately: it enables the compiler to compare view trees structurally
at compile time and generate the most efficient diffing code. If body were any View, that structural comparison
would be impossible.
Existential Types: any Protocol
An existential type, written as any Protocol, is a type-erased container — often called an “existential box”. The box
holds any value that conforms to the protocol, plus a pointer to the type’s witness table so the runtime knows which
method implementations to call. The actual type inside the box can vary at runtime.
// ✅ This compiles — `any` allows multiple underlying types
func makeRenderer(forProduction: Bool) -> any Renderable {
if forProduction {
return PixarFilm(
title: "Inside Out 2",
resolution: .init(width: 4096, height: 2160)
)
} else {
return PreviewRenderer(resolution: .init(width: 1280, height: 720))
}
}
// Storing a heterogeneous collection — only possible with existentials
let renderQueue: [any Renderable] = [
PixarFilm(title: "Brave", resolution: .init(width: 4096, height: 2160)),
PreviewRenderer(resolution: .init(width: 1280, height: 720)),
PixarFilm(title: "Coco", resolution: .init(width: 4096, height: 2160))
]
// Dynamic dispatch: the runtime looks up `render` in the witness table
// for each element's actual type
for renderer in renderQueue {
_ = renderer.render(frame: 0)
}
Swift 5.7 (SE-0352) made the any keyword mandatory for existentials — previously you could write Renderable as a
type and the compiler would silently treat it as any Renderable. The explicit any syntax is intentional: it makes
the cost visible at the call site. Every any is a reminder that you’re opting into dynamic dispatch and potential heap
allocation.
Advanced Usage
Existential any with Associated Types
Before Swift 5.7, protocols with associated types could not be used as existentials at all — you’d get the infamous “can only be used as a generic constraint” error. Swift 5.7 unlocked some existential uses via primary associated types, but with restrictions:
// Primary associated type syntax (Swift 5.7+)
protocol Studio<Film> {
associatedtype Film: Identifiable
func produce() -> Film
}
// With a primary associated type you can constrain the existential:
// `any Studio<PixarFilm>` — the Film type is pinned to PixarFilm
func scheduleProduction(studio: any Studio<PixarFilm>) {
let film = studio.produce()
print("Scheduling render for film id: \(film.id)")
}
Warning: Even with primary associated types, calling a method on
any Protocolthat returnsSelfor takes a generic parameter still produces a compiler error. The existential container cannot satisfy those requirements without knowing the concrete type.
Reverse Opaque Types (Swift 5.7+)
some can also appear in parameter position, which is syntactic sugar for a generic parameter:
// These two signatures are equivalent:
func scheduleRender(using renderer: some Renderable) { ... }
func scheduleRender<R: Renderable>(using renderer: R) { ... }
This is useful when you want to express “I accept anything Renderable” without cluttering the signature with angle brackets. Under the hood the compiler generates the same specialised code either way.
Unwrapping an Existential Back to Its Concrete Type
When you hold an any Renderable and need concrete-type behaviour, use a conditional cast:
func upgradeIfPossible(renderer: any Renderable) -> any Renderable {
if let film = renderer as? PixarFilm {
// Now we have the concrete PixarFilm — static dispatch resumes
return PixarFilm(title: film.title + " (4K HDR)", resolution: film.resolution)
}
return renderer
}
Performance Considerations
The performance difference between some and any is real and measurable in hot paths.
An existential container has a fixed three-word inline buffer (24 bytes on 64-bit platforms). Values that fit inside the buffer are stored inline; values larger than 24 bytes are heap-allocated. Every method call goes through the protocol witness table — that’s one indirection for the function pointer plus one for the value — preventing inlining and the compiler optimisations that follow from it.
| Dispatch mechanism | Value storage | Inlining possible | Compiler specialisation |
|---|---|---|---|
some Protocol (opaque) | Stack (concrete type) | Yes | Yes — full specialisation |
any Protocol (existential) | Inline buffer or heap | No | No |
| Concrete type | Stack | Yes | Yes |
Generic <T: Protocol> | Stack (per specialisation) | Yes | Yes — per concrete type |
For a rendering loop processing thousands of frames, the difference between some and any can be the difference
between a tight inlined loop and thousands of witness-table lookups per second.
Apple Docs: The WWDC 2022 session “Embrace Swift Generics” (Session 110352) includes an excellent visual explanation of the existential box and the overhead it introduces. Worth watching alongside “Design Protocol Interfaces in Swift” (Session 110353).
Apple Docs:
Opaque and Boxed Protocol Types— The Swift Programming Language
For large structs — anything over 24 bytes — any Protocol causes a heap allocation each time a new existential box is
created. In a tight loop this adds up to measurable allocator pressure. You can verify this in Instruments using the
Allocations instrument: filter by “Existential” to see protocol box allocations grouped by call site.
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| Single concrete return type, caller doesn’t need to know it | some Protocol — maximum compiler info |
Heterogeneous collection ([any Renderable]) | any Protocol — type erasure by definition |
| Protocol has associated types and you’re storing instances | any Protocol with primary associated type constraint; otherwise a generic type parameter |
| Generic algorithm over many conforming types | Concrete generic <T: Protocol> — best perf, no boxing |
| Public API, reserve right to change the concrete type | some Protocol — opaque return is ABI-stable |
| Hot path (rendering, layout, physics) | Concrete type or some — avoid the existential box |
| Dependency injection / mocking in tests | any Protocol parameter — most flexibility to swap impls |
Summary
some Protocolis an opaque type: the compiler knows the single concrete type behind the abstraction; callers don’t. It enables static dispatch, inlining, and compiler specialisation.any Protocolis an existential type: a runtime box that can hold any conforming type. It enables heterogeneous collections and conditional return types, but introduces dynamic dispatch and potential heap allocation.- The mandatory
anykeyword (SE-0352) makes the cost of existential boxing visible — treat everyanyas an intentional trade-off, not a default. - For hot paths, prefer
some, concrete generics, or concrete types overany. Measure with Instruments before and after if you’re unsure. - Associated types restrict existential usage significantly; prefer opaque or generic parameters when working with
protocols like
CollectionorIdentifiable.
Once you’re comfortable with opaque and existential types, the next logical step is understanding the feature that makes many protocol-with-associated-type patterns impossible to box as existentials: Associated Types in Swift Protocols.