简体   繁体   中英

Kotlin equivalent of Omit TypeScript utility type

I have two data classes in Kotlin that are very similar. The only difference is that one has an ID field and the other doesn't (the ID is generated if the model has been stored in the database only).

data class RouteWithId(
    val id: String,
    val name: String,
    val description: String,
    val comments: List<Comment>,
    val media: List<Media>,
    val points: List<RoutePoint>,
    val userId: String,
    val status: RouteState,
    val tracks: List<TrackInfo>,
) {
    enum class RouteState(val value: String){
        IN_REVIEW("in-review"),
        PUBLISHED("published");
    }
}
data class Route(
    val name: String,
    val description: String,
    val comments: List<Comment>,
    val media: List<Media>,
    val points: List<RoutePoint>,
    val userId: String,
    val status: RouteState,
    val tracks: List<TrackInfo>,
) {
    enum class RouteState(val value: String){
        IN_REVIEW("in-review"),
        PUBLISHED("published");
    }
}

I've tried to unify them with a nullable id field, but many times, it just creates a lot of noise since I know that in several places, the ID would exist (any postprocessing after loading from the database).

I've seen that TypeScript has an utility type called Omit which can be used to derive a type from another type but omitting some field.

This would be perfect for my use case, since I know that after loading from a database the field is going to be there, and before that I also know it won't be there.

Do you know how to achieve the same thing in Kotlin?

There aren't union types in Kotlin, but you can use inheritance to achieve this:

class Route(val name: String, val description: String, /* ... */) {
    enum class RouteState(val value: String){
        IN_REVIEW("in-review"),
        PUBLISHED("published");
    }
    
    fun withId(id: Int): DBRoute = DBRoute(id, name, description, /* ... */)
}

class DBRoute(val id: Int, name: String, description: String, /* ... */) : Route(name, description) {    
    fun asRoute(): Route = Route(name, description, /* ... */)
}

This way, you can convert a Route to a DBRoute and vice-versa. The drawback of this is that you'll have to write all the fields from Route in the constructor of DBRoute .

I don't know if it would be good for you, but you could also use a Map<Int, Route> without needing to create a new class, but this will fit better to a class if you need some operations to envolving the ID somehow.

TLDR : You cannot achieve what you want "properly" in an Object-Oriented Language like Kotlin .

Answer:

First, let's recap one of the principles of an Object-Oriented Language: Inheritance . ( See here for full ist of principles )

Inheritance. Relationships and subclasses between objects can be assigned, allowing developers to reuse a common logic while still maintaining a unique hierarchy. This property of OOP forces a more thorough data analysis, reduces development time and ensures a higher level of accuracy.

So by defenition of Inheritance, the subclass is NOT given an option to CHOOSE what to inherit. That means, It has to inherit all the properties of its parent class; it cannot be picky with what to inherit.

For example, if the parent class has a Lung Cancer property, the child class MUST also have Lung Cancer property. The child class cannot say "Uh, Lung Cancer is bad, so I don't want to inherit it.".

Second, let's refer to the definition of Data class in Kotlin. ( Definition of Data Class ) Kotlin has set a very strict rule in Data class such that "A data class is NOT allowed to have a child class and a parent class". That means, inheritance is not suitable when you are using Data Class . Sadly enough, if you want to perform inheritance in Kotlin, you cannot use Data Class , just use normal class.

However, if you really have to do it, you could "cheat" through OOP and make use of abstract and interface classes in Kotlin. Apparently enough, this is a really bad practice in OOP. I would advise you to rethink your modeling systems again. (Did you consider using Optional in kotlin?)

Here is my example for you:

//create an interface class that 'enforces ID'
interface HasID {
    val id: Int
}

//Data Class must have ID
data class RouteWithId(val length: Int, override val id: Int) : HasID

//Data Class without ID
data class RouteWithOutId(val name: String)

Let me know if you have any more questions. Thanks

I've also desired something like this for a long time. Unfortunately I don't think there's a good answer. The best I can imagine is writing a custom annotation library using KAPT, KotlinPoet, or other to generate the derived classes for you. Maybe an API something like this:

// would generate SimplePerson class with only name and age fields
@Pick("SimplePerson", fields=["name", "age"])
// would generate an AgelessPerson class without the age field
@Omit("AgelessPerson", fields=["age"])
data class Person(val name: String, val age: Int, heightCm: Int, weight: Int)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM