Swift
generics
inheritance
programming
classes

Limitation with classes derived from generic classes in swift

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Swift generics are powerful, but subclassing a generic class introduces several restrictions that do not exist with protocols or non-generic classes. The main limitations involve Objective-C interoperability, covariance, stored property overriding, and runtime type behavior. Understanding these constraints helps you decide when to use generic class inheritance versus protocol-based composition.

Basic Generic Class and Subclass

swift
1class Box<T> {
2    var value: T
3    init(value: T) { self.value = value }
4}
5
6// Subclass that fixes the generic parameter
7class StringBox: Box<String> {
8    func uppercased() -> String {
9        return value.uppercased()
10    }
11}
12
13// Subclass that keeps the generic parameter open
14class LabeledBox<T>: Box<T> {
15    var label: String
16    init(value: T, label: String) {
17        self.label = label
18        super.init(value: value)
19    }
20}

Both patterns work, but the limitations below apply.

Limitation 1: No @objc on Generic Subclasses

Classes that inherit from a generic class cannot be exposed to Objective-C:

swift
1class Repository<T> {
2    func fetch() -> T? { return nil }
3}
4
5class UserRepository: Repository<User> {
6    // ERROR: Generic subclass of 'Repository' cannot be
7    // represented in Objective-C
8    @objc func refreshFromNetwork() { }
9}

This means you cannot use generic subclasses as @IBAction targets, NSObject subclasses with dynamic dispatch, or @objc protocol conformances. The workaround is to wrap the generic logic in a non-generic class or use a protocol with an associated type instead.

Limitation 2: No Covariance for Generic Classes

Swift arrays are covariant ([Dog] is a subtype of [Animal]), but user-defined generic classes are invariant:

swift
1class Animal {}
2class Dog: Animal {}
3
4class Cage<T> {
5    var occupant: T?
6}
7
8let dogCage = Cage<Dog>()
9
10// ERROR: Cannot assign value of type 'Cage<Dog>' to type 'Cage<Animal>'
11let animalCage: Cage<Animal> = dogCage

Even though Dog is a subclass of Animal, Cage<Dog> is not a subclass of Cage<Animal>. This is because Cage<T> has a settable property of type T, so allowing the assignment would break type safety (you could put a Cat into a Cage<Dog> through the Cage<Animal> reference).

The workaround is to use a protocol with an associated type or erase the type:

swift
1protocol AnyAnimalCage {
2    var anyOccupant: Animal? { get }
3}
4
5extension Cage: AnyAnimalCage where T: Animal {
6    var anyOccupant: Animal? { return occupant }
7}
8
9let cages: [AnyAnimalCage] = [Cage<Dog>(), Cage<Cat>()]

Limitation 3: Cannot Override Stored Properties with Different Types

A subclass of a generic class cannot override a stored property to narrow its type:

swift
1class Container<T> {
2    var items: [T] = []
3}
4
5class StringContainer: Container<String> {
6    // ERROR: Cannot override stored property 'items' with a stored property
7    // override var items: [String] = ["default"]
8
9    // OK: Override a computed property or method instead
10    override var description: String {
11        return items.joined(separator: ", ")
12    }
13}

You can add new stored properties and override methods, but not replace inherited stored properties. Use computed properties or methods to customize behavior.

Limitation 4: Type Information Lost at Runtime

Generic type parameters are erased at runtime. This affects is and as checks:

swift
1class Wrapper<T> {
2    var value: T
3    init(_ value: T) { self.value = value }
4}
5
6let intWrapper = Wrapper(42)
7let anyWrapper: Any = intWrapper
8
9// You can check the outer class but not the generic parameter
10print(anyWrapper is Wrapper<Int>)     // true
11print(anyWrapper is Wrapper<String>)  // false at compile time...
12                                      // but runtime behavior depends on context
13
14// Cannot switch on generic type parameter
15func checkType<T>(_ box: Wrapper<T>) {
16    // T is not available for runtime reflection
17    // Mirror(reflecting: box) won't show T's actual type name
18}

For runtime type discrimination, use an enum or a protocol with concrete conformances instead of relying on generic class checks.

Limitation 5: Required Initializers and Generics

If a generic superclass defines a required initializer, every subclass must implement it — even if the subclass fixes the generic parameter:

swift
1class Base<T> {
2    var value: T
3    required init(value: T) {
4        self.value = value
5    }
6}
7
8class Derived: Base<Int> {
9    // Must provide the required init, even though T is now Int
10    required init(value: Int) {
11        super.init(value: value)
12    }
13}

This becomes verbose in deep hierarchies. If the initializer signature depends on T, each subclass must restate it with the concrete type.

Protocol-Based Alternative

When generic class limitations become burdensome, consider using protocols with associated types:

swift
1protocol Storable {
2    associatedtype Item
3    var items: [Item] { get set }
4    func add(_ item: Item)
5}
6
7struct StringStore: Storable {
8    var items: [String] = []
9    func add(_ item: String) { /* ... */ }
10}
11
12struct IntStore: Storable {
13    var items: [Int] = []
14    func add(_ item: Int) { /* ... */ }
15}

Protocols avoid the inheritance chain, work with @objc conformance (when the associated type is concrete), and are generally more flexible in Swift.

Common Pitfalls

  • Expecting covariance: Box<Dog> is not a subtype of Box<Animal>. Use type erasure or a protocol to achieve polymorphism across different generic specializations.
  • Using generic subclasses with Interface Builder: @IBAction and @IBOutlet require @objc, which is unavailable on generic class subclasses. Extract the IB-connected code into a non-generic class.
  • Deep generic inheritance hierarchies: Each layer must forward generic parameters and required initializers, creating boilerplate. Prefer composition (hold a generic object as a property) over inheritance.
  • Runtime type checks on generic parameters: is Wrapper<Int> may not behave as expected in all contexts because Swift erases generic types at runtime. Use concrete wrapper types or enums instead.
  • Overriding methods with generic constraints: A subclass cannot add new constraints to an inherited method's generic parameter. Define the constraint on the superclass or use a protocol extension with a where clause.

Summary

  • Generic class subclasses cannot be marked @objc or used with Interface Builder
  • User-defined generic classes are invariant — Box<Dog> is not a subtype of Box<Animal>
  • Stored properties from generic superclasses cannot be overridden with narrower types
  • Generic type parameters are erased at runtime, limiting is/as checks
  • Required initializers must be restated in every subclass with concrete type parameters
  • Prefer protocols with associated types over deep generic class hierarchies when these limitations become blocking

Course illustration
Course illustration

All Rights Reserved.