Skip to content

Confusing list type mismatch at runtime #3185

Closed
@matthew-carroll

Description

@matthew-carroll

I ran into a runtime type mismatch for lists that I expected to be of the same type. I'll provide a repro of the issue, and then I'll explain why this was confusing.

The following code sample reproduces the issue in Dart Pad.

void main() {
  print("Start");
  
  final editor = Editor(
    requestHandlers: defaultRequestHandlers,
  );
  
  editor.requestHandlers.insertAll(0, [
    (request) => Command2(),
  ]);
  
  print("End");
}

class Editor {
  Editor({
    List<EditRequestHandler>? requestHandlers,
  }) : requestHandlers = requestHandlers ?? [];
  
  final List<EditRequestHandler> requestHandlers;
}

class Request1 implements EditRequest {}

class Command1 implements EditCommand {}

class Request2 implements EditRequest {}

class Command2 implements EditCommand {}

class EditRequest {}

class EditCommand {}

typedef EditRequestHandler = EditCommand? Function(EditRequest);

final defaultRequestHandlers = [
  (request) => request is Request1
    ? Command1()
    : null,
];

Running this code results in the following error:

Start
Uncaught Error: TypeError: Instance of 'JSArray<(EditRequest) => EditCommand?>': type 'JSArray<(EditRequest) => EditCommand?>' is not a subtype of type 'Iterable<(dynamic) => Command1?>'

The fix for this issue is to make the following change:

// Change
final defaultRequestHandlers = [
  (request) => request is Request1
    ? Command1()
    : null,
];

// To
final defaultRequestHandlers = <EditRequestHandler>[
  (request) => request is Request1
    ? Command1()
    : null,
];

It looks like, in the absence of an explicit type declaration, the Dart runtime chose an implied list generic type from the first item that happened to be declared in the defaultRequestHandlers list, which seems to then be tracked into the Editor instance. By pushing the narrower implied type into the Editor, insertion operations on the editor's requestHandlers list fail when they're expected to succeed.

I can understand why Dart may have implied a List generic type based on the first item in the list, or based on a union of the items in the list, but this situation is a problem for at least a couple reasons.

First, I received no compiler complaint. This was a very confusing bug in my application and it took a lot of guessing to figure out why Dart was blowing up at runtime.

Second, the error message, while probably accurate, barely helped me comprehend the issue. All of my type contracts seemed correct and yet, mysteriously, I was losing the parameter type at runtime.

Metadata

Metadata

Assignees

No one assigned

    Labels

    requestRequests to resolve a particular developer problem

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions