Dart's 'Never' Type & JS Interop: Why It's A Problem

by Henrik Larsen 53 views

Hey guys! Let's dive into a quirky corner of Dart when it meets JavaScript interop. It's about a type called Never and why it's causing a bit of a headache when we're trying to bridge Dart and JavaScript code. So, what's the fuss all about? Let's break it down and make it super clear, even if you're not a Dart wizard.

The Curious Case of Never in Dart-JS Interop

In Dart, Never is this special type that basically means "this function is never going to return normally." Think of functions that always throw an error or get stuck in an infinite loop. They never complete their execution and hand back a value. Now, when we're using Dart's dart:js_interop library to talk to JavaScript, things get a little tricky with Never.

The problem arises when you try to convert a Dart function that returns Never into a JavaScript function using .toJS. The Dart compiler throws a fit, and you'll see an error message complaining about invalid types in the function signature. Let's look at an example to make this crystal clear:

import 'dart:js_interop';

void main() {
  (() => throw "oh no").toJS;
}

If you run this code, you'll get hit with an error that looks something like this:

Error: Function converted via 'toJS' contains invalid types in its function signature: '*Never* Function()'.
  (() => throw "oh no").toJS;
                        ^

This error basically says, "Hey, I can't handle a function that returns Never when you're trying to make it a JavaScript function!" It's like trying to fit a square peg in a round hole. Dart's Never doesn't quite translate neatly into JavaScript's type system.

So, why is this happening? Well, JavaScript doesn't have a direct equivalent to Dart's Never type. JavaScript functions can throw errors, but they don't explicitly declare that they will never return normally. This mismatch between the two type systems is the root of our problem. To fully grasp this, we need to dive a bit deeper into what Never means in Dart and how types are handled in JavaScript interop.

Understanding Never in Dart

In Dart, Never is a bottom type. This means it's a subtype of all other types. It represents the return type of a function that never completes normally. This could be due to throwing an exception, entering an infinite loop, or explicitly exiting the program. Functions that return Never are useful for signaling that a particular code path is unreachable or that an error condition has occurred.

For example:

Never throwError(String message) {
  throw Exception(message);
}

void main() {
  print('This will be printed.');
  throwError('Something went wrong!');
  print('This will not be printed.'); // Unreachable code
}

In this example, throwError has a return type of Never because it always throws an exception. The second print statement will never be executed because throwError prevents the function from returning normally. This is a powerful concept in Dart for expressing control flow and error handling. Understanding Never is crucial for writing robust and maintainable Dart code.

The JavaScript Interop Challenge

Now, let's bring JavaScript into the picture. JavaScript's type system is more dynamic and less strict than Dart's. While JavaScript has ways to indicate that a function might throw an error, it doesn't have a built-in type that perfectly mirrors Dart's Never. This is where the impedance mismatch occurs. When you try to use .toJS to convert a Dart function with a Never return type to a JavaScript function, the Dart compiler struggles to find a suitable JavaScript type to represent Never. This results in the error we saw earlier.

The Dart team has worked hard to make JavaScript interop as seamless as possible, but there are inherent limitations due to the differences between the two languages. Bridging these gaps requires careful consideration of how types are translated and what the expected behavior should be. In the case of Never, the lack of a direct equivalent in JavaScript makes the conversion problematic.

The Workaround: A Bit of a Bodge

Okay, so we've established that Never and JavaScript interop aren't exactly best buddies. But fear not! There's a workaround, albeit a slightly clunky one. Instead of using an anonymous function that implicitly returns Never, you can define a named function with a void return type. This sidesteps the issue because void is a more palatable type for JavaScript interop.

Here's how it looks:

import 'dart:js_interop';

void main() {
  void throwError() {
    throw "oh no";
  }
  
  throwError.toJS;
}

In this example, we've defined a function throwError with a void return type. Even though throwError always throws an exception and never actually returns, declaring it as void makes the Dart compiler happy when we use .toJS. This workaround allows the code to compile and run without errors.

Why This Feels a Bit Annoying

While this workaround gets the job done, it's not ideal. It feels a bit like we're lying to the type system. We know the function never returns, but we're telling the compiler it returns void. This can lead to code that's less clear and harder to reason about. It also means we're losing some of the type safety that Dart provides.

More importantly, this workaround highlights a deeper issue: we can't accurately type a JavaScript function that can't return. If we want to be precise about our types, we should be able to express the concept of a function that never returns in our JavaScript interop code. This limitation makes it harder to write robust and well-typed code that bridges Dart and JavaScript.

The Real Problem: Accurate Typing in JS Interop

The core issue here is that we're losing fidelity in our type system when we cross the Dart-JavaScript boundary. We want to be able to express the full range of Dart types in our JavaScript interop code, including Never. This allows us to write more accurate type signatures and catch potential errors at compile time.

Imagine a scenario where you're working with a JavaScript library that has a function that throws an error under certain conditions. Ideally, you'd want to represent this function in Dart with a return type that reflects this behavior, perhaps something that indicates it might not return normally. Without proper support for Never or a similar concept, you're forced to use less precise types, which can make your code more prone to errors.

Accurate typing is crucial for maintaining code quality and preventing runtime surprises. When we can't accurately represent the behavior of JavaScript functions in Dart, we're essentially flying blind. This can lead to bugs that are difficult to track down and fix.

What's the Solution? A Call for Better Interop

So, what's the solution? Well, the Dart team needs to find a way to better represent Never (or a similar concept) in JavaScript interop. This might involve introducing a new type or finding a clever way to map Dart's Never to an existing JavaScript construct. The goal is to allow developers to accurately type JavaScript functions that might not return normally.

This is not a trivial problem, as it requires careful consideration of both Dart's and JavaScript's type systems. However, it's an important step towards making Dart's JavaScript interop more robust and user-friendly. A better solution would allow us to write code that's both type-safe and expressive, making it easier to bridge the gap between Dart and JavaScript.

The Future of Dart and JavaScript

As Dart continues to evolve and become a more popular choice for web and mobile development, the need for seamless JavaScript interop will only grow. The ability to accurately represent types across the Dart-JavaScript boundary is essential for building complex and reliable applications. Addressing the Never issue is just one piece of the puzzle, but it's a crucial one.

The Dart team is constantly working to improve the language and its ecosystem. By tackling challenges like this, they're making Dart an even more powerful and versatile tool for developers. So, while the current situation with Never might be a bit annoying, there's reason to be optimistic about the future of Dart and JavaScript interop. The future is bright for Dart developers!

In Conclusion: Let's Make Never a First-Class Citizen in JS Interop

In summary, the fact that Never can't be directly used as a return type in Dart's JavaScript interop is a pain point. It forces us to use workarounds that compromise type safety and makes it harder to accurately represent JavaScript functions in Dart. The core issue is the mismatch between Dart's strict type system and JavaScript's more dynamic one.

While there's a workaround, it's not ideal. We need a better solution that allows us to express the concept of a function that never returns in our JavaScript interop code. This will lead to more robust, maintainable, and type-safe applications. It's time to make Never (or its equivalent) a first-class citizen in Dart's JavaScript interop. Let's hope the Dart team is listening!

So, what are your thoughts on this? Have you run into this issue yourself? Let's chat in the comments!