Sometimes it's neccessary to build an object across multiple screens, but updating the object's properties be optional or explictly unwrapped can lead to bugs and crashes.

TypeScript has a built-in type called Partial, which takes an existing type and makes each of its properties optional, and applies very well to this scenario.

To demonstrate the use of Partial I will use some simple structs with a few let properties.

struct Order {

    let userId: Int

    let itemIds: [Int]

    let promoCode: String?

    let address: Address

    let billingDetails: BillingDetails

}

struct Address {

    let name: String

    let firstLine: String

    let additionalLines: [String]

    let city: String

    let postCode: String

}

struct BillingDetails {

    let creditCardNumber: String

    let ccv: String

    let address: Address

}

To support building this object up over time, e.g. across multiple screens, there needs to be a way to store each of the values and create the object when all of the values have been provided.

The basic implementation

struct Partial<Wrapped> {

    private var values: [PartialKeyPath<Wrapped>: Any] = [:]

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> ValueType? {
        get {
            return values[key] as? ValueType
        }
        set {
            values[key] = newValue
        }
    }

}

For very simple use cases this is all that's needed!

You can build up an object by subscripting it with a KeyPath of the wrapped type.

var partialOrder = Partial<Order>()
partialOrder[\.userId] // nil
partialOrder[\.userId] = 123
partialOrder[\.userId] // 123

However, Partials can be much for more powerful.

Initialisation of the Wrapped Type

One of the issues with Partials as basic as this is that the initialisation of the wrapped type can be a bit cumbersome.

struct Order {

    // ...

    init?(partial: Partial<Order>) {
        guard let userId = partial[\.userId] else { return nil }
        guard let itemIds = partial[\.itemIds] else { return nil }

        self.userId = userId
        self.itemIds = itemIds
        self.promoCode = partial[\.promoCode]

        // Much check for both scenarios
        let partialAddress = partial[partial: \Order.address]
        if let name = partialAddress[\.name],
            let firstLine = partialAddress[\.firstLine],
            let additionalLines = partialAddress[\.additionalLines],
            let city = partialAddress[\.city],
            let postCode = partialAddress[\.postCode] {
            self.address = Address(name: name, firstLine: firstLine, additionalLines: additionalLines, city: city, postCode: postCode)
        } else if let address = partial[\.address] {
            self.address = address
        } else {
            return nil
        }

        // Same must be done for `billingDetails`...
    }

This can be improved by providing a function marked as throws, defining a new protocol, and adding a new subscript that can "unwrap" any values stored in sub-Partials.

protocol PartialConvertible {

    init(partial: Partial<Self>) throws

}

struct Partial<Wrapped> {

    // ...

    enum Error: Swift.Error {
        case missingKey(PartialKeyPath<Wrapped>)
        case invalidValueType(key: PartialKeyPath<Wrapped>, actualValue: Any)
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        } else {
            return Error.missingKey(key)
        }
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType where ValueType: PartialConvertible {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else if let partial = value as? Partial<ValueType> {
                return try ValueType(partial: partial)
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        } else {
            throw Error.missingKey(key)
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> ValueType? where ValueType: PartialConvertible {
        get {
            return try? value(for: key)
        }
        set {
            values[key] = newValue
        }
    }

}

struct ExportOptions: PartialConvertible {

    // ...

   init(partial: Partial<Order>) throws {
        userId = try partial.value(for: \.userId)
        itemIds = try partial.value(for: \.itemIds)
        promoCode = try partial.value(for: \.promoCode)
        address = try partial.value(for: \.address)
        billingDetails = try partial.value(for: \.billingDetails)
    }

}

extension Order.Address: PartialConvertible {

    init(partial: Partial<Order.Address>) throws {
        name = try partial.value(for: \.name)
        firstLine = try partial.value(for: \.firstLine)
        additionalLines = try partial.value(for: \.additionalLines)
        city = try partial.value(for: \.city)
        postCode = try partial.value(for: \.postCode)
    }

}

extension Order.BillingDetails: PartialConvertible {

    init(partial: Partial<Order.BillingDetails>) throws {
        creditCardNumber = try partial.value(for: \.creditCardNumber)
        ccv = try partial.value(for: \.ccv)
        address = try partial.value(for: \.address)
    }

}

Much better!

Recursive Partials

Partials on their own are great, but once you try to access a property of a property of a Partial it stops working quite as expected.

partialOrder[\.address][\.name] = "Santa Claus" // Not possible

This can be massively improved by adding another subscript and value(for:) function.


struct Partial<Wrapped> {

    // ...

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType where ValueType: PartialConvertible {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else if let partial = value as? Partial<ValueType> {
                return try ValueType(partial: partial)
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else {
            throw Error.missingKey(key)
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> Partial<ValueType> where ValueType: PartialConvertible {
        get {
            et partial = values[key] as? Partial<ValueType> {
                return partial
            } else {
                return Partial<ValueType>()
            }
        }
        set {
            values[key] = newValue
        }
    }

}

partialOrder[\.address][\.name] // nil
partialOrder[\.address][\.name] = "Johnny Appleseed"
partialOrder[\.address][\.name] // "Johnny Appleseed"

However, because it will always return a Partial, there will be an issue if the value has been explictly set elsewhere:

partialOrder[\.address] = Address(name: "Johnny Appleseed", ...)
partialOrder[\.address] // An empty `Partial`
try? Order.Address(partial: partialOrder[\.address]) // nil

This can be fixed by updating Partial to support a backing value, allowing the stored value to be wrapped.

struct Partial<Wrapped> {

    // ...

    private var backingValue: Wrapped? = nil

    init(backingValue: Wrapped? = nil) {
        self.backingValue = backingValue
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        } else {
            throw Error.missingKey(key)
        }
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType where ValueType: PartialConvertible {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else if let partial = value as? Partial<ValueType> {
                return try ValueType(partial: partial)
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        } else {
            throw Error.missingKey(key)
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> ValueType? {
        get {
            return try? value(for: key)
        }
        set {
            values[key] = newValue
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> Partial<ValueType> where ValueType: PartialConvertible {
        get {
            if let value = try? self.value(for: key) {
                return Partial<ValueType>(backingValue: value)
            } else if let partial = values[key] as? Partial<ValueType> {
                return partial
            } else {
                return Partial<ValueType>()
            }
        }
        set {
            values[key] = newValue
        }
    }

}

partialOrder[\.address] = Address(name: "Mr Appleseed", ...)
partialOrder[\.address][\.name] // "Mr Appleseed"
partialOrder[\.billingAddress][\.address] = partialOrder[\.address]
partialOrder[\.billingAddress][\.address][\.name] = "Johnny Appleseed"
partialOrder[\.billingAddress][\.address][\.name] // "Johnny Appleseed"

Dealing with Optionals

When using a property of Wrapped that's optional, such as promoCode on Order, the type of partial[\.promoCode] will be String??. To work around this every function and subscript needs to be duplicated to support a key of type KeyPath<Wrapped, ValueType?>.

For the sake of brevity only one of these is shown below.

struct Partial<Wrapped> {

    // ...

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType?>) -> ValueType? {
        get {
            if let value = try? value(for: key) {
                return value
            } else {
                return nil
            }
        }
        set {
            values[key] = newValue
        }
    }

}

Swift will pick the right one based on context.

Downsides

Using Partial does have some downsides. One is that you still have to create a custom init function, something that could be fixed by adding Partial to the standard library or made easier using metaprogramming tools such as Sourcery.

Another downside is that Xcode will not provide autocomplete suggestions for KeyPaths, unless the type is provider before the period.

partialOrder[\.userId] // Will not autocomplete
partialOrder[\Order.userId] // Will autocomplete

This is not an issue with Partial itself, but is a shortcoming due to its reliance on KeyPath

Full Code

Below is the full code, plus a full example showing all the ways that Partial can be used.

If you want to try Partial yourself you can download the playground, or download Partial.swift and add it to your project. Both linked versions also include CustomStringConvertible and CustomDebugStringConvertible conformance, which can greatly improve debugging, but has been omitted for the sake of brevity below.

struct Partial<Wrapped> {

    enum Error: Swift.Error {
        case missingKey(PartialKeyPath<Wrapped>)
        case invalidValueType(key: PartialKeyPath<Wrapped>, actualValue: Any)
    }

    private var values: [PartialKeyPath<Wrapped>: Any] = [:]

    private var backingValue: Wrapped? = nil

    init(backingValue: Wrapped? = nil) {
        self.backingValue = backingValue
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        } else {
            throw Error.missingKey(key)
        }
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType?>) throws -> ValueType {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        } else {
            throw Error.missingKey(key)
        }
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType>) throws -> ValueType where ValueType: PartialConvertible {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else if let partial = value as? Partial<ValueType> {
                return try ValueType(partial: partial)
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        } else {
            throw Error.missingKey(key)
        }
    }

    func value<ValueType>(for key: KeyPath<Wrapped, ValueType?>) throws -> ValueType where ValueType: PartialConvertible {
        if let value = values[key] {
            if let value = value as? ValueType {
                return value
            } else if let partial = value as? Partial<ValueType> {
                return try ValueType(partial: partial)
            } else {
                throw Error.invalidValueType(key: key, actualValue: value)
            }
        } else if let value = backingValue?[keyPath: key] {
            return value
        } else {
            throw Error.missingKey(key)
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> ValueType? {
        get {
            return try? value(for: key)
        }
        set {
            values[key] = newValue
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType?>) -> ValueType? {
        get {
            return try? value(for: key)
        }
        set {
            values[key] = newValue
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType>) -> Partial<ValueType> where ValueType: PartialConvertible {
        get {
            if let value = try? self.value(for: key) {
                return Partial<ValueType>(backingValue: value)
            } else if let partial = values[key] as? Partial<ValueType> {
                return partial
            } else {
                return Partial<ValueType>()
            }
        }
        set {
            values[key] = newValue
        }
    }

    subscript<ValueType>(key: KeyPath<Wrapped, ValueType?>) -> Partial<ValueType> where ValueType: PartialConvertible {
        get {
            if let value = try? self.value(for: key) {
                return Partial<ValueType>(backingValue: value)
            } else if let partial = values[key] as? Partial<ValueType> {
                return partial
            } else {
                return Partial<ValueType>()
            }
        }
        set {
            values[key] = newValue
        }
    }

}

var partialOrder = Partial<Order>()
partialOrder[\.userId] = 123
partialOrder[\.itemIds] = [1, 4, 7]
partialOrder[\.promoCode] = "HELLO10"
partialOrder[\.address] = Address(name: "Johnny Appleseed", firstLine: "One Infinite Loop", additionalLines: ["Cupertino"], city: "CA", postCode: "95014")
partialOrder[\.billingDetails][\.creditCardNumber] = "1111 2222 3333 4444"
partialOrder[\.billingDetails][\.ccv] = "123"
partialOrder[\.billingDetails][\.address] = partialOrder[\.address]
partialOrder[\.billingDetails][\.address][\.name] = "Santa Claus"
partialOrder[\.billingDetails][\.address]?.name // "Santa Clause"
partialOrder[\.billingDetails][\.address][\.firstLine] = "Santa's Grotto"
partialOrder[\.billingDetails][\.address][\.additionalLines] = []
partialOrder[\.billingDetails][\.address][\.city] = "Reindeerland"
partialOrder[\.billingDetails][\.address][\.postCode] = "XM4 5HQ"

do {
    let order = try Order(partial: partialOrder)
    order.userId // 123
    order.address.name // Johnny Appleseed
    order.billingDetails.address.name // "Santa Clause"
} catch {
    error
}

Special Thanks

Shaps helped me a lot with this post, from working with me through the evolution of the implementation to reading draft of this post, for which I am very greatful.