Introduction to Swift Keypaths

PartialKeyPath

PartialKeyPath

PartialKeyPath is a type-erased KeyPath that erases the Value type parameter.

As we've seen in the previous chapter, sometimes you want to have a KeyPath that does not require a Value type parameter. That is, what the PartialKeyPath is for. Its type signature is PartialKeyPath<Root>. As you can see, there is no Value type anymore. This KeyPath, again, is read-only. However, it is very useful because it allows you to be much more flexible when storing keypaths in arrays or writing functions that accept keypaths. Here is a quick example:

/// Value would be `String`
let a: PartialKeyPath<User> = \User.name

/// Value would be `Int`
let b: PartialKeyPath<User> = \User.age

/// Value would be `Address`
let c: PartialKeyPath<User> = \User.address

See how these totally different types (KeyPath<User, String>, KeyPath<User, Int>, ...) are actually stored with the same type, just PartialKeyPath<User>. We type-erase the Value parameter.

This is useful because it allows you to call the same function with different types of keypaths:

func acceptKeyPath(_ keyPath: PartialKeyPath<User>) {
  ...
}
acceptKeyPath(\User.age)
acceptKeyPath(\User.username)

More importantly, it allows us to solve the issue we had with the DebugPrinter in the previous code. We can now implement is as follows:

/// Dynamically define a debug description for an object
class DebugPrinter<T> where T: AnyObject {
    var keyPaths: [(String?, PartialKeyPath<T>)] = []
    let reference: T
    let prefix: String

    init(_ prefixString: String, for instance: T) {
        reference = instance
        prefix = prefixString
    }

    func addLog(_ path: PartialKeyPath<T>, prefix: String? = nil) {
        keyPaths.append((prefix, path))
    }

    func log() {
        print(prefix, terminator: ": ")
        for entry in keyPaths {
          if let prefix = entry.0 { print(prefix, terminator: "") }
          print(reference[keyPath: entry.1], terminator: ", ")
        }
    }
}

Just by replacing KeyPath<T, String> with PartialKeyPath<T> we could fix the issue with this code, and now it can be used with all types.

Now, you're probably wondering whether there is a KeyPath type that also type-erases the Root type parameter, and in fact, there is! Next up, the appropriately named AnyKeyPath.