top of page

Swift Study Notes

💡 Remember This

Boxed protocol types store a concrete value inside a container while exposing only the protocol's interface.

Existential types are Swift's name for boxed protocol values whose concrete type has been erased from the surrounding code.

Opaque types hide a concrete type from the caller while still preserving that exact concrete type throughout the program.

What Are Existential Types?

An existential type allows Swift to store a value without exposing its exact concrete type.

The compiler still knows that the value conforms to a particular protocol, so code can use the properties and methods required by that protocol.

In modern Swift, an existential type is written using the any keyword.

Start With a Protocol

Consider a protocol named Animal.

protocol Animal {
    var name: String { get }
    func makeSound() -> String
}

Different concrete types can conform to the same protocol.

struct Dog: Animal {
    let name: String

    func makeSound() -> String {
        "Woof"
    }
}

struct Cat: Animal {
    let name: String

    func makeSound() -> String {
        "Meow"
    }
}

Dog and Cat are concrete types.

The compiler knows the exact stored properties, memory layout, and implementation used by each type.

Creating an Existential Value

A value whose type is any Animal can contain any concrete value that conforms to Animal.

let animal: any Animal = Dog(name: "Milo")

The concrete value stored inside animal is a Dog.

However, the static type of the constant is any Animal rather than Dog.

Code using the constant knows that the value has a name and can make a sound because those requirements are declared by the Animal protocol.

print(animal.name)
print(animal.makeSound())

Why Is It Called an Existential Type?

The name can be understood as the following statement:

There exists a concrete type that conforms to Animal, and a value of that type is stored here.

The concrete type still exists at runtime.

Swift has simply hidden that type behind the protocol interface.

The value has not lost its concrete type.

Code using the existential no longer depends on knowing what that concrete type is.

The any Keyword

Older Swift code often used a protocol name directly as a type.

let animal: Animal = Dog(name: "Milo")

Modern Swift uses the any keyword to make the existential behaviour explicit.

let animal: any Animal = Dog(name: "Milo")

The word any communicates that the stored value may be any concrete type that conforms to Animal.

This makes it easier to distinguish an existential value from other uses of protocols in Swift.

Storing Different Concrete Types

Existential types are useful when several different concrete types need to be treated uniformly.

let animals: [any Animal] = [
    Dog(name: "Milo"),
    Cat(name: "Luna"),
    Dog(name: "Ralph")
]

Every element in the array has the static type any Animal.

The values stored inside the existential containers can still have different concrete types.

for animal in animals {
    print("\(animal.name): \(animal.makeSound())")
}

This allows unrelated concrete types to be stored in the same collection as long as they conform to the same protocol.

An Existential Is a Container

An existential can be thought of as a container that stores a concrete value together with the information Swift needs to use that value through a protocol.

Conceptually, Swift needs to know:

  • Where the underlying value is stored.
  • What the concrete type of the value is.
  • How that concrete type implements the protocol requirements.

Swift uses runtime type information and a protocol witness table to call the correct implementation of each requirement.

For example, calling makeSound() on an existential containing a Dog must call Dog.makeSound().

The same call on an existential containing a Cat must call Cat.makeSound().

Important

An existential does not mean that the value has no concrete type.

It means that the concrete type has been erased from the interface used by the surrounding code.

Existential Types and Concrete Types

A concrete value preserves all information about its exact type.

let dog = Dog(name: "Milo")

The compiler knows that dog is a Dog.

An existential value hides that concrete type.

let animal: any Animal = dog

The compiler now treats animal as an existential container whose stored value conforms to Animal.

Only the interface made available through Animal can be used directly.

Existential Types and Generics

Existential types and generic constraints both work with protocols, but they preserve different information.

func describe(_ animal: any Animal) {
    print(animal.makeSound())
}

The parameter above is an existential value.

Its concrete type may vary at runtime.

func describe<A: Animal>(_ animal: A) {
    print(animal.makeSound())
}

The generic version preserves the concrete type as A.

When the function receives a Dog, A is Dog.

When the function receives a Cat, A is Cat.

A useful way to remember the distinction is:

  • any Animal stores a value after erasing its concrete type.
  • A: Animal preserves a particular concrete type through a generic relationship.

Use an existential when the concrete type genuinely needs to vary at runtime.

Use a generic when preserving the concrete type is important to the relationship expressed by the code.

Existential Types and Opaque Types

The some keyword creates an opaque type.

An opaque type hides a concrete type from the caller while still preserving that type internally.

func makeAnimal() -> some Animal {
    Dog(name: "Milo")
}

The caller does not know that the returned value is a Dog.

However, the compiler knows that the function always returns one fixed concrete type.

An existential return type allows the concrete type to vary.

func makeAnimal(prefersCats: Bool) -> any Animal {
    if prefersCats {
        return Cat(name: "Luna")
    } else {
        return Dog(name: "Milo")
    }
}

The function can return a Cat during one call and a Dog during another.

A useful summary is:

  • some Animal hides one fixed concrete type.
  • any Animal can contain different conforming concrete types.

Why Existential Types Can Have a Cost

Existential types provide runtime flexibility, but that flexibility can require additional work.

Swift may need to:

  • Store the value inside an existential container.
  • Use indirect storage when the value does not fit inside the container.
  • Look up protocol implementations through a witness table.
  • Perform dynamic dispatch instead of calling a statically known implementation.

This does not mean existential types should be avoided.

The cost is often entirely reasonable when the design requires different concrete types to be treated uniformly.

Important

Do not replace an existential with a generic only because generics may allow more optimisation.

Choose the type that correctly represents the behaviour your API requires.

When Should You Use an Existential Type?

Use any Protocol when the identity of the concrete type is not important to the surrounding code and the stored type may need to vary.

Common examples include:

  • A collection containing different conforming types.
  • A stored property that can hold different implementations.
  • A dependency that may be replaced at runtime.
  • An API boundary that intentionally hides implementation details.
  • A plugin or delegate system that accepts unrelated conforming types.

Do not introduce an existential when the concrete type never needs to vary and preserving its identity would make the API clearer.

A Practical Comparison

// Existential
// The concrete type may vary at runtime.

var currentAnimal: any Animal = Dog(name: "Milo")
currentAnimal = Cat(name: "Luna")

// Opaque type
// The implementation returns one hidden concrete type.

func defaultAnimal() -> some Animal {
    Dog(name: "Milo")
}

// Generic
// The caller supplies a concrete type that is preserved as A.

func feed<A: Animal>(_ animal: A) {
    print("Feeding \(animal.name)")
}

Common Interview Questions

What Is an Existential Type?

An existential type is a type-erased container that can hold a value of any concrete type conforming to a specified protocol.

It is written using any followed by the protocol name.

Why Does Swift Use the any Keyword?

The any keyword makes type erasure explicit.

It shows that the value is being stored inside an existential container and that its concrete type may vary.

What Is the Difference Between any and some?

any Protocol creates an existential container whose underlying concrete type may vary at runtime.

some Protocol hides one fixed concrete type while preserving its identity internally.

What Is the Difference Between an Existential and a Generic?

An existential erases the concrete type so that different conforming values can be treated uniformly.

A generic parameter preserves a particular concrete type and can express relationships involving that type.

Do Existential Types Have a Performance Cost?

They can require boxing, indirect storage, dynamic dispatch, and runtime metadata.

These costs are the result of the runtime flexibility existential types provide.

Final Revision Note

An existential type trades concrete type information for flexibility.

Swift still guarantees that the stored value conforms to the required protocol.

The surrounding code can use the protocol interface without knowing the exact type stored inside the container.

Remember

any Animal means that some concrete Animal type exists inside the container, but the surrounding code does not depend on knowing which concrete type it is.

any Animal   // A boxed value whose concrete type may vary.
some Animal  // One hidden but fixed concrete type.
A: Animal    // A generic concrete type preserved as A.
bottom of page