Why does the call to sendUser2() throw a compiler ...
# stdlib
o
Why does the call to sendUser2() throw a compiler error?
Copy code
value class UserId(private val value: Int)

fun sendUser1(userId: UserId) = ...
fun sendUser2(userId: Int) = ...

// Compiler error: required: UserId, found: Int. Makes sense because an Int isn't always a UserId.
sendUser1(1)

// Compiler error: required: Int, found: UserId. Does not make sense because a UserId is always an Int.
sendUser2(UserId(1))
Why doesn't the compiler see that type UserId is of type Int? It makes it harder for a project to adopt value classes if the new types aren't compatible with it's super type
f
Because
Int
isn't the super-type of
UserId
. 😉
o
That appears to be the case apparently, but I don't see how that makes sense. In the end, UserId is an Int. Why was the decision made to do otherwise?
h
the point of value classes is to have a different type, so you can't mix up the different
Int
types. If you want to use them interchangably, use a
typealias
instead.
☝️ 1
j
If UserId is detected as Int, this feature should be useless because I can strong type things
o
Can't use typealias, because it isn't as typesafe as a new type. Besides, an Id isn't an alias for Int. typealias UserId = Int, means that UserId is the same as Int, and Int is the same as UserId, which isn't what's happening here.
j
so I could just use Int directly instead of inline classes
o
@Javier no, because if the sendUser function required a UserId instead of an Int, and you would pass an Int, the compiler would still throw an error, and that makes sense
j
Copy code
data class SomeClass(
    val idOne: IdOne,
    val idTwo: IdTho,
    ...
)
both are string
but they are different
o
Because an UserId IS an Int, but an Int IS NOT an UserId
j
I don't want passing a String could be a valid value
ah, I see your point, but that can be error prone...
o
How so
Copy code
value class UserId(private val value: Int)

fun sendUser1(userId: UserId) = ...
fun sendUser2(userId: Int) = ...

// Compiler error: required: UserId, found: Int. Makes sense because an Int isn't always a UserId.
sendUser1(1)

// Compiler error: required: Int, found: UserId. Does not make sense because a UserId is always an Int.
sendUser2(UserId(1))
👀 1
w
I’d say what you request is just against the philosophy behind value classes — they are different types, period. If that’s just a temporary state (you mentioned it makes it harder to adopt value classes), why not just write
userId.value
?
o
@wasyl
userId.value
is the compromise I decided to make in the end, yes, but it's ugly (requires to remove the private modifier too) and feels unnecessary. But perhaps that isn't an issue for partial refactors I guess. A UserId with supertype Int makes a UserId still a different type than the Int, so it doesn't change anything on that part.
k
Imagine a legacy function
setMonth(mm: Int)
, and a
value class Year(private val yyyy: Int)
. Then you could erroneously type
setMonth(Year(1922))
- we don't want the compiler to accept that, but it would if all Years were Ints.
☝️ 5
c
If it worked like you wanted, how would value classes with multiple attributes behave?
o
I see the big picture now, thanks