`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

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 Protocol that returns Self or 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 mechanismValue storageInlining possibleCompiler specialisation
some Protocol (opaque)Stack (concrete type)YesYes — full specialisation
any Protocol (existential)Inline buffer or heapNoNo
Concrete typeStackYesYes
Generic <T: Protocol>Stack (per specialisation)YesYes — 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)

ScenarioRecommendation
Single concrete return type, caller doesn’t need to know itsome Protocol — maximum compiler info
Heterogeneous collection ([any Renderable])any Protocol — type erasure by definition
Protocol has associated types and you’re storing instancesany Protocol with primary associated type constraint; otherwise a generic type parameter
Generic algorithm over many conforming typesConcrete generic <T: Protocol> — best perf, no boxing
Public API, reserve right to change the concrete typesome Protocol — opaque return is ABI-stable
Hot path (rendering, layout, physics)Concrete type or some — avoid the existential box
Dependency injection / mocking in testsany Protocol parameter — most flexibility to swap impls

Summary

  • some Protocol is 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 Protocol is 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 any keyword (SE-0352) makes the cost of existential boxing visible — treat every any as an intentional trade-off, not a default.
  • For hot paths, prefer some, concrete generics, or concrete types over any. 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 Collection or Identifiable.

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.