I think I may have already seen this suggestion at...
# language-proposals
j
I think I may have already seen this suggestion at one point, but I could make my multiplatform libraries lighter weight with it - I need a
type
.
// expect files only
expect type ViewNode
expect type TextInput: ViewNode
expect val TextInput.content: String
// actual files only
actual typealias ViewNode = HTMLElement  // js
actual typealias ViewNode = View  // android
actual typealias ViewNode = UIView  // ios
actual typealias TextInput = HTMLInputElement  // js
actual typealias TextInput = EditText  // android
actual typealias TextInput = UITextField  // ios
actual fun TextInput.content get() = this.value  // js
actual fun TextInput.content get() = this.text  // android
actual fun TextInput.content get() = this.text  // ios
type
would have no restrictions about what the actual is - it could be a
class
,
interface
,
open class
, or more. Consequently, you wouldn't be able to use the type for anything other than an input, output, or receiver to a function.
2
c
Consequently, you wouldn’t be able to use the type for anything other than an input, output, or receiver to a function.
You aren’t creating “types” here, only type aliases; any aliases for the same type (and that type itself) are interchangeable. For example, aliasing MyString = String - anywhere that can use a String can use a MyString, and conversely anywhere that uses MyString can take a String. Value classes are the pattern for creating almost-zero-abstraction types where the compiler enforces type safety.
j
That's a fair point, and I'd be willing to use value classes, but they don't work with expect/actual at the moment I think. That would be a better solution. I haven't found any combination of syntax yet that allows for a value class with different underlying types per platform. Am I missing something there?
r
Could you give an example of how this would be useful? I'm having a hard time imagining a scenario where it would be beneficial to have a type you can't do anything with. What good is it as an parameter or result of a function if you can't actually use it? It seems you could only have pass through functions.
j
Look at the example I already provided - I could declare native UI components across platforms and use them directly, rather than using a wrapping class. The restrictions only need to exist in common code - once you're in platform code, you can do whatever you need to with the original type, knowing it's a class/interface/something else.
r
I get that, but I'm still not seeing what benefit you could get by putting the types in common if they can't be used in the common code.
Could you give an example of such a function in common that would benefit from having these types?
j
Copy code
// common code
expect typealias ViewNode
expect typealias TextInput: ViewNode
expect fun textInput(): TextInput
expect fun col(setup: ()->Unit): ViewNode
expect fun button(label: String, action: ()->Unit): ViewNode
expect var TextInput.text: String
fun sample() {
    col {
        val email = textInput()
        val password = textInput()
        button("submit") {
            callApi(email.text, password.text)
        }
    }
}
r
Sorry, I misread
j
no worries - hope that makes more sense
r
It does indeed, thanks. Can you elaborate on what you mean by "lighter weight"? Do wrapper classes introduce a significant amount of processing or memory overhead? I guess I can see how that might happen if your code handles a whole lot of `ViewNode`s at once, but I'm doubtful it would be significant compared to the allocations for the underlying types.
j
Most UIs have a lot of view nodes, so it seems reasonable. But more importantly... binary size. The wrapper classes take space in the binary that these wouldn't, especially if we can mark the actuals as inlined.
r
Ah, I see. That makes sense. Thanks for explaining
y
Copy code
// expect files only
value class ViewNode internal constructor(internal val underlying: Any?)
value class TextInput internal constructor(val viewMode: ViewNode)
expect val TextInput.content: String
// actual files only
fun HTMLElement.toViewNode(): ViewNode = ViewNode(this) // js
val ViewNode.htmlElement get() = underlying as HTMLElement // js
fun View.toViewNode(): ViewNode = ViewNode(this) // android
val ViewNode.view get() = underlying as View // android
fun UIView.toViewNode(): ViewNode = ViewNode(this) // ios
val ViewNode.uiView get() = underlying as UIView // ios
fun HTMLInputElement.toTextInput(): TextInput = TextInput(toViewNode()) // js
val TextInput.htmlInputElement get() = viewMode.htmlElement as HTMLInputElement // js
fun EditText.toTextInput(): TextInput = TextInput(toViewNode()) // android
val TextInput.editText get() = viewMode.view as EditText // android
fun UITextField.toTextInput(): TextInput = TextInput(toViewNode()) // ios
val TextInput.uiTextField get() = underlying as UITextField // ios
actual fun TextInput.content get() = htmlInputElement.value  // js
actual fun TextInput.content get() = editText.text  // android
actual fun TextInput.content get() = uiTextField.text  // ios