Swift some vs any - Understanding Opaque Types and Existential Types

March 14, 2026

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:

  • some
  • any

These keywords correspond to two different concepts in Swift’s type system:

  • Opaque Types (some)
  • Existential Types (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:

  • What opaque and existential types are
  • Why Swift introduced some and any
  • How they differ conceptually
  • Their performance characteristics
  • How Swift implements them internally
  • Best practices for using them in real-world code

The Problem: Protocols as Types

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 mode

This ambiguity created a need for two different abstraction mechanisms:

  1. A way to return a specific concrete type while hiding its identity
  2. A way to store any value that conforms to a protocol

Swift addresses these needs using:

  • some → opaque types
  • any → existential types

Opaque Types (some)

What Is an Opaque Type?

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.

Example

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)
  • The caller only knows that the return type conforms to Shape
  • The underlying type is hidden

Usage:

let shape = makeShape()
print(shape.area())

Even though the caller sees Shape, the compiler still knows the returned type is Circle.

Why Opaque Types Matter

Opaque types allow you to hide implementation details while preserving static type information.

This has two major advantages.

API Stability

If you expose a concrete type:

func makeShape() -> Circle

Your API becomes tightly coupled to the implementation.

Instead you can write:

func makeShape() -> some Shape

This allows you to change the underlying type later without breaking callers.

Performance

Because the compiler still knows the exact type, Swift can perform powerful optimizations:

  • Static dispatch
  • Function inlining
  • Compile-time specialization

As a result, opaque types have almost zero runtime overhead.

Important Rule of Opaque Types

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 types

Why?

Because some Shape means:

There exists one specific concrete type, but its identity is hidden.

Allowing multiple types would violate that guarantee.

Real-World Example: SwiftUI

SwiftUI relies heavily on opaque types.

You will frequently see this declaration:

var body: some View

Example:

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:

  • avoid expensive type erasure
  • maintain compile-time safety
  • generate highly optimized view hierarchies

Existential Types (any)

What Is an Existential Type?

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.

Example

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:

  • a Dog
  • a Cat

The concrete type is determined at runtime.

How Existential Types Work Internally

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 Table
Stored Value

The 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.

Type Metadata

Swift stores metadata describing the underlying type.

This allows the runtime to know things like:

  • the size of the value
  • alignment requirements
  • how to copy or destroy it
Witness Table

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:

  • Look up the witness table inside the existential container
  • Find the function pointer for speak
  • Call the correct implementation

This is known as dynamic dispatch via witness tables.


Performance Comparison: Generics vs some vs any

Swift supports three different abstraction mechanisms:

  • Generics
  • Opaque types (some)
  • Existential types (any)

Although they look similar in code, they behave differently at the compiler level.

Generics

Example:

func process<T: Shape>(_ shape: T) {
    print(shape.area())
}

The compiler specializes this function for each concrete type.

Advantages:

  • Full compile-time knowledge
  • Static dispatch
  • Maximum performance

Opaque Types

Example:

func makeShape() -> some Shape {
    Circle(radius: 10)
}

Opaque types behave similarly to generics internally because the compiler still knows the underlying type.

Advantages:

  • Encapsulation of implementation
  • Static dispatch
  • Zero runtime overhead

Existential Types

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:

  • existential containers
  • witness table lookups
  • dynamic dispatch

Conceptual Performance Comparison

Abstraction Dispatch Runtime Cost Typical Use
Generics Static None Algorithms
some Static None Returning protocol types
any Dynamic Small overhead Heterogeneous storage

When Should You Use Each?

Prefer Generics

When writing reusable algorithms.

func process<T: Shape>(_ shape: T)

Prefer some

When returning protocol types while hiding implementation details.

func makeView() -> some View

Use any When Necessary

When storing heterogeneous values or working with runtime polymorphism.

var animals: [any Animal] = [
    Dog(),
    Cat()
]

A Simple Mental Model

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.


Conclusion

Swift introduced some and any to make protocol abstraction more explicit and more efficient.

They represent two different design trade-offs:

  • Opaque types (some) hide a concrete type while preserving compile-time optimizations.
  • Existential types (any) allow dynamic polymorphism by storing values of multiple conforming types.

As a practical guideline:

  • Prefer generics for reusable algorithms.
  • Prefer some for return types.
  • Use 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 ☕️