Search code examples
kotlinkotlin-js-interop

How to represent multiple types (union types) when targeting JavaScript


What I would like to do is use a generic type that can be one of three other types.

Here's an example with a function:

fun <T> get(key: String) : T where T: String, T: Number, T: Boolean {}

The above code doesn't work, so how should I go about doing this?


Solution

  • For KotlinJS you can use ts2kt to translate your TypeScript definitions to Kotlin. It does support Union Types but maybe not all cases are perfect. There are tests for unionTypes in ts2kt that shed light on how they are handled now and you can do something similar for anything you are creating by hand when targetting the JavaScript platform.

    Further work is mentioned in comments to issue #41 - to add better Union Type support. Lastly there is at least one discussion thread on the topic that indicates:

    In JS and TS in most cases union types used as the alternative to overloading, so in the near future, we going to use overloading for that when it possible. Also,​ we think about providing the additional way to specify union types for native declarations.

    There is another Stack Overflow question talking about this and gives a few current options: Kotlin and discriminated unions (sum types) which has answers that are valid across all Kotlin target platforms.

    Specifically for the JavaScript target you can consider using the dynamic type. I see at least one test case in ts2kt that is using this type. This example starts with this TypeScript code:

    declare class Foo
    
    type Key = Key2 | number;
    type Key2 = string | Foo;
    declare var fooKey: Key;
    
    declare function barKey(a: Key|number);
    declare function barList(a: List<Key>);
    declare function barArray(a: Key[]);
    
    interface Parent {
        (...children: Key[]): Foo;
    }
    

    And generates this Kotlin using dynamic as a return type in place of the union type; and in other cases overloading the method signature to handle the union type (some comments added by me):

    external open class Foo
    
    // using dynamic in place of union type
    external var fooKey: dynamic /* String | Foo | Number */ = definedExternally
    
    // using method overloading in place of union type
    external fun barKey(a: String): Unit = definedExternally
    external fun barKey(a: Foo): Unit = definedExternally
    external fun barKey(a: Number): Unit = definedExternally
    
    // using dynamic in place of union type
    external fun barList(a: List<dynamic /* String | Foo | Number */>): Unit = definedExternally
    external fun barArray(a: Array<dynamic /* String | Foo | Number */>): Unit = definedExternally
    
    external interface Parent {
        // using method overloading in place of union type
        @nativeInvoke
        fun invoke(vararg children: String): Foo
        @nativeInvoke
        fun invoke(vararg children: Foo): Foo
        @nativeInvoke
        fun invoke(vararg children: Number): Foo
    }
    

    But again, you should review all of the ts2kt test cases for union types to see other ideas including for handling of undefined.