I have a generic function for querying an API that looks something like:
inline fun <reified T : Any> queryApiX(
queryName: String,
accountToken: String,
): List<T> {
val jsonNode = makeRequest(queryName, accountToken)
return deserialize<T>(jsonNode)
}
What I’d like is to be able to define the bound T : XQueryableEntity, such that all implementations of XQueryableEntity must have a static/companion object member “queryName”, so none of my call sites have to know about and repeat the query name and it can just be defined on the type:
class XUser : XQueryableEntity {
companion object {
val queryName = "getUsers" // compile-time checked that this exists
}
}
inline fun <reified T : XQueryableEntity> queryApiX(
accountToken: String,
): List<T> {
// accessible via bounded type parameter
val jsonNode = makeRequest(T::queryName, accountToken)
return deserialize<T>(jsonNode)
}
val users: List<XUser> = queryApiX(accountToken)
But there doesn’t seem to be any way to inherit any kind of static property that can be accessed in a type-safe way on a type parameter without having an instance of it.
This doesn’t help, the point is I need the queryName before I have any given XUser. I should clarify I’ve pretty much gathered you can’t express this kind of requirement, this is essentially a feature request.
One option would be to have your users pass in the implementation explicitly, but it can still hold the type, so that it does double duty as a type inference hint for T:
interface XQueryableEntityCompanion<T: Any> {
val queryName: String
}
inline fun <reified T : Any> queryApiX(
companion: XQueryableEntityCompanion<T>,
accountToken: String,
) = ...
// You can even theoretically have this unsafe method if you want:
inline fun <reified T : Any> queryApiX(
accountToken: String,
) = queryApiX<T>(T::class.companionObjectInstance, accountToken) // requires kotlin.reflect.full
//Usage
class XUser {
companion object: XQueryableEntityCompanion<XUser> {
val queryName = "getUsers"
}
}
queryApiX(XUser, "foo)
I think you could make this safer by checking if the companion object is an instance of XQueryableEntityCompanion. I think you could also check if the type parameter of the XQueryableEntityCompanion instance is the same as the reified type that’s been provided.
Yeah I know, it’s not possible to do it at compile time without extra steps. This is simply an improvement to @kyay10’s solution, since it’s currently not safe at all; it could fail at runtime with a pretty unhelpful error about a wrong class time or wrong parameter type. In fact, I’m not even sure his code would compile, because the compiler can’t guarantee that T::class.companionObjectInstance is an instance of XQueryableEntityCompanion.
While I do not expect the team to implement something like C# static abstract, I could imagine a companion-generic-contraint, that would allow us to obtain the companion of a type safely.
interface XQueryableEntity { … }
interface XQueryableEntityMetadata<E : XQueryableEntity> {
fun makeRequest(accountToken: String): List<E>
}
class XUser : XQueryableEntity {
companion object : XQueryableEntityMetadata<XUser> {
override fun makeRequest(accountToken: String) =
makeRequest("getUsers", accountToken)
}
}
inline fun <reified T : XQueryableEntity> queryApiX(
metadata: XQueryableEntityMetadata<T>,
accountToken: String,
): List<T> {
val jsonNode = metadata.makeRequest(accountToken)
return deserialize<T>(jsonNode)
}
val users = queryApiX(XUser, accountToken)
It’s basically the same idea as @kyay10 but slightly safer through the type bounds in the companion interface, and because the companion becomes responsible for the request.