Search code examples
flutterdartdart-async

Why doesn't piping nullable stream into a non-nullable stream controller cause a runtime but not a compile-time error?


The following code compiles:

import 'dart:async';

void main() {
  final streamNullable = StreamController<int?>();
  final streamNotNullable = StreamController<int>();
  
  streamNullable.stream.pipe(streamNotNullable);
}

But will produce a runtime error:

TypeError: Instance of '_ControllerStream<int?>': type '_ControllerStream<int?>' is not a subtype of type 'Stream<int>'

... which is totally expected because we are trying to pipe nullable values into a non-nullable sink.

However, it surprises me that this doesn't cause a compile time error! Why didn't the type-system catch this?

As a side-note, this is only happening with a difference of the nullable-qualifier, not if I pass completely different types. For example:

final streamNullable = StreamController<int?>();
final streamNotNullable = StreamController<double>();
  
streamNullable.stream.pipe(streamNotNullable);

does not compile.


Solution

  • That is an edge case in the lsp, to simplify it a bit, assume that null is not a type but the absence of an Object. Because the lsp is confined to dart code, native code and vm runtime definitions are outside of it's view and the lsp must assume that it 'just works' after passing the basic checks.

    Can the lsp just be more strict?, no, the is check can't compare with null.

    You can expect this behavior when a function is marked with external.

      int? a = 1;
      print('a is int - ${a is int}'); // warning - always true
    
    
      final sn = <int?>[null];
    
      Object v;
      v = List<num?>.from(sn);    // valid compile / valid runtime
      v = List<double?>.from(sn); // valid compile / valid runtime
      v = List<int>.from(sn);     // valid compile / runtime error
      v = List<num>.from(sn);     // valid compile / runtime error
      v = List<double>.from(sn);  // valid compile / runtime error