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

In other words, how do I implement type-specific solutions for different types in a union type set?

Given the following code...

type FieldType interface {
    string | int
}

type Field[T FieldType] struct {
    name         string
    defaultValue T
}

func NewField[T FieldType](name string, defaultValue T) *Field[T] {
    return &Field[T]{
        name:         name,
        defaultValue: defaultValue,
    }
}

func (f *Field[T]) Name() string {
    return f.name
}

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

the compiler shows the error:

field.go:37:9: cannot use value (variable of type string) as type T in return statement

Is there a way to provide implementations for all possible FieldTypes?

Like...

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

func (f *Field[int]) Get() (int, error) {
    raw, ok := os.LookupEnv(f.name)
    if !ok {
        return f.defaultValue, nil
    }
    value, err := strconv.ParseInt(raw, 10, 64)
    if err != nil {
        return *new(T), err
    }
    return int(value), nil
}

Any hint would be welcome.



Solution 1:[1]

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/LHqizyNL9lP

Solution 2:[2]

Ok, the type switch works if reflections are used.

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

    v := reflect.ValueOf(new(T))

    switch v.Type().Elem().Kind() {
    case reflect.String:
        v.Elem().Set(reflect.ValueOf(raw))

    case reflect.Int:
        value, err := strconv.ParseInt(raw, 10, 64)
        if err != nil {
            return f.defaultValue, err
        }
        v.Elem().Set(reflect.ValueOf(int(value)))
    }

    return v.Elem().Interface().(T), nil
}

But better solutions are very welcome ;-)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 phil