How to let subclass inherit a method referencing its own type?

This is something that actually came up at work in a TypeScript context, but when I tried to implement it in Swift for comparison, and I found it was trickier than I expected.

What is the best way to define a type method in a class, such that when the method is inherited by subclasses, the type of the method's parameter can reference the type of the subclass?

The most concise example of the kind of thing I want to do is this non-working code:

class Configurable {
    
    static func configure( _ configurator: (Self) -> Void) -> Self {
        let result = Self.init()
        configurator(result)
        return result
    }
}

class PastaLover: Configurable {
    var favoritePasta = "ravioli"
}

let spaghettiLover = PastaLover.configure { x in
    x.favoritePasta = "🍝"
}

That does not work, because: 'Self' is only available in a protocol or as the result of a method in a class; did you mean 'Configurable'?

But no, I don't want to do that because then the x.favoritePasta = "🍝" line would get the error Value of type 'Configurable' has no member 'favoritePasta'. In the subclass, the parameter to the configurator function needs to be of the subclass type, not the parent Configurable type.

So, OK, I thought, I'll try defining it in a protocol, and implementing it in a protocol extension:

protocol BaseConfigurable {
    
     static func configure( _ configurator: (Self) -> Void) -> Self;
}

extension BaseConfigurable where Self: Configurable {
    
    static func configure( _ configurator: (Self) -> Void) -> Self {
        let result = Self.init();
        configurator(result);
        return result;
    }
}

class Configurable: BaseConfigurable {
    
    required init() {}
        // Unrelated: we need this required init, otherwise we get this error: Constructing an object of class type 'Self' with a metatype value must use a 'required' initializer
}

That seemed to almost work, but it wouldn't build, because: Protocol 'BaseConfigurable' requirement 'configure' cannot be satisfied by a non-final class ('Configurable') because it uses 'Self' in a non-parameter, non-result type position.

Oops! I don't want to mark Configurable as final because it is a base class, intended to have a big hierarchy of many subclasses!

Eventually, applying the "fail, then google the error message" technique, I eventually found a Stack Overflow post that gave me the hint I needed to make it work: remove the method from the protocol definition! (:thinking:...)

// A more complete version of this code with printing examples is at:
// https://gist.github.com/masonmark/9036c1767ffef24c15e450478861cdde

protocol BaseConfigurable {}

extension BaseConfigurable where Self: Configurable {
    
    static func configure( _ configurator: (Self) -> Void) -> Self {
        let result = Self.init();
        configurator(result);
        return result;
    }
}

class Configurable: BaseConfigurable {
    
    required init() {} // required or won't compile
}

class Person: Configurable {
    var name = "Alice"
    var age  = 0
}

class FireFighter: Person {
    var helmetSize: Int?
    var hasLicenseToDriveFireEngine = false
}

let lisa = Person.configure() { lisa in
    lisa.name = "Lisa"
    lisa.age = 39
}

// or, pass in a configurator function

func configureBiff(biff: FireFighter) {
    biff.hasLicenseToDriveFireEngine = true;
    biff.helmetSize = 15
    biff.age = 21
    biff.name = "Biff Wigginberger Jr."
}

let biff = FireFighter.configure(configureBiff)

OK! That does work. But, it seems a little convoluted and weird.

Is there a better way to do this?

If you don't really need Configurable to be a base class, only that all conforming instances are class-bounded (declared with class):

protocol Configurable: AnyObject {
    init()
}

extension Configurable {
    static func configure( _ configurator: (Self) -> Void) -> Self {
        let result = Self.init()
        configurator(result)
        return result
    }
}

Or if you need Configurable to be base class (though I don't see any need for that, protocol should be capable to do more or less the same thing). This is as simple as I can get:

protocol ConfigurableProtocol: Configurable { }

class Configurable: ConfigurableProtocol {
    required init() {}
}

extension ConfigurableProtocol {
    static func configure(_ configurator: (Self) -> Void) -> Self {
        let result = Self.init()
        configurator(result)
        return result
    }
}

Then the rest is the same:

class PastaLover: Configurable {
    var favoritePasta = "ravioli"

    required init() { }
}

let spaghettiLover = PastaLover.configure { x in
    x.favoritePasta = "🍝"
}
spaghettiLover.favoritePasta // "🍝"

Oh, that is definitely simpler and better. There's really no requirement to require a base class; that was just an artifact of the original TypeScript implementation.

Thanks!

(I posted an updated Swift gist with these simplifications. For the curious, the TypeScript version is here.)

Terms of Service

Privacy Policy

Cookie Policy