Tutorials Ultimate Swift Series Chapter 20

Generics

SwiftChapter 20 of the Ultimate Swift Series38 minJune 8, 2026Intermediate

You've been writing generics since your very first array. [Int], [String], [any Uploadable] -- each is a different concrete type stamped out from one blueprint, Array. That you reach for them without a second thought is the whole point: generics are meant to disappear into the background. But it would be a mistake to conclude that generics are about collections. They're about something deeper -- using one set of types to manufacture another -- and arrays are just the most familiar place that machinery shows up.

This chapter builds the idea from the ground up, on an example this app lives and breathes: turning raw material into App Store assets. By the end you'll see arrays, dictionaries, and optionals with new eyes -- not as special built-ins, but as ordinary generic types you could have written yourself.

The last chapter ended on a clue: when you wrote func abort(_ transfer: inout some Uploadable & Chunked), that little some quietly turned a plain function into a generic one. This chapter is where that machinery comes fully into view.

Two Ways to Model a Pipeline

Imagine the part of this app that prepares assets for upload. You deal in a few kinds of asset -- screenshots, app previews, icons -- and for each kind there's a corresponding exporter that knows how to render and package it. There are two fundamentally different ways to model that "one exporter per asset kind" relationship. The difference between them is the seed of everything in this chapter.

Modeling With Values

The first approach varies values, not types. Define one type for the kind of asset:

enum AssetKind {
    case screenshot
    case appPreview
    case icon
}

Then model the exporter as a single type whose value records which kind of asset it handles:

struct ExporterKind {
    var produces: AssetKind
}
 
let shotExporter = ExporterKind(produces: .screenshot)
let iconExporter = ExporterKind(produces: .icon)

Two things are worth naming here. First, there's exactly one type for asset kinds (AssetKind) and one for exporters (ExporterKind); the variety lives entirely in the values those types take. Second -- and this is the good part -- one set of values determines another. The range of possible ExporterKind values mirrors the range of possible AssetKind values. The day you start shipping app-preview videos, you add case appPreview to AssetKind and you can immediately write ExporterKind(produces: .appPreview). The relationship maintains itself.

Contrast that with the brittle alternative -- a second, unrelated enum:

enum LooseExporterKind {
    case screenshotExporter
    case iconExporter
}

Nothing connects LooseExporterKind to AssetKind. Add AssetKind.appPreview and forget to add LooseExporterKind.appPreviewExporter, and the two drift out of sync with no complaint from the compiler. The ExporterKind version is better precisely because it encodes the dependency: every AssetKind value implies a matching ExporterKind.

Modeling With Types

Now flip the whole thing around. Instead of one AssetKind type with several values, suppose you model each kind of asset as its own type -- a natural choice if each asset behaves differently, with its own methods:

class Screenshot {}
class AppPreview {}
class Icon {}

How do you represent the matching exporters now? The obvious move is one exporter type per asset type:

class ScreenshotExporter {}
class AppPreviewExporter {}
class IconExporter {}

But this is the brittle LooseExporterKind problem all over again, just at the level of types. Nothing forces a new asset type to come with an exporter type. You're back to maintaining the relationship by hand.

What you want is to say it once: for every possible asset type, there is a corresponding exporter type -- automatically, without writing each one out. Manufacturing a family of types from another family of types is exactly the problem generics were invented to solve.

Anatomy of a Generic Type

Generics let one set of types define a new set of types. Here's the entire idea in a single line:

class Exporter<Item> {}

That declaration, on its own, conjures up every corresponding exporter type at once -- Exporter<Screenshot>, Exporter<AppPreview>, Exporter<Icon>, and one for any other type you could name. They're real types; prove it by making values, spelling out the full type:

var shotExporter = Exporter<Screenshot>()
var iconExporter = Exporter<Icon>()

So what is Exporter? Strictly, it isn't a type at all. It's a recipe for making types -- a template the compiler follows. You can see that it's a recipe rather than a finished dish by trying to use it with no filling:

var mystery = Exporter()   // ⛔ generic parameter 'Item' could not be inferred

The compiler has no idea what kind of exporter you mean. That Item in angle brackets is the type parameter: a placeholder for a real type you supply later. Fill it in -- Exporter<Screenshot> -- and the recipe yields a genuine, concrete type. Exporter<Screenshot> and Exporter<Icon> are different types, even though they came from the same blueprint. These finished types are called specializations of the generic.

To define a generic type, then, you choose two things: the name of the type and the name of its type parameter. Pick a parameter name that reads well in context. You'll see bare T (for "Type") in throwaway examples, but when the placeholder has a real job, give it a real name. Item says "the thing this exporter exports" far better than T would.

A whole family from one declaration

Exporter<Item> doesn't define a type. It defines every type of the form Exporter<…> -- one specialization for each concrete type you could substitute for Item. One line of source, an unbounded family of types. That leverage is the reason generics exist.

Notice that, so far, Exporter neither stores anything nor even uses Item. At this point generics are purely a way to systematically define sets of types. Usually, though, you want the parameter to pull its weight.

Putting the Type Parameter to Work

Give the assets an identity, and make the exporter actually hold the assets it's responsible for. Each exporter keeps two of them on hand -- the asset it's currently preparing and a fallback to swap in if rendering fails:

class Screenshot {
    var label: String
    init(label: String) { self.label = label }
}
 
class AppPreview {
    var label: String
    init(label: String) { self.label = label }
}
 
class Exporter<Item> {
    var name: String
    var primary: Item
    var fallback: Item
 
    init(name: String, primary: Item, fallback: Item) {
        self.name = name
        self.primary = primary
        self.fallback = fallback
    }
}

Look at what Item is doing now. The primary and fallback properties both have type Item, so an Exporter<Screenshot> holds two screenshots and an Exporter<AppPreview> holds two previews -- never a mix. Just as a function parameter becomes a usable name inside the function body, a type parameter becomes a usable type throughout the generic's definition: in stored properties, computed properties, method signatures, and nested types alike.

When you create one, Swift figures out the specialization from the values you pass and enforces, at compile time, that both assets are the same kind:

let heroExporter = Exporter(
    name: "Hero shots",
    primary: Screenshot(label: "home-hero"),
    fallback: Screenshot(label: "home-hero-alt")
)

You never wrote <Screenshot> anywhere. Because primary and fallback are both Screenshot values, Swift infers heroExporter to be an Exporter<Screenshot>. Hand it one screenshot and one preview and it won't compile -- the two Item slots must agree.

Generic Functions

Functions can be generic too. The type-parameter list goes right after the function's name, and you use those parameters through the rest of the signature and body. Here's one that takes two values and hands them back in the opposite order, swapping their types along the way:

func reordered<A, B>(_ first: A, _ second: B) -> (B, A) {
    (second, first)
}
 
reordered(412, "1.4.0")   // returns ("1.4.0", 412)

A generic function shows off a genuinely confusing bit of syntax: it has two parameter lists. There's the type-parameter list <A, B> and the ordinary value-parameter list (_ first: A, _ second: B). Keep them straight by thinking of the type parameters as arguments for the compiler. You feed it A and B, and it stamps out one concrete, non-generic version of reordered for that pair of types -- the same way Exporter<Item> let it stamp out screenshot exporters and icon exporters on demand.

Mini-exercise

Constraining the Type Parameter

That last mini-exercise exposes a gap. Item is a free-for-all: you can specialize Exporter on Int, String, anything -- including types that make no sense as assets. A type parameter, as written, is unlike a function parameter in this one respect. When you write func render(shot: Screenshot), the caller can only pass a Screenshot. You'd like the same discipline for type parameters: a way to say "Item may only be an asset."

You get it with a type constraint. The simplest kind attaches a protocol (or class) requirement directly to the parameter:

class Exporter<Item: Asset> {
    /* body unchanged */
}

The : Asset says the type filling Item must conform to the Asset protocol (or, if Asset were a class, be that class or a subclass). Define the protocol around the one capability every asset shares -- it answers to a label -- and bolt conformance onto the existing types with extensions, exactly the retroactive trick from the last chapter:

protocol Asset {
    var label: String { get }
}
 
extension Screenshot: Asset {}
extension AppPreview: Asset {}

Both already have a label property, so the empty extensions are enough. Now Exporter<Int> is a compile error, and Exporter<Screenshot> sails through.

Constraints earn their keep in functions, where they unlock the parameter's capabilities. An unconstrained generic is almost useless to write against:

func announce<Item>(_ asset: Item) {
    // 'asset' could be literally anything — there's nothing you can do with it.
}

Because Item could be any type at all, the compiler lets you assume nothing about it. Add the constraint and the body comes alive:

func announce<Item: Asset>(_ asset: Item) {
    print("Preparing “\(asset.label)” for upload…")
}

Now Item is known to be an Asset, so asset.label is fair game. There's a cleaner way to write the very same function using some, the keyword you met last chapter:

func announce(_ asset: some Asset) {
    print("Preparing “\(asset.label)” for upload…")
}

This says exactly what the angle-bracket version said -- announce is generic over any Asset -- but reads better without the type-parameter list, and states the constraint right where the parameter is. When a generic function uses its type parameter only once, some is the preferred style.

The where Clause

Simple constraints cover the common case. For richer relationships there's the generic where clause, which you can attach to functions, types, methods, protocols, and extensions to constrain type parameters and associated types. Re-spelling announce with one looks like this:

func announce<Item>(_ asset: Item) where Item: Asset {
    print("Preparing “\(asset.label)” for upload…")
}

For this case, some Asset is still the nicer style -- but where is doing the same job, and it scales to constraints that : Asset can't express. Its real power shows in extensions. Suppose you want every array of screenshots -- and only those -- to gain a method for laying out a contact sheet:

extension Array where Element: Screenshot {
    func contactSheet() {
        forEach { print(\($0.label)") }
    }
}

[Screenshot] now has contactSheet(); [Int] does not. The capability appears only when the element type satisfies the constraint.

Conditional Conformance

where on an extension can do something even more striking: make a type conform to a protocol only when its contents do. Say anything that can be drawn to a PNG is Renderable:

protocol Renderable {
    func render()
}
 
extension Screenshot: Renderable {
    func render() {
        print("Rendering “\(label)” to PNG")
    }
}
 
extension Array: Renderable where Element: Renderable {
    func render() {
        forEach { $0.render() }
    }
}

That last extension reads: an array is Renderable whenever its elements are Renderable, and rendering the array means rendering each element. So [Screenshot] is now Renderable for free, while [Int] is not. This is conditional conformance -- a quiet but far-reaching tool for building capabilities out of smaller ones.

Capturing the Concrete Type

Generics aren't only for defining types; they're also how you recover type information you'd otherwise throw away. Start with a mixed bag of assets returned from somewhere in the app:

let pool: [any Asset] = [
    Screenshot(label: "home-hero"),
    AppPreview(label: "feature-tour"),
    Screenshot(label: "settings"),
]

It's typed [any Asset] -- an array of assorted things that are each some Asset, their concrete types erased into existential boxes. Now write a function to find a screenshot by label, and another for previews:

func firstScreenshot(label: String, in pool: [any Asset]) -> Screenshot? {
    pool.lazy.compactMap { $0 as? Screenshot }.first { $0.label == label }
}
 
func firstPreview(label: String, in pool: [any Asset]) -> AppPreview? {
    pool.lazy.compactMap { $0 as? AppPreview }.first { $0.label == label }
}

Each lazily walks the pool, keeps only the matching concrete type, and returns the first with the right label. They're identical except for the type Screenshot versus AppPreview -- and you'd have to write another for every new asset kind. That repetition is begging for a generic.

You might first try erasing the type entirely:

func firstAsset(label: String, in pool: [any Asset]) -> (any Asset)? {
    pool.first { $0.label == label }
}

This finds any asset with the label -- but it hands back an any Asset, throwing away the very thing you cared about. Ask for a screenshot named "home-hero" and you might get a preview that happens to share the label. The type information you need is gone.

A generic captures it. The first parameter is a metatype -- the type itself, passed as a value -- which lets the compiler infer Item and pin down what you're looking for:

func firstAsset<Item: Asset>(_ kind: Item.Type, label: String,
                             in pool: [any Asset]) -> Item? {
    pool.lazy.compactMap { $0 as? Item }.first { $0.label == label }
}

You call it by handing over the type with .self:

firstAsset(Screenshot.self, label: "home-hero", in: pool)   // Screenshot?
firstAsset(AppPreview.self, label: "feature-tour", in: pool) // AppPreview?

One function, every asset kind, and -- crucially -- it returns the concrete type. Because the result is a real Screenshot?, you can immediately use anything a Screenshot provides:

firstAsset(Screenshot.self, label: "home-hero", in: pool)?.render()
// Rendering “home-hero” to PNG

That render() call compiles only because the return type is concrete. The same call against a type that isn't Renderable would be rejected at compile time -- which is the whole payoff of keeping the type around instead of erasing it.

some as a return type hides, generics reveal

You could declare the return as (some Asset)? -- an opaque return type, which conceals the concrete type behind the Asset interface. That's the right tool when you want to hide implementation details from callers. Here it's the wrong tool: hiding the type is the one thing you're trying not to do. Returning the concrete Item? gives callers the full type back. Reach for opaque returns to hide complexity, and plain generic returns to preserve type information.

The Generics You Already Use

With the fundamentals in hand, the standard library's most familiar types reveal themselves as ordinary generics.

Arrays

Array doesn't just illustrate generics -- supporting homogeneous arrays safely was a founding motivation for inventing them. You've used Array all along, almost always through syntactic sugar. These two declarations are identical:

let buildNumbers: [Int] = [410, 411, 412]
let sameThing: Array<Int> = [410, 411, 412]

[Element] is shorthand for Array<Element>, fully interchangeable -- you can even call the initializer either way, [Int]() or Array<Int>(). Once the compiler knows (or infers) the element type at one point, it flags any stray non-Int everywhere else, before the program ever runs. Because arrays only need indexed access, they place no constraint on Element -- any type at all can be stored.

Dictionaries

A generic can take more than one type parameter, each with its own constraints, and Dictionary is the clean example. Its declaration carries two:

struct Dictionary<Key: Hashable, Value> { /* … */ }

Key and Value are the key and value types. The constraint Key: Hashable isn't decoration -- a dictionary is a hash map, so it must be able to hash its keys to find things fast, and only Hashable types can be hashed. You supply both parameters as a comma-separated list, or lean on the usual sugar and inference:

let releaseNotes: Dictionary<String, String> = ["en-US": "Bug fixes"]
let localized: [String: String] = ["en-US": "Bug fixes", "de-DE": "Fehlerbehebungen"]
let inferred = ["en-US": "Bug fixes", "fr-FR": "Corrections de bugs"]

[Key: Value] is just Dictionary<Key, Value> wearing a friendlier face.

Optionals

No tour of generics is complete without the optional -- which is, underneath, a perfectly ordinary generic enum you could have written yourself. Suppose a build's release notes are optional. Before you knew about generics, you might have hand-rolled a type for it:

enum OptionalNotes {
    case none
    case some(String)
}

And if a build's review-notes field were also optional, you'd write another near-identical type. Generalize over the wrapped type and the duplication collapses:

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

That's essentially Swift's own Optional<Wrapped>, near-verbatim from the standard library. It's almost a plain generic enum -- "almost," because you'd interact with it through its full form:

var notes: Optional<String> = .none
if notes == .none { /* no notes */ }

whereas in real code you write the sugared version, which means the same thing:

var notes: String? = nil
if notes == nil { /* no notes */ }

The language grants optionals special syntax -- Wrapped? for the type, nil for .none on any specialization -- exactly as it does for arrays and dictionaries. Strip the sugar away and what's left is a generic enumeration, no more magical than the Exporter you built by hand.

Challenge

Challenge 1: turn the exporter into a collection

The Exporter<Item> you built holds exactly two assets -- a primary and a fallback. Real exporters accumulate a changing number of assets over a session: one, a dozen, fifty. You'd like to write code like this:

let queue = Exporter<Screenshot>(name: "Launch set")
 
queue.add(Screenshot(label: "home-hero"))
queue.add(Screenshot(label: "settings"))
 
queue.count            // 2
queue.asset(at: 0)     // the hero screenshot

Of course, you're describing your old friend Array<Element>. Your challenge: rework Exporter<Item> to offer this interface. Wrap a private array of Item inside the class, then expose add(_:), a count property, and an asset(at:) method that reaches into it. (As always, try it before peeking at a solution.)

Key Points

You've now seen the two halves of Swift's type system click together. Protocols are the contracts; generics are the code you write once against those contracts so it works across every type that signs them -- one sort() for all Comparables, one Exporter for every Asset. With structs, classes, enums, protocols, and generics in hand, you have the full vocabulary the standard library itself is built from. The fundamentals are complete; everything from here is putting them to work building real things.

Ch 19: ProtocolsComing Soon →
SwiftUIUltimate SwiftUI SeriesSwiftUI tutorials for building native app screens, layouts, navigation, and state-driven interfaces.Ship iOSShip iOS Apps SeriesShipping workflows for iOS apps: signing, TestFlight, App Store Connect, CI, and release hygiene.DeliveryModern Delivery PipelineCI/CD, review, runner, and deploy workflows for teams shipping apps and websites safely.

Ship your apps faster

When you're ready to publish your Swift app to the App Store, Simple App Shipper handles metadata, screenshots, TestFlight, and submissions — all in one place.

Try Simple App Shipper
5 free articles remainingSubscribe for unlimited access