Would like a way to avoid raw generic types

Tangentially related to #31410.

While migrating swaths of internal code (probably in the tens-of-thousands-of-lines, myself), one of the most common errors users are running into when migrating to Dart2 are so-called “naked” raw types (is there a better name?). For example:

// Works as the user intended.
// List<int> x = <int>[1];
var x = [1];
// Does _not_ work as the user intended.
// List<dynamic> x = <dynamic>[1];
List x = [1];

Here is a more extreme example:

typedef Iterable<CacheEntry<K, V>> CacheEvictionFunction<K, V>(
  List<CacheEntry<K, V>> entries, 
  int entriesToBeTrimmed,
);

class CacheConfig {
  // In Dart1 this was "fine".
  // In Dart2 this fails, quite late, at runtime:
  // 
  // CacheEvictionFunction<dynamic, dynamic> "inferred"
  final CacheEvictionFunction _evictionImpl;

  CacheConfig(this._evictionImpl);
}

My 🔥 take: In Dart 2 + some flag, “raw” types that are implicitly dynamic should:

  • Be a compile-time error (“You must specify a type or “)
  • Work similar to Java’s <?> (wildcard operator). That would mean:
// --no-implicit-dynamic
//
// List<int> x = <int>[1];
List x = [1];
// --no-implicit-dynamic
//
// Compile-time error: Must specify a type 'T' for List<T>
List x;
// --no-implicit-dynamic
//
// OK.
List<dynamic> x;

… if we are accepting name nominations, I nominate --strict-raw-types.

Author: Fantashit

5 thoughts on “Would like a way to avoid raw generic types

  1. so-called “naked” types (is there a better name?)

    Java calls them “raw types”, which I hear on the Dart team sometimes.

    My 🔥 take: In Dart 2 + some flag, “naked” types that are implicitly dynamic should:

    Be a compile-time error (“You must specify a type or “)

    I agree, it should be a compile error. We need to be clear about the several different places a raw type (or a thing that looks like a raw type) can appear. Here’s the behavior I’d like:

    // Error, raw type in annotation:
    Set a = null;
    
    // Error, raw type nested in annotation:
    List<Set> b = null;
    
    // No error, bare class name has type arguments inferred:
    Set<int> c = new Set();
    
    // Error, could not infer type argument for bare class name:
    var d = new Set();
    
    // Error, type inference does not fill in "half-complete" type annotations:
    List<Set<int>> e = new List<Set>();

    As for naming the flag, maybe --strict-raw-types?

  2. Hit another case internally. Accidental omission of type parameters extending/implementing:

    abstract class HasValue<T> {
      T get value;
    }
    
    class WizBang extends HasValue {
      @override
      String get value => 'WizBang';
    }

    This is totally valid code, and nothing is flagged in the analyzer or otherwise.

    At runtime, trying to use a WizBang as a HasValue<String> (or similar) fails. It took quite a bit of groking to finally understand that HasValue had a type parameter T that was omitted. I’d like to see our hypothetical --strict-raw-types warn if a type with type parameters is implemented without bounds or “passing through” params:

    // OK
    class WizBang extends HasValue<String> {}
    
    // OK
    class WizBang<T> extends HasValue<T> {}
    
    // Still OK, at least it is explicit
    class WizBang extends HasValue<dynamic> {}
    
    // BAD
    class WizBang extends HasValue {}
  3. Hit a particularly nasty case internally that took 3 of us ~2 hours to resolve:

    class Manager {
      Response<SubContext> getResponse() {
        return new Response<SubContext>(new SubContext(), (_) => new Response<SubContext>(_, null));
      }
    }
    
    typedef Callback<T extends Context> = Response<T> Function(T context);
    
    class Context {}
    
    class SubContext extends Context {}
    
    class Response<T extends Context> {
      final T context;
      final Callback<T> callback;
      Response(this.context, this.callback);
    }
    
    void main() {
      Manager manager = new Manager();
      Response<Context> response = manager.getResponse();
      
      // Response<SubContext>
      print(response.runtimeType);
      
      // Uncaught exception:
      // TypeError: Closure 'Manager_getResponse_closure': 
      //   type '(SubContext) => Response<SubContext>' is not a subtype of type '(Context) => Response<Context>'
      print(response.callback.runtimeType);
    }
    

    See if you can spot the source of the error!

    This is because raw type of Response is a Response<Context>, not a Response<SubContext>. Even though there is very judicious use of type arguments across (as far as the internal team thought), it is actually weaker than using var (using var response1 = actually fixes the bug).

    Why? Because response.callback is statically saying give me a Callback<Context>, though, at runtime, we’ve encoded a Callback<SubContext>, and contravariance law says this is a bad. Wamp wamp.

  4. We’ll need to add official documentation for this, but for now the instructions are:

    strict-inference and strict-raw-types can be configured via the language section of the analysis_options.yaml file, for example:

    analyzer:
      language:
        strict-inference: true
        strict-raw-types: true

Comments are closed.