Chapters

Hide chapters

Swift Apprentice: Fundamentals

Second Edition · iOS 18 · Swift 6 · Xcode 16.2

Section III: Building Your Own Types

Section 3: 9 chapters
Show chapters Hide chapters

18. Generics
Written by Alexis Gallagher

The truth is, you already know about generics. Every time you use a Swift array, you’re using generics. This observation might give the impression that generics are about collections, but that impression is incorrect. In this chapter, you’ll learn the fundamentals of generics, giving you a solid foundation for understanding how to write generic code. Finally, you’ll loop back to look at generic types in the Swift standard library — arrays, dictionaries and optionals — using this new perspective.

Introducing Generics

To get started, consider how you might model pets and their keepers. You could do this using different values for each or by using different types for each. You’ll see that by using types, instead of values, the Swift type checker can reason about your code at compile time. Not only do you need to do less at runtime, but you can catch problems that would have gone under the radar had you just used values. Your code also runs faster.

Values Defined by Other Values

Suppose you’re running a pet shop that sells only dogs and cats and want to use Swift to model that business. To start, you define a type, PetKind, that can hold two possible values corresponding to the two kinds of pets that you sell:

enum PetKind {
  case cat
  case dog
}

So far, so good. Now you want to model the animals and the employees, the pet keepers who look after them. Your employees are highly specialized. Some keepers only look after cats, and others only dogs.

So you define a KeeperKind type as follows:

struct KeeperKind {
  var keeperOf: PetKind
}

Then you can initialize a catKeeper and dogKeeper in the following way:

let catKeeper = KeeperKind(keeperOf: .cat)
let dogKeeper = KeeperKind(keeperOf: .dog)

There are two points to note about how you’re modeling your shop.

First, you’re representing the different kinds of pets and keepers by varying the values of types. There’s only one type for pet kinds — PetKind — and one type for keeper kinds — KeeperKind. Different kinds of pets are represented only by distinct values of the PetKind type, just as different kinds of keepers are represented by distinct values of the KeeperKind type.

Second, one range of possible values determines another range of possible values. Specifically, the range of possible KeeperKind values mirrors the range of possible PetKind values.

If your store started selling birds, you’d simply add a .bird member to the PetKind enumeration, and you’d immediately be able to initialize a value describing a bird keeper, KeeperKind(keeperOf: .bird). And if you started selling a hundred different kinds of pets, you’d immediately be able to represent a hundred different kinds of keepers.

In contrast, you could have defined a second unrelated enumeration instead of KeeperKind:

enum EnumKeeperKind {
  case catKeeper
  case dogKeeper
}

In this case, only your diligence in always updating one type to mirror the other would enforce this relationship. If you added PetKind.snake but forgot to add EnumKeeperKind.snakeKeeper, things would get out of whack.

But with KeeperKind, you explicitly established the relationship via a property of type PetKind. Every possible PetKind value implies a corresponding KeeperKind value. Or you could say the set of potential PetKind values defines the set of possible KeeperKind values.

To summarize, you can depict the relationship like so:

.cat KeeperKind(keeperOf:.cat) PetKind values KeeperKind values .dog .etc KeeperKind(keeperOf:.dog) etc.

Types Defined by Other Types

The model above fundamentally works by varying the values of types. Now consider another way to model the pet-to-keeper system — by varying the types themselves.

Suppose that instead of defining a single type, PetKind, representing all kinds of pets, you chose to define a distinct type for every kind of pet you sell.

Distinct types are a plausible choice if you’re working in an object-oriented style, where you model the pets’ behaviors with different methods for each pet. Then you’d have the following:

class Cat {}
class Dog {}

Now how do you represent the corresponding kinds of keepers? You could simply write the following:

class KeeperOfCats {}
class KeeperOfDogs {}

But that’s no good. This approach has exactly the same problem as manually defining a parallel enum of KeeperKind values — it relies on you to enforce the required domain relationship of one kind of keeper for every kind of pet.

What you’d like is a way to declare a relationship just like the one you established for values.

You’d like to declare that every possible pet type implies the existence of a corresponding keeper type, a correspondence that you’d depict like so:

Cat Keeper (of Cat...) Pet types Keeper types Dog etc. Keeper (of Dog...) etc.

You’d like to establish that, for every possible pet type, there’s a corresponding Keeper type. But you don’t want to do this manually. You want a way to automatically define a set of new types for all the keepers.

Automatic type generation, it turns out, is the problem generics solve!

Anatomy of Generic Types

Generics provide a mechanism for using one set of types to define a new set of types.

In your example, you can define a generic type for keepers, like so:

class Keeper<Animal> {}

This definition immediately defines all the corresponding keeper types, as desired:

Cat Keeper<Cat> Pet types Keeper types Dog Keeper<Dog>

You can verify these types are real by creating values of them, specifying the entire type in the initializer:

var aCatKeeper = Keeper<Cat>()

What’s going on here? First, Keeper is the name of a generic type.

But you might say that a generic type isn’t a type at all. It’s more like a recipe for making real types or concrete types. One sign of this is the error you get if you try to instantiate it in isolation:

The compiler complains here that “generic parameter ‘Animal’ could not be inferred” because it doesn’t know what kind of keeper you want. That Animal in angle brackets is the type parameter that specifies the type for the kind of animal you’re keeping.

Once you provide the required type parameter, as in Keeper<Cat>, the generic Keeper becomes a new concrete type. Keeper<Cat> is different from Keeper<Dog>, even though they started from the same generic type. These resulting concrete types are called specializations of the generic type.

To summarize the mechanics, to define a generic type like Keeper<Animal>, you only need to choose the name of the generic type and the type parameter. The name of the type parameter, also called a placeholder, should clarify the relationship between the type parameter and the generic type. You’ll encounter names like T (short for Type) from time to time but avoid these names when the placeholder has a well-defined role, such as Animal.

In one stroke, the generic type Keeper<Animal> defines a family of new types. Those are all the specializations of Keeper<Animal> implied by all possible concrete types that could substitute for the type parameter Animal.

Notice that the type Keeper doesn’t currently store anything or even use the type parameter Animal in any way. Essentially, generics are a way to systematically define sets of types.

Using Type Parameters

Usually, though, you’ll want to do something with type parameters.

Suppose you want to keep better track of individuals. First, you enrich your type definitions to include identifiers, such as names. Adding it lets every value represent the identity of an individual animal or keeper:

class Cat {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Dog {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Keeper<Animal> {
  var name: String

  init(name: String) {
    self.name = name
  }
}

You also want to track which keeper looks after which animals. Suppose every keeper is responsible for one animal in the morning and another in the afternoon. You can express this by adding properties for the morning and afternoon animals. But what type should those properties have?

If a particular keeper only manages dogs, then the properties must only hold dogs. And if cats, then cats. In general, if it’s a keeper of Animal, then the morning and afternoon animal properties should be of type Animal.

To express this, you merely need to use the type parameter that previously only distinguished the nature of your keeper types:

class Keeper<Animal> {
  var name: String
  var morningCare: Animal
  var afternoonCare: Animal

  init(name: String, morningCare: Animal, afternoonCare: Animal) {
    self.name = name
    self.morningCare = morningCare
    self.afternoonCare = afternoonCare
  }
}

Using Animal in the body of the generic type definition above, you can express that the morning and afternoon animals must be the kind of animal the keeper knows best. Just as function parameters become constants to use within the body of your function definition, you can use type parameters such as Animal throughout your type definitions. You can use the type parameter anywhere in the definition of Keeper<Animal> for stored properties, computed properties, method signatures and nested types.

Now when you instantiate a Keeper, Swift will make sure, at compile-time, that the morning and afternoon types are the same:

let jason = Keeper(name: "Jason",
                   morningCare: Cat(name: "Whiskers"),
                   afternoonCare: Cat(name: "Sleepy"))

Here, the keeper, Jason, manages the cat Whiskers in the morning and the cat Sleepy in the afternoon. The type of jason is Keeper<Cat>. Note that you did not have to specify a value for the type parameter.

Because you used instances of Cat as the values for morningCare and afternoonCare, Swift knows the type of jason should be Keeper<Cat>.

Generic Function Parameters

Functions can be generic as well. A function’s type parameter list comes after the function name. You can then use the generic parameters in the rest of the definition.

This function takes two arguments and swaps their order:

func swapped<T, U>(_ x: T, _ y: U) -> (U, T) {
  (y, x)
}

swapped(33, "Jay")  // returns ("Jay", 33)

A generic function definition demonstrates a confusing aspect of the syntax: having both type parameters and function parameters. You have both the generic parameter list of type parameters <T, U> and the list of function parameters (_ x: T, _ y: U).

Think of the type parameters as arguments for the compiler, which it uses to define one possible function. Just as your generic Keeper type meant the compiler could make dog keepers, cat keepers and any other kind of keeper, the compiler can now make a non-generic specialized swapped function for any two types for you to use.

Mini-Exercises

  • Try instantiating another Keeper, but this time for dogs.

  • What would happen if you tried to instantiate a Keeper with a dog in the morning and a cat in the afternoon?

  • What happens if you try instantiating a Keeper but for strings?

Type Constrained Generics

In your definition of Keeper, the identifier Animal serves as a type parameter, a named placeholder for some concrete type you supply later.

This is much like the parameter name cat in an ordinary function like func feed(cat: Cat) { /* open can, etc... */ }. But when calling this function, you can’t simply pass any argument. You can only pass values of type Cat.

However, at present, you could offer any type for Animal, even something nonsensically unlike an animal, like a String or Int.

Being able to use anything is no good. You’d like a mechanism more closely analogous to a function parameter. You want a feature that lets you restrict the types allowed in the type parameter. In Swift, you do this with various kinds of type constraints.

A simple kind of type constraint applies directly to a type parameter, and it looks like this:

class Keeper<Animal: Pet> {
   /* definition body as before */
}

Here, the constraint : Pet requires that the type assigned to Animal must be a subclass of Pet if Pet is a class or must implement the Pet protocol if Pet is a protocol.

For instance, to comply with the constraint established by the revised Keeper definition, you could redefine Cat and other animals to implement Pet, or you could retro-actively model conformance to the protocol by using an extension as you did in the previous Chapter:

protocol Pet {
  var name: String { get }  // all pets respond to a name
}
extension Cat: Pet {}
extension Dog: Pet {}

This code works because Cat and Dog already implement a name stored property. Now you can use this new protocol.

Suppose you want to implement a generic function that works with any type. You can start by writing this:

func callForDinner<Animal>(_ pet: Animal) {
   // What can you write here?
}

Here you have a generic type of Animal that could literally be anything. Because it can be anything, the compiler can’t make assumptions about what it is. That makes it very challenging to write the implementation. That’s where a protocol comes in. Add a Pet protocol constraint like so:

func callForDinner<Animal: Pet>(_ pet: Animal) {
   print("Here \(pet.name)-\(pet.name)! Dinner time!")
}

The generic type Animal conforms to Pet and can use the name property in the body to properly call the pet in for dinner. You can write the same function in a better way using the some keyword. It looks like this:

func callForDinner(_ pet: some Pet) {
  print("Here \(pet.name)-\(pet.name)! Dinner time!")
}

This generic function expresses the same thing as the previous version. This style is preferable because it’s more readable without angle brackets, and it more directly states the constraints.

There is a more full-featured way of expressing constraints that you’ll learn about next.

Conditional Conformance

In addition to simple type constraints, you can define more complex type constraints using a generic where clause. You can use a where clause in defining functions, types, member functions, protocols, and extensions. It can constrain type parameters and associated types, letting you define rich relationships on top of generic types.

To begin with, this is how you could use a where clause to implement the callForDinner() function. It looks like this:

func callForDinner<Animal>(_ pet: Animal) where Animal: Pet {
  print("Here \(pet.name)-\(pet.name)! Dinner time!")
}

Even though callForDinner(_ pet: some Pet) is the preferred style for this case, this shows how you can use the where clause to accomplish the same thing. The real power happens with more complex relationships.

Type constraints on extensions are instrumental. For example, suppose you want all Cat arrays to support the method meow(). You can use an extension to specify that when the array’s Element is a Cat, then the array provides meow():

extension Array where Element: Cat {
  func meow() {
    forEach { print("\($0.name) says meow!") }
  }
}

You can even specify that a type should conform to some protocol only if it meets certain constraints. Suppose that anything that can meow is a Meowable. You could write that every Array is Meowable if its elements are Meowable, as follows:

protocol Meowable {
  func meow()
}

extension Cat: Meowable {
  func meow() {
    print("\(self.name) says meow!")
  }
}

extension Array: Meowable where Element: Meowable {
  func meow() {
    forEach { $0.meow() }
  }
}

This code demonstrates conditional conformance, a subtle but powerful mechanism of composition.

Advanced Generic Parameters

Suppose you wish to write a function to find a lost animal. You start with an array of lost animals:

let lostPets: [any Pet] = [Cat(name: "Whiskers"), Dog(name: "Hachiko")]

Since all types of pets can become lost, you use the existential box type any Pet. Strictly speaking, since protocol Pet doesn’t contain associated types, you can drop the any keyword, and it will still compile, but it is better style to include it.

You can implement a non-generic find function like this:

/// Return a lost Cat.
func findLostCat(name: String, among lost: [any Pet]) -> Cat? {
  lost.lazy.compactMap {
    $0 as? Cat
  }.first {
    $0.name == name
  }
}

This method lazily iterates over the lost list looking for Cats and returning the first with a matching name. Now define one for dogs:

/// Return a lost Dog.
func findLostDog(name: String, among lost: [any Pet]) -> Dog? {
  lost.lazy.compactMap {
    $0 as? Dog
  }.first {
    $0.name == name
  }
}

You probably notice a lot of repetition in that code except for the type Cat and Dog. Every time there is a new Pet type such as a Goldfish, Chinchilla or Iguana, you need to write a new function. That could be better.

You could write something more “generic”:

func findLostPet(name: String, among lost: [any Pet]) -> (any Pet)? {
  lost.first { $0.name == name}
}

This code will find any kind of pet – in fact, any Pet, the existential boxed type which can contain any concrete type conforming to the Pet protocol. However, it does not use or report valuable type information. What if someone was looking for a Cat and it returned a Goldfish named “Whiskers”?

You can use a generic to capture that lost type information:

func findLost<Animal: Pet>(_ petType: Animal.Type, name: String, among lost: [any Pet]) -> (some Pet)? {
  lost.lazy.compactMap {
    $0 as? Animal
  }.first {
    $0.name == name
  }
}

You can call it this way:

findLost(Cat.self, name: "Whiskers", among:lostPets)
findLost(Dog.self, name: "Hachiko", among:lostPets)

The first parameter lets the compiler infer the generic type. It has the requirement that it must conform to the Pet protocol. findLost(_:name:among:) works for any Pet conforming Animal type you can dream up. The function body can use the inferred type to filter down to the correct type.

The return type is a concrete type, some Pet, which conceals the details of the specific type. This some Protocol return type is called an opaque return type and is used to hide complexity.

In this case, such hiding doesn’t provide any advantage, and you are better off returning the concrete type, which was constrained to the input type.

func findLost<Animal: Pet>(_ petType: Animal.Type, name: String, among lost: [any Pet]) -> Animal? {
  lost.lazy.compactMap {
    $0 as? Animal
  }.first {
    $0.name == name
  }
}

Now you have complete type information so can call any method that the concrete type implements.

findLost(Cat.self, name: "Whiskers", among:lostPets)?.meow()
// Whiskers says meow!

Because findLost(_:name:among:) returns a Cat type, you can call meow(). The same code used with Dog.self would not compile since Dog has no meow() method.

Arrays

While the original Keeper type illustrates that a generic type doesn’t need to store anything or use its type parameter, Array, one of the most common generic types, does both.

The need for generic arrays was part of the original motivation to invent generic types. Since many programs need homogeneous arrays, generic arrays make all that code safer. Once the compiler infers (or is told) the type of an array’s elements at one point in the code, it can spot any deviations at other points before the program runs.

You’ve been using Array all along, but only with syntactic sugar: [Element] instead of Array<Element>. Consider an array declared like so:

let animalAges: [Int] = [2,5,7,9]

This code is equivalent to the following:

let animalAges: Array<Int> = [2,5,7,9]

Array<Element> and [Element] are completely interchangeable. So you could even call an array’s default initializer by writing [Int]() instead of Array<Int>().

Since Swift arrays allow indexed access to a sequence of elements, they impose no requirements on their Element type. But this isn’t always the case.

Dictionaries

Swift generics allow multiple type parameters, each with unique constraints. A Dictionary is a straightforward example of this.

Dictionary has two type parameters in the comma-separated generic parameter list that falls between the angle brackets, as you can see in its declaration:

struct Dictionary<Key: Hashable, Value> // etc..

Key and Value represent the types of the dictionary’s keys and values. The type constraint Key: Hashable requires that any type serving as the dictionary’s key be hashable because the dictionary is a hash map and must hash its keys to enable fast lookup.

To instantiate types such as Dictionary with multiple type parameters, simply provide a comma-separated type argument list:

let intNames: Dictionary<Int, String> = [42: "forty-two"]

As with arrays, dictionaries get special treatment in Swift since they’re built-in and rather common. You’ve already seen the shorthand notation [Key: Value], and you can also use type inference:

let intNames2: [Int: String] = [42: "forty-two", 7: "seven"]
let intNames3 = [42: "forty-two", 7: "seven"]

Optionals

Finally, no discussion of generics would be complete without mentioning optionals. Optionals are enumerations, but they’re just another generic type, which you could have defined yourself.

Suppose you were writing an app that lets a user enter her birthdate in a form but doesn’t require it. You might find it handy to define an enum type as follows:

enum OptionalDate {
  case none
  case some(Date)
}

Similarly, if another form allowed but didn’t require the user to enter her last name, you might define the following type:

enum OptionalString {
  case none
  case some(String)
}

Then you could capture all the information a user did or did not enter into a struct with properties of those types:

struct FormResults {
  // other properties here
  var birthday: OptionalDate
  var lastName: OptionalString
}

And if you found yourself doing this repeatedly for new types, at some point, you’d want to generalize this into a generic type that could support any type in the future. Therefore, you’d write the following:

enum Optional<Wrapped> {
  case none
  case some(Wrapped)
}

At this point, you would have reproduced Swift’s own Optional<Wrapped> type since this is quite close to the definition in the Swift standard library! It turns out Optional<Wrapped> is close to being a plain old generic type, like one you could write yourself.

Why “close”? It would only be a plain old generic type if you interacted with optionals only by writing out their full types, like so:

var birthdate: Optional<Date> = .none
if birthdate == .none {
  // no birthdate
}

But, of course, it’s more common and conventional to write something like this:

var birthdate: Date? = nil
if birthdate == nil {
  // no birthdate
}

Those two code blocks say the same thing. The second relies on special language support for optionals: the Wrapped? shorthand syntax for specifying the optional type Optional<Wrapped> and nil, which can stand for the .none value of an Optional<Wrapped> specialized on any type.

As with arrays and dictionaries, optionals get a privileged place in the language with this syntax to be more concise. But all of these features provide more convenient ways to access the underlying type, which is simply a generic enumeration type.

Challenge

Before moving on, here is a challenge to test your knowledge of generics. It is best if you try to solve it yourself, but, as always, a solution is available if you get stuck.

Challenge 1: Build a Collection

Consider the pet and keeper examples from earlier in the chapter:

class Cat {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Dog {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Keeper<Animal> {
  var name: String
  var morningCare: Animal
  var afternoonCare: Animal

  init(name: String, morningCare: Animal, afternoonCare: Animal) {
    self.name = name
    self.morningCare = morningCare
    self.afternoonCare = afternoonCare
  }
}

Imagine that instead of looking after only two animals, every keeper looks after a changing number of animals throughout the day. It could be one, two, or ten animals per keeper instead of just morning and afternoon ones. You’d have to do things like the following:

let christine = Keeper<Cat>(name: "Christine")

christine.lookAfter(someCat)
christine.lookAfter(anotherCat)

You’d want access to the count of animals for a keeper like christine.countAnimals and to access the 51st animal via a zero-based index like christine.animalAtIndex(50).

Of course, you’re describing your old friend, the array type, Array<Element>!

Your challenge is updating the Keeper type to have this interface. You’ll probably want to include a private array inside Keeper and then provide methods and properties on Keeper to allow outside access to the array.

Key Points

  • Generics are everywhere in Swift: optionals, arrays, dictionaries, other collection structures, and most basic operators like + and ==.
  • Generics express systematic variation at the level of types via type parameters that range over possible concrete types.
  • Generics are like functions for the compiler. They are evaluated at compile-time and result in new types – specializations of the generic type.
  • A generic type is not a concrete type but more like a recipe, program, or template for defining new types.
  • Swift provides a rich system of type constraints, which lets you specify what types are allowed for various type parameters.
  • some Protocol refers to a concrete, generic type, while any Protocol refers to a concrete type in an existential box.
  • There are many ways to write generics with constraints, the most general being the generic where clause.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.