dartgenericsvariance

Workaround for function type invariance in heterogeneous list


I have the following Dart code

void main() {
  final filters = [
    FilterOption(
      label: 'Category',
      hint: 'All categories',
      items: A.values,
      onChange: (_) => print('Not implemented yet'),
      itemLabeler: (item) => item.toString().toUpperCase()
    ),
    FilterOption(
      label: 'Time',
      hint: 'All time',
      items: B.values,
      onChange: (_) => print('Not implemented yet'),
      itemLabeler: (item) => item.toString().toLowerCase()
    )
  ];
  try {
    filters.first.itemLabeler;
  } catch(e) {
    print(e);
  }
  print('Program terminated');
}

enum A {
  
  here, are, some, options
  
}

enum B {
  
  here, are, another, few, options
  
}

class FilterOption<T> {

  final String label;

  final String hint;

  final List<T> items;

  final void Function(T item) onChange;

  final String Function(T item) itemLabeler;

  const FilterOption({
    required this.label,
    required this.hint,
    required this.items,
    required this.onChange,
    required this.itemLabeler
  });

}

This is the output on DartPad

TypeError: Instance of '(A) => String': type '(A) => String' is not a subtype of type '(_Enum) => String'
Program terminated

I am aware of similar questions on StackOverflow, however, many either don't have answers to the problem or suggest implementation outside of a list. I would like to be able to send any number of FilterOptions to some UI element to display. I would also like the generic type to be any data type, not just enums. How can I solve this problem? Thank you to anyone who can offer any insight or solution.


Solution

  • Assuming filters are not just on enums, and don't have a common supertype, there are a few things you can try.

    First, you can try to cast the FilterOption before using it.

    void main() {
      final filters = [
        FilterOption(
          label: 'Category',
          hint: 'All categories',
          items: A.values,
          onChange: (_) => print('Not implemented yet'),
          itemLabeler: (item) => item.toString().toUpperCase(),
        ),
        FilterOption(
          label: 'Time',
          hint: 'All time',
          items: B.values,
          onChange: (_) => print('Not implemented yet'),
          itemLabeler: (item) => item.toString().toLowerCase(),
        ),
      ];
      try {
        (filters.first as FilterOption<A>).itemLabeler;
      } catch (e) {
        print(e);
      }
      print('Program terminated');
    }
    
    enum A { here, are, some, options }
    
    enum B { here, are, another, few, options }
    
    class FilterOption<T> {
    
      final String label;
    
      final String hint;
    
      final List<T> items;
    
      final void Function(T item) onChange;
    
      final String Function(T item) itemLabeler;
    
      const FilterOption({
        required this.label,
        required this.hint,
        required this.items,
        required this.onChange,
        required this.itemLabeler
      });
    
    }
    

    Second, you can promote by testing with is or pattern matching with case before using.

    void main() {
      final filters = [
        FilterOption(
          label: 'Category',
          hint: 'All categories',
          items: A.values,
          onChange: (_) => print('Not implemented yet'),
          itemLabeler: (item) => item.toString().toUpperCase(),
        ),
        FilterOption(
          label: 'Time',
          hint: 'All time',
          items: B.values,
          onChange: (_) => print('Not implemented yet'),
          itemLabeler: (item) => item.toString().toLowerCase(),
        ),
      ];
      try {
        // copy to local variable then check with is
        var first = filters.first;
        if (first is FilterOption<A>) {
          first.itemLabeler;
        }
        // check with pattern match
        if (filters.last case FilterOption<B> last) {
          last.itemLabeler;
        }
      } catch (e) {
        print(e);
      }
      print('Program terminated');
    }
    
    enum A { here, are, some, options }
    
    enum B { here, are, another, few, options }
    
    class FilterOption<T> {
      final String label;
    
      final String hint;
    
      final List<T> items;
    
      final void Function(T item) onChange;
    
      final String Function(T item) itemLabeler;
    
      const FilterOption({
        required this.label,
        required this.hint,
        required this.items,
        required this.onChange,
        required this.itemLabeler,
      });
    }
    

    Third, if you have a limited number of types you want to use, say Enum, String, and int, you could maybe get away with FilterOption a sealed class. This would allow you to exhaustively cover all options when switching over the data.

    void main() {
      List<FilterOption> filters = [
        FilterOptionEnum(
          label: 'Category',
          hint: 'All categories',
          items: A.values,
          onChange: (_) => print('Not implemented yet'),
          itemLabeler: (item) => item.toString().toUpperCase(),
        ),
        FilterOptionEnum(
          label: 'Time',
          hint: 'All time',
          items: B.values,
          onChange: (_) => print('Not implemented yet'),
          itemLabeler: (item) => item.toString().toLowerCase(),
        ),
        FilterOptionString(
          label: 'String',
          hint: 'All string',
          items: ['hello', 'world'],
          onChange: (_) => print('Not implemented yet'),
          itemLabeler: (item) => item.toString().toLowerCase(),
        ),
      ];
      try {
        switch (filters.first) {
          case FilterOptionEnum<A> a:
            a.itemLabeler;
          case FilterOptionEnum<B> b:
            b.itemLabeler;
          case FilterOptionEnum e:
            e.itemLabeler;
          case FilterOptionInt i:
            i.itemLabeler;
          case FilterOptionString s:
            s.itemLabeler;
        }
      } catch (e) {
        print(e);
      }
      print('Program terminated');
    }
    
    enum A { here, are, some, options }
    
    enum B { here, are, another, few, options }
    
    sealed class FilterOption<T> {
      final String label;
    
      final String hint;
    
      final List<T> items;
    
      final void Function(T item) onChange;
    
      final String Function(T item) itemLabeler;
    
      const FilterOption({
        required this.label,
        required this.hint,
        required this.items,
        required this.onChange,
        required this.itemLabeler,
      });
    }
    
    class FilterOptionEnum<T extends Enum> extends FilterOption<T> {
      FilterOptionEnum({
        required super.label,
        required super.hint,
        required super.items,
        required super.onChange,
        required super.itemLabeler,
      });
    }
    
    class FilterOptionInt extends FilterOption<int> {
      FilterOptionInt({
        required super.label,
        required super.hint,
        required super.items,
        required super.onChange,
        required super.itemLabeler,
      });
    }
    
    class FilterOptionString extends FilterOption<String> {
      FilterOptionString({
        required super.label,
        required super.hint,
        required super.items,
        required super.onChange,
        required super.itemLabeler,
      });
    }
    

    Fourth, you can try to wrap the callback functions in method calls:

    void main() {
      final filters = [
        FilterOption(
          label: 'Category',
          hint: 'All categories',
          items: A.values,
          onChange: (_) => print('Not implemented yet'),
          itemLabeler: (item) => item.toString().toUpperCase(),
        ),
        FilterOption(
          label: 'Time',
          hint: 'All time',
          items: B.values,
          onChange: (_) => print('Not implemented yet'),
          itemLabeler: (item) => item.toString().toLowerCase(),
        ),
      ];
      try {
        filters.first.itemLabeler;
      } catch (e) {
        print(e);
      }
      print('Program terminated');
    }
    
    enum A { here, are, some, options }
    
    enum B { here, are, another, few, options }
    
    class FilterOption<T> {
      final String label;
    
      final String hint;
    
      final List<T> items;
    
      final void Function(T item) _onChange;
    
      final String Function(T item) _itemLabeler;
    
      void onChange(T item) => _onChange(item);
      String itemLabeler(T item) => _itemLabeler(item);
    
      const FilterOption({
        required this.label,
        required this.hint,
        required this.items,
        required void Function(T item) onChange,
        required String Function(T item) itemLabeler,
      })  : _onChange = onChange,
            _itemLabeler = itemLabeler;
    }