Opaque and Boxed Protocol Types

When you want to hide the concrete type behind a protocol, Swift gives you two tools that look similar but behave very differently: opaque types, written some Protocol, and boxed protocol types (also called existentials), written any Protocol. An opaque type hides one specific type while preserving its identity; a boxed type can hold any conforming value but loses that identity. Choosing correctly matters for both type safety and performance.

The problem each solves

Both features let an API talk about "a value conforming to a protocol" without naming the exact type. But they answer two different questions:

The motivating problem is the same one associated types create: a protocol with associated types or Self requirements (like Equatable) historically couldn't be used as an ordinary type. Opaque types solve that for the return-a-known-type case; boxed types solve it for the hold-many-types case.

Opaque types with some

An opaque return type is declared by writing some before the protocol. The function returns one specific concrete type that satisfies the protocol, but that type is hidden from the caller. Crucially, it must be the same type on every execution path.

protocol Shape {
    func draw() -> String
}

struct Square: Shape {
    var size: Int
    func draw() -> String {
        String(repeating: "*", count: size)
    }
}

// Returns a concrete type the caller never has to name
func makeSquare(_ n: Int) -> some Shape {
    Square(size: n)
}

let s = makeSquare(4)
print(s.draw())   // → ****

The caller knows s is "some Shape," can call its protocol methods, and can rely on type identity — but cannot see that it's a Square. You're free to change the underlying type later without breaking callers, the same way a private property hides storage.

Watch out: a some function must return one single concrete type. Returning a Square on one branch and a different shape on another is a compile error — the opaque type is fixed, not variable.

Why some preserves type identity

Because an opaque type stands for one specific type, the compiler can keep track of relationships that any erases. This is why SwiftUI's body is some View: it returns one precise (often enormous, generated) view type, and the framework relies on knowing that type statically.

func doubled() -> some Equatable {
    [1, 2, 3]          // concrete type is [Int]
}

let a = doubled()
let b = doubled()
print(a == b)   // → true — both are the same hidden type, so == is allowed

Comparing a == b works because the compiler knows both are the identical underlying type. With a boxed any Equatable this comparison wouldn't even compile, because == needs both sides to be statically the same type.

Boxed protocol types with any

A boxed protocol type, written any Protocol, is an existential: a container that can hold a value of any conforming type, with the concrete type decided at runtime. This is what lets you mix different types in one variable or collection.

struct Circle: Shape {
    func draw() -> String { "O" }
}

// A heterogeneous array — each element may be a different concrete type
let shapes: [any Shape] = [Square(size: 2), Circle(), Square(size: 5)]

for shape in shapes {
    print(shape.draw())
}
// → **
// → O
// → *****

The box stores the value plus a pointer to its type's protocol witness table, so the right draw() is dispatched dynamically at runtime. You can reassign a var x: any Shape from a Square to a Circle — the concrete type genuinely varies. Since Swift 6, using a protocol as a type requires the explicit any spelling.

Differences from ordinary protocol types

"Boxed protocol type" is the modern name for using a protocol directly as a type — any Shape is the explicit form of what older Swift wrote as just Shape. The any keyword exists to make the cost and semantics visible at the use site rather than hidden. The real contrast worth internalizing is opaque versus boxed:

some Protocol (opaque)any Protocol (boxed)
Underlying typeOne fixed concrete typeMay differ per value, at runtime
Compiler knows the type?Yes (just hidden from callers)No — erased into a box
Type identity preserved?YesNo
Works with Self/associated-type protocols?YesLimited (some operations unavailable)
Heterogeneous collections?NoYes
Runtime costNone (static dispatch, specialized)Boxing + dynamic dispatch

A key consequence: protocols with associated types or Self requirements work freely as some, but as any some of their members become inaccessible because the box can't supply a single concrete associated type. The compiler "opens" an existential only in limited situations.

Note: some and any both differ from generics. A generic parameter <T: Shape> lets the caller choose the type; some Shape as a return type lets the function choose and hide it. In parameter position, some Shape is in fact shorthand for an unnamed generic parameter.

some in parameter position

Writing some on a parameter is sugar for a generic type parameter: the caller picks one concrete type, and the function works with it statically and efficiently. The two declarations below are equivalent.

func render(_ shape: some Shape) {
    print(shape.draw())
}

// is shorthand for:
func render<S: Shape>(_ shape: S) {
    print(shape.draw())
}

render(Circle())   // → O

When to use some versus any

A practical rule of thumb:

Tip: if your code compiles with both, prefer some — it's faster and more capable. Switch to any the moment the compiler tells you the type must vary at runtime (for example, "use of protocol as a type must be written any" when building a heterogeneous collection).

You can also convert between them: an any Shape can be passed where some Shape is expected via "opening" the existential, and a some Shape value can be stored into an any Shape box. Going from some to any is always safe; the reverse may require the box to be opened by the compiler at a call boundary.

Wrapping up

Opaque types (some) and boxed protocol types (any) both hide a concrete type behind a protocol, but they sit at opposite ends of a trade-off. some commits to one fixed underlying type, keeps its identity, supports associated-type protocols, and compiles to efficient static code — ideal for return values, parameters, and stable APIs. any erases the type into a box so you can hold many different conformers at runtime, at the cost of dynamic dispatch and some lost capabilities. Default to some, and use any deliberately when runtime variability is the whole point.