[dart:js_interop/ddc/dart2js] Implement JS types using @staticInterop

Currently, dart:_js_types types are all typedefs in the web backends.
This leads to inconsistent semantics, since you can statically pass
Strings to JSString, for example. You cannot do this in dart2wasm.
In order to ensure consistent semantics, we reify these types using
a custom @staticInterop lowering. They all get erased to their
respective Dart type. When we have inline classes, these types
should be implemented using inline classes.

Note that Interceptor will not work for this use case. The reified
type of JS primitives are Dart types e.g. String, bool, and therefore
can not be casted to Interceptor.

In order to do this, the eraser is refactored and the JS backends use
shared erasure code to either erase/emit types.

Tests are added to make sure you need to go through a conversion or
cast to pass Dart objects to JS types.

CoreLibraryReviewExempt: Backend-specific internal library changes.
Change-Id: I5942be628102919ec167f094cfe10fced606363c
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/295105
Commit-Queue: Srujan Gaddam <srujzs@google.com>
Reviewed-by: Joshua Litt <joshualitt@google.com>
Reviewed-by: Nicholas Shahan <nshahan@google.com>
diff --git a/pkg/_js_interop_checks/lib/src/transformations/static_interop_class_eraser.dart b/pkg/_js_interop_checks/lib/src/transformations/static_interop_class_eraser.dart
index 9a68341..5aa2895 100644
--- a/pkg/_js_interop_checks/lib/src/transformations/static_interop_class_eraser.dart
+++ b/pkg/_js_interop_checks/lib/src/transformations/static_interop_class_eraser.dart
@@ -13,14 +13,97 @@
 import 'package:kernel/src/constant_replacer.dart';
 import 'package:kernel/src/replacement_visitor.dart';
 
+/// Erasure function for `dart:_js_types` `@staticInterop` types.
+InterfaceType transformJSTypesForJSCompilers(
+    CoreTypes coreTypes, InterfaceType staticInteropType) {
+  final className = staticInteropType.classNode.name;
+  Class erasedClass;
+  var typeArguments = staticInteropType.typeArguments;
+  switch (className) {
+    case 'JSAny':
+      erasedClass = coreTypes.objectClass;
+      break;
+    case 'JSObject':
+      erasedClass = coreTypes.index.getClass('dart:_interceptors', 'JSObject');
+      break;
+    case 'JSFunction':
+      erasedClass = coreTypes.functionClass;
+      break;
+    case 'JSExportedDartFunction':
+      erasedClass = coreTypes.functionClass;
+      break;
+    case 'JSArray':
+      erasedClass = coreTypes.listClass;
+      typeArguments = [coreTypes.objectNullableRawType];
+      break;
+    case 'JSExportedDartObject':
+      erasedClass = coreTypes.objectClass;
+      break;
+    case 'JSArrayBuffer':
+      erasedClass = coreTypes.index.getClass('dart:typed_data', 'ByteBuffer');
+      break;
+    case 'JSDataView':
+      erasedClass = coreTypes.index.getClass('dart:typed_data', 'ByteData');
+      break;
+    case 'JSTypedArray':
+      erasedClass = coreTypes.index.getClass('dart:typed_data', 'TypedData');
+      break;
+    case 'JSInt8Array':
+      erasedClass = coreTypes.index.getClass('dart:typed_data', 'Int8List');
+      break;
+    case 'JSUint8Array':
+      erasedClass = coreTypes.index.getClass('dart:typed_data', 'Uint8List');
+      break;
+    case 'JSUint8ClampedArray':
+      erasedClass =
+          coreTypes.index.getClass('dart:typed_data', 'Uint8ClampedList');
+      break;
+    case 'JSInt16Array':
+      erasedClass = coreTypes.index.getClass('dart:typed_data', 'Int16List');
+      break;
+    case 'JSUint16Array':
+      erasedClass = coreTypes.index.getClass('dart:typed_data', 'Uint16List');
+      break;
+    case 'JSInt32Array':
+      erasedClass = coreTypes.index.getClass('dart:typed_data', 'Int32List');
+      break;
+    case 'JSUint32Array':
+      erasedClass = coreTypes.index.getClass('dart:typed_data', 'Uint32List');
+      break;
+    case 'JSFloat32Array':
+      erasedClass = coreTypes.index.getClass('dart:typed_data', 'Float32List');
+      break;
+    case 'JSFloat64Array':
+      erasedClass = coreTypes.index.getClass('dart:typed_data', 'Float64List');
+      break;
+    case 'JSNumber':
+      erasedClass = coreTypes.doubleClass;
+      break;
+    case 'JSBoolean':
+      erasedClass = coreTypes.boolClass;
+      break;
+    case 'JSString':
+      erasedClass = coreTypes.stringClass;
+      break;
+    case 'JSPromise':
+      erasedClass = coreTypes.index.getClass('dart:_interceptors', 'JSObject');
+      break;
+    default:
+      throw 'Unimplemented `dart:_js_types`: $className';
+  }
+  return InterfaceType(
+      erasedClass, staticInteropType.declaredNullability, typeArguments);
+}
+
 class _TypeSubstitutor extends ReplacementVisitor {
-  final Class _javaScriptObject;
-  _TypeSubstitutor(this._javaScriptObject);
+  final InterfaceType Function(InterfaceType staticInteropType)
+      _eraseStaticInteropType;
+  _TypeSubstitutor(this._eraseStaticInteropType);
 
   @override
   DartType? visitInterfaceType(InterfaceType type, int variance) {
     if (hasStaticInteropAnnotation(type.classNode)) {
-      return InterfaceType(_javaScriptObject, type.declaredNullability);
+      return _eraseStaticInteropType(type);
     }
     return super.visitInterfaceType(type, variance);
   }
@@ -29,12 +112,16 @@
 /// Erases usage of `@JS` classes that are annotated with `@staticInterop` in
 /// favor of `JavaScriptObject`.
 class StaticInteropClassEraser extends Transformer {
-  final Class _javaScriptObject;
   final CloneVisitorNotMembers _cloner = CloneVisitorNotMembers();
   late final _StaticInteropConstantReplacer _constantReplacer;
   late final _TypeSubstitutor _typeSubstitutor;
   Component? currentComponent;
   ReferenceFromIndex? referenceFromIndex;
+  // Custom erasure function for `@staticInterop` types. This is useful for when
+  // they should be erased to another type besides `JavaScriptObject`, like in
+  // dart2wasm.
+  late final InterfaceType Function(InterfaceType staticInteropType)
+      _eraseStaticInteropType;
   // Visiting core libraries that don't contain `@staticInterop` adds overhead.
   // To avoid this, we use an allowlist that contains libraries that we know use
   // `@staticInterop`.
@@ -45,12 +132,22 @@
   };
 
   StaticInteropClassEraser(CoreTypes coreTypes, this.referenceFromIndex,
-      {String libraryForJavaScriptObject = 'dart:_interceptors',
-      String classNameOfJavaScriptObject = 'JavaScriptObject',
-      Set<String> additionalCoreLibraries = const {}})
-      : _javaScriptObject = coreTypes.index
-            .getClass(libraryForJavaScriptObject, classNameOfJavaScriptObject) {
-    _typeSubstitutor = _TypeSubstitutor(_javaScriptObject);
+      {InterfaceType Function(InterfaceType staticInteropType)?
+          eraseStaticInteropType,
+      Set<String> additionalCoreLibraries = const {}}) {
+    final dartJsTypes = Uri.parse('dart:_js_types');
+    _eraseStaticInteropType = eraseStaticInteropType ??
+        (staticInteropType) {
+          if (staticInteropType.classNode.enclosingLibrary.importUri ==
+              dartJsTypes) {
+            return transformJSTypesForJSCompilers(coreTypes, staticInteropType);
+          }
+          return InterfaceType(
+              coreTypes.index
+                  .getClass('dart:_interceptors', 'JavaScriptObject'),
+              staticInteropType.declaredNullability);
+        };
+    _typeSubstitutor = _TypeSubstitutor(_eraseStaticInteropType);
     _constantReplacer = _StaticInteropConstantReplacer(this);
     _erasableCoreLibraries.addAll(additionalCoreLibraries);
   }
@@ -178,8 +275,8 @@
       var newInvocation = super.visitConstructorInvocation(node) as Expression;
       return AsExpression(
           newInvocation,
-          InterfaceType(_javaScriptObject,
-              node.target.function.returnType.declaredNullability))
+          _eraseStaticInteropType(
+              node.target.function.returnType as InterfaceType))
         ..fileOffset = newInvocation.fileOffset;
     }
     return super.visitConstructorInvocation(node);
@@ -208,9 +305,7 @@
         // Add a cast so that the result gets typed as `JavaScriptObject`.
         var newInvocation = super.visitStaticInvocation(node) as Expression;
         return AsExpression(
-            newInvocation,
-            InterfaceType(_javaScriptObject,
-                node.target.function.returnType.declaredNullability))
+            newInvocation, node.target.function.returnType as InterfaceType)
           ..fileOffset = newInvocation.fileOffset;
       }
     }
diff --git a/pkg/compiler/lib/src/kernel/dart2js_target.dart b/pkg/compiler/lib/src/kernel/dart2js_target.dart
index b6a3b91..e04a298 100644
--- a/pkg/compiler/lib/src/kernel/dart2js_target.dart
+++ b/pkg/compiler/lib/src/kernel/dart2js_target.dart
@@ -112,7 +112,8 @@
         'dart:_late_helper',
         'dart:js',
         'dart:js_interop',
-        'dart:js_util'
+        'dart:js_util',
+        'dart:typed_data',
       ];
 
   @override
diff --git a/pkg/compiler/lib/src/phase/load_kernel.dart b/pkg/compiler/lib/src/phase/load_kernel.dart
index 84c8b91..008f3b3 100644
--- a/pkg/compiler/lib/src/phase/load_kernel.dart
+++ b/pkg/compiler/lib/src/phase/load_kernel.dart
@@ -133,7 +133,9 @@
   // containing a stub definition is invalidated, and then reloaded, because
   // we need to keep existing references to that stub valid. Here, we have the
   // whole program, and therefore do not need it.
-  StaticInteropClassEraser(ir.CoreTypes(component), null)
+  ir.CoreTypes coreTypes = ir.CoreTypes(component);
+  StaticInteropClassEraser(coreTypes, null,
+          additionalCoreLibraries: {'_js_types', 'js_interop'})
       .visitComponent(component);
 }
 
diff --git a/pkg/dart2wasm/lib/js/runtime_generator.dart b/pkg/dart2wasm/lib/js/runtime_generator.dart
index 207e7af..7387982 100644
--- a/pkg/dart2wasm/lib/js/runtime_generator.dart
+++ b/pkg/dart2wasm/lib/js/runtime_generator.dart
@@ -25,9 +25,10 @@
 
   // We want static types to help us specialize methods based on receivers.
   // Therefore, erasure must come after the lowering.
+  final jsValueClass = coreTypes.index.getClass('dart:_js_helper', 'JSValue');
   final staticInteropClassEraser = StaticInteropClassEraser(coreTypes, null,
-      libraryForJavaScriptObject: 'dart:_js_helper',
-      classNameOfJavaScriptObject: 'JSValue',
+      eraseStaticInteropType: (staticInteropType) =>
+          InterfaceType(jsValueClass, staticInteropType.declaredNullability),
       additionalCoreLibraries: {'_js_helper', '_js_types', 'js_interop'});
   for (Library library in interopDependentLibraries) {
     staticInteropClassEraser.visitLibrary(library);
diff --git a/pkg/dev_compiler/lib/src/kernel/compiler.dart b/pkg/dev_compiler/lib/src/kernel/compiler.dart
index 607a998..708d4f9 100644
--- a/pkg/dev_compiler/lib/src/kernel/compiler.dart
+++ b/pkg/dev_compiler/lib/src/kernel/compiler.dart
@@ -7,6 +7,8 @@
 import 'dart:io' as io;
 import 'dart:math' show max, min;
 
+import 'package:_js_interop_checks/src/transformations/static_interop_class_eraser.dart'
+    show transformJSTypesForJSCompilers;
 import 'package:collection/collection.dart'
     show IterableExtension, IterableNullableExtension;
 import 'package:front_end/src/api_unstable/ddc.dart';
@@ -3231,8 +3233,13 @@
         ? getLocalClassName(c)
         : _emitJsNameWithoutGlobal(c);
     if (jsName != null) {
-      typeRep = runtimeCall('packageJSType(#, #)',
-          [js.escapedString(jsName), js.boolean(isStaticInteropType(c))]);
+      if (isDartJSTypesType(c)) {
+        typeRep = visitInterfaceType(
+            transformJSTypesForJSCompilers(_coreTypes, type));
+      } else {
+        typeRep = runtimeCall('packageJSType(#, #)',
+            [js.escapedString(jsName), js.boolean(isStaticInteropType(c))]);
+      }
     }
 
     if (typeRep != null) {
diff --git a/pkg/dev_compiler/lib/src/kernel/js_interop.dart b/pkg/dev_compiler/lib/src/kernel/js_interop.dart
index a9663a5..b3a7b04 100644
--- a/pkg/dev_compiler/lib/src/kernel/js_interop.dart
+++ b/pkg/dev_compiler/lib/src/kernel/js_interop.dart
@@ -138,6 +138,12 @@
 // need to be `external` in an `@JS` class.
 bool hasJSInteropAnnotation(Class c) => c.annotations.any(isPublicJSAnnotation);
 
+/// Returns true iff [c] is a class from `dart:_js_types` implemented using
+/// `@staticInterop`.
+bool isDartJSTypesType(Class c) =>
+    _isLibrary(c.enclosingLibrary, const ['dart:_js_types']) &&
+    isStaticInteropType(c);
+
 /// Returns true iff this element is a JS interop member.
 ///
 /// JS annotations are required explicitly on classes. Other elements, such as
diff --git a/pkg/dev_compiler/lib/src/kernel/target.dart b/pkg/dev_compiler/lib/src/kernel/target.dart
index f3dd50d..95cbf83 100644
--- a/pkg/dev_compiler/lib/src/kernel/target.dart
+++ b/pkg/dev_compiler/lib/src/kernel/target.dart
@@ -102,6 +102,7 @@
         'dart:js_interop',
         'dart:math',
         'dart:svg',
+        'dart:typed_data',
         'dart:web_audio',
         'dart:web_gl',
         'dart:_foreign_helper',
diff --git a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart
index 7d5ffb1..dadf384 100644
--- a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart
+++ b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart
@@ -21,12 +21,13 @@
 /// [JSExportedDartFunction] <-> [Function]
 extension JSExportedDartFunctionToFunction on JSExportedDartFunction {
   @patch
-  Function get toDart => this;
+  Function get toDart => this as Function;
 }
 
 extension FunctionToJSExportedDartFunction on Function {
   @patch
-  JSExportedDartFunction get toJS => allowInterop(this);
+  JSExportedDartFunction get toJS =>
+      allowInterop(this) as JSExportedDartFunction;
 }
 
 /// [JSExportedDartObject] <-> [Object]
@@ -37,7 +38,7 @@
 
 extension ObjectToJSExportedDartObject on Object {
   @patch
-  JSExportedDartObject get toJS => this;
+  JSExportedDartObject get toJS => this as JSExportedDartObject;
 }
 
 /// [JSPromise] -> [Future<JSAny?>].
@@ -49,122 +50,122 @@
 /// [JSArrayBuffer] <-> [ByteBuffer]
 extension JSArrayBufferToByteBuffer on JSArrayBuffer {
   @patch
-  ByteBuffer get toDart => this;
+  ByteBuffer get toDart => this as ByteBuffer;
 }
 
 extension ByteBufferToJSArrayBuffer on ByteBuffer {
   @patch
-  JSArrayBuffer get toJS => this;
+  JSArrayBuffer get toJS => this as JSArrayBuffer;
 }
 
 /// [JSDataView] <-> [ByteData]
 extension JSDataViewToByteData on JSDataView {
   @patch
-  ByteData get toDart => this;
+  ByteData get toDart => this as ByteData;
 }
 
 extension ByteDataToJSDataView on ByteData {
   @patch
-  JSDataView get toJS => this;
+  JSDataView get toJS => this as JSDataView;
 }
 
 /// [JSInt8Array] <-> [Int8List]
 extension JSInt8ArrayToInt8List on JSInt8Array {
   @patch
-  Int8List get toDart => this;
+  Int8List get toDart => this as Int8List;
 }
 
 extension Int8ListToJSInt8Array on Int8List {
   @patch
-  JSInt8Array get toJS => this;
+  JSInt8Array get toJS => this as JSInt8Array;
 }
 
 /// [JSUint8Array] <-> [Uint8List]
 extension JSUint8ArrayToUint8List on JSUint8Array {
   @patch
-  Uint8List get toDart => this;
+  Uint8List get toDart => this as Uint8List;
 }
 
 extension Uint8ListToJSUint8Array on Uint8List {
   @patch
-  JSUint8Array get toJS => this;
+  JSUint8Array get toJS => this as JSUint8Array;
 }
 
 /// [JSUint8ClampedArray] <-> [Uint8ClampedList]
 extension JSUint8ClampedArrayToUint8ClampedList on JSUint8ClampedArray {
   @patch
-  Uint8ClampedList get toDart => this;
+  Uint8ClampedList get toDart => this as Uint8ClampedList;
 }
 
 extension Uint8ClampedListToJSUint8ClampedArray on Uint8ClampedList {
   @patch
-  JSUint8ClampedArray get toJS => this;
+  JSUint8ClampedArray get toJS => this as JSUint8ClampedArray;
 }
 
 /// [JSInt16Array] <-> [Int16List]
 extension JSInt16ArrayToInt16List on JSInt16Array {
   @patch
-  Int16List get toDart => this;
+  Int16List get toDart => this as Int16List;
 }
 
 extension Int16ListToJSInt16Array on Int16List {
   @patch
-  JSInt16Array get toJS => this;
+  JSInt16Array get toJS => this as JSInt16Array;
 }
 
 /// [JSUint16Array] <-> [Uint16List]
 extension JSUint16ArrayToInt16List on JSUint16Array {
   @patch
-  Uint16List get toDart => this;
+  Uint16List get toDart => this as Uint16List;
 }
 
 extension Uint16ListToJSInt16Array on Uint16List {
   @patch
-  JSUint16Array get toJS => this;
+  JSUint16Array get toJS => this as JSUint16Array;
 }
 
 /// [JSInt32Array] <-> [Int32List]
 extension JSInt32ArrayToInt32List on JSInt32Array {
   @patch
-  Int32List get toDart => this;
+  Int32List get toDart => this as Int32List;
 }
 
 extension Int32ListToJSInt32Array on Int32List {
   @patch
-  JSInt32Array get toJS => this;
+  JSInt32Array get toJS => this as JSInt32Array;
 }
 
 /// [JSUint32Array] <-> [Uint32List]
 extension JSUint32ArrayToUint32List on JSUint32Array {
   @patch
-  Uint32List get toDart => this;
+  Uint32List get toDart => this as Uint32List;
 }
 
 extension Uint32ListToJSUint32Array on Uint32List {
   @patch
-  JSUint32Array get toJS => this;
+  JSUint32Array get toJS => this as JSUint32Array;
 }
 
 /// [JSFloat32Array] <-> [Float32List]
 extension JSFloat32ArrayToFloat32List on JSFloat32Array {
   @patch
-  Float32List get toDart => this;
+  Float32List get toDart => this as Float32List;
 }
 
 extension Float32ListToJSFloat32Array on Float32List {
   @patch
-  JSFloat32Array get toJS => this;
+  JSFloat32Array get toJS => this as JSFloat32Array;
 }
 
 /// [JSFloat64Array] <-> [Float64List]
 extension JSFloat64ArrayToFloat64List on JSFloat64Array {
   @patch
-  Float64List get toDart => this;
+  Float64List get toDart => this as Float64List;
 }
 
 extension Float64ListToJSFloat64Array on Float64List {
   @patch
-  JSFloat64Array get toJS => this;
+  JSFloat64Array get toJS => this as JSFloat64Array;
 }
 
 /// [JSArray] <-> [List]
@@ -175,38 +176,38 @@
 
 extension ListToJSArray on List<JSAny?> {
   @patch
-  JSArray get toJS => this;
+  JSArray get toJS => this as JSArray;
 }
 
 /// [JSNumber] <-> [double]
 extension JSNumberToDouble on JSNumber {
   @patch
-  double get toDart => this;
+  double get toDart => this as double;
 }
 
 extension DoubleToJSNumber on double {
   @patch
-  JSNumber get toJS => this;
+  JSNumber get toJS => this as JSNumber;
 }
 
 /// [JSBoolean] <-> [bool]
 extension JSBooleanToBool on JSBoolean {
   @patch
-  bool get toDart => this;
+  bool get toDart => this as bool;
 }
 
 extension BoolToJSBoolean on bool {
   @patch
-  JSBoolean get toJS => this;
+  JSBoolean get toJS => this as JSBoolean;
 }
 
 /// [JSString] <-> [String]
 extension JSStringToString on JSString {
   @patch
-  String get toDart => this;
+  String get toDart => this as String;
 }
 
 extension StringToJSString on String {
   @patch
-  JSString get toJS => this;
+  JSString get toJS => this as JSString;
 }
diff --git a/sdk/lib/_internal/js_shared/lib/js_types.dart b/sdk/lib/_internal/js_shared/lib/js_types.dart
index e1f66e3..632bb94 100644
--- a/sdk/lib/_internal/js_shared/lib/js_types.dart
+++ b/sdk/lib/_internal/js_shared/lib/js_types.dart
@@ -4,47 +4,126 @@
 
 /// This library exists to act as a uniform abstraction layer between the user
 /// facing JS interop libraries and backend specific internal representations of
-/// JS types. For consistency, all of the web backends have a version of this
-/// library.
+/// JS types.
+///
+/// For consistency, all of the web backends have a version of this library.
+///
+/// For the time being, all JS types are erased to their respective Dart type at
+/// runtime e.g. [JSString] -> [String]. Eventually, when we have inline
+/// classes, we may choose to either:
+///
+/// 1. Use [Object] as the representation type.
+/// 2. Have some analog to dart2wasm's [JSValue] as the representation type in
+/// order to separate the Dart and JS type hierarchies at runtime.
+/// 3. Continue using the respective Dart type.
+///
+/// Note that we can't use [Interceptor] to do option #2. [Interceptor] is a
+/// supertype of types like [interceptors.JSString], but not a supertype of the
+/// core types like [String]. This becomes relevant when we use external APIs.
+/// External APIs get lowered to `js_util` calls, which cast the return value.
+/// If a function returns a JavaScript string, it gets reified as a Dart
+/// [String] for example. Then when we cast to [JSString] in `js_util`, we get
+/// a cast failure, as [String] is not a subtype of [Interceptor].
+///
+/// For specific details of the JS type hierarchy, please see
+/// `sdk/lib/js_interop/js_interop.dart`.
 library _js_types;
 
 import 'dart:_js_annotations';
-import 'dart:_interceptors' as interceptors;
-import 'dart:_internal' show patch;
 import 'dart:typed_data';
 
-/// For the time being, all JS types are conflated with Dart types on JS
-/// backends. See `sdk/lib/_internal/wasm/lib/js_types.dart` for more details.
+@JS()
+@staticInterop
+class JSAny {}
 
-/// For specific details of the JS type hierarchy, please see
-/// `sdk/lib/js_interop/js_interop.dart`.
-/// TODO(joshualitt): Some users may want type safety instead of conflating Dart
-/// types and JS types. For those users, we should have an opt-in flag where we
-/// swap out these typedef for actual types that would not implicitly coerce
-/// statically to their Dart counterparts and back.
-typedef JSAny = Object;
-typedef JSObject = interceptors.JSObject;
-typedef JSFunction = Function;
-typedef JSExportedDartFunction = Function;
-typedef JSArray = List<JSAny?>;
-typedef JSExportedDartObject = Object;
-typedef JSArrayBuffer = ByteBuffer;
-typedef JSDataView = ByteData;
-typedef JSTypedArray = TypedData;
-typedef JSInt8Array = Int8List;
-typedef JSUint8Array = Uint8List;
-typedef JSUint8ClampedArray = Uint8ClampedList;
-typedef JSInt16Array = Int16List;
-typedef JSUint16Array = Uint16List;
-typedef JSInt32Array = Int32List;
-typedef JSUint32Array = Uint32List;
-typedef JSFloat32Array = Float32List;
-typedef JSFloat64Array = Float64List;
-typedef JSNumber = double;
-typedef JSBoolean = bool;
-typedef JSString = String;
+@JS()
+@staticInterop
+class JSObject implements JSAny {}
+
+@JS()
+@staticInterop
+class JSFunction implements JSObject {}
+
+@JS()
+@staticInterop
+class JSExportedDartFunction implements JSFunction {}
+
+@JS('Array')
+@staticInterop
+class JSArray implements JSObject {
+  external factory JSArray();
+  external factory JSArray.withLength(JSNumber length);
+}
+
+@JS()
+@staticInterop
+class JSExportedDartObject implements JSObject {}
+
+@JS()
+@staticInterop
+class JSArrayBuffer implements JSObject {}
+
+@JS()
+@staticInterop
+class JSDataView implements JSObject {}
+
+@JS()
+@staticInterop
+class JSTypedArray implements JSObject {}
+
+@JS()
+@staticInterop
+class JSInt8Array implements JSTypedArray {}
+
+@JS()
+@staticInterop
+class JSUint8Array implements JSTypedArray {}
+
+@JS()
+@staticInterop
+class JSUint8ClampedArray implements JSTypedArray {}
+
+@JS()
+@staticInterop
+class JSInt16Array implements JSTypedArray {}
+
+@JS()
+@staticInterop
+class JSUint16Array implements JSTypedArray {}
+
+@JS()
+@staticInterop
+class JSInt32Array implements JSTypedArray {}
+
+@JS()
+@staticInterop
+class JSUint32Array implements JSTypedArray {}
+
+@JS()
+@staticInterop
+class JSFloat32Array implements JSTypedArray {}
+
+@JS()
+@staticInterop
+class JSFloat64Array implements JSTypedArray {}
+
+@JS()
+@staticInterop
+class JSNumber implements JSAny {}
+
+@JS()
+@staticInterop
+class JSBoolean implements JSAny {}
+
+@JS()
+@staticInterop
+class JSString implements JSAny {}
+
+/// [JSVoid] is just a typedef for [void]. While we could just use
+/// `JSUndefined`, in the future we may be able to use this to elide `return`s
+/// in JS trampolines.
 typedef JSVoid = void;
 
 @JS()
 @staticInterop
-class JSPromise {}
+class JSPromise implements JSObject {}
diff --git a/sdk/lib/_internal/wasm/lib/js_types.dart b/sdk/lib/_internal/wasm/lib/js_types.dart
index a06cbb1..8011b4b 100644
--- a/sdk/lib/_internal/wasm/lib/js_types.dart
+++ b/sdk/lib/_internal/wasm/lib/js_types.dart
@@ -11,11 +11,13 @@
 import 'dart:_js_annotations';
 
 /// Note that the semantics of JS types on Wasm backends are slightly different
-/// from the JS backends as we use static interop, and thus [JSValue], to
-/// implement all of the other JS types, whereas the JS backends conflate Dart
-/// types and JS types.  Because we're not sure exactly where things will end
-/// up, we're moving gradually towards consistent semantics across all web
-/// backends. A gradual path to consistent semantics might look something like:
+/// from the JS backends. They all use `@staticInterop` currently, but Wasm
+/// erases to [JSValue], while the JS backends erase each JS type to its
+/// respective Dart type.
+///
+/// Because we're not sure exactly where things will end up, we're moving
+/// gradually towards consistent semantics across all web backends. A gradual
+/// path to consistent semantics might look something like:
 /// 1) Launch MVP with JS backends conflating Dart types and JS types, and Wasm
 ///    backends implementing JS types with boxes. On Wasm backends, users will
 ///    have to explicitly coerce Dart types to JS types, possibly with some
diff --git a/sdk/lib/js_interop/js_interop.dart b/sdk/lib/js_interop/js_interop.dart
index 2b9e103..d728b5c 100644
--- a/sdk/lib/js_interop/js_interop.dart
+++ b/sdk/lib/js_interop/js_interop.dart
@@ -43,6 +43,19 @@
   const ObjectLiteral();
 }
 
+/// The JS types users should use to write their external APIs.
+///
+/// These are meant to separate the Dart and JS type hierarchies statically.
+///
+/// **WARNING**:
+/// For now, the runtime semantics between backends may differ and may not be
+/// intuitive e.g. casting to [JSString] may give you inconsistent and
+/// surprising results depending on the value. It is preferred to always use the
+/// conversion functions e.g. `toJS` and `toDart`. The only runtime semantics
+/// stability we can guarantee is if a value actually is the JS type you are
+/// type-checking with/casting to e.g. `obj as JSString` will continue to work
+/// if `obj` actually is a JavaScript string.
+
 /// The overall top type in the JS types hierarchy.
 typedef JSAny = js_types.JSAny;
 
diff --git a/tests/lib/js/static_interop_test/js_types_static_errors_test.dart b/tests/lib/js/static_interop_test/js_types_static_errors_test.dart
new file mode 100644
index 0000000..61cd645
--- /dev/null
+++ b/tests/lib/js/static_interop_test/js_types_static_errors_test.dart
@@ -0,0 +1,183 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Check that JS types are not typedefs and are actual separate types. This
+// makes sure that users can't assign unrelated types to the JS type without a
+// conversion.
+
+import 'dart:js_interop';
+import 'dart:typed_data';
+
+class DartObject {}
+
+void main() {
+  // [JSAny] != [Object]
+  ((JSAny jsAny) {})(DartObject() as Object);
+  //                 ^^^^^^^^^^^^^^^^^^^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+  //                              ^
+  // [web] The argument type 'Object' can't be assigned to the parameter type 'JSAny'.
+
+  // [JSObject] != [Object]
+  ((JSObject jsObj) {})(DartObject() as Object);
+  //                    ^^^^^^^^^^^^^^^^^^^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+  //                                 ^
+  // [web] The argument type 'Object' can't be assigned to the parameter type 'JSObject'.
+
+  // [JSFunction] != [Function]
+  ((JSFunction jsFun) {})(() {} as Function);
+  //                      ^^^^^^^^^^^^^^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+  //                            ^
+  // [web] The argument type 'Function' can't be assigned to the parameter type 'JSFunction'.
+
+  // [JSExportedDartFunction] != [Function]
+  ((JSExportedDartFunction jsFun) {})(() {} as Function);
+  //                                  ^^^^^^^^^^^^^^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+  //                                        ^
+  // [web] The argument type 'Function' can't be assigned to the parameter type 'JSExportedDartFunction'.
+
+  // [JSExportedDartObject] != [Object]
+  ((JSExportedDartObject jsObj) {})(DartObject());
+  //                                ^
+  // [web] The argument type 'DartObject' can't be assigned to the parameter type 'JSExportedDartObject'.
+  //                                ^^^^^^^^^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSArray] != [List<JSAny?>]
+  List<JSAny?> dartArr = <JSAny?>[1.0.toJS, 'foo'.toJS];
+  ((JSArray jsArr) {})(dartArr);
+  //                   ^
+  // [web] The argument type 'List<JSAny?>' can't be assigned to the parameter type 'JSArray'.
+  //                   ^^^^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSArrayBuffer] != [ByteBuffer]
+  ByteBuffer dartBuf = Uint8List.fromList([0, 255, 0, 255]).buffer;
+  ((JSArrayBuffer jsBuf) {})(dartBuf);
+  //                         ^
+  // [web] The argument type 'ByteBuffer' can't be assigned to the parameter type 'JSArrayBuffer'.
+  //                         ^^^^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSDataView] != [ByteData]
+  ByteData dartDat = Uint8List.fromList([0, 255, 0, 255]).buffer.asByteData();
+  ((JSDataView jsDat) {})(dartDat);
+  //                      ^
+  // [web] The argument type 'ByteData' can't be assigned to the parameter type 'JSDataView'.
+  //                      ^^^^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSTypedArray]s != [TypedData]s
+  TypedData typedData = Int8List.fromList([-128, 0, 127]);
+  ((JSTypedArray jsTypedArray) {})(typedData);
+  //                               ^
+  // [web] The argument type 'TypedData' can't be assigned to the parameter type 'JSTypedArray'.
+  //                               ^^^^^^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSInt8Array]s != [Int8List]s
+  Int8List ai8 = Int8List.fromList([-128, 0, 127]);
+  ((JSInt8Array jsAi8) {})(ai8);
+  //                       ^
+  // [web] The argument type 'Int8List' can't be assigned to the parameter type 'JSInt8Array'.
+  //                       ^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSUint8Array] != [Uint8List]
+  Uint8List au8 = Uint8List.fromList([-1, 0, 255, 256]);
+  ((JSUint8Array jsAu8) {})(au8);
+  //                        ^
+  // [web] The argument type 'Uint8List' can't be assigned to the parameter type 'JSUint8Array'.
+  //                        ^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSUint8ClampedArray] != [Uint8ClampedList]
+  Uint8ClampedList ac8 = Uint8ClampedList.fromList([-1, 0, 255, 256]);
+  ((JSUint8ClampedArray jsAc8) {})(ac8);
+  //                               ^
+  // [web] The argument type 'Uint8ClampedList' can't be assigned to the parameter type 'JSUint8ClampedArray'.
+  //                               ^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSInt16Array] != [Int16List]
+  Int16List ai16 = Int16List.fromList([-32769, -32768, 0, 32767, 32768]);
+  ((JSInt16Array jsAi16) {})(ai16);
+  //                         ^
+  // [web] The argument type 'Int16List' can't be assigned to the parameter type 'JSInt16Array'.
+  //                         ^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSUint16Array] != [Uint16List]
+  Uint16List au16 = Uint16List.fromList([-1, 0, 65535, 65536]);
+  ((JSUint16Array jsAu16) {})(au16);
+  //                          ^
+  // [web] The argument type 'Uint16List' can't be assigned to the parameter type 'JSUint16Array'.
+  //                          ^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSInt32Array] != [Int32List]
+  Int32List ai32 = Int32List.fromList([-2147483648, 0, 2147483647]);
+  ((JSInt32Array jsAi32) {})(ai32);
+  //                         ^
+  // [web] The argument type 'Int32List' can't be assigned to the parameter type 'JSInt32Array'.
+  //                         ^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSUint32Array] != [Uint32List]
+  Uint32List au32 = Uint32List.fromList([-1, 0, 4294967295, 4294967296]);
+  ((JSUint32Array jsAu32) {})(au32);
+  //                          ^
+  // [web] The argument type 'Uint32List' can't be assigned to the parameter type 'JSUint32Array'.
+  //                          ^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSFloat32Array] != [Float32List]
+  Float32List af32 =
+      Float32List.fromList([-1000.488, -0.00001, 0.0001, 10004.888]);
+  ((JSFloat32Array jsAf32) {})(af32);
+  //                           ^
+  // [web] The argument type 'Float32List' can't be assigned to the parameter type 'JSFloat32Array'.
+  //                           ^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSFloat64Array] != [Float64List]
+  Float64List af64 =
+      Float64List.fromList([-1000.488, -0.00001, 0.0001, 10004.888]);
+  ((JSFloat64Array jsAf64) {})(af64);
+  //                           ^
+  // [web] The argument type 'Float64List' can't be assigned to the parameter type 'JSFloat64Array'.
+  //                           ^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSNumber] != [double]
+  ((JSNumber jsNum) {})(4.5);
+  //                    ^
+  // [web] The argument type 'double' can't be assigned to the parameter type 'JSNumber'.
+  //                    ^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSBoolean] != [bool]
+  ((JSBoolean jsBool) {})(true);
+  //                      ^
+  // [web] The argument type 'bool' can't be assigned to the parameter type 'JSBoolean'.
+  //                      ^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSString] != [String]
+  ((JSString jsStr) {})('foo');
+  //                    ^
+  // [web] The argument type 'String' can't be assigned to the parameter type 'JSString'.
+  //                    ^^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+
+  // [JSPromise] != [Future]
+  ((JSPromise promise) {})(Future<void>.delayed(Duration.zero));
+  //                       ^
+  // [web] The argument type 'Future<void>' can't be assigned to the parameter type 'JSPromise'.
+  //                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  // [analyzer] COMPILE_TIME_ERROR.ARGUMENT_TYPE_NOT_ASSIGNABLE
+}