Skip to content

Opaque Types in Swift: Understanding the `some` Keyword

Posted on:February 15, 2025
· 6 min read

If you’ve written SwiftUI, you’ve typed some View more times than you can count. It’s easy to treat it as boilerplate and move on. But some and any aren’t interchangeable — they solve different problems, and picking the wrong one means lost performance, lost type info, or code that simply won’t compile.

What’s wrong with just returning a protocol?

Take a protocol and two structs:

protocol Shape {
    func area() -> Double
}

struct Circle: Shape {
    var radius: Double
    func area() -> Double { .pi * radius * radius }
}

struct Rectangle: Shape {
    var width, height: Double
    func area() -> Double { width * height }
}

Returning Shape directly uses an existential — the compiler wraps the value in a box and dispatches method calls at runtime through a witness table. It works, but three things break:

func makeShape(kind: String) -> Shape {
    kind == "circle" ? Circle(radius: 5) : Rectangle(width: 3, height: 4)
}

Method calls become runtime lookups. Large values hit the heap. And protocols with associated types stop working entirely:

protocol Container {
    associatedtype Element
    var count: Int { get }
    func append(_: Element)
    func element(at: Int) -> Element
}

// ERROR: Protocol 'Container' can only be used as a generic constraint
func makeContainer() -> Container { ... }

The compiler can’t figure out Element at the call site because the existential erased it. Dead end.

some keeps the type around

This is where some comes in. It says: “I return one concrete type, it conforms to this protocol, and you don’t get to know which one.” The compiler knows the real type — it just hides the name from callers.

struct ArrayContainer<T>: Container {
    typealias Element = T
    var items: [T] = []
    var count: Int { items.count }
    func append(_ item: T) { items.append(item) }
    func element(at i: Int) -> T { items[i] }
}

func makeContainer() -> some Container {
    ArrayContainer<String>(items: ["hello", "world"])
}

let c = makeContainer()
c.append("swift")          // Element resolved as String at compile time
let first = c.element(at: 0) // String, not some existential

The compiler saw ArrayContainer<String>, filled in Element = String, and generated direct calls. No boxing, no runtime dispatch. This is why some View works in SwiftUI — View has an associated type Body, and the compiler traces the actual type through nested views like VStack<TupleView<(Text, Button<Text>)>> so you never have to.

some vs any vs generics

block-beta columns 3 block:Existential:1 columns 1 b["any Shape"] c["Heap box
Dynamic dispatch
No associated types"] end block:Opaque:1 columns 1 e["some Shape"] f["Stack inline
Static dispatch
Associated types work
Single concrete type"] end block:Generic:1 columns 1 h["T: Shape"] i["Static dispatch
Caller picks T
Caller sees T"] end

The key difference from generics: with <T: Shape>, the caller picks the type. With some Shape, the function picks. The caller only knows it conforms to Shape. I can swap the underlying type in a future update and nobody’s code breaks — the type identity is scoped to my function.

Why it’s faster

// Existential (any Shape):
// Stack layout: (valueBuffer: 24 bytes, witnessTable: pointer)
// Call:         buffer.witnessTable.area(buffer.valueBuffer)
//               Two indirections, can't inline

// Opaque (some Shape):
// Stack layout: Concrete type sits inline (Circle = 8 bytes)
// Call:         Circle.area() — direct and inlinable

In tight loops the difference matters — some can be 2–5× faster because the call is static and the compiler can inline it. For UI code the performance gap is negligible, but the associated type support alone makes some necessary when you need it.

some in parameters (Swift 5.7+)

A nice syntactic shortcut:

// Before 5.7:
func draw<T: Shape>(_ shapes: [T]) { ... }

// After:
func draw(_ shapes: [some Shape]) { ... }

Every element in the array shares the same concrete type. If I need a mix of Circles and Rectangles, I use any:

func drawMixed(_ shapes: [any Shape]) { ... }  // mixed types
func drawAllSame(_ shapes: [some Shape]) { ... } // all same type

A gotcha: type identity is per-declaration

Two functions returning some Shape produce different opaque types, even if both return Circle:

func makeRedCircle() -> some Shape { Circle(radius: 5) }
func makeBlueCircle() -> some Shape { Circle(radius: 5) }

let a = makeRedCircle()
let b = makeBlueCircle()
// a and b are different types — can't assign one to the other

The opaque type is tied to the function that produced it. This trips people up, but it’s by design — library authors can change the underlying type without breaking anyone’s code. Within a single scope, though, the compiler connects the dots:

var x = makeRedCircle()
x = Circle(radius: 10)            // fine, compiler knows x is Circle
x = Rectangle(width: 1, height: 2) // nope

How the compiler processes some

Under the hood, some P triggers a few steps:

  1. The compiler checks every return and verifies they all produce the same concrete type.
  2. It plugs that type into every associated type of P.
  3. It emits direct calls instead of witness table indirection.
  4. With optimizations on, it inlines those calls.

The opaque type is essentially a type alias the caller can’t name. It’s monomorphized like a C++ template, but the caller never sees the definition.

When to use what

Use some when the protocol has associated types (you often have no choice), when you return one concrete type and want static dispatch, or when you’re hiding implementation details in a public API.

Use any when you need a mixed collection like [any Shape], when the concrete type varies at runtime, or when building type-erased wrappers (AnyPublisher, AnyView).

If neither fits, a plain generic <T: P> is probably what you want.


Quick reference:

  • some: one type, compiler knows it, static dispatch, associated types work — caller can’t name it.
  • any: multiple types, runtime dispatch, no associated types — caller loses type info.
  • <T: P>: one type, static dispatch, associated types work — caller picks and sees T.