design patterns – How to make this builder less hacky?

I’m trying to make a safe builder using Kotlin that can be partially built and then copied, perhaps multiple times, to avoid repetition. Note that the properties that are reused aren’t always the same – I’ve reused name and real here, but sometimes age and real or only age might be reused. This is what it looks like (Main.kt):

import builders.*

fun main() {
    val bobProto = personBuilder()
        .name("Bob")
        .real(false)

    val bobSenior = bobProto.copy()
        .age(100)
        .build()

    val bobJunior = bobProto.copy()
        .age(0)
        .build()

    println("Bob Sr. = $bobSenior, Bob Jr. = $bobJunior")
}

This code builds two instances of Person that share a couple of properties but differ in another. Person is defined in people/Person.kt as such:

package people

data class Person(val name: String, val age: Int, val real: Boolean)

The implementation of the builder and other functions is in builders/PersonBuilder.kt:

package builders

import people.Person

object SetProp
object NotSetProp

open class PersonBuilder<N, A, R> {
    var _name: String? = null
    var _age: Int? = null
    var _real: Boolean? = null
}

fun personBuilder(): PersonBuilder<NotSetProp, NotSetProp, NotSetProp> = PersonBuilder()

fun <A, R, T> T.name(name: String): PersonBuilder<SetProp, A, R> where T : PersonBuilder<NotSetProp, A, R> {
    this._name = name
    return this as PersonBuilder<SetProp, A, R>
}

fun <N, R, T> T.age(age: Int): PersonBuilder<N, SetProp, R> where T : PersonBuilder<N, NotSetProp, R> {
    this._age = age
    return this as PersonBuilder<N, SetProp, R>
}

fun <N, A, T> T.real(real: Boolean): PersonBuilder<N, A, SetProp> where T : PersonBuilder<N, A, NotSetProp> {
    this._real = real
    return this as PersonBuilder<N, A, SetProp>
}

fun <N, A, R, T : PersonBuilder<N, A, R>> T.copy(): T {
    val pb = PersonBuilder<N, A, R>()
    pb._name = this._name
    pb._age = this._age
    pb._real = this._real
    return pb as T
}

fun <T> T.build(): Person where T : PersonBuilder<SetProp, SetProp, SetProp> =
    Person(this._name!!, this._age!!, this._real!!)

This works, but because it’s a bit hacky, the error message when a property is left out before calling build or when a property is set twice is cryptic – it simply says that the extension method could not be used because of a receiver type mismatch. There doesn’t seem to be a way to add custom error messages, though, and Kotlin’s contracts didn’t seem to help here either.

Furthermore, the properties of the builder (_name, _age, and _real) have to be public for the extension functions and so can be accessed from anywhere. I’ve made their names start with underscores, but that doesn’t keep them from being visible.

I’d like to know if there’s a more idiomatic way to make a safe builder that checks that all properties are initialized exactly once at compile-time, or at least if there are any minor improvements I can make in this code.