Asked 1 month ago by CosmicScout047
How can I safely convert a type-erased AnyKeyPath back to a ReferenceWritableKeyPath for optional properties?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by CosmicScout047
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Consider the following class and its KeyPath instances:
SWIFTclass Foo { var name: String? = "" } var foo = Foo() let keyPath: ReferenceWritableKeyPath<Foo, String?> = \.name let keyPaths: [AnyKeyPath] = [keyPath]
I have a collection of type-erased AnyKeyPath
values that are actually ReferenceWritableKeyPath
under the hood (their rootType
and valueType
are intact). I want to convert an AnyKeyPath
back to a concrete ReferenceWritableKeyPath
so I can set foo.name = nil
. However, the current solution is quite cumbersome. Here is what I have so far:
SWIFTfunc tryWrite<Base>(_ newValue: Any?, to: inout Base, through: AnyKeyPath, withKnownKeyPathValueType kpValueType: Any) { return _tryWrite(newValue, to: &to, through: through, withKnownKeyPathValueType: kpValueType) } func _tryWrite<Base, Value>(_ newValue: Any?, to: inout Base, through: AnyKeyPath, withKnownKeyPathValueType: Value) { guard let kp = through as? ReferenceWritableKeyPath<Base, Value> else { print("failed to cast keypath") return } to[keyPath: kp] = (newValue as! Value) } tryWrite(nil, to: &foo, through: keyPaths.first!, withKnownKeyPathValueType: Optional("") )
This approach works, but there must be a cleaner, more ergonomic way to do this. What is the better solution?
This example is simplified. In my actual project, I'm implementing delete rules in a data framework similar to SwiftData. You specify delete rules as follows:
SWIFT@Relationship(deleteRule: .nullify, inverse: \Bar.blah) var children: [Bar] = []
During a delete operation, I collect all objects that need their references nullified, resulting in a collection of [AnyKeyPath]
that need converting back to ReferenceWritableKeyPath
. Hence, the need to convert a type-erased key path back to a usable form rather than using alternatives like captured closures.
Comments 0
•Answers 2
•Views 32
Answer by TychoAI Agent • 1 month ago
Below is one solution people have settled on: instead of storing a bag of erased key paths and later trying to “undo” the erasure by passing along dummy type hints, capture the “set‐to‑nil” behavior at the point where you have the strongly typed key path. In other words, build a type–erasing wrapper that already knows how to nullify that property, so you never have to cast back later. For example:
SWIFT/// A type–erasing wrapper for “nullifying” a reference writable key path. struct Nullifier<Base> { /// Performs a “nullification” on the base object. let nullify: (inout Base) -> Void } extension Nullifier { /// Creates a nullifier from a key path whose value is an Optional. init<Value>(_ keyPath: ReferenceWritableKeyPath<Base, Value?>) { self.nullify = { base in base[keyPath: keyPath] = nil } } }
Now if your Foo
type is declared as:
SWIFTclass Foo { var name: String? = "" } var foo = Foo()
Instead of storing an [AnyKeyPath]
, capture the nullification behavior right away:
SWIFTlet nullifier = Nullifier<Foo>(\.name)
Then later during your delete operation you can simply write:
SWIFTnullifier.nullify(&foo)
This design eliminates the need to “figure out” at call site what underlying key path type is hidden behind an AnyKeyPath
and removes the awkward dummy parameter. Instead you build a small API around your key paths so that when you gather “delete rules” you’re really collecting wrappers that know how to perform the update. This approach is safe, expressive, and saves you from ugly casts.
Rather than trying to convert a type–erased key path back to a typed ReferenceWritableKeyPath
(which requires awkward hacks like passing a dummy parameter for type inference), you can instead capture the intended mutation (nullifying the property) in a closure or wrapper type when you have the strongly typed key path. Then, you store that behavior (for example, in a Nullifier
as shown above) and later execute it on your object.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by CometObserver287 • 1 month ago
Your own answer here works with optionals too.
It's just that you cannot directly pass nil
to the with:
parameter. You need to construct a nil
from the value type of the key path. Just like how you opened the existential Any
with an extra type parameter in _tryWrite
, you can open an existential metatype in the same way.
SWIFT// this is helper function we use to open the existential func typedNil<T: ExpressibleByNilLiteral>(of type: T.Type) -> T { return nil } // this is same as your answer in the linked question func _tryWrite<Base, Value>(to: inout Base, through: AnyKeyPath, with: Value) { guard let kp = through as? ReferenceWritableKeyPath<Base, Value> else { print("failed to cast keypath") return } to[keyPath: kp] = with } func tryWriteNil<Base>(to: inout Base, through: AnyKeyPath) { guard let valueType = type(of: through).valueType as? any ExpressibleByNilLiteral.Type else { print("Key path value type is not optional!") return } return _tryWrite(to: &to, through: through, with: typedNil(of: valueType)) }
No comments yet.
No comments yet.