Inline classes instantiation should not need a constructor

For this inline class:

inline class Password(val value: String)
val securePassword = Password("Don't try this in production")

Why does it need a constructor? won’t it be simpler to just write:

val securePassword: Password = "Don't try this in production"
//val simpleString: String = SecurePassword //can't do that. not the same type

Am I missing here something?

If I’m understanding correctly, you are wondering why inline classes do not allow for implicitly construction*.

Just to make sure we’re on the same page:

  • This request could include any class with a single param (e.x. class Password(val value: String) but we can limit it to inline classes if the argument would be stronger.
  • Because it’s an inline class there will not be an init block.
  • Typealias do allow your example code:
typealias Email = String
val email: Email = "Don't try this in production" // This works

inline class Password(val value: String)
val securePassword: Password = "Don't try this in production" // This does not work

So you ask why would this not be allowed if it’s simpler?
I’d reword “simpler” to “fewer characters” or “more concise”, whichever is more convincing.

I think the reason to not allow this is that it makes things less clear. Although it is more concise, it blurs the meaning of an inline class IMO. The whole point of using a wrapping class over a typealias is to prevent code similar to this example.


Here’s my take. I talk about inline types but I would apply the same reasoning to a normal wrapping class.

One of the primary goals of inline types is to make them distinctly different things than their wrapped type. We don’t want the consumer assigning to/from, using in place of, or comparing inline types to their backing type without code explicitly allow it.

This is why inline classes differ from typealias. Both inline classes and typealias create a class that can be used to name a primitive type. And both can be used as targets for methods/params/generics (anywhere a type is used).

But unlike typealias, with inline classes we get to say that our inline class is not its backing type. No applying code that wasn’t explicitly written to handle our inline class, no assignments to the encoded type, no in-place-of’s, no implicitly conversions, no treating Password like it’s a String– all because we don’t want that as the author (otherwise we would use typealias).

When you remove the explicit constructor call for inline classes like in your suggestion, you make the distinction a little more blurry to the reader. One might read it and think that a Password is a string (or at least could be considered as such). While we as the authors of the class know that Password is backed by a String, we’ve made the conscious decision to block the consumer from writing code that treats it as a String.

If we instead wanted consumers to know about the connection between Password and it’s internal encoding as a String, we can use typealias. Otherwise, we use a wrapping class–If that class is encoded as a primitive or String, it’s probably a good idea to make that wrapper an inline class for performance reasons.

TL:DR
We don’t want the consumer to consider Password's internal encoding over its actual type. Inline types force this distinction in code and the way we construct an inline classes should not suggest to the reader that String is any different than a normal constructor param.

3 Likes

Simply, the only reason that inline classes exist is to allow for constructing that class in specific ways and to convert between different inline classes explicitly. For example, consider that we have 2 inline classes: Minutes, and Seconds. Both of these are backed under the hood by an Double property. To convert between them, there’s a toMinutes method on Seconds, and a toSeconds method on Minutes. These methods divide and multiply as appropriate. If there was implicit conversion however, then a function that takes a Minutes parameter would accept a Seconds parameter and treat it as minutes (i.e. if you had a Seconds of value 30 and there was implicit conversion, you could pass it to a function that takes Minutes and then it would think that it’s a minutes of value 30, which is obviously problematic.)

If you do really want implicit conversion, use a type alias.