How to assign or return generic T that is constrained by union?

The error occurs because operations that involve a type parameter (including assignments and returns) must be valid for all types in its type set.
In case of string | int, there isn’t a common operation to initialize their value from a string.

However you still have a couple options:

Type-switch on T

You use the field with the generic type T in a type-switch, and temporarily set the values with concrete types into an interface{}/any. Then type-assert the interface back to T in order to return it. Beware that this assertion is unchecked, so it may panic if for some reason ret holds something that isn’t in the type set of T. Of course you can check it with comma-ok but it’s still a run-time assertion:

func (f *Field[T]) Get() (T, error) {
    value, ok := os.LookupEnv(f.name)
    if !ok {
        return f.defaultValue, nil
    }
    var ret any
    switch any(f.defaultValue).(type) {
    case string:
        ret = value

    case int:
        // don't actually ignore errors
        i, _ := strconv.ParseInt(value, 10, 64)
        ret = int(i)
    }
    return ret.(T), nil
}

Type-switch on *T

You can further simplify the code above and get rid of the empty interface. In this case you take the address of the T-type variable and switch on the pointer types. This is fully type-checked at compile time:

func (f *Field[T]) Get() (T, error) {
    value, ok := env[f.name]
    if !ok {
        return f.defaultValue, nil
    }

    var ret T
    switch p := any(&ret).(type) {
    case *string:
        *p = value

    case *int:
        i, _ := strconv.ParseInt(value, 10, 64)
        *p = int(i)
    }
    // ret has the zero value if no case matches
    return ret, nil
}

Note that in both cases you must convert the T value to an interface{}/any in order to use it in a type switch. You can’t type-switch directly on T.

Playground with map to simulate os.LookupEnv: https://go.dev/play/p/JVBEZwCXRMW

Leave a Comment