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! (...)
// 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?