[interop] Add Support for Anonymous Declarations (#434)

This adds support for generating declarations for anonymous declarations, such as anonymous objects, unions, closures and constructor types.

This also adds support for nullable types (types unioned with undefined and/or null).

A hashing function is used for consistently hashing string objects, which is used for hashing identifiers for anonymous unions, objects and more, as well as comparing such names to allow reusing of such types.

* wip: union types

* wip: completed anonymous unions

* implemented JS Tuple

* completed nullability support for `undefined` and `null`

* wip: object decl

* implemented anonymous objects

* added support for closures and constructors

* wip: type hierarchy

* implemented sub type deduction

* added type generics to union types

* changed `.reduce` to `.fold`

* resolved newline and license headers

* isNullable updates and renamed `DeclarationAssociatedType`

* updated algorithm to use LCA via Topological Ordering

* resolved some more comments

* rm hasher_test

* resolved some more comments

* refactored tuple generation (common types) and more

* added LCA test

* removed stray prints

* added doc support and resolved merge

* updated documentation formatting using formatting.dart
diff --git a/web_generator/analysis_options.yaml b/web_generator/analysis_options.yaml
index 0242634..5cb8d06 100644
--- a/web_generator/analysis_options.yaml
+++ b/web_generator/analysis_options.yaml
@@ -6,6 +6,8 @@
     strict-casts: true
     strict-inference: true
     strict-raw-types: true
+  errors: 
+    comment_references: ignore
 
 linter:
   rules:
diff --git a/web_generator/lib/src/ast/base.dart b/web_generator/lib/src/ast/base.dart
index 54158a6..bc356b7 100644
--- a/web_generator/lib/src/ast/base.dart
+++ b/web_generator/lib/src/ast/base.dart
@@ -45,7 +45,6 @@
 }
 
 sealed class Node {
-  String? get name;
   abstract final ID id;
   String? get dartName;
 
@@ -55,7 +54,6 @@
 }
 
 abstract class Declaration extends Node {
-  @override
   String get name;
 
   @override
@@ -66,11 +64,19 @@
   @override
   abstract String name;
 
-  abstract Documentation? documentation;
-
-  ReferredType asReferredType([List<Type>? typeArgs, String? url]) =>
+  ReferredType asReferredType(
+          [List<Type>? typeArgs, bool isNullable = false, String? url]) =>
       ReferredType(
-          name: name, declaration: this, typeParams: typeArgs ?? [], url: url);
+          name: name,
+          declaration: this,
+          typeParams: typeArgs ?? [],
+          url: url,
+          isNullable: isNullable);
+}
+
+abstract interface class DocumentedDeclaration {
+  /// The documentation associated with the given declaration
+  abstract Documentation? documentation;
 }
 
 abstract interface class ExportableDeclaration extends Declaration {
@@ -88,8 +94,20 @@
   @override
   String? dartName;
 
+  /// Whether the given type is nullable or not
+  /// (unioned with `undefined` or `null`)
+  abstract bool isNullable;
+
   @override
   Reference emit([covariant TypeOptions? options]);
+
+  @override
+  bool operator ==(Object other) {
+    return other is Type && id == other.id;
+  }
+
+  @override
+  int get hashCode => Object.hashAll([id]);
 }
 
 abstract class FieldDeclaration extends NamedDeclaration {
@@ -106,6 +124,12 @@
 
 enum DeclScope { private, protected, public }
 
+abstract class DeclarationType<T extends Declaration> extends Type {
+  T get declaration;
+
+  String get declarationName;
+}
+
 class ParameterDeclaration {
   final String name;
 
@@ -127,3 +151,7 @@
       ..type = type.emit(TypeOptions(nullable: optional)));
   }
 }
+
+abstract class NamedType extends Type {
+  String get name;
+}
diff --git a/web_generator/lib/src/ast/builtin.dart b/web_generator/lib/src/ast/builtin.dart
index 24d8358..a46d7e3 100644
--- a/web_generator/lib/src/ast/builtin.dart
+++ b/web_generator/lib/src/ast/builtin.dart
@@ -12,7 +12,7 @@
 
 /// A built in type supported by `dart:js_interop` or by this library
 /// (with generated declarations)
-class BuiltinType extends Type {
+class BuiltinType extends NamedType {
   @override
   final String name;
 
@@ -21,15 +21,15 @@
   /// Whether the given type is present in "dart:js_interop"
   final bool fromDartJSInterop;
 
-  // TODO(nikeokoronkwo): Types in general should have an `isNullable`
-  //  property on them to indicate nullability for Dart generated code.
-  final bool? isNullable;
+  @override
+  bool isNullable;
 
   BuiltinType(
       {required this.name,
       this.typeParams = const [],
       this.fromDartJSInterop = false,
-      this.isNullable});
+      bool? isNullable})
+      : isNullable = isNullable ?? false;
 
   @override
   ID get id => ID(type: 'type', name: name);
@@ -39,16 +39,14 @@
 
   @override
   Reference emit([TypeOptions? options]) {
-    options ??= TypeOptions();
-
     return TypeReference((t) => t
       ..symbol = name
       ..types.addAll(typeParams
           // if there is only one type param, and it is void, ignore
           .where((p) => typeParams.length != 1 || p != $voidType)
-          .map((p) => p.emit(TypeOptions())))
+          .map((p) => p.emit(options)))
       ..url = fromDartJSInterop ? 'dart:js_interop' : null
-      ..isNullable = isNullable ?? options!.nullable);
+      ..isNullable = isNullable || (options?.nullable ?? false));
   }
 
   static final BuiltinType $voidType = BuiltinType(name: 'void');
@@ -81,7 +79,10 @@
               name: 'JSString', fromDartJSInterop: true, isNullable: isNullable)
           : BuiltinType(name: 'String', isNullable: isNullable),
       PrimitiveType.$void || PrimitiveType.undefined => $voidType,
-      PrimitiveType.any || PrimitiveType.unknown => anyType,
+      PrimitiveType.any => (isNullable ?? false)
+          ? anyType
+          : BuiltinType(name: 'JSAny', fromDartJSInterop: true),
+      PrimitiveType.unknown => anyType,
       PrimitiveType.object => BuiltinType(
           name: 'JSObject', fromDartJSInterop: true, isNullable: isNullable),
       PrimitiveType.symbol => BuiltinType(
@@ -128,13 +129,14 @@
   }
 }
 
-class PackageWebType extends Type {
+class PackageWebType extends NamedType {
   @override
   final String name;
 
   final List<Type> typeParams;
 
-  final bool? isNullable;
+  @override
+  bool isNullable;
 
   @override
   ID get id => ID(type: 'type', name: name);
@@ -143,12 +145,12 @@
   String? get dartName => null;
 
   PackageWebType._(
-      {required this.name, this.typeParams = const [], this.isNullable});
+      {required this.name,
+      this.typeParams = const [],
+      this.isNullable = false});
 
   @override
   Reference emit([TypeOptions? options]) {
-    options ??= TypeOptions();
-
     // TODO: We can make this a shared function as it is called a lot
     //  between types
     return TypeReference((t) => t
@@ -156,16 +158,16 @@
       ..types.addAll(typeParams
           // if there is only one type param, and it is void, ignore
           .where((p) => typeParams.length != 1 || p != BuiltinType.$voidType)
-          .map((p) => p.emit(TypeOptions())))
+          .map((p) => p.emit(options)))
       ..url = 'package:web/web.dart'
-      ..isNullable = isNullable ?? options!.nullable);
+      ..isNullable = isNullable || (options?.nullable ?? false));
   }
 
   static PackageWebType parse(String name,
       {bool? isNullable, List<Type> typeParams = const []}) {
     return PackageWebType._(
         name: renameMap.containsKey(name) ? renameMap[name]! : name,
-        isNullable: isNullable,
+        isNullable: isNullable ?? false,
         typeParams: typeParams);
   }
 }
diff --git a/web_generator/lib/src/ast/declarations.dart b/web_generator/lib/src/ast/declarations.dart
index 6fc2357..1ede563 100644
--- a/web_generator/lib/src/ast/declarations.dart
+++ b/web_generator/lib/src/ast/declarations.dart
@@ -12,7 +12,8 @@
 import 'helpers.dart';
 import 'types.dart';
 
-abstract class NestableDeclaration extends NamedDeclaration {
+abstract class NestableDeclaration extends NamedDeclaration
+    implements DocumentedDeclaration {
   NestableDeclaration? get parent;
 
   String get qualifiedName =>
@@ -68,11 +69,15 @@
       this.parent,
       this.documentation});
 
-  ExtensionType _emit(
-      [covariant DeclarationOptions? options,
-      bool abstract = false,
+  /// [useFirstExtendeeAsRepType] is used to assert that the extension type
+  /// generated has a representation type of the first member of [extendees]
+  /// if any.
+  ExtensionType _emit(covariant DeclarationOptions? options,
+      {bool abstract = false,
       List<Type> extendees = const [],
-      List<Type> implementees = const []]) {
+      List<Type> implementees = const [],
+      bool useFirstExtendeeAsRepType = false,
+      bool objectLiteralConstructor = false}) {
     options ??= DeclarationOptions();
 
     final hierarchy = getMemberHierarchy(this);
@@ -100,8 +105,8 @@
     methodDecs.addAll(operators.where((p) => p.scope == DeclScope.public).map(
         (m) => m.emit(options!..override = isOverride(m.dartName ?? m.name))));
 
-    final repType = this is ClassDeclaration
-        ? getClassRepresentationType(this as ClassDeclaration)
+    final repType = useFirstExtendeeAsRepType || this is ClassDeclaration
+        ? getRepresentationType(this)
         : BuiltinType.primitiveType(PrimitiveType.object, isNullable: false);
 
     return ExtensionType((e) => e
@@ -129,6 +134,15 @@
       ..types
           .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions())))
       ..constructors.addAll([
+        if (objectLiteralConstructor)
+          Constructor((c) => c
+            ..external = true
+            ..optionalParameters.addAll(properties
+                .where((p) => p.scope == DeclScope.public)
+                .map((p) => Parameter((param) => param
+                  ..named = true
+                  ..name = p.name
+                  ..type = p.type.emit(options?.toTypeOptions()))))),
         if (!abstract)
           if (constructors.isEmpty && this is ClassDeclaration)
             ConstructorDeclaration.defaultFor(this).emit(options)
@@ -140,14 +154,16 @@
   }
 }
 
-abstract class MemberDeclaration {
+abstract class MemberDeclaration implements DocumentedDeclaration {
   late final TypeDeclaration parent;
 
   abstract final DeclScope scope;
+
+  String? get name;
 }
 
 class VariableDeclaration extends FieldDeclaration
-    implements ExportableDeclaration {
+    implements ExportableDeclaration, DocumentedDeclaration {
   /// The variable modifier, as represented in TypeScript
   VariableModifier modifier;
 
@@ -204,16 +220,16 @@
 
   @override
   ReferredType<VariableDeclaration> asReferredType(
-      [List<Type>? typeArgs, String? url]) {
+      [List<Type>? typeArgs, bool? isNullable, String? url]) {
     return ReferredType<VariableDeclaration>.fromType(type, this,
-        typeParams: typeArgs ?? [], url: url);
+        typeParams: typeArgs ?? [], url: url, isNullable: isNullable ?? false);
   }
 }
 
 enum VariableModifier { let, $const, $var }
 
 class FunctionDeclaration extends CallableDeclaration
-    implements ExportableDeclaration {
+    implements ExportableDeclaration, DocumentedDeclaration {
   @override
   String name;
 
@@ -273,16 +289,16 @@
 
   @override
   ReferredType<FunctionDeclaration> asReferredType(
-      [List<Type>? typeArgs, String? url]) {
+      [List<Type>? typeArgs, bool? isNullable, String? url]) {
     // TODO: We could do better here and make the function type typed
     return ReferredType<FunctionDeclaration>.fromType(
         BuiltinType.referred('Function', typeParams: typeArgs ?? [])!, this,
-        typeParams: typeArgs ?? [], url: url);
+        typeParams: typeArgs ?? [], url: url, isNullable: isNullable ?? false);
   }
 }
 
 class EnumDeclaration extends NestableDeclaration
-    implements ExportableDeclaration {
+    implements ExportableDeclaration, DocumentedDeclaration {
   @override
   String name;
 
@@ -395,7 +411,7 @@
 }
 
 class TypeAliasDeclaration extends NamedDeclaration
-    implements ExportableDeclaration {
+    implements ExportableDeclaration, DocumentedDeclaration {
   @override
   String name;
 
@@ -484,11 +500,9 @@
 
   @override
   ExtensionType emit([covariant DeclarationOptions? options]) {
-    options ??= DeclarationOptions();
-    options.static = true;
+    options?.static = true;
 
     final (doc, annotations) = generateFromDocumentation(documentation);
-
     // static props and vars
     final methods = <Method>[];
     final fields = <Field>[];
@@ -496,12 +510,14 @@
     for (final decl in topLevelDeclarations) {
       if (decl case final VariableDeclaration variable) {
         if (variable.modifier == VariableModifier.$const) {
-          methods.add(variable.emit(options) as Method);
+          methods.add(variable.emit(options ?? DeclarationOptions(static: true))
+              as Method);
         } else {
-          fields.add(variable.emit(options) as Field);
+          fields.add(variable.emit(options ?? DeclarationOptions(static: true))
+              as Field);
         }
       } else if (decl case final FunctionDeclaration fn) {
-        methods.add(fn.emit(options));
+        methods.add(fn.emit(options ?? DeclarationOptions(static: true)));
       }
     }
 
@@ -624,8 +640,10 @@
 
   @override
   ExtensionType emit([covariant DeclarationOptions? options]) {
-    return super._emit(options, abstract,
-        [if (extendedType case final extendee?) extendee], implementedTypes);
+    return super._emit(options,
+        abstract: abstract,
+        extendees: [if (extendedType case final extendee?) extendee],
+        implementees: implementedTypes);
   }
 
   @override
@@ -647,6 +665,15 @@
 
   final List<Type> extendedTypes;
 
+  /// This asserts that the extension type generated produces a rep type
+  /// other than its default, which is denoted by the first member of
+  /// [extendedTypes] if any.
+  final bool assertRepType;
+
+  /// This asserts generating a constructor for creating the given interface
+  /// as an object literal via an object literal constructor
+  final bool objectLiteralConstructor;
+
   InterfaceDeclaration(
       {required super.name,
       required super.exported,
@@ -658,16 +685,17 @@
       super.properties,
       super.operators,
       super.constructors,
+      this.assertRepType = false,
+      this.objectLiteralConstructor = false,
       super.documentation})
       : _id = id;
 
   @override
   ExtensionType emit([covariant DeclarationOptions? options]) {
-    return super._emit(
-      options,
-      false,
-      extendedTypes,
-    );
+    return super._emit(options,
+        extendees: extendedTypes,
+        useFirstExtendeeAsRepType: assertRepType,
+        objectLiteralConstructor: objectLiteralConstructor);
   }
 }
 
@@ -874,12 +902,14 @@
 
   final List<ParameterDeclaration> parameters;
 
+  @override
   final String? name;
 
   final ID id;
 
   final String? dartName;
 
+  @override
   Documentation? documentation;
 
   ConstructorDeclaration(
diff --git a/web_generator/lib/src/ast/helpers.dart b/web_generator/lib/src/ast/helpers.dart
index 472ade6..4d071e9 100644
--- a/web_generator/lib/src/ast/helpers.dart
+++ b/web_generator/lib/src/ast/helpers.dart
@@ -2,10 +2,10 @@
 // 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.
 
-import 'dart:convert';
-
 import 'package:code_builder/code_builder.dart';
 
+import '../formatting.dart';
+import '../interop_gen/namer.dart';
 import 'base.dart';
 import 'builtin.dart';
 import 'declarations.dart';
@@ -99,15 +99,23 @@
   return members;
 }
 
-Type getClassRepresentationType(ClassDeclaration cl) {
-  if (cl.extendedType case final extendee?) {
+Type getRepresentationType(TypeDeclaration td) {
+  if (td case ClassDeclaration(extendedType: final extendee?)) {
     return switch (extendee) {
-      final ClassDeclaration classExtendee =>
-        getClassRepresentationType(classExtendee),
+      ReferredType(declaration: final d) when d is TypeDeclaration =>
+        getRepresentationType(d),
+      final BuiltinType b => b,
+      _ => BuiltinType.primitiveType(PrimitiveType.object, isNullable: false)
+    };
+  } else if (td case InterfaceDeclaration(extendedTypes: [final extendee])) {
+    return switch (extendee) {
+      ReferredType(declaration: final d) when d is TypeDeclaration =>
+        getRepresentationType(d),
+      final BuiltinType b => b,
       _ => BuiltinType.primitiveType(PrimitiveType.object, isNullable: false)
     };
   } else {
-    final primitiveType = switch (cl.name) {
+    final primitiveType = switch (td.name) {
       'Array' => PrimitiveType.array,
       _ => PrimitiveType.object
     };
@@ -124,10 +132,8 @@
     return ([], docs.annotations.map((d) => d.emit()).toList());
   }
   return (
-    const LineSplitter()
-        .convert(docs.docs.trim())
-        .map((d) => '/// $d')
-        .toList(),
+    // setting it at 80 may not work depending on how long the sentence is
+    formatDocs(docs.docs.trim(), 78),
     docs.annotations.map((d) => d.emit()).toList()
   );
 }
@@ -152,3 +158,190 @@
 
   return (requiredParams, optionalParams);
 }
+
+/// Recursively get the generic types specified in a given type [t]
+List<GenericType> getGenericTypes(Type t) {
+  final types = <(String, Type?)>[];
+  switch (t) {
+    case GenericType():
+      types.add((t.name, t.constraint));
+      break;
+    case ReferredType(typeParams: final referredTypeParams):
+    case UnionType(types: final referredTypeParams):
+      for (final referredTypeParam in referredTypeParams) {
+        types.addAll(getGenericTypes(referredTypeParam)
+            .map((t) => (t.name, t.constraint)));
+      }
+      break;
+    case ObjectLiteralType(
+        properties: final objectProps,
+        methods: final objectMethods,
+        constructors: final objectConstructors,
+        operators: final objectOperators
+      ):
+      for (final PropertyDeclaration(type: propType) in objectProps) {
+        types.addAll(
+            getGenericTypes(propType).map((t) => (t.name, t.constraint)));
+      }
+
+      for (final MethodDeclaration(
+            typeParameters: alreadyEstablishedTypeParams,
+            returnType: methodType,
+            parameters: methodParams
+          ) in objectMethods) {
+        final typeParams = [methodType, ...methodParams.map((p) => p.type)];
+
+        for (final type in typeParams) {
+          final genericTypes = getGenericTypes(type);
+          for (final genericType in genericTypes) {
+            if (!alreadyEstablishedTypeParams
+                .any((al) => al.name == genericType.name)) {
+              types.add((genericType.name, genericType.constraint));
+            }
+          }
+        }
+      }
+
+      for (final ConstructorDeclaration(parameters: methodParams)
+          in objectConstructors) {
+        for (final ParameterDeclaration(type: methodParamType)
+            in methodParams) {
+          types.addAll(getGenericTypes(methodParamType)
+              .map((t) => (t.name, t.constraint)));
+        }
+      }
+
+      for (final OperatorDeclaration(
+            typeParameters: alreadyEstablishedTypeParams,
+            returnType: methodType,
+            parameters: methodParams
+          ) in objectOperators) {
+        final typeParams = [methodType, ...methodParams.map((p) => p.type)];
+
+        for (final type in typeParams) {
+          final genericTypes = getGenericTypes(type);
+          for (final genericType in genericTypes) {
+            if (!alreadyEstablishedTypeParams
+                .any((al) => al.name == genericType.name)) {
+              types.add((genericType.name, genericType.constraint));
+            }
+          }
+        }
+      }
+      break;
+    case ClosureType(
+        typeParameters: final alreadyEstablishedTypeParams,
+        returnType: final closureType,
+        parameters: final closureParams
+      ):
+      for (final type in [closureType, ...closureParams.map((p) => p.type)]) {
+        final genericTypes = getGenericTypes(type);
+        for (final genericType in genericTypes) {
+          if (!alreadyEstablishedTypeParams
+              .any((al) => al.name == genericType.name)) {
+            types.add((genericType.name, genericType.constraint));
+          }
+        }
+      }
+      break;
+    default:
+      break;
+  }
+
+  // Types are cloned so that modifications to constraints can happen without
+  // affecting initial references
+  return types.map((t) => GenericType(name: t.$1, constraint: t.$2)).toList();
+}
+
+Type desugarTypeAliases(Type t) {
+  if (t case final ReferredType ref
+      when ref.declaration is TypeAliasDeclaration) {
+    return desugarTypeAliases((ref.declaration as TypeAliasDeclaration).type);
+  }
+  return t;
+}
+
+class TupleDeclaration extends NamedDeclaration
+    implements ExportableDeclaration {
+  @override
+  bool get exported => true;
+
+  @override
+  ID get id => ID(type: 'tuple', name: name);
+
+  final int count;
+
+  final bool readonly;
+
+  TupleDeclaration({required this.count, this.readonly = false});
+
+  @override
+  String? dartName;
+
+  @override
+  String get name => readonly ? 'JSReadonlyTuple$count' : 'JSTuple$count';
+
+  @override
+  set name(String name) {
+    throw Exception('Forbidden: Cannot set name on tuple declaration');
+  }
+
+  /// Creates a tuple from types.
+  ///
+  /// The type args represent the tuple types for the tuple declaration
+  @override
+  TupleType asReferredType(
+      [List<Type>? typeArgs, bool isNullable = false, String? url]) {
+    assert(typeArgs?.length == count,
+        'Type arguments must equal the number of tuples supported');
+    return TupleType(types: typeArgs ?? [], tupleDeclUrl: url);
+  }
+
+  @override
+  Spec emit([covariant DeclarationOptions? options]) {
+    options ??= DeclarationOptions();
+
+    final repType = BuiltinType.primitiveType(PrimitiveType.array,
+        shouldEmitJsType: true, typeParams: [BuiltinType.anyType]);
+
+    return ExtensionType((e) => e
+      ..name = name
+      ..primaryConstructorName = '_'
+      ..representationDeclaration = RepresentationDeclaration((r) => r
+        ..name = '_'
+        ..declaredRepresentationType = repType.emit())
+      ..implements.addAll([if (repType != BuiltinType.anyType) repType.emit()])
+      ..types.addAll(List.generate(
+          count,
+          (index) => TypeReference((t) => t
+            ..symbol = String.fromCharCode(65 + index)
+            ..bound = BuiltinType.anyType.emit())))
+      ..methods.addAll([
+        ...List.generate(count, (index) {
+          final returnType = String.fromCharCode(65 + index);
+          return Method((m) => m
+            ..name = '\$${index + 1}'
+            ..returns = refer(returnType)
+            ..type = MethodType.getter
+            ..body = refer('_')
+                .index(literalNum(index))
+                .asA(refer(returnType))
+                .code);
+        }),
+        if (!readonly)
+          ...List.generate(count, (index) {
+            final returnType = String.fromCharCode(65 + index);
+            return Method((m) => m
+              ..name = '\$${index + 1}'
+              ..type = MethodType.setter
+              ..requiredParameters.add(Parameter((p) => p
+                ..name = 'newValue'
+                ..type = refer(returnType)))
+              ..body = refer('_')
+                  .index(literalNum(index))
+                  .assign(refer('newValue'))
+                  .code);
+          })
+      ]));
+  }
+}
diff --git a/web_generator/lib/src/ast/types.dart b/web_generator/lib/src/ast/types.dart
index 8c8a7c1..5e5552a 100644
--- a/web_generator/lib/src/ast/types.dart
+++ b/web_generator/lib/src/ast/types.dart
@@ -4,18 +4,24 @@
 
 import 'package:code_builder/code_builder.dart';
 import '../interop_gen/namer.dart';
+import '../interop_gen/sub_type.dart';
+import '../utils/case.dart';
 import 'base.dart';
 import 'builtin.dart';
 import 'declarations.dart';
+import 'helpers.dart';
 
 /// A type referring to a type in the TypeScript AST
-class ReferredType<T extends Declaration> extends Type {
+class ReferredType<T extends Declaration> extends NamedType {
   @override
   String name;
 
   @override
   ID get id => ID(type: 'type', name: name);
 
+  @override
+  bool isNullable;
+
   T declaration;
 
   List<Type> typeParams;
@@ -26,10 +32,13 @@
       {required this.name,
       required this.declaration,
       this.typeParams = const [],
-      this.url});
+      this.url,
+      this.isNullable = false});
 
   factory ReferredType.fromType(Type type, T declaration,
-      {List<Type> typeParams, String? url}) = ReferredDeclarationType;
+      {List<Type> typeParams,
+      String? url,
+      bool isNullable}) = ReferredDeclarationType;
 
   @override
   Reference emit([TypeOptions? options]) {
@@ -38,7 +47,7 @@
           ? (declaration as NestableDeclaration).completedDartName
           : declaration.dartName ?? declaration.name
       ..types.addAll(typeParams.map((t) => t.emit(options)))
-      ..isNullable = options?.nullable
+      ..isNullable = (options?.nullable ?? false) || isNullable
       ..url = options?.url ?? url);
   }
 }
@@ -47,44 +56,86 @@
   Type type;
 
   @override
-  String get name => type.name ?? declaration.name;
+  String get name =>
+      type is NamedType ? (type as NamedType).name : declaration.name;
 
   ReferredDeclarationType(this.type, T declaration,
-      {super.typeParams, super.url})
+      {super.typeParams, super.url, super.isNullable})
       : super(name: declaration.name, declaration: declaration);
 
   @override
   Reference emit([covariant TypeOptions? options]) {
     options ??= TypeOptions();
     options.url = super.url;
+    options.nullable = super.isNullable;
 
     return type.emit(options);
   }
 }
 
-// TODO(https://github.com/dart-lang/web/issues/385): Implement Support for UnionType (including implementing `emit`)
-class UnionType extends Type {
+class TupleType extends ReferredType<TupleDeclaration> {
   final List<Type> types;
 
-  UnionType({required this.types});
+  @override
+  List<Type> get typeParams => types;
+
+  TupleType(
+      {required this.types, super.isNullable, required String? tupleDeclUrl})
+      : super(
+            declaration: TupleDeclaration(count: types.length),
+            name: 'JSTuple${types.length}',
+            url: tupleDeclUrl);
+
+  @override
+  ID get id => ID(type: 'type', name: types.map((t) => t.id.name).join(','));
+
+  @override
+  int get hashCode => Object.hashAllUnordered(types);
+
+  @override
+  bool operator ==(Object other) {
+    return other is TupleType && other.types.every(types.contains);
+  }
+}
+
+class UnionType extends DeclarationType {
+  final List<Type> types;
+
+  @override
+  bool isNullable;
+
+  @override
+  String declarationName;
+
+  UnionType(
+      {required this.types, required String name, this.isNullable = false})
+      : declarationName = name;
 
   @override
   ID get id => ID(type: 'type', name: types.map((t) => t.id.name).join('|'));
 
   @override
-  String? get name => null;
+  Declaration get declaration => _UnionDeclaration(
+      name: declarationName, types: types, isNullable: isNullable);
 
   @override
   Reference emit([TypeOptions? options]) {
-    throw UnimplementedError('TODO: Implement UnionType.emit');
+    return TypeReference((t) => t
+      ..symbol = declarationName
+      ..isNullable = (options?.nullable ?? false) || isNullable);
+  }
+
+  @override
+  int get hashCode => Object.hashAllUnordered(types);
+
+  @override
+  bool operator ==(Object other) {
+    return other is TupleType && other.types.every(types.contains);
   }
 }
 
-// TODO: Handle naming anonymous declarations
-// TODO: Extract having a declaration associated with a type to its own type
-//  (e.g DeclarationAssociatedType)
 class HomogenousEnumType<T extends LiteralType, D extends Declaration>
-    extends UnionType {
+    extends UnionType implements DeclarationType {
   final List<T> _types;
 
   @override
@@ -92,17 +143,12 @@
 
   final Type baseType;
 
-  final bool isNullable;
-
-  String declarationName;
-
   HomogenousEnumType(
-      {required List<T> types, this.isNullable = false, required String name})
-      : declarationName = name,
-        _types = types,
-        baseType = types.first.baseType,
-        super(types: types);
+      {required List<T> super.types, super.isNullable, required super.name})
+      : _types = types,
+        baseType = types.first.baseType;
 
+  @override
   EnumDeclaration get declaration => EnumDeclaration(
       name: declarationName,
       dartName: UniqueNamer.makeNonConflicting(declarationName),
@@ -117,25 +163,23 @@
         );
       }).toList(),
       exported: true);
-
-  @override
-  Reference emit([TypeOptions? options]) {
-    return TypeReference((t) => t
-      ..symbol = declarationName
-      ..isNullable = options?.nullable ?? isNullable);
-  }
 }
 
 /// The base class for a type generic (like 'T')
-class GenericType extends Type {
+class GenericType extends NamedType {
   @override
   final String name;
 
-  final Type? constraint;
+  Type? constraint;
 
   final Declaration? parent;
 
-  GenericType({required this.name, this.constraint, this.parent});
+  @override
+  bool isNullable = false;
+
+  GenericType(
+      {required this.name, this.constraint, this.parent, bool? isNullable})
+      : isNullable = isNullable ?? false;
 
   @override
   ID get id =>
@@ -145,7 +189,17 @@
   Reference emit([TypeOptions? options]) => TypeReference((t) => t
     ..symbol = name
     ..bound = constraint?.emit()
-    ..isNullable = options?.nullable);
+    ..isNullable = (options?.nullable ?? false) || isNullable);
+
+  @override
+  bool operator ==(Object other) {
+    return other is GenericType &&
+        other.name == name &&
+        other.constraint == constraint;
+  }
+
+  @override
+  int get hashCode => Object.hash(name, constraint);
 }
 
 /// A type representing a bare literal, such as `null`, a string or number
@@ -155,6 +209,8 @@
   final Object? value;
 
   @override
+  bool isNullable;
+
   String get name => switch (kind) {
         LiteralKind.$null => 'null',
         LiteralKind.int || LiteralKind.double => 'number',
@@ -169,15 +225,27 @@
     return BuiltinType.primitiveType(primitive);
   }
 
-  LiteralType({required this.kind, required this.value});
+  LiteralType(
+      {required this.kind, required this.value, this.isNullable = false});
 
   @override
   Reference emit([TypeOptions? options]) {
+    options ??= TypeOptions();
+    options.nullable = isNullable;
+
     return baseType.emit(options);
   }
 
   @override
-  ID get id => ID(type: 'type', name: name);
+  ID get id => ID(type: 'type', name: '$name.$value');
+
+  @override
+  bool operator ==(Object other) {
+    return other is LiteralType && other.name == name && other.value == value;
+  }
+
+  @override
+  int get hashCode => Object.hash(name, value);
 }
 
 enum LiteralKind {
@@ -196,3 +264,331 @@
         LiteralKind.$true || LiteralKind.$false => PrimitiveType.boolean
       };
 }
+
+class ObjectLiteralType extends DeclarationType<TypeDeclaration> {
+  final List<PropertyDeclaration> properties;
+
+  final List<MethodDeclaration> methods;
+
+  final List<ConstructorDeclaration> constructors;
+
+  final List<OperatorDeclaration> operators;
+
+  @override
+  bool isNullable;
+
+  @override
+  final String declarationName;
+
+  @override
+  final ID id;
+
+  ObjectLiteralType(
+      {required String name,
+      required this.id,
+      this.properties = const [],
+      this.methods = const [],
+      this.constructors = const [],
+      this.operators = const [],
+      this.isNullable = false})
+      : declarationName = name;
+
+  @override
+  TypeDeclaration get declaration => InterfaceDeclaration(
+      name: declarationName,
+      exported: true,
+      id: ID(type: 'interface', name: id.name),
+      objectLiteralConstructor: true,
+      properties: properties,
+      methods: methods,
+      operators: operators,
+      constructors: constructors,
+      typeParameters: getGenericTypes(this).map((g) {
+        g.constraint ??= BuiltinType.anyType;
+        return g;
+      }).toList());
+
+  @override
+  Reference emit([TypeOptions? options]) {
+    return TypeReference((t) => t
+      ..symbol = declarationName
+      ..isNullable = options?.nullable ?? isNullable
+      ..types.addAll(getGenericTypes(this).map((t) => t.emit(options))));
+  }
+}
+
+sealed class ClosureType extends DeclarationType {
+  final List<ParameterDeclaration> parameters;
+  final Type returnType;
+  final List<GenericType> typeParameters;
+  @override
+  bool isNullable;
+
+  @override
+  final String declarationName;
+
+  @override
+  final ID id;
+
+  ClosureType({
+    required String name,
+    required this.id,
+    required this.returnType,
+    this.typeParameters = const [],
+    this.parameters = const [],
+    this.isNullable = false,
+  }) : declarationName = name;
+
+  @override
+  Reference emit([TypeOptions? options]) {
+    return TypeReference((t) => t
+      ..symbol = declarationName
+      ..isNullable = options?.nullable ?? isNullable);
+  }
+}
+
+class ConstructorType extends ClosureType {
+  ConstructorType(
+      {required super.name,
+      required super.id,
+      required super.returnType,
+      super.typeParameters,
+      super.parameters,
+      super.isNullable});
+
+  @override
+  CallableDeclaration get declaration => _ConstructorDeclaration(
+      name: declarationName,
+      returnType: returnType,
+      parameters: parameters,
+      typeParameters: typeParameters);
+}
+
+class FunctionType extends ClosureType {
+  FunctionType(
+      {required super.name,
+      required super.id,
+      required super.returnType,
+      super.typeParameters,
+      super.parameters,
+      super.isNullable});
+
+  @override
+  InterfaceDeclaration get declaration => InterfaceDeclaration(
+          name: declarationName,
+          exported: true,
+          id: ID(type: 'interface', name: declarationName),
+          typeParameters: typeParameters,
+          assertRepType: true,
+          extendedTypes: [
+            BuiltinType.referred('Function')!
+          ],
+          methods: [
+            MethodDeclaration(
+                name: 'call',
+                id: const ID(type: 'fun', name: 'call'),
+                returnType: returnType,
+                parameters: parameters,
+                typeParameters: typeParameters)
+          ]);
+}
+
+class _ConstructorDeclaration extends CallableDeclaration
+    implements ExportableDeclaration {
+  @override
+  bool get exported => true;
+
+  @override
+  ID get id => ID(type: 'closure', name: name);
+
+  @override
+  String? dartName;
+
+  @override
+  String name;
+
+  @override
+  List<ParameterDeclaration> parameters;
+
+  @override
+  Type returnType;
+
+  @override
+  List<GenericType> typeParameters;
+
+  _ConstructorDeclaration(
+      {required this.name,
+      this.parameters = const [],
+      this.typeParameters = const [],
+      required this.returnType});
+
+  @override
+  Spec emit([covariant DeclarationOptions? options]) {
+    final (requiredParams, optionalParams) =
+        emitParameters(parameters, options);
+
+    final repType = BuiltinType.referred('Function')!;
+
+    final isNamedParams = desugarTypeAliases(returnType) is ObjectLiteralType &&
+        (desugarTypeAliases(returnType) as ObjectLiteralType)
+            .constructors
+            .isEmpty;
+
+    return ExtensionType((eType) => eType
+      ..name = name
+      ..primaryConstructorName = '_'
+      ..representationDeclaration = RepresentationDeclaration((r) => r
+        ..declaredRepresentationType = repType.emit(options?.toTypeOptions())
+        ..name = '_')
+      ..implements.add(repType.emit(options?.toTypeOptions()))
+      ..types
+          .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions())))
+      ..methods.add(Method((m) => m
+        ..name = 'call'
+        ..types
+            .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions())))
+        ..returns = returnType.emit(options?.toTypeOptions())
+        ..requiredParameters.addAll(requiredParams)
+        ..optionalParameters.addAll(optionalParams)
+        ..lambda = true
+        ..body = returnType
+            .emit(options?.toTypeOptions())
+            .call(
+                isNamedParams
+                    ? []
+                    : [
+                        ...requiredParams.map((p) => refer(p.name)),
+                        if (optionalParams.isNotEmpty)
+                          ...optionalParams.map((p) => refer(p.name))
+                      ],
+                isNamedParams
+                    ? [
+                        ...requiredParams.map((p) => (p.name, p.type)),
+                        if (optionalParams.isNotEmpty)
+                          ...optionalParams.map((p) => (p.name, p.type))
+                      ].asMap().map((_, v) {
+                        final (name, type) = v;
+                        final isNumType = type?.symbol == 'num';
+                        return MapEntry(
+                            name,
+                            isNumType
+                                ? refer(name).property('toDouble').call([])
+                                : refer(name));
+                      })
+                    : {},
+                typeParameters
+                    .map((t) => t.emit(options?.toTypeOptions()))
+                    .toList())
+            .code)));
+  }
+}
+
+// TODO: Merge properties/methods of related types
+class _UnionDeclaration extends NamedDeclaration
+    implements ExportableDeclaration {
+  @override
+  bool get exported => true;
+
+  @override
+  ID get id => ID(type: 'union', name: name);
+
+  bool isNullable;
+
+  List<Type> types;
+
+  List<GenericType> typeParameters;
+
+  _UnionDeclaration(
+      {required this.name,
+      this.types = const [],
+      this.isNullable = false,
+      List<GenericType>? typeParams})
+      : typeParameters = typeParams ?? [] {
+    if (typeParams == null) {
+      for (final type in types) {
+        typeParameters.addAll(getGenericTypes(type).map((t) {
+          t.constraint ??= BuiltinType.anyType;
+          return t;
+        }));
+      }
+    }
+  }
+
+  @override
+  String? dartName;
+
+  @override
+  String name;
+
+  @override
+  Spec emit([covariant DeclarationOptions? options]) {
+    options ??= DeclarationOptions();
+
+    final repType =
+        getLowestCommonAncestorOfTypes(types, isNullable: isNullable);
+
+    return ExtensionType((e) => e
+      ..name = name
+      ..primaryConstructorName = '_'
+      ..representationDeclaration = RepresentationDeclaration((r) => r
+        ..name = '_'
+        ..declaredRepresentationType = repType.emit(options?.toTypeOptions()))
+      ..implements.addAll([repType.emit(options?.toTypeOptions())])
+      ..types
+          .addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions())))
+      ..methods.addAll(types.map((t) {
+        final type = t.emit(options?.toTypeOptions());
+        final jsTypeAlt = getJSTypeAlternative(t);
+        return Method((m) {
+          final word = switch (t) {
+            DeclarationType(declarationName: final declName) => declName,
+            NamedType(name: final typeName, dartName: final dartTypeName) =>
+              dartTypeName ?? typeName,
+            _ => t.dartName ?? t.id.name
+          };
+          m
+            ..type = MethodType.getter
+            ..name = 'as${uppercaseFirstLetter(word)}'
+            ..returns = type
+            ..body = jsTypeAlt.id == t.id
+                ? refer('_').asA(type).code
+                : switch (t) {
+                    BuiltinType(name: final n) when n == 'int' => refer('_')
+                        .asA(jsTypeAlt.emit(options?.toTypeOptions()))
+                        .property('toDartInt')
+                        .code,
+                    BuiltinType(name: final n)
+                        when n == 'double' || n == 'num' =>
+                      refer('_')
+                          .asA(jsTypeAlt.emit(options?.toTypeOptions()))
+                          .property('toDartDouble')
+                          .code,
+                    BuiltinType() => refer('_')
+                        .asA(jsTypeAlt.emit(options?.toTypeOptions()))
+                        .property('toDart')
+                        .code,
+                    ReferredType(
+                      declaration: final decl,
+                      name: final n,
+                      url: final url
+                    )
+                        when decl is EnumDeclaration =>
+                      refer(n, url).property('_').call([
+                        refer('_')
+                            .asA(jsTypeAlt.emit(options?.toTypeOptions()))
+                            .property(decl.baseType is NamedType
+                                ? switch ((decl.baseType as NamedType).name) {
+                                    'int' => 'toDartInt',
+                                    'num' || 'double' => 'toDartDouble',
+                                    _ => 'toDart'
+                                  }
+                                : 'toDart')
+                      ]).code,
+                    _ => refer('_')
+                        .asA(jsTypeAlt.emit(options?.toTypeOptions()))
+                        .code
+                  };
+        });
+      })));
+  }
+}
diff --git a/web_generator/lib/src/dart_main.dart b/web_generator/lib/src/dart_main.dart
index 0b4cde2..2f5a8ef 100644
--- a/web_generator/lib/src/dart_main.dart
+++ b/web_generator/lib/src/dart_main.dart
@@ -87,14 +87,19 @@
   if (generatedCodeMap.isEmpty) return;
 
   // write code to file
-  if (generatedCodeMap.length == 1) {
-    final entry = generatedCodeMap.entries.first;
-    fs.writeFileSync(configOutput.toJS, entry.value.toJS);
-  } else {
+  if (dartDeclarations.multiFileOutput) {
     for (final entry in generatedCodeMap.entries) {
       fs.writeFileSync(
           p.join(configOutput, p.basename(entry.key)).toJS, entry.value.toJS);
     }
+  } else {
+    final entry = generatedCodeMap.entries.first;
+    fs.writeFileSync(configOutput.toJS, entry.value.toJS);
+    for (final entry in generatedCodeMap.entries.skip(1)) {
+      fs.writeFileSync(
+          p.join(p.dirname(configOutput), p.basename(entry.key)).toJS,
+          entry.value.toJS);
+    }
   }
 }
 
diff --git a/web_generator/lib/src/interop_gen/hasher.dart b/web_generator/lib/src/interop_gen/hasher.dart
new file mode 100644
index 0000000..117bfa0
--- /dev/null
+++ b/web_generator/lib/src/interop_gen/hasher.dart
@@ -0,0 +1,67 @@
+// Copyright (c) 2025, 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.
+
+import 'dart:convert';
+
+import 'package:convert/convert.dart';
+import 'package:crypto/crypto.dart';
+
+/// A hasher is used to give a unique hash to a given anonymous declaration
+class AnonymousHasher {
+  static String hashUnion(List<String> parts) {
+    final cloneParts = parts;
+    cloneParts.sort((a, b) => a.compareTo(b));
+
+    return _hashValues(cloneParts).toString().substring(0, 7);
+  }
+
+  static String hashTuple(List<String> parts) {
+    return _hashValues(parts).toString().substring(0, 7);
+  }
+
+  static String hashObject(List<(String, String)> parts) {
+    final cloneParts = parts;
+    cloneParts.sort((a, b) => a.$1.compareTo(b.$1));
+
+    final hashes = cloneParts.map((v) {
+      return _hashValues([v.$1, v.$2]).toString();
+    });
+
+    return _hashValues(hashes).toString().substring(0, 7);
+  }
+
+  static String hashFun(List<(String, String)> params, String returnType,
+      [bool constructor = false]) {
+    final hashes = params.map((v) {
+      return _hashValues([v.$1, v.$2]).toString();
+    });
+    final paramHash = _hashValues(hashes);
+    return _hashValues(
+            [constructor.toString(), paramHash.toString(), returnType])
+        .toString()
+        .substring(0, 7);
+  }
+}
+
+// TODO: A better way for hashing values
+int _hashValues(Iterable<String> values) {
+  final output = AccumulatorSink<Digest>();
+  final input = sha512.startChunkedConversion(output);
+
+  for (final v in values) {
+    final encoded = jsonEncode(v);
+    input.add(utf8.encode(encoded));
+  }
+
+  input.close();
+  final digest = output.events.single.bytes;
+
+  return BigInt.parse(
+          digest
+              .sublist(0, 8)
+              .map((b) => b.toRadixString(16).padLeft(2, '0'))
+              .join(),
+          radix: 16)
+      .toInt();
+}
diff --git a/web_generator/lib/src/interop_gen/namer.dart b/web_generator/lib/src/interop_gen/namer.dart
index 519e144..1e64351 100644
--- a/web_generator/lib/src/interop_gen/namer.dart
+++ b/web_generator/lib/src/interop_gen/namer.dart
@@ -26,6 +26,17 @@
 
   @override
   String toString() => '$type#$name${index != null ? '#$index' : ''}';
+
+  @override
+  bool operator ==(Object other) {
+    return other is ID &&
+        other.name == name &&
+        other.type == type &&
+        other.index == index;
+  }
+
+  @override
+  int get hashCode => Object.hash(type, name, index);
 }
 
 class UniqueNamer {
diff --git a/web_generator/lib/src/interop_gen/sub_type.dart b/web_generator/lib/src/interop_gen/sub_type.dart
new file mode 100644
index 0000000..0168986
--- /dev/null
+++ b/web_generator/lib/src/interop_gen/sub_type.dart
@@ -0,0 +1,434 @@
+// Copyright (c) 2025, 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.
+
+import 'package:collection/collection.dart';
+import 'package:meta/meta.dart';
+
+import '../ast/base.dart';
+import '../ast/builtin.dart';
+import '../ast/declarations.dart';
+import '../ast/helpers.dart';
+import '../ast/types.dart';
+import '../js_type_supertypes.dart';
+import 'hasher.dart';
+import 'transform.dart';
+
+/// A directed acyclic graph representation of an inverted type hierarchy,
+/// where a node (type) is connected to other nodes such that the given type
+/// is directed to a supertype of the given type.
+class TypeHierarchy {
+  List<TypeHierarchy> nodes = [];
+
+  String value;
+
+  TypeHierarchy(this.value);
+
+  Set<String>? _cachedTypeHierarchy;
+
+  TypeHierarchy? getMapWithValue(String value) {
+    if (this.value == value) return this;
+    if (nodes.isEmpty) return null;
+    return nodes.where((v) => v.getMapWithValue(value) != null).firstOrNull;
+  }
+
+  @visibleForTesting
+  TypeHierarchy getMapWithLookup(Iterable<int> path) {
+    if (path.isEmpty) return this;
+    return nodes[path.first].getMapWithLookup(path.skip(1));
+  }
+
+  @visibleForTesting
+  String getValueWithLookup(Iterable<int> path) {
+    if (path.isEmpty) return value;
+    return nodes[path.first].getValueWithLookup(path.skip(1));
+  }
+
+  @visibleForTesting
+  ({int level, List<int> path})? lookup(String value) {
+    return _lookup(value, 0, []);
+  }
+
+  ({int level, List<int> path})? _lookup(
+      String value, int level, List<int> indexPath) {
+    if (this.value == value) return (level: level, path: indexPath);
+    if (nodes.isEmpty) {
+      return null;
+    } else {
+      // find value
+      return nodes.mapIndexed((index, node) {
+        final lookupVal = node._lookup(value, level + 1, [...indexPath, index]);
+        return lookupVal;
+      }).firstWhereOrNull((v) => v != null);
+    }
+  }
+
+  void addChainedValues(Iterable<String> list) {
+    if (list.isEmpty) {
+      return;
+    } else if (list.length == 1) {
+      nodes.add(TypeHierarchy(list.single));
+      return;
+    }
+    nodes.add(TypeHierarchy(list.first)..addChainedValues(list.skip(1)));
+  }
+
+  Set<String> expand() {
+    if (_cachedTypeHierarchy != null) return _cachedTypeHierarchy!;
+    final set = <String>{value};
+    for (final node in nodes) {
+      set.add(node.value);
+      set.addAll(node.expand());
+    }
+
+    _cachedTypeHierarchy ??= set;
+
+    return set;
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return other is TypeHierarchy && other.value == value;
+  }
+
+  @override
+  int get hashCode => Object.hashAll([value]);
+
+  @override
+  String toString() => value.toString();
+}
+
+Map<String, TypeHierarchy> _cachedTrees = {};
+
+/// Given a set of type hierarchies, form a map indexing the number of edges
+/// directed at a given node (the center for a specific [TypeHierarchy]) to the
+/// node.
+Map<TypeHierarchy, int> generateMapFromNodes(List<TypeHierarchy> nodes) {
+  final graph = <TypeHierarchy, int>{};
+  for (final node in nodes) {
+    graph[node] = 0;
+    _mapFromNodes(node.nodes, graph);
+  }
+  return graph;
+}
+
+void _mapFromNodes(List<TypeHierarchy> nodes, Map<TypeHierarchy, int> map) {
+  for (final node in nodes) {
+    map.update(node, (v) => v++, ifAbsent: () => 1);
+    _mapFromNodes(node.nodes, map);
+  }
+}
+
+/// Given a set of type hierarchies, form a topologically sorted list using
+/// (a modified version of) Kahn's algorithm on the type hierarchies,
+/// sorting each level as a [Set] of distinct types.
+///
+/// Returns a list of sets of strings forming the levels in the topological
+/// ordering
+List<Set<String>> topologicalList(List<TypeHierarchy> nodes) {
+  final graph = generateMapFromNodes(nodes);
+  final outputList = <Set<String>>[];
+
+  final nodesWithoutEdges = <TypeHierarchy>{};
+  for (final MapEntry(key: node, value: noOfEdges) in graph.entries) {
+    if (noOfEdges == 0) nodesWithoutEdges.add(node);
+  }
+
+  while (nodesWithoutEdges.isNotEmpty) {
+    final listToAdd = <String>{};
+    final nodesToAddNext = <TypeHierarchy>{};
+    for (final node in nodesWithoutEdges) {
+      listToAdd.add(node.value);
+      for (final n in node.nodes) {
+        graph[n] = graph[n]! - 1;
+        if (graph[n] == 0) {
+          nodesToAddNext.add(n);
+        }
+      }
+    }
+
+    nodesWithoutEdges.clear();
+    nodesWithoutEdges.addAll(nodesToAddNext);
+    outputList.add(listToAdd);
+  }
+
+  return outputList;
+}
+
+@visibleForTesting
+void clearTypeHierarchyCache() => _cachedTrees.clear();
+
+/// Given a [Type], get its ancestoral graph (a DAG) and form a [TypeHierarchy]
+/// from it.
+///
+/// The function recursively goes through the types [type] inherits from,
+/// depending on what kind of type it is, and follows this ancestral tree up
+/// until it gets to the final ancestor: `JSAny` (all types inherit `JSAny`)
+TypeHierarchy getTypeHierarchy(Type type) {
+  if (type case final ReferredType ref
+      when ref.declaration is TypeAliasDeclaration) {
+    return getTypeHierarchy((ref.declaration as TypeAliasDeclaration).type);
+  }
+
+  final name = type is NamedType ? type.name : type.id.name;
+  return _cachedTrees.putIfAbsent(name, () {
+    final hierarchy = TypeHierarchy(name);
+
+    switch (type) {
+      case HomogenousEnumType(types: final homogenousTypes):
+        hierarchy.nodes.add(getTypeHierarchy(homogenousTypes.first.baseType));
+        break;
+      case UnionType(types: final types):
+        // subtype is union
+        hierarchy.nodes
+            .add(getTypeHierarchy(getLowestCommonAncestorOfTypes(types)));
+        break;
+      case TupleType(types: final types):
+        // subtype is JSArray<union>
+        hierarchy.nodes.add(getTypeHierarchy(BuiltinType.primitiveType(
+            PrimitiveType.array,
+            typeParams: [getLowestCommonAncestorOfTypes(types)])));
+        break;
+      case GenericType(constraint: final constraintedType):
+        hierarchy.nodes
+            .add(getTypeHierarchy(constraintedType ?? BuiltinType.anyType));
+        break;
+      case ReferredDeclarationType(type: final referredType):
+        return getTypeHierarchy(referredType);
+      case ReferredType(declaration: final decl) when decl is ClassDeclaration:
+        final types = [
+          if (decl.extendedType != null) decl.extendedType!,
+          ...decl.implementedTypes,
+        ];
+        if (types.isEmpty) {
+          hierarchy.nodes.add(getTypeHierarchy(
+              BuiltinType.primitiveType(PrimitiveType.object)));
+        } else {
+          for (final t in types) {
+            hierarchy.nodes.add(getTypeHierarchy(t));
+          }
+        }
+        break;
+      case ReferredType(declaration: final decl)
+          when decl is InterfaceDeclaration:
+        if (decl.extendedTypes.isEmpty) {
+          hierarchy.nodes.add(getTypeHierarchy(
+              BuiltinType.primitiveType(PrimitiveType.object)));
+        } else {
+          for (final t in decl.extendedTypes) {
+            hierarchy.nodes.add(getTypeHierarchy(t));
+          }
+        }
+        break;
+      case ReferredType(declaration: final decl)
+          when decl is NamespaceDeclaration:
+      case ObjectLiteralType():
+        // subtype is JSObject
+        hierarchy.nodes.add(
+            getTypeHierarchy(BuiltinType.primitiveType(PrimitiveType.object)));
+        break;
+      case ReferredType(declaration: final decl)
+          when decl is FunctionDeclaration:
+      case ClosureType():
+        // subtype is JSFunction
+        hierarchy.nodes
+            .add(getTypeHierarchy(BuiltinType.referred('Function')!));
+        break;
+      case LiteralType(baseType: final baseType):
+        hierarchy.nodes.add(getTypeHierarchy(baseType));
+        break;
+
+      case BuiltinType():
+        // we can only use JS types
+        final BuiltinType(name: jsName) =
+            getJSTypeAlternative(type) as BuiltinType;
+
+        var value = jsTypeSupertypes[jsName];
+        final list = <String>[];
+        while (value != null) {
+          list.add(value);
+          value = jsTypeSupertypes[value];
+        }
+        hierarchy.addChainedValues(list);
+        break;
+      default:
+        print('WARN: Could not get type hierarchy for type of kind '
+            '${type.runtimeType}. Skipping...');
+        break;
+    }
+    return hierarchy;
+  });
+}
+
+TypeMap createTypeMap(List<Type> types, {TypeMap? map}) {
+  final outputMap = map ??
+      TypeMap({
+        'JSBoolean': BuiltinType.primitiveType(PrimitiveType.boolean,
+            shouldEmitJsType: true),
+        'JSNumber': BuiltinType.primitiveType(PrimitiveType.num,
+            shouldEmitJsType: true),
+        'JSObject': BuiltinType.primitiveType(PrimitiveType.object,
+            shouldEmitJsType: true),
+        'JSString': BuiltinType.primitiveType(PrimitiveType.string,
+            shouldEmitJsType: true),
+        'JSVoid': BuiltinType.primitiveType(PrimitiveType.$void,
+            shouldEmitJsType: true),
+        'JSSymbol': BuiltinType.primitiveType(PrimitiveType.symbol,
+            shouldEmitJsType: true),
+        'JSBigInt': BuiltinType.primitiveType(PrimitiveType.bigint,
+            shouldEmitJsType: true),
+        'JSAny': BuiltinType.primitiveType(PrimitiveType.any),
+        'JSTypedArray':
+            BuiltinType(name: 'JSTypedArray', fromDartJSInterop: true),
+      });
+
+  void addToMap(Type type) {
+    outputMap[type is NamedType ? type.name : type.id.name] = type;
+  }
+
+  for (final type in types) {
+    final name = type is NamedType ? type.name : type.id.name;
+
+    if (outputMap.containsKey(name)) continue;
+
+    addToMap(type);
+    switch (type) {
+      case ReferredDeclarationType(type: final referredType):
+        outputMap.addAll(createTypeMap([referredType], map: outputMap));
+        break;
+      case ReferredType(declaration: final decl) when decl is ClassDeclaration:
+        outputMap.addAll(createTypeMap([
+          if (decl.extendedType != null) decl.extendedType!,
+          ...decl.implementedTypes
+        ], map: outputMap));
+      case ReferredType(declaration: final decl)
+          when decl is FunctionDeclaration:
+        addToMap(BuiltinType.referred('Function')!);
+        break;
+      case ReferredType(declaration: final decl)
+          when decl is InterfaceDeclaration:
+        outputMap
+            .addAll(createTypeMap([...decl.extendedTypes], map: outputMap));
+      case HomogenousEnumType(types: final homogenousTypes):
+        outputMap.addAll(
+            createTypeMap([homogenousTypes.first.baseType], map: outputMap));
+        break;
+      case TupleType(types: final types):
+      case UnionType(types: final types):
+        outputMap.addAll(createTypeMap(types, map: outputMap));
+        break;
+      case GenericType(constraint: final constrainedType?):
+        outputMap.addAll(createTypeMap([constrainedType], map: outputMap));
+        break;
+      default:
+        break;
+    }
+  }
+
+  return outputMap;
+}
+
+/// Given a list of types, usually from a union, get the subtype shared by the
+/// types by getting the lowest common ancestor between the given types.
+///
+/// If a [typeMap] is not provided, it generates the smallest necessary typemap
+/// for the types. If only one type is provided, that type is returned.
+///
+/// Types may have more than one type in common. In such case, a union of those
+/// common types is returned by the given function.
+Type getLowestCommonAncestorOfTypes(List<Type> types,
+    {bool isNullable = false, TypeMap? typeMap}) {
+  typeMap ??= createTypeMap(types);
+
+  if (types.isEmpty) throw Exception('You must pass types');
+  if (types.singleOrNull case final singleType?) {
+    return singleType..isNullable = isNullable;
+  }
+
+  if (_getSharedPrimitiveTypeIfAny(types, isNullable: isNullable)
+      case final t?) {
+    return t;
+  }
+
+  // Calculate the intersection of all type hierarchies
+  final typeMaps = types.map(getTypeHierarchy);
+  final parentHierarchy = typeMaps.map((map) => map.expand());
+  final commonTypes =
+      parentHierarchy.reduce((val, element) => val.intersection(element));
+
+  final topoList = topologicalList(typeMaps.toList());
+  for (final level in topoList) {
+    final typesAtLevel = commonTypes.intersection(level);
+    // look for level where common types are present
+    // the LCA are on the same topological level.
+    if (typesAtLevel.isNotEmpty) {
+      if (typesAtLevel.singleOrNull case final finalType?) {
+        return deduceType(finalType, typeMap);
+      } else {
+        return UnionType(
+            types: typesAtLevel.map((c) => deduceType(c, typeMap!)).toList(),
+            name: 'AnonymousUnion_'
+                '${AnonymousHasher.hashUnion(commonTypes.toList())}');
+      }
+    }
+  }
+
+  return BuiltinType.primitiveType(PrimitiveType.any);
+}
+
+Type deduceType(String name, TypeMap map) {
+  final referredType =
+      BuiltinType.referred(name.startsWith('JS') ? name.substring(2) : name);
+  if (referredType != null) return referredType;
+  return map[name] ?? BuiltinType.primitiveType(PrimitiveType.any);
+}
+
+/// Checks if there is a type shared between the types, usually in the
+/// case of a literal
+Type? _getSharedPrimitiveTypeIfAny(List<Type> types, {bool isNullable = true}) {
+  LiteralKind? kind;
+  Type? equalType;
+  bool? isNull;
+  var allEqualTypes = true;
+  var allLiteralTypes = true;
+
+  for (final t in types) {
+    if (t is LiteralType) {
+      if (t.kind == LiteralKind.$null) {
+        isNull ??= true;
+        continue;
+      }
+      kind ??= t.kind;
+      if (kind != t.kind) {
+        allEqualTypes = false;
+        break;
+      }
+    } else {
+      allLiteralTypes = false;
+      equalType ??= t;
+      if (equalType.id != t.id) {
+        allEqualTypes = false;
+        break;
+      }
+    }
+  }
+
+  if (allEqualTypes) {
+    if (allLiteralTypes) {
+      final primitiveType = switch (kind) {
+        LiteralKind.string => PrimitiveType.string,
+        LiteralKind.$false || LiteralKind.$true => PrimitiveType.boolean,
+        LiteralKind.double => PrimitiveType.double,
+        LiteralKind.int => PrimitiveType.int,
+        _ => PrimitiveType.any
+      };
+
+      return BuiltinType.primitiveType(primitiveType,
+          isNullable: isNull ?? isNullable);
+    } else {
+      return equalType!;
+    }
+  }
+
+  return null;
+}
diff --git a/web_generator/lib/src/interop_gen/transform.dart b/web_generator/lib/src/interop_gen/transform.dart
index f60bfde..87d5d67 100644
--- a/web_generator/lib/src/interop_gen/transform.dart
+++ b/web_generator/lib/src/interop_gen/transform.dart
@@ -7,10 +7,12 @@
 
 import 'package:code_builder/code_builder.dart';
 import 'package:dart_style/dart_style.dart';
+import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
 
 import '../ast/base.dart';
 import '../ast/declarations.dart';
+import '../ast/helpers.dart';
 import '../config.dart';
 import '../js/helpers.dart';
 import '../js/typescript.dart' as ts;
@@ -28,8 +30,11 @@
 
 class TransformResult {
   ProgramDeclarationMap programDeclarationMap;
+  ProgramDeclarationMap commonTypes;
+  bool multiFileOutput;
 
-  TransformResult._(this.programDeclarationMap);
+  TransformResult._(this.programDeclarationMap, {this.commonTypes = const {}})
+      : multiFileOutput = programDeclarationMap.length > 1;
 
   // TODO(https://github.com/dart-lang/web/issues/388): Handle union of overloads
   //  (namespaces + functions, multiple interfaces, etc)
@@ -39,7 +44,7 @@
 
     _setGlobalOptions(config);
 
-    return programDeclarationMap.map((file, declMap) {
+    return {...programDeclarationMap, ...commonTypes}.map((file, declMap) {
       final emitter =
           DartEmitter.scoped(useNullSafetySyntax: true, orderDirectives: true);
       final specs = declMap.values
@@ -60,14 +65,29 @@
             return l;
           }));
         }
+        var parentCaseIgnore = false;
+        var anonymousIgnore = false;
+        var tupleDecl = false;
+
+        for (final value in declMap.values) {
+          if (value is TupleDeclaration) tupleDecl = true;
+          if (value.id.name.contains('Anonymous')) anonymousIgnore = true;
+          if (value case NestableDeclaration(parent: final _?)) {
+            parentCaseIgnore = true;
+          }
+        }
         l
-          ..ignoreForFile.addAll([
+          ..ignoreForFile.addAll({
             'constant_identifier_names',
             'non_constant_identifier_names',
-            if (declMap.values
-                .any((d) => d is NestableDeclaration && d.parent != null))
+            if (parentCaseIgnore) 'camel_case_types',
+            if (anonymousIgnore) ...[
               'camel_case_types',
-          ])
+              'library_private_types_in_public_api',
+              'unnecessary_parenthesis'
+            ],
+            if (tupleDecl) 'unnecessary_parenthesis',
+          })
           ..body.addAll(specs);
       });
       return MapEntry(
@@ -79,10 +99,11 @@
 }
 
 /// A map of declarations, where the key is the declaration's stringified [ID].
-extension type NodeMap._(Map<String, Node> decls) implements Map<String, Node> {
-  NodeMap([Map<String, Node>? decls]) : decls = decls ?? <String, Node>{};
+extension type NodeMap<N extends Node>._(Map<String, N> decls)
+    implements Map<String, N> {
+  NodeMap([Map<String, N>? decls]) : decls = decls ?? <String, N>{};
 
-  List<Node> findByName(String name) {
+  List<N> findByName(String name) {
     return decls.entries
         .where((e) {
           final n = UniqueNamer.parse(e.key).name;
@@ -95,7 +116,7 @@
         .toList();
   }
 
-  List<Node> findByQualifiedName(QualifiedName qName) {
+  List<N> findByQualifiedName(QualifiedName qName) {
     return decls.entries
         .where((e) {
           final name = UniqueNamer.parse(e.key).name;
@@ -106,7 +127,14 @@
         .toList();
   }
 
-  void add(Node decl) => decls[decl.id.toString()] = decl;
+  void add(N decl) => decls[decl.id.toString()] = decl;
+}
+
+extension type TypeMap._(Map<String, Type> types) implements NodeMap<Type> {
+  TypeMap([Map<String, Type>? types]) : types = types ?? <String, Type>{};
+
+  @redeclare
+  void add(Type decl) => types[decl.id.toString()] = decl;
 }
 
 /// A program map is a map used for handling the context of
@@ -136,6 +164,11 @@
   /// The typescript program for the given project
   final ts.TSProgram program;
 
+  /// Common types shared across files in the program.
+  ///
+  /// This includes builtin supported types like `JSTuple`
+  final p.PathMap<NodeMap<NamedDeclaration>> _commonTypes = p.PathMap.of({});
+
   /// The type checker for the given program
   ///
   /// It is generated as this to prevent having to regenerate it multiple times
@@ -192,6 +225,31 @@
     return name == null ? null : nodeMap.findByName(name);
   }
 
+  (String, NamedDeclaration)? getCommonType(String name,
+      {(String, NamedDeclaration)? ifAbsent}) {
+    try {
+      final MapEntry(key: url, value: nodeMap) = _commonTypes.entries
+          .firstWhere((e) => e.value.containsKey(name), orElse: () {
+        if (ifAbsent case (final file, final decl)) {
+          _commonTypes.update(
+            file,
+            (nodeMap) => nodeMap..add(decl),
+            ifAbsent: () => NodeMap()..add(decl),
+          );
+          return MapEntry(file, _commonTypes[file]!);
+        }
+        throw Exception('Could not find common type for decl $name');
+      });
+
+      if ((url, nodeMap) case (final declUrl?, final declarationMap)) {
+        return (declUrl, declarationMap.findByName(name).first);
+      }
+    } on Exception {
+      return null;
+    }
+    return null;
+  }
+
   /// Get the node map for a given [file],
   /// transforming it and generating it if needed.
   NodeMap getNodeMap(String file) {
@@ -275,6 +333,7 @@
       outputNodeMap[file!] = programMap.getNodeMap(file);
     }
 
-    return TransformResult._(outputNodeMap);
+    return TransformResult._(outputNodeMap,
+        commonTypes: programMap._commonTypes.cast());
   }
 }
diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart
index a026f22..167ed26 100644
--- a/web_generator/lib/src/interop_gen/transform/transformer.dart
+++ b/web_generator/lib/src/interop_gen/transform/transformer.dart
@@ -15,6 +15,7 @@
 import '../../js/helpers.dart';
 import '../../js/typescript.dart' as ts;
 import '../../js/typescript.types.dart';
+import '../hasher.dart';
 import '../namer.dart';
 import '../qualified_name.dart';
 import '../transform.dart';
@@ -52,7 +53,7 @@
   final NodeMap nodeMap = NodeMap();
 
   /// A map of types
-  final NodeMap typeMap = NodeMap();
+  final TypeMap typeMap = TypeMap();
 
   /// The program map
   final ProgramMap programMap;
@@ -502,7 +503,7 @@
   }
 
   PropertyDeclaration _transformProperty(TSPropertyEntity property,
-      {required UniqueNamer parentNamer, required TypeDeclaration parent}) {
+      {required UniqueNamer parentNamer, TypeDeclaration? parent}) {
     final name = property.name.text;
 
     final (:id, name: dartName) = parentNamer.makeUnique(name, 'var');
@@ -515,13 +516,13 @@
       // check if
       final referredType = type as TSTypeReferenceNode;
       final referredTypeName = parseQualifiedName(referredType.typeName);
-      if (referredTypeName.asName == parent.name) {
-        propType = parent.asReferredType(type.typeArguments?.toDart
+      if (referredTypeName.asName == parent?.name) {
+        propType = parent?.asReferredType(type.typeArguments?.toDart
             .map((t) => _transformType(t, typeArg: true))
             .toList());
       }
     } else if (property.type case final type? when ts.isThisTypeNode(type)) {
-      propType = parent.asReferredType(parent.typeParameters);
+      propType = parent?.asReferredType(parent.typeParameters);
     }
 
     final propertyDeclaration = PropertyDeclaration(
@@ -537,12 +538,13 @@
         readonly: isReadonly,
         isNullable: property.questionToken != null,
         documentation: _parseAndTransformDocumentation(property));
-    propertyDeclaration.parent = parent;
+
+    if (parent != null) propertyDeclaration.parent = parent;
     return propertyDeclaration;
   }
 
   MethodDeclaration _transformMethod(TSMethodEntity method,
-      {required UniqueNamer parentNamer, required TypeDeclaration parent}) {
+      {required UniqueNamer parentNamer, TypeDeclaration? parent}) {
     final name = method.name.text;
     // TODO(nikeokoronkwo): Let's make the unique name types enums
     //  or extension types to track the type more easily
@@ -560,13 +562,13 @@
       // check if
       final referredType = type as TSTypeReferenceNode;
       final referredTypeName = parseQualifiedName(referredType.typeName);
-      if (referredTypeName.asName == parent.name) {
-        methodType = parent.asReferredType(type.typeArguments?.toDart
+      if (referredTypeName.asName == parent?.name) {
+        methodType = parent?.asReferredType(type.typeArguments?.toDart
             .map((t) => _transformType(t, typeArg: true))
             .toList());
       }
     } else if (method.type case final type? when ts.isThisTypeNode(type)) {
-      methodType = parent.asReferredType(parent.typeParameters);
+      methodType = parent?.asReferredType(parent.typeParameters);
     }
 
     final methodDeclaration = MethodDeclaration(
@@ -581,13 +583,13 @@
           if (paramRawType case final ty? when ts.isTypeReferenceNode(ty)) {
             final referredType = ty as TSTypeReferenceNode;
             final referredTypeName = parseQualifiedName(referredType.typeName);
-            if (referredTypeName.asName == parent.name) {
-              paramType = parent.asReferredType(ty.typeArguments?.toDart
+            if (referredTypeName.asName == parent?.name) {
+              paramType = parent?.asReferredType(ty.typeArguments?.toDart
                   .map((t) => _transformType(t, typeArg: true))
                   .toList());
             }
           } else if (paramRawType case final ty? when ts.isThisTypeNode(ty)) {
-            paramType = parent.asReferredType(parent.typeParameters);
+            paramType = parent?.asReferredType(parent.typeParameters);
           }
           return _transformParameter(t, paramType);
         }).toList(),
@@ -600,7 +602,8 @@
         isNullable: (method.kind == TSSyntaxKind.MethodSignature) &&
             (method as TSMethodSignature).questionToken != null,
         documentation: _parseAndTransformDocumentation(method));
-    methodDeclaration.parent = parent;
+
+    if (parent != null) methodDeclaration.parent = parent;
     return methodDeclaration;
   }
 
@@ -633,7 +636,7 @@
   MethodDeclaration _transformCallSignature(
       TSCallSignatureDeclaration callSignature,
       {required UniqueNamer parentNamer,
-      required TypeDeclaration parent}) {
+      TypeDeclaration? parent}) {
     final (:id, name: dartName) = parentNamer.makeUnique('call', 'fun');
 
     final params = callSignature.parameters.toDart;
@@ -645,14 +648,14 @@
       // check if
       final referredType = type as TSTypeReferenceNode;
       final referredTypeName = parseQualifiedName(referredType.typeName);
-      if (referredTypeName.asName == parent.name) {
-        methodType = parent.asReferredType(type.typeArguments?.toDart
+      if (referredTypeName.asName == parent?.name) {
+        methodType = parent?.asReferredType(type.typeArguments?.toDart
             .map((t) => _transformType(t, typeArg: true))
             .toList());
       }
     } else if (callSignature.type case final type?
         when ts.isThisTypeNode(type)) {
-      methodType = parent.asReferredType(parent.typeParameters);
+      methodType = parent?.asReferredType(parent.typeParameters);
     }
 
     final methodDeclaration = MethodDeclaration(
@@ -667,14 +670,15 @@
                 ? _transformType(callSignature.type!)
                 : BuiltinType.anyType),
         documentation: _parseAndTransformDocumentation(callSignature));
-    methodDeclaration.parent = parent;
+
+    if (parent != null) methodDeclaration.parent = parent;
     return methodDeclaration;
   }
 
   // TODO: Handling overloading of indexers
   (OperatorDeclaration, OperatorDeclaration?) _transformIndexer(
       TSIndexSignatureDeclaration indexSignature,
-      {required TypeDeclaration parent}) {
+      {TypeDeclaration? parent}) {
     final params = indexSignature.parameters.toDart;
 
     final typeParams = indexSignature.typeParameters?.toDart;
@@ -687,14 +691,14 @@
       // check if
       final referredType = type as TSTypeReferenceNode;
       final referredTypeName = parseQualifiedName(referredType.typeName);
-      if (referredTypeName.asName == parent.name) {
-        indexerType = parent.asReferredType(type.typeArguments?.toDart
+      if (referredTypeName.asName == parent?.name) {
+        indexerType = parent?.asReferredType(type.typeArguments?.toDart
             .map((t) => _transformType(t, typeArg: true))
             .toList());
       }
     } else if (indexSignature.type case final type
         when ts.isThisTypeNode(type)) {
-      indexerType = parent.asReferredType(parent.typeParameters);
+      indexerType = parent?.asReferredType(parent.typeParameters);
     }
 
     final doc = _parseAndTransformDocumentation(indexSignature);
@@ -720,13 +724,15 @@
             documentation: doc)
         : null;
 
-    getOperatorDeclaration.parent = parent;
-    setOperatorDeclaration?.parent = parent;
+    if (parent != null) {
+      getOperatorDeclaration.parent = parent;
+      setOperatorDeclaration?.parent = parent;
+    }
     return (getOperatorDeclaration, setOperatorDeclaration);
   }
 
   MethodDeclaration _transformGetter(TSGetAccessorDeclaration getter,
-      {required UniqueNamer parentNamer, required TypeDeclaration parent}) {
+      {required UniqueNamer parentNamer, TypeDeclaration? parent}) {
     final name = getter.name.text;
     final (:id, name: dartName) = parentNamer.makeUnique(name, 'get');
 
@@ -742,13 +748,13 @@
       // check if
       final referredType = type as TSTypeReferenceNode;
       final referredTypeName = parseQualifiedName(referredType.typeName);
-      if (referredTypeName.asName == parent.name) {
-        methodType = parent.asReferredType(type.typeArguments?.toDart
+      if (referredTypeName.asName == parent?.name) {
+        methodType = parent?.asReferredType(type.typeArguments?.toDart
             .map((t) => _transformType(t, typeArg: true))
             .toList());
       }
     } else if (getter.type case final type? when ts.isThisTypeNode(type)) {
-      methodType = parent.asReferredType(parent.typeParameters);
+      methodType = parent?.asReferredType(parent.typeParameters);
     }
 
     final methodDeclaration = MethodDeclaration(
@@ -765,12 +771,13 @@
                 ? _transformType(getter.type!)
                 : BuiltinType.anyType),
         documentation: _parseAndTransformDocumentation(getter));
-    methodDeclaration.parent = parent;
+
+    if (parent != null) methodDeclaration.parent = parent;
     return methodDeclaration;
   }
 
   MethodDeclaration _transformSetter(TSSetAccessorDeclaration setter,
-      {required UniqueNamer parentNamer, required TypeDeclaration parent}) {
+      {required UniqueNamer parentNamer, TypeDeclaration? parent}) {
     final name = setter.name.text;
     final (:id, name: dartName) = parentNamer.makeUnique(name, 'set');
 
@@ -792,13 +799,13 @@
           if (paramRawType case final ty? when ts.isTypeReferenceNode(ty)) {
             final referredType = ty as TSTypeReferenceNode;
             final referredTypeName = parseQualifiedName(referredType.typeName);
-            if (referredTypeName.asName == parent.name) {
-              paramType = parent.asReferredType(ty.typeArguments?.toDart
+            if (referredTypeName.asName == parent?.name) {
+              paramType = parent?.asReferredType(ty.typeArguments?.toDart
                   .map((t) => _transformType(t, typeArg: true))
                   .toList());
             }
           } else if (paramRawType case final ty? when ts.isThisTypeNode(ty)) {
-            paramType = parent.asReferredType(parent.typeParameters);
+            paramType = parent?.asReferredType(parent.typeParameters);
           }
           return _transformParameter(t, paramType);
         }).toList(),
@@ -809,7 +816,8 @@
             ? _transformType(setter.type!)
             : BuiltinType.anyType,
         documentation: _parseAndTransformDocumentation(setter));
-    methodDeclaration.parent = parent;
+
+    if (parent != null) methodDeclaration.parent = parent;
     return methodDeclaration;
   }
 
@@ -1049,36 +1057,190 @@
   /// [typeArg] represents whether the [TSTypeNode] is being passed in the
   /// context of a type argument, as Dart core types are not allowed in
   /// type arguments
+  ///
+  /// [isNullable] means that the given type is nullable, usually when it is
+  /// unionized with `undefined` or `null`
   // TODO(nikeokoronkwo): Add support for constructor and function types,
   //  https://github.com/dart-lang/web/issues/410
   //  https://github.com/dart-lang/web/issues/422
   Type _transformType(TSTypeNode type,
-      {bool parameter = false, bool typeArg = false}) {
+      {bool parameter = false, bool typeArg = false, bool? isNullable}) {
     switch (type.kind) {
       case TSSyntaxKind.ParenthesizedType:
         return _transformType((type as TSParenthesizedTypeNode).type,
-            parameter: parameter, typeArg: typeArg);
+            parameter: parameter, typeArg: typeArg, isNullable: isNullable);
       case TSSyntaxKind.TypeReference:
         final refType = type as TSTypeReferenceNode;
 
-        return _getTypeFromTypeNode(refType, typeArg: typeArg);
+        return _getTypeFromTypeNode(refType,
+            typeArg: typeArg, isNullable: isNullable ?? false);
+      case TSSyntaxKind.TypeLiteral:
+        // type literal
+        final typeLiteralNode = type as TSTypeLiteralNode;
+
+        // lists
+        final properties = <PropertyDeclaration>[];
+        final methods = <MethodDeclaration>[];
+        final constructors = <ConstructorDeclaration>[];
+        final operators = <OperatorDeclaration>[];
+
+        final typeNamer = ScopedUniqueNamer({'get', 'set'});
+
+        // mark the default constructor as used
+        typeNamer.markUsed('', 'constructor');
+        typeNamer.markUsed('unnamed', 'constructor');
+
+        // transform decls
+        for (final member in typeLiteralNode.members.toDart) {
+          switch (member.kind) {
+            case TSSyntaxKind.PropertySignature:
+              final prop = _transformProperty(member as TSPropertySignature,
+                  parentNamer: typeNamer);
+              properties.add(prop);
+            case TSSyntaxKind.MethodSignature:
+              final method = _transformMethod(member as TSMethodSignature,
+                  parentNamer: typeNamer);
+              methods.add(method);
+            case TSSyntaxKind.IndexSignature:
+              final (opGet, opSetOrNull) = _transformIndexer(
+                member as TSIndexSignatureDeclaration,
+              );
+              operators.add(opGet);
+              if (opSetOrNull case final opSet?) {
+                operators.add(opSet);
+              }
+            case TSSyntaxKind.CallSignature:
+              final callSignature = _transformCallSignature(
+                member as TSCallSignatureDeclaration,
+                parentNamer: typeNamer,
+              );
+              methods.add(callSignature);
+            case TSSyntaxKind.ConstructSignature:
+              final constructor = _transformConstructor(
+                  member as TSConstructSignatureDeclaration,
+                  parentNamer: typeNamer);
+              constructors.add(constructor);
+            case TSSyntaxKind.GetAccessor:
+              final getter = _transformGetter(
+                  member as TSGetAccessorDeclaration,
+                  parentNamer: typeNamer);
+              methods.add(getter);
+              break;
+            case TSSyntaxKind.SetAccessor:
+              final setter = _transformSetter(
+                  member as TSSetAccessorDeclaration,
+                  parentNamer: typeNamer);
+              methods.add(setter);
+              break;
+            default:
+              break;
+          }
+        }
+
+        // get a name
+        final name = 'AnonymousType_${AnonymousHasher.hashObject([
+              ...properties.map((p) => (p.name, p.type.id.name)),
+              ...methods.map((p) => (p.name, p.returnType.id.name)),
+              ...constructors.map((p) => (
+                    p.name ?? 'new',
+                    p.parameters.map((a) => a.type.id.name).join(',')
+                  )),
+              ...operators.map((p) => (p.name, p.returnType.id.name)),
+            ])}';
+
+        // get an expected id
+        final expectedId = ID(type: 'type', name: name);
+        if (typeMap.containsKey(expectedId.toString())) {
+          return typeMap[expectedId.toString()] as ObjectLiteralType;
+        }
+
+        final anonymousTypeObject = ObjectLiteralType(
+          name: name,
+          id: expectedId,
+          properties: properties,
+          methods: methods,
+          operators: operators,
+          constructors: constructors,
+        );
+
+        final anonymousType = typeMap.putIfAbsent(expectedId.toString(), () {
+          namer.markUsed(name);
+          return anonymousTypeObject;
+        }) as ObjectLiteralType;
+
+        return anonymousType..isNullable = isNullable ?? false;
+      case TSSyntaxKind.ConstructorType || TSSyntaxKind.FunctionType:
+        final funType = type as TSFunctionOrConstructorTypeNodeBase;
+
+        final parameters =
+            funType.parameters.toDart.map(_transformParameter).toList();
+
+        final typeParameters = funType.typeParameters?.toDart
+                .map(_transformTypeParamDeclaration)
+                .toList() ??
+            [];
+
+        final returnType = _transformType(funType.type);
+
+        final isConstructor = type.kind == TSSyntaxKind.ConstructorType;
+
+        final name = '_Anonymous${isConstructor ? 'Constructor' : 'Function'}_'
+            '${AnonymousHasher.hashFun(parameters.map((a) => (
+                  a.name,
+                  a.type.id.name
+                )).toList(), returnType.id.name, isConstructor)}';
+
+        final expectedId = ID(type: 'type', name: name);
+        if (typeMap.containsKey(expectedId.toString())) {
+          return typeMap[expectedId.toString()] as ClosureType;
+        }
+
+        final closureTypeObject = isConstructor
+            ? ConstructorType(
+                name: name,
+                id: expectedId,
+                returnType: returnType,
+                parameters: parameters,
+                typeParameters: typeParameters)
+            : FunctionType(
+                name: name,
+                id: expectedId,
+                returnType: returnType,
+                parameters: parameters,
+                typeParameters: typeParameters);
+
+        final closureType = typeMap.putIfAbsent(expectedId.toString(), () {
+          namer.markUsed(name);
+          return closureTypeObject;
+        }) as ClosureType;
+
+        return closureType..isNullable = isNullable ?? false;
       case TSSyntaxKind.UnionType:
         final unionType = type as TSUnionTypeNode;
-        // TODO: Unions
-        final types = unionType.types.toDart.map<Type>(_transformType).toList();
+        final unionTypes = unionType.types.toDart;
+        final nonNullableUnionTypes = unionTypes
+            .where((t) =>
+                t.kind != TSSyntaxKind.UndefinedKeyword &&
+                !(t.kind == TSSyntaxKind.LiteralType &&
+                    (t as TSLiteralTypeNode).literal.kind ==
+                        TSSyntaxKind.NullKeyword))
+            .toList();
+        final shouldBeNullable =
+            nonNullableUnionTypes.length != unionTypes.length;
+
+        if (nonNullableUnionTypes.singleOrNull case final singleTypeNode?) {
+          return _transformType(singleTypeNode, isNullable: shouldBeNullable);
+        }
+
+        final types = nonNullableUnionTypes.map<Type>(_transformType).toList();
 
         var isHomogenous = true;
         final nonNullLiteralTypes = <LiteralType>[];
         var onlyContainsBooleanTypes = true;
-        var isNullable = false;
         LiteralType? firstNonNullablePrimitiveType;
 
         for (final type in types) {
           if (type is LiteralType) {
-            if (type.kind == LiteralKind.$null) {
-              isNullable = true;
-              continue;
-            }
             firstNonNullablePrimitiveType ??= type;
             onlyContainsBooleanTypes &= (type.kind == LiteralKind.$true) ||
                 (type.kind == LiteralKind.$false);
@@ -1092,38 +1254,62 @@
           }
         }
 
-        // check if it is a union of literals
-        if (isHomogenous) {
-          if (nonNullLiteralTypes.isNotEmpty && onlyContainsBooleanTypes) {
-            return BuiltinType.primitiveType(PrimitiveType.boolean,
-                isNullable: isNullable);
-          }
-
-          final expectedId =
-              ID(type: 'type', name: types.map((t) => t.id.name).join('|'));
-
-          if (typeMap.containsKey(expectedId.toString())) {
-            return typeMap[expectedId.toString()] as UnionType;
-          }
-
-          final (id: _, name: name) =
-              namer.makeUnique('AnonymousUnion', 'type');
-
-          // TODO: Handle similar types here...
-          final homogenousEnumType = HomogenousEnumType(
-              types: nonNullLiteralTypes, isNullable: isNullable, name: name);
-
-          return typeMap.putIfAbsent(
-                  expectedId.toString(), () => homogenousEnumType)
-              as HomogenousEnumType;
+        if (isHomogenous &&
+            nonNullLiteralTypes.isNotEmpty &&
+            onlyContainsBooleanTypes) {
+          return BuiltinType.primitiveType(PrimitiveType.boolean,
+              isNullable: shouldBeNullable);
         }
 
-        return UnionType(types: types);
+        final idMap = isHomogenous
+            ? nonNullLiteralTypes.map((t) => t.value.toString())
+            : types.map((t) => t.id.name);
+
+        final expectedId = ID(type: 'type', name: idMap.join('|'));
+
+        if (typeMap.containsKey(expectedId.toString())) {
+          return typeMap[expectedId.toString()] as UnionType;
+        }
+
+        final name =
+            'AnonymousUnion_${AnonymousHasher.hashUnion(idMap.toList())}';
+
+        final un = isHomogenous
+            ? HomogenousEnumType(types: nonNullLiteralTypes, name: name)
+            : UnionType(types: types, name: name);
+
+        final unType = typeMap.putIfAbsent(expectedId.toString(), () {
+          namer.markUsed(name);
+          return un;
+        });
+        return unType..isNullable = shouldBeNullable;
+
+      case TSSyntaxKind.TupleType:
+        // tuple type is array
+        final tupleType = type as TSTupleTypeNode;
+        // TODO: Handle named tuple params (`[x: number, y: number]`)
+        final types = tupleType.elements.toDart
+            .map<Type>((t) => _transformType(t, typeArg: true))
+            .toList();
+
+        // we will work based on the length of the types
+        final typeLength = types.length;
+
+        // check if a tuple of a certain length already exists
+        // generate if not
+        final (tupleUrl, tupleDeclaration) = programMap.getCommonType(
+            'JSTuple$typeLength',
+            ifAbsent: ('_tuples.dart', TupleDeclaration(count: typeLength)))!;
+
+        return tupleDeclaration.asReferredType(
+            types, isNullable ?? false, tupleUrl);
+
       case TSSyntaxKind.LiteralType:
         final literalType = type as TSLiteralTypeNode;
         final literal = literalType.literal;
 
         return LiteralType(
+            isNullable: isNullable ?? false,
             kind: switch (literal.kind) {
               // TODO: Will we support Regex?
               TSSyntaxKind.NumericLiteral => num.parse(literal.text) is int
@@ -1153,12 +1339,16 @@
         final typeArguments = typeQuery.typeArguments?.toDart;
 
         return _getTypeFromDeclaration(exprName, typeArguments,
-            typeArg: typeArg, isNotTypableDeclaration: true);
+            typeArg: typeArg,
+            isNotTypableDeclaration: true,
+            isNullable: isNullable ?? false);
       case TSSyntaxKind.ArrayType:
-        return BuiltinType.primitiveType(PrimitiveType.array, typeParams: [
-          getJSTypeAlternative(
-              _transformType((type as TSArrayTypeNode).elementType))
-        ]);
+        return BuiltinType.primitiveType(PrimitiveType.array,
+            typeParams: [
+              getJSTypeAlternative(
+                  _transformType((type as TSArrayTypeNode).elementType))
+            ],
+            isNullable: isNullable);
       default:
         // check for primitive type via its kind
         final primitiveType = switch (type.kind) {
@@ -1179,7 +1369,8 @@
         };
 
         return BuiltinType.primitiveType(primitiveType,
-            shouldEmitJsType: typeArg ? true : null);
+            shouldEmitJsType: typeArg ? true : null,
+            isNullable: primitiveType == PrimitiveType.any ? true : isNullable);
     }
   }
 
@@ -1217,13 +1408,12 @@
   ///
   /// The referred type may accept [typeArguments], which are passed as well.
   Type _searchForDeclRecursive(
-    Iterable<QualifiedNamePart> name,
-    TSSymbol symbol, {
-    NamespaceDeclaration? parent,
-    List<TSTypeNode>? typeArguments,
-    bool isNotTypableDeclaration = false,
-    bool typeArg = false,
-  }) {
+      Iterable<QualifiedNamePart> name, TSSymbol symbol,
+      {NamespaceDeclaration? parent,
+      List<TSTypeNode>? typeArguments,
+      bool isNotTypableDeclaration = false,
+      bool typeArg = false,
+      bool isNullable = false}) {
     // get name and map
     final firstName = name.first.part;
 
@@ -1251,12 +1441,14 @@
 
         exportSet.removeWhere((e) => e.name == aliasedSymbolName);
         exportSet.add(ExportReference(aliasedSymbolName, as: firstName));
+        // TODO: Is nullable
         return _getTypeFromSymbol(
             aliasedSymbol,
             typeChecker.getTypeOfSymbol(aliasedSymbol),
             typeArguments,
             typeArg,
-            isNotTypableDeclaration);
+            isNotTypableDeclaration,
+            isNullable);
       }
 
       while (firstDecl.name?.text != firstName &&
@@ -1318,19 +1510,23 @@
         case TypeAliasDeclaration(type: final t):
         case EnumDeclaration(baseType: final t):
           final jsType = getJSTypeAlternative(t);
-          if (jsType != t && typeArg) return jsType;
+          if (jsType != t && typeArg) {
+            return jsType..isNullable = isNullable;
+          }
       }
 
       final asReferredType = decl.asReferredType(
-        (typeArguments ?? [])
-            .map((type) => _transformType(type, typeArg: true))
-            .toList(),
-      );
+          (typeArguments ?? [])
+              .map((type) => _transformType(type, typeArg: true))
+              .toList(),
+          isNullable);
 
       if (asReferredType case ReferredDeclarationType(type: final type)
           when type is BuiltinType) {
         final jsType = getJSTypeAlternative(type);
-        if (jsType != type && typeArg) asReferredType.type = jsType;
+        if (jsType != type && typeArg) {
+          asReferredType.type = jsType..isNullable = isNullable;
+        }
       }
 
       return asReferredType;
@@ -1344,12 +1540,16 @@
           if (rest.singleOrNull?.part case final generic?
               when typeParams.any((t) => t.name == generic)) {
             final typeParam = typeParams.firstWhere((t) => t.name == generic);
-            return GenericType(name: typeParam.name, parent: decl);
+            return GenericType(
+                name: typeParam.name, parent: decl, isNullable: isNullable);
           }
           break;
         case final NamespaceDeclaration n:
           final searchForDeclRecursive = _searchForDeclRecursive(rest, symbol,
-              typeArguments: typeArguments, typeArg: typeArg, parent: n);
+              typeArguments: typeArguments,
+              typeArg: typeArg,
+              parent: n,
+              isNullable: isNullable);
           if (parent == null) {
             nodeMap.update(decl.id.toString(), (v) => n);
           }
@@ -1368,7 +1568,8 @@
   Type _getTypeFromTypeNode(TSTypeReferenceNode node,
       {List<TSTypeNode>? typeArguments,
       bool typeArg = false,
-      bool isNotTypableDeclaration = false}) {
+      bool isNotTypableDeclaration = false,
+      bool isNullable = false}) {
     typeArguments ??= node.typeArguments?.toDart;
     final typeName = node.typeName;
 
@@ -1386,8 +1587,8 @@
       symbol = type?.aliasSymbol ?? type?.symbol;
     }
 
-    return _getTypeFromSymbol(
-        symbol, type, typeArguments, isNotTypableDeclaration, typeArg);
+    return _getTypeFromSymbol(symbol, type, typeArguments,
+        isNotTypableDeclaration, typeArg, isNullable);
   }
 
   /// Given a [TSSymbol] for a given TS node or declaration, and its associated
@@ -1410,7 +1611,8 @@
       TSType? type,
       List<TSTypeNode>? typeArguments,
       bool isNotTypableDeclaration,
-      bool typeArg) {
+      bool typeArg,
+      bool isNullable) {
     final declarations = symbol!.getDeclarations()?.toDart ?? [];
 
     // get decl qualified name
@@ -1425,7 +1627,8 @@
 
       if (type?.isTypeParameter() ?? false) {
         // generic type
-        return GenericType(name: fullyQualifiedName.last.part);
+        return GenericType(
+            name: fullyQualifiedName.last.part, isNullable: isNullable);
       }
 
       // meaning others are imported
@@ -1435,7 +1638,8 @@
       final supportedType = BuiltinType.referred(firstName,
           typeParams: (typeArguments ?? [])
               .map((t) => getJSTypeAlternative(_transformType(t)))
-              .toList());
+              .toList(),
+          isNullable: isNullable);
       if (supportedType case final resultType?) {
         return resultType;
       }
@@ -1453,7 +1657,8 @@
             typeParams: (typeArguments ?? [])
                 .map(_transformType)
                 .map(getJSTypeAlternative)
-                .toList());
+                .toList(),
+            isNullable: isNullable);
       }
 
       // TODO(nikeokoronkwo): Update the version of typescript we are using
@@ -1504,7 +1709,9 @@
           case TypeAliasDeclaration(type: final t):
           case EnumDeclaration(baseType: final t):
             final jsType = getJSTypeAlternative(t);
-            if (jsType != t) return jsType;
+            if (jsType != t) {
+              return jsType..isNullable = isNullable;
+            }
         }
       }
 
@@ -1513,12 +1720,15 @@
             (typeArguments ?? [])
                 .map((type) => _transformType(type, typeArg: true))
                 .toList(),
+            isNullable,
             relativePath?.replaceFirst('.d.ts', '.dart'));
 
         if (outputType case ReferredDeclarationType(type: final type)
             when type is BuiltinType && typeArg) {
           final jsType = getJSTypeAlternative(type);
-          if (jsType != type) outputType.type = jsType;
+          if (jsType != type) {
+            outputType.type = jsType..isNullable = isNullable;
+          }
         }
 
         return outputType;
@@ -1533,14 +1743,16 @@
         // let's just handle them before-hand
         if (type?.isTypeParameter() ?? false) {
           // generic type
-          return GenericType(name: fullyQualifiedName.last.part);
+          return GenericType(
+              name: fullyQualifiedName.last.part, isNullable: isNullable);
         }
 
         // recursiveness
         return _searchForDeclRecursive(fullyQualifiedName, symbol,
             typeArguments: typeArguments,
             typeArg: typeArg,
-            isNotTypableDeclaration: isNotTypableDeclaration);
+            isNotTypableDeclaration: isNotTypableDeclaration,
+            isNullable: isNullable);
       } else {
         // if import there and not this file, imported from specified file
         final importUrl =
@@ -1564,7 +1776,9 @@
             case TypeAliasDeclaration(type: final t):
             case EnumDeclaration(baseType: final t):
               final jsType = getJSTypeAlternative(t);
-              if (jsType != t) return jsType;
+              if (jsType != t) {
+                return jsType..isNullable = isNullable;
+              }
           }
         }
 
@@ -1573,12 +1787,15 @@
               (typeArguments ?? [])
                   .map((type) => _transformType(type, typeArg: true))
                   .toList(),
+              isNullable,
               relativePath?.replaceFirst('.d.ts', '.dart'));
 
           if (outputType case ReferredDeclarationType(type: final type)
               when type is BuiltinType && typeArg) {
             final jsType = getJSTypeAlternative(type);
-            if (jsType != type) outputType.type = jsType;
+            if (jsType != type) {
+              outputType.type = jsType..isNullable = isNullable;
+            }
           }
 
           return outputType;
@@ -1614,7 +1831,8 @@
       @UnionOf([TSIdentifier, TSQualifiedName]) TSNode typeName,
       List<TSTypeNode>? typeArguments,
       {bool typeArg = false,
-      bool isNotTypableDeclaration = false}) {
+      bool isNotTypableDeclaration = false,
+      bool isNullable = false}) {
     // union assertion
     assert(typeName.kind == TSSyntaxKind.Identifier ||
         typeName.kind == TSSyntaxKind.QualifiedName);
@@ -1622,7 +1840,7 @@
     final symbol = typeChecker.getSymbolAtLocation(typeName);
 
     return _getTypeFromSymbol(symbol, typeChecker.getTypeOfSymbol(symbol!),
-        typeArguments, isNotTypableDeclaration, typeArg);
+        typeArguments, isNotTypableDeclaration, typeArg, isNullable);
   }
 
   /// Extracts associated documentation (JSDoc) from a [TSNode] and transforms
@@ -1745,7 +1963,7 @@
       final decls = nodeMap.findByName(part.text);
       if (decls.isNotEmpty) {
         final firstNode = decls.first;
-        return firstNode.dartName ?? firstNode.name ?? firstNode.id.name;
+        return firstNode.dartName ?? (firstNode as Declaration).name;
       } else {
         return part.text;
       }
@@ -1846,7 +2064,7 @@
     void updateFilteredDeclsForDecl(Node? decl, NodeMap filteredDeclarations) {
       switch (decl) {
         case final VariableDeclaration v:
-          if (v.type is! BuiltinType) filteredDeclarations.add(v.type);
+          filteredDeclarations.add(v.type);
           break;
         case final CallableDeclaration f:
           filteredDeclarations.addAll(getCallableDependencies(f));
@@ -1854,7 +2072,7 @@
         case final EnumDeclaration _:
           break;
         case final TypeAliasDeclaration t:
-          if (decl.type is! BuiltinType) filteredDeclarations.add(t.type);
+          filteredDeclarations.add(t.type);
           break;
         case final TypeDeclaration t:
           for (final con in t.constructors) {
@@ -1915,11 +2133,19 @@
         case final HomogenousEnumType hu:
           filteredDeclarations.add(hu.declaration);
           break;
+        case final TupleType t:
+          filteredDeclarations.addAll({
+            for (final t in t.types.where((t) => t is! BuiltinType))
+              t.id.toString(): t
+          });
         case final UnionType u:
           filteredDeclarations.addAll({
             for (final t in u.types.where((t) => t is! BuiltinType))
               t.id.toString(): t
           });
+          filteredDeclarations.add(u.declaration);
+        case final DeclarationType d:
+          filteredDeclarations.add(d.declaration);
           break;
         case BuiltinType(typeParams: final typeParams)
             when typeParams.isNotEmpty:
diff --git a/web_generator/lib/src/js/typescript.types.dart b/web_generator/lib/src/js/typescript.types.dart
index b0c51b6..d2c322d 100644
--- a/web_generator/lib/src/js/typescript.types.dart
+++ b/web_generator/lib/src/js/typescript.types.dart
@@ -90,8 +90,13 @@
   static const TSSyntaxKind ThisType = TSSyntaxKind._(197);
   static const TSSyntaxKind TypeQuery = TSSyntaxKind._(186);
   static const TSSyntaxKind ParenthesizedType = TSSyntaxKind._(196);
+  static const TSSyntaxKind TupleType = TSSyntaxKind._(189);
+  static const TSSyntaxKind NamedTupleMember = TSSyntaxKind._(202);
+  static const TSSyntaxKind TypeLiteral = TSSyntaxKind._(187);
+  static const TSSyntaxKind FunctionType = TSSyntaxKind._(184);
+  static const TSSyntaxKind ConstructorType = TSSyntaxKind._(185);
 
-  /// Other
+  // Other
   static const TSSyntaxKind Identifier = TSSyntaxKind._(80);
   static const TSSyntaxKind QualifiedName = TSSyntaxKind._(166);
   static const TSSyntaxKind PropertyAccessExpression = TSSyntaxKind._(211);
@@ -184,6 +189,47 @@
   external TSTypeNode get type;
 }
 
+@JS('TupleTypeNode')
+extension type TSTupleTypeNode._(JSObject _) implements TSTypeNode {
+  external TSNodeArray<TSTypeNode> get elements;
+}
+
+@JS('NamedTupleMember')
+extension type TSNamedTupleMember._(JSObject _)
+    implements TSTypeNode, TSDeclaration {
+  external TSToken? get dotDotDotToken;
+  external TSIdentifier get name;
+  external TSToken? get questionToken;
+  external TSTypeNode get type;
+}
+
+@JS('TypeLiteralNode')
+extension type TSTypeLiteralNode._(JSObject _)
+    implements TSTypeNode, TSDeclaration {
+  external TSNodeArray<TSTypeElement> get members;
+}
+
+@JS('FunctionOrConstructorTypeNodeBase')
+extension type TSFunctionOrConstructorTypeNodeBase._(JSObject _)
+    implements TSTypeNode, TSSignatureDeclarationBase {
+  external TSTypeNode get type;
+}
+
+@JS('FunctionTypeNode')
+extension type TSFunctionTypeNode._(JSObject _)
+    implements TSFunctionOrConstructorTypeNodeBase {
+  @redeclare
+  TSSyntaxKind get kind => TSSyntaxKind.FunctionType;
+}
+
+@JS('ConstructorTypeNode')
+extension type TSConstructorTypeNode._(JSObject _)
+    implements TSFunctionOrConstructorTypeNodeBase {
+  @redeclare
+  TSSyntaxKind get kind => TSSyntaxKind.ConstructorType;
+  external TSNodeArray<TSNode>? get modifiers;
+}
+
 @JS('Expression')
 extension type TSExpression._(JSObject _) implements TSNode {}
 
diff --git a/web_generator/lib/src/utils/case.dart b/web_generator/lib/src/utils/case.dart
new file mode 100644
index 0000000..652494b
--- /dev/null
+++ b/web_generator/lib/src/utils/case.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2025, 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.
+
+String uppercaseFirstLetter(String word) {
+  return word.replaceFirst(word[0], word[0].toUpperCase());
+}
diff --git a/web_generator/pubspec.yaml b/web_generator/pubspec.yaml
index ccd0d04..f03237a 100644
--- a/web_generator/pubspec.yaml
+++ b/web_generator/pubspec.yaml
@@ -11,6 +11,9 @@
   analyzer: ^7.4.0
   args: ^2.4.0
   code_builder: ^4.10.0
+  collection: ^1.19.1
+  convert: ^3.1.2
+  crypto: ^3.0.6
   dart_flutter_team_lints: ^3.0.0
   dart_style: ^3.0.0
   io: ^1.0.4
diff --git a/web_generator/test/integration/interop_gen/_tuples.dart b/web_generator/test/integration/interop_gen/_tuples.dart
new file mode 100644
index 0000000..ff7ac1f
--- /dev/null
+++ b/web_generator/test/integration/interop_gen/_tuples.dart
@@ -0,0 +1,35 @@
+// ignore_for_file: constant_identifier_names, non_constant_identifier_names
+// ignore_for_file: unnecessary_parenthesis
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:js_interop' as _i1;
+
+extension type JSTuple2<A extends _i1.JSAny?, B extends _i1.JSAny?>._(
+    _i1.JSArray<_i1.JSAny?> _) implements _i1.JSArray<_i1.JSAny?> {
+  A get $1 => (_[0] as A);
+
+  B get $2 => (_[1] as B);
+
+  set $1(A newValue) => _[0] = newValue;
+
+  set $2(B newValue) => _[1] = newValue;
+}
+extension type JSTuple4<A extends _i1.JSAny?, B extends _i1.JSAny?,
+        C extends _i1.JSAny?, D extends _i1.JSAny?>._(_i1.JSArray<_i1.JSAny?> _)
+    implements _i1.JSArray<_i1.JSAny?> {
+  A get $1 => (_[0] as A);
+
+  B get $2 => (_[1] as B);
+
+  C get $3 => (_[2] as C);
+
+  D get $4 => (_[3] as D);
+
+  set $1(A newValue) => _[0] = newValue;
+
+  set $2(B newValue) => _[1] = newValue;
+
+  set $3(C newValue) => _[2] = newValue;
+
+  set $4(D newValue) => _[3] = newValue;
+}
diff --git a/web_generator/test/integration/interop_gen/classes_expected.dart b/web_generator/test/integration/interop_gen/classes_expected.dart
index 49d010f..05330aa 100644
--- a/web_generator/test/integration/interop_gen/classes_expected.dart
+++ b/web_generator/test/integration/interop_gen/classes_expected.dart
@@ -1,4 +1,6 @@
-// ignore_for_file: constant_identifier_names, non_constant_identifier_names
+// ignore_for_file: camel_case_types, constant_identifier_names
+// ignore_for_file: library_private_types_in_public_api
+// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis
 
 // ignore_for_file: no_leading_underscores_for_library_prefixes
 import 'dart:js_interop' as _i1;
@@ -276,7 +278,7 @@
     implements Epahs<TMeta> {
   external EpahsImpl(
     String name, [
-    AnonymousUnion$1? type,
+    AnonymousUnion_1113974? type,
   ]);
 
   external factory EpahsImpl.$1(Epahs<TMeta> config);
@@ -294,19 +296,22 @@
   @_i2.redeclare
   external double area();
   @_i1.JS('area')
-  external String area$1(AnonymousUnion unit);
+  external String area$1(AnonymousUnion_1594664 unit);
   external static EpahsImpl getById(String id);
 
   /// Returns a string representation of an object.
   @_i1.JS('toString')
   external String toString$();
 }
-extension type const AnonymousUnion$1._(String _) {
-  static const AnonymousUnion$1 circle = AnonymousUnion$1._('circle');
+extension type const AnonymousUnion_1113974._(String _) {
+  static const AnonymousUnion_1113974 circle =
+      AnonymousUnion_1113974._('circle');
 
-  static const AnonymousUnion$1 rectangle = AnonymousUnion$1._('rectangle');
+  static const AnonymousUnion_1113974 rectangle =
+      AnonymousUnion_1113974._('rectangle');
 
-  static const AnonymousUnion$1 polygon = AnonymousUnion$1._('polygon');
+  static const AnonymousUnion_1113974 polygon =
+      AnonymousUnion_1113974._('polygon');
 }
 extension type Epahs<TMetadata extends _i1.JSAny?>._(_i1.JSObject _)
     implements _i1.JSObject {
@@ -315,11 +320,11 @@
   external String get id;
   external double area();
   @_i1.JS('area')
-  external String area$1(AnonymousUnion unit);
+  external String area$1(AnonymousUnion_1594664 unit);
   external _i1.JSFunction? get onUpdate;
 }
-extension type const AnonymousUnion._(String _) {
-  static const AnonymousUnion cm2 = AnonymousUnion._('cm2');
+extension type const AnonymousUnion_1594664._(String _) {
+  static const AnonymousUnion_1594664 cm2 = AnonymousUnion_1594664._('cm2');
 
-  static const AnonymousUnion in2 = AnonymousUnion._('in2');
+  static const AnonymousUnion_1594664 in2 = AnonymousUnion_1594664._('in2');
 }
diff --git a/web_generator/test/integration/interop_gen/enum_expected.dart b/web_generator/test/integration/interop_gen/enum_expected.dart
index 8a49518..01e5be7 100644
--- a/web_generator/test/integration/interop_gen/enum_expected.dart
+++ b/web_generator/test/integration/interop_gen/enum_expected.dart
@@ -1,4 +1,6 @@
-// ignore_for_file: constant_identifier_names, non_constant_identifier_names
+// ignore_for_file: camel_case_types, constant_identifier_names
+// ignore_for_file: library_private_types_in_public_api
+// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis
 
 // ignore_for_file: no_leading_underscores_for_library_prefixes
 import 'dart:js_interop' as _i1;
@@ -71,7 +73,7 @@
 
   static const HttpMethod DELETE = HttpMethod._('DELETE');
 }
-extension type BooleanLike._(_i1.JSAny? _) {
+extension type BooleanLike._(_i1.JSAny _) {
   static final BooleanLike No = BooleanLike._(0.toJS);
 
   static final BooleanLike Yes = BooleanLike._('YES'.toJS);
@@ -98,7 +100,7 @@
 
   external static MathConstants Length;
 }
-extension type SomeRandomEnumValues._(_i1.JSAny? _) {
+extension type SomeRandomEnumValues._(_i1.JSAny _) {
   static final SomeRandomEnumValues moment = SomeRandomEnumValues._(2.toJS);
 
   static final SomeRandomEnumValues true$ = SomeRandomEnumValues._(6.28.toJS);
@@ -111,59 +113,64 @@
 @_i1.JS()
 external Permissions get userPermissions;
 @_i1.JS()
-external AnonymousUnion currentTheme;
+external AnonymousUnion_3616711 currentTheme;
 @_i1.JS()
-external AnonymousUnion$1 buttonState;
+external AnonymousUnion_2355602 buttonState;
 @_i1.JS()
-external AnonymousUnion$2 retriesLeft;
+external AnonymousUnion_7456409 retriesLeft;
 @_i1.JS()
-external AnonymousUnion$3? get direction;
+external AnonymousUnion_1008525? get direction;
 @_i1.JS()
-external AnonymousUnion$4 get someUnionEnum;
+external AnonymousUnion_4522673 get someUnionEnum;
 @_i1.JS()
 external bool get myBooleanEnum;
-extension type const AnonymousUnion._(String _) {
-  static const AnonymousUnion light = AnonymousUnion._('light');
+extension type const AnonymousUnion_3616711._(String _) {
+  static const AnonymousUnion_3616711 light = AnonymousUnion_3616711._('light');
 
-  static const AnonymousUnion dark = AnonymousUnion._('dark');
+  static const AnonymousUnion_3616711 dark = AnonymousUnion_3616711._('dark');
 
-  static const AnonymousUnion system = AnonymousUnion._('system');
+  static const AnonymousUnion_3616711 system =
+      AnonymousUnion_3616711._('system');
 }
-extension type const AnonymousUnion$1._(String _) {
-  static const AnonymousUnion$1 default$ = AnonymousUnion$1._('default');
+extension type const AnonymousUnion_2355602._(String _) {
+  static const AnonymousUnion_2355602 default$ =
+      AnonymousUnion_2355602._('default');
 
-  static const AnonymousUnion$1 hovered = AnonymousUnion$1._('hovered');
+  static const AnonymousUnion_2355602 hovered =
+      AnonymousUnion_2355602._('hovered');
 
-  static const AnonymousUnion$1 pressed = AnonymousUnion$1._('pressed');
+  static const AnonymousUnion_2355602 pressed =
+      AnonymousUnion_2355602._('pressed');
 
-  static const AnonymousUnion$1 disabled = AnonymousUnion$1._('disabled');
+  static const AnonymousUnion_2355602 disabled =
+      AnonymousUnion_2355602._('disabled');
 }
-extension type const AnonymousUnion$2._(num _) {
-  static const AnonymousUnion$2 $0 = AnonymousUnion$2._(0);
+extension type const AnonymousUnion_7456409._(num _) {
+  static const AnonymousUnion_7456409 $0 = AnonymousUnion_7456409._(0);
 
-  static const AnonymousUnion$2 $1 = AnonymousUnion$2._(1);
+  static const AnonymousUnion_7456409 $1 = AnonymousUnion_7456409._(1);
 
-  static const AnonymousUnion$2 $2 = AnonymousUnion$2._(2);
+  static const AnonymousUnion_7456409 $2 = AnonymousUnion_7456409._(2);
 
-  static const AnonymousUnion$2 $3 = AnonymousUnion$2._(3);
+  static const AnonymousUnion_7456409 $3 = AnonymousUnion_7456409._(3);
 }
-extension type const AnonymousUnion$3._(String _) {
-  static const AnonymousUnion$3 N = AnonymousUnion$3._('N');
+extension type const AnonymousUnion_1008525._(String _) {
+  static const AnonymousUnion_1008525 N = AnonymousUnion_1008525._('N');
 
-  static const AnonymousUnion$3 S = AnonymousUnion$3._('S');
+  static const AnonymousUnion_1008525 S = AnonymousUnion_1008525._('S');
 
-  static const AnonymousUnion$3 E = AnonymousUnion$3._('E');
+  static const AnonymousUnion_1008525 E = AnonymousUnion_1008525._('E');
 
-  static const AnonymousUnion$3 W = AnonymousUnion$3._('W');
+  static const AnonymousUnion_1008525 W = AnonymousUnion_1008525._('W');
 }
-extension type const AnonymousUnion$4._(num _) {
-  static const AnonymousUnion$4 $2 = AnonymousUnion$4._(2);
+extension type const AnonymousUnion_4522673._(num _) {
+  static const AnonymousUnion_4522673 $2 = AnonymousUnion_4522673._(2);
 
-  static const AnonymousUnion$4 $4 = AnonymousUnion$4._(4);
+  static const AnonymousUnion_4522673 $4 = AnonymousUnion_4522673._(4);
 
-  static const AnonymousUnion$4 $6 = AnonymousUnion$4._(6);
+  static const AnonymousUnion_4522673 $6 = AnonymousUnion_4522673._(6);
 
-  static const AnonymousUnion$4 $8 = AnonymousUnion$4._(8);
+  static const AnonymousUnion_4522673 $8 = AnonymousUnion_4522673._(8);
 
-  static const AnonymousUnion$4 $10 = AnonymousUnion$4._(10);
+  static const AnonymousUnion_4522673 $10 = AnonymousUnion_4522673._(10);
 }
diff --git a/web_generator/test/integration/interop_gen/interfaces_expected.dart b/web_generator/test/integration/interop_gen/interfaces_expected.dart
index 41262e7..a8a7ffa 100644
--- a/web_generator/test/integration/interop_gen/interfaces_expected.dart
+++ b/web_generator/test/integration/interop_gen/interfaces_expected.dart
@@ -1,10 +1,12 @@
-// ignore_for_file: constant_identifier_names, non_constant_identifier_names
+// ignore_for_file: camel_case_types, constant_identifier_names
+// ignore_for_file: library_private_types_in_public_api
+// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis
 
 // ignore_for_file: no_leading_underscores_for_library_prefixes
 import 'dart:js_interop' as _i1;
 
 extension type ILogger._(_i1.JSObject _) implements _i1.JSObject {
-  external AnonymousUnion? level;
+  external AnonymousUnion_1584724? level;
 
   external String get name;
   external void log(String message);
@@ -66,14 +68,14 @@
 external LinkedList get rootList;
 @_i1.JS()
 external Comparator<_i1.JSNumber> get compareNumbers;
-extension type const AnonymousUnion._(String _) {
-  static const AnonymousUnion debug = AnonymousUnion._('debug');
+extension type const AnonymousUnion_1584724._(String _) {
+  static const AnonymousUnion_1584724 debug = AnonymousUnion_1584724._('debug');
 
-  static const AnonymousUnion info = AnonymousUnion._('info');
+  static const AnonymousUnion_1584724 info = AnonymousUnion_1584724._('info');
 
-  static const AnonymousUnion warn = AnonymousUnion._('warn');
+  static const AnonymousUnion_1584724 warn = AnonymousUnion_1584724._('warn');
 
-  static const AnonymousUnion error = AnonymousUnion._('error');
+  static const AnonymousUnion_1584724 error = AnonymousUnion_1584724._('error');
 }
 extension type LinkedList._(_i1.JSObject _) implements _i1.JSObject {
   external LinkedList next();
diff --git a/web_generator/test/integration/interop_gen/namespaces_expected.dart b/web_generator/test/integration/interop_gen/namespaces_expected.dart
index 8227441..ea1d357 100644
--- a/web_generator/test/integration/interop_gen/namespaces_expected.dart
+++ b/web_generator/test/integration/interop_gen/namespaces_expected.dart
@@ -1,5 +1,6 @@
 // ignore_for_file: camel_case_types, constant_identifier_names
-// ignore_for_file: non_constant_identifier_names
+// ignore_for_file: library_private_types_in_public_api
+// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis
 
 // ignore_for_file: no_leading_underscores_for_library_prefixes
 import 'dart:js_interop' as _i1;
@@ -106,7 +107,7 @@
 extension type Security_AuthService._(_i1.JSObject _) implements _i1.JSObject {
   external Security_AuthService();
 
-  external Security_IAuthToken login(
+  external Security_IAuthToken? login(
     String username,
     String password,
   );
@@ -117,7 +118,7 @@
 @_i1.JS('Data.IRepository')
 extension type Data_IRepository<T extends _i1.JSAny?>._(_i1.JSObject _)
     implements _i1.JSObject {
-  external T findById(num id);
+  external T? findById(num id);
   external _i1.JSArray<T> findAll();
   external void save(T entity);
 }
@@ -311,7 +312,7 @@
   external void save(EnterpriseApp_Models_Product item);
   external void add(EnterpriseApp_Models_Product product);
   @_i1.JS('get')
-  external EnterpriseApp_Models_Product get$(num id);
+  external EnterpriseApp_Models_Product get$(AnonymousUnion_1467782 id);
   @_i2.redeclare
   external _i1.JSArray<EnterpriseApp_Models_Product> getAll();
 }
@@ -327,3 +328,8 @@
   external static void renderUserList(
       _i1.JSArray<EnterpriseApp_Models_User> users);
 }
+extension type AnonymousUnion_1467782._(_i1.JSAny _) implements _i1.JSAny {
+  String get asString => (_ as _i1.JSString).toDart;
+
+  double get asDouble => (_ as _i1.JSNumber).toDartDouble;
+}
diff --git a/web_generator/test/integration/interop_gen/namespaces_input.d.ts b/web_generator/test/integration/interop_gen/namespaces_input.d.ts
index 7b1d3b8..724223f 100644
--- a/web_generator/test/integration/interop_gen/namespaces_input.d.ts
+++ b/web_generator/test/integration/interop_gen/namespaces_input.d.ts
@@ -34,7 +34,7 @@
      */
     class AuthService {
         private logs;
-        login(username: string, password: string): IAuthToken;
+        login(username: string, password: string): IAuthToken | undefined;
     }
 }
 export declare namespace Data {
@@ -43,7 +43,7 @@
      * T can be a class from another namespace, like Models.User.
      */
     interface IRepository<T> {
-        findById(id: number): T;
+        findById(id: number): T | undefined;
         findAll(): T[];
         save(entity: T): void;
     }
@@ -133,7 +133,7 @@
             save(item: Models.Product): void;
             private products;
             add(product: Models.Product): void;
-            get(id: number): Models.Product;
+            get(id: string | number): Models.Product;
             getAll(): Models.Product[];
         }
     }
diff --git a/web_generator/test/integration/interop_gen/project/output/b.dart b/web_generator/test/integration/interop_gen/project/output/b.dart
index fee72b9..dd1b683 100644
--- a/web_generator/test/integration/interop_gen/project/output/b.dart
+++ b/web_generator/test/integration/interop_gen/project/output/b.dart
@@ -1,4 +1,6 @@
-// ignore_for_file: constant_identifier_names, non_constant_identifier_names
+// ignore_for_file: camel_case_types, constant_identifier_names
+// ignore_for_file: library_private_types_in_public_api
+// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis
 
 // ignore_for_file: no_leading_underscores_for_library_prefixes
 import 'dart:js_interop' as _i1;
@@ -248,7 +250,7 @@
     implements Epahs<TMeta> {
   external EpahsImpl(
     String name, [
-    AnonymousUnion$1? type,
+    AnonymousUnion_1113974? type,
   ]);
 
   external factory EpahsImpl.$1(Epahs<TMeta> config);
@@ -266,7 +268,7 @@
   @_i2.redeclare
   external double area();
   @_i1.JS('area')
-  external String area$1(AnonymousUnion unit);
+  external String area$1(AnonymousUnion_1594664 unit);
   external static EpahsImpl getById(String id);
 
   /// Returns a string representation of an object.
@@ -274,12 +276,15 @@
   external String toString$();
 }
 extension type Point._(_i1.JSObject _) implements _i1.JSObject {}
-extension type const AnonymousUnion$1._(String _) {
-  static const AnonymousUnion$1 circle = AnonymousUnion$1._('circle');
+extension type const AnonymousUnion_1113974._(String _) {
+  static const AnonymousUnion_1113974 circle =
+      AnonymousUnion_1113974._('circle');
 
-  static const AnonymousUnion$1 rectangle = AnonymousUnion$1._('rectangle');
+  static const AnonymousUnion_1113974 rectangle =
+      AnonymousUnion_1113974._('rectangle');
 
-  static const AnonymousUnion$1 polygon = AnonymousUnion$1._('polygon');
+  static const AnonymousUnion_1113974 polygon =
+      AnonymousUnion_1113974._('polygon');
 }
 extension type Epahs<TMetadata extends _i1.JSAny?>._(_i1.JSObject _)
     implements _i1.JSObject {
@@ -288,11 +293,11 @@
   external String get id;
   external double area();
   @_i1.JS('area')
-  external String area$1(AnonymousUnion unit);
+  external String area$1(AnonymousUnion_1594664 unit);
   external _i1.JSFunction? get onUpdate;
 }
-extension type const AnonymousUnion._(String _) {
-  static const AnonymousUnion cm2 = AnonymousUnion._('cm2');
+extension type const AnonymousUnion_1594664._(String _) {
+  static const AnonymousUnion_1594664 cm2 = AnonymousUnion_1594664._('cm2');
 
-  static const AnonymousUnion in2 = AnonymousUnion._('in2');
+  static const AnonymousUnion_1594664 in2 = AnonymousUnion_1594664._('in2');
 }
diff --git a/web_generator/test/integration/interop_gen/project/output/c.dart b/web_generator/test/integration/interop_gen/project/output/c.dart
index ba70d51..4d1081d 100644
--- a/web_generator/test/integration/interop_gen/project/output/c.dart
+++ b/web_generator/test/integration/interop_gen/project/output/c.dart
@@ -1,4 +1,6 @@
-// ignore_for_file: constant_identifier_names, non_constant_identifier_names
+// ignore_for_file: camel_case_types, constant_identifier_names
+// ignore_for_file: library_private_types_in_public_api
+// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis
 
 // ignore_for_file: no_leading_underscores_for_library_prefixes
 import 'dart:js_interop' as _i1;
@@ -20,7 +22,7 @@
 /// Represents a basic logger interface with optional flush capability.
 extension type ILogger._(_i1.JSObject _) implements _i1.JSObject {
   /// Logging level. Defaults to "info" if unspecified.
-  external AnonymousUnion? level;
+  external AnonymousUnion_1584724? level;
 
   /// Name of the logger (e.g., subsystem or module).
   external String get name;
@@ -132,14 +134,14 @@
 /// A numeric comparator for sorting numbers.
 @_i1.JS()
 external Comparator<_i1.JSNumber> get compareNumbers;
-extension type const AnonymousUnion._(String _) {
-  static const AnonymousUnion debug = AnonymousUnion._('debug');
+extension type const AnonymousUnion_1584724._(String _) {
+  static const AnonymousUnion_1584724 debug = AnonymousUnion_1584724._('debug');
 
-  static const AnonymousUnion info = AnonymousUnion._('info');
+  static const AnonymousUnion_1584724 info = AnonymousUnion_1584724._('info');
 
-  static const AnonymousUnion warn = AnonymousUnion._('warn');
+  static const AnonymousUnion_1584724 warn = AnonymousUnion_1584724._('warn');
 
-  static const AnonymousUnion error = AnonymousUnion._('error');
+  static const AnonymousUnion_1584724 error = AnonymousUnion_1584724._('error');
 }
 
 /// A basic self-referencing linked list node.
diff --git a/web_generator/test/integration/interop_gen/ts_typing_expected.dart b/web_generator/test/integration/interop_gen/ts_typing_expected.dart
index 65e7fe9..9962ca3 100644
--- a/web_generator/test/integration/interop_gen/ts_typing_expected.dart
+++ b/web_generator/test/integration/interop_gen/ts_typing_expected.dart
@@ -1,13 +1,22 @@
-// ignore_for_file: constant_identifier_names, non_constant_identifier_names
+// ignore_for_file: camel_case_types, constant_identifier_names
+// ignore_for_file: library_private_types_in_public_api
+// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis
 
 // ignore_for_file: no_leading_underscores_for_library_prefixes
 import 'dart:js_interop' as _i1;
 
+import 'package:web/web.dart' as _i3;
+
+import '_tuples.dart' as _i2;
+
 @_i1.JS()
 external String myFunction(String param);
 @_i1.JS()
 external String myEnclosingFunction(_i1.JSFunction func);
 @_i1.JS()
+external _i1.JSArray<AnonymousType_9143117<T>>
+    indexedArray<T extends _i1.JSAny?>(_i1.JSArray<T> arr);
+@_i1.JS()
 external String get myString;
 @_i1.JS()
 external _i1.JSArray<_i1.JSNumber> get myNumberArray;
@@ -40,7 +49,202 @@
 external ComposedType get myComposedType;
 @_i1.JS()
 external ComposedType<_i1.JSString> get myComposedMyString;
+@_i1.JS()
+external AnonymousUnion_5411652 get myUnion;
+@_i1.JS()
+external AnonymousUnion_5411652 get myCloneUnion;
+@_i1.JS()
+external AnonymousUnion_3781934 get mySecondUnion;
+@_i1.JS()
+external bool? get booleanOrUndefined;
+@_i1.JS()
+external AnonymousUnion_6216705? get image;
+@_i1.JS()
+external _i2.JSTuple2<_i1.JSString, _i1.JSNumber> get myTuple;
+@_i1.JS()
+external _i2.JSTuple2<_i1.JSString, _i1.JSString> get mySecondTuple;
+@_i1.JS()
+external _i2.JSTuple2<_i1.JSString, _i1.JSString> get myCloneTuple;
+@_i1.JS()
+external _i2.JSTuple4<_i1.JSString, _i1.JSNumber, _i1.JSBoolean, _i1.JSSymbol>
+    get typesAsTuple;
+@_i1.JS()
+external AnonymousUnion_7503220 get eightOrSixteen;
+@_i1.JS()
+external AnonymousType_2194029 get randomNonTypedProduct;
+@_i1.JS()
+external AnonymousType_1358595 get config;
+extension type MyProduct._(_i1.JSObject _) implements Product {
+  external MyProduct(
+    num id,
+    String name,
+    num price,
+  );
+
+  external double id;
+
+  external String name;
+
+  external double price;
+}
+@_i1.JS()
+external AnonymousUnion_1189263 get responseObject;
+@_i1.JS()
+external _AnonymousConstructor_1059824 get productConstr;
+@_i1.JS()
+external _AnonymousFunction_1331744 get createDiscountCalculator;
+@_i1.JS()
+external _AnonymousFunction_7147484 get applyDiscount;
+@_i1.JS()
+external _i1.JSArray<AnonymousType_5780756> get shoppingCart;
+@_i1.JS()
+external _AnonymousFunction_2181528 get createLogger;
+@_i1.JS()
+external _AnonymousFunction_1707607 get appLogger;
+extension type AnonymousType_9143117<T extends _i1.JSAny?>._(_i1.JSObject _)
+    implements _i1.JSObject {
+  external AnonymousType_9143117({
+    double id,
+    T value,
+  });
+
+  external double id;
+
+  external T value;
+}
 extension type ComposedType<T extends _i1.JSAny?>._(_i1.JSObject _)
     implements _i1.JSObject {
   external T enclosed;
 }
+extension type AnonymousUnion_5411652._(_i1.JSAny _) implements _i1.JSAny {
+  bool get asBool => (_ as _i1.JSBoolean).toDart;
+
+  String get asString => (_ as _i1.JSString).toDart;
+}
+extension type AnonymousUnion_3781934._(_i1.JSAny _) implements _i1.JSAny {
+  double get asDouble => (_ as _i1.JSNumber).toDartDouble;
+
+  String get asString => (_ as _i1.JSString).toDart;
+
+  MyEnum get asMyEnum => MyEnum._((_ as _i1.JSNumber).toDartInt);
+
+  ComposedType get asComposedType => (_ as ComposedType);
+}
+extension type AnonymousUnion_6216705._(_i1.JSAny _) implements _i1.JSAny {
+  String get asString => (_ as _i1.JSString).toDart;
+
+  _i3.URL get asURL => (_ as _i3.URL);
+}
+extension type AnonymousUnion_7503220._(_i1.JSTypedArray _)
+    implements _i1.JSTypedArray {
+  _i1.JSUint8Array get asJSUint8Array => (_ as _i1.JSUint8Array);
+
+  _i1.JSUint16Array get asJSUint16Array => (_ as _i1.JSUint16Array);
+}
+extension type AnonymousType_2194029._(_i1.JSObject _) implements _i1.JSObject {
+  external AnonymousType_2194029({
+    double id,
+    String name,
+    double price,
+  });
+
+  external double id;
+
+  external String name;
+
+  external double price;
+}
+extension type AnonymousType_1358595._(_i1.JSObject _) implements _i1.JSObject {
+  external AnonymousType_1358595({
+    double discountRate,
+    double taxRate,
+  });
+
+  external double discountRate;
+
+  external double taxRate;
+}
+typedef Product = AnonymousType_2194029;
+extension type AnonymousUnion_1189263._(_i1.JSObject _)
+    implements _i1.JSObject {
+  AnonymousType_2773310 get asAnonymousType_2773310 =>
+      (_ as AnonymousType_2773310);
+
+  AnonymousType_1487785 get asAnonymousType_1487785 =>
+      (_ as AnonymousType_1487785);
+}
+extension type AnonymousType_2773310._(_i1.JSObject _) implements _i1.JSObject {
+  external AnonymousType_2773310({
+    String id,
+    _i1.JSAny? value,
+  });
+
+  external String id;
+
+  external _i1.JSAny? value;
+}
+extension type AnonymousType_1487785._(_i1.JSObject _) implements _i1.JSObject {
+  external AnonymousType_1487785({
+    String id,
+    String error,
+    _i1.JSAny? data,
+  });
+
+  external String id;
+
+  external String error;
+
+  external _i1.JSAny? data;
+}
+extension type _AnonymousConstructor_1059824._(_i1.JSFunction _)
+    implements _i1.JSFunction {
+  Product call(
+    num id,
+    String name,
+    num price,
+  ) =>
+      Product(
+        id: id.toDouble(),
+        name: name,
+        price: price.toDouble(),
+      );
+}
+extension type _AnonymousFunction_1331744._(_i1.JSFunction _)
+    implements _i1.JSFunction {
+  external _AnonymousFunction_7147484 call(num rate);
+}
+extension type _AnonymousFunction_7147484._(_i1.JSFunction _)
+    implements _i1.JSFunction {
+  external double call(num originalPrice);
+}
+extension type AnonymousType_5780756._(_i1.JSObject _) implements _i1.JSObject {
+  external AnonymousType_5780756({
+    double calculatedPrice,
+    _AnonymousFunction_4113003 displayInfo,
+    double id,
+    String name,
+    double price,
+  });
+
+  external double calculatedPrice;
+
+  external _AnonymousFunction_4113003 displayInfo;
+
+  external double id;
+
+  external String name;
+
+  external double price;
+}
+extension type _AnonymousFunction_4113003._(_i1.JSFunction _)
+    implements _i1.JSFunction {
+  external void call();
+}
+extension type _AnonymousFunction_2181528._(_i1.JSFunction _)
+    implements _i1.JSFunction {
+  external _AnonymousFunction_1707607 call(String prefix);
+}
+extension type _AnonymousFunction_1707607._(_i1.JSFunction _)
+    implements _i1.JSFunction {
+  external void call(String message);
+}
diff --git a/web_generator/test/integration/interop_gen/ts_typing_input.d.ts b/web_generator/test/integration/interop_gen/ts_typing_input.d.ts
index e665d78..b47a6cc 100644
--- a/web_generator/test/integration/interop_gen/ts_typing_input.d.ts
+++ b/web_generator/test/integration/interop_gen/ts_typing_input.d.ts
@@ -17,9 +17,59 @@
 export declare function myFunction(param: string): string;
 export declare let myFunctionAlias: typeof myFunction;
 export declare let myFunctionAlias2: typeof myFunctionAlias;
-/** @todo [@nikeokoronkwo] support var declarations as well as var statements */
 // export declare let myPreClone: typeof myComposedType;
 export declare function myEnclosingFunction(func: typeof myFunction): string;
 export declare const myEnclosingFunctionAlias: typeof myEnclosingFunction;
 export declare const myComposedType: ComposedType;
 export declare const myComposedMyString: ComposedType<typeof myString>;
+export declare const myUnion: boolean | string;
+export declare const myCloneUnion: boolean | string;
+export declare const mySecondUnion: number | string | MyEnum | ComposedType;
+export declare const booleanOrUndefined: boolean | undefined;
+export declare const image: string | URL | null;
+export declare const myTuple: [string, number];
+export declare const mySecondTuple: [string, string];
+export declare const myCloneTuple: [string, string];
+export declare const typesAsTuple: [string, number, boolean, symbol];
+export declare const eightOrSixteen: Uint8Array | Uint16Array;
+type Product = {
+    id: number;
+    name: string;
+    price: number;
+};
+export declare const randomNonTypedProduct: {
+    id: number;
+    name: string;
+    price: number;
+};
+export declare const config: {
+    discountRate: number;
+    taxRate: number;
+};
+export declare class MyProduct implements Product {
+    id: number;
+    name: string;
+    price: number;
+    constructor(id: number, name: string, price: number);
+}
+export function indexedArray<T>(arr: T[]): { id: number, value: T }[];
+export const responseObject: {
+    id: string;
+    value: any;
+} | {
+    id: string;
+    error: string;
+    data: any;
+}
+export declare const productConstr: new (id: number, name: string, price: number) => Product;
+export declare const createDiscountCalculator: (rate: number) => (originalPrice: number) => number;
+export declare const applyDiscount: (originalPrice: number) => number;
+export declare const shoppingCart: {
+    calculatedPrice: number;
+    displayInfo: () => void;
+    id: number;
+    name: string;
+    price: number;
+}[];
+export declare const createLogger: (prefix: string) => (message: string) => void;
+export declare const appLogger: (message: string) => void;
diff --git a/web_generator/test/integration/interop_gen/typealias_expected.dart b/web_generator/test/integration/interop_gen/typealias_expected.dart
index f5aa6ce..d229bb1 100644
--- a/web_generator/test/integration/interop_gen/typealias_expected.dart
+++ b/web_generator/test/integration/interop_gen/typealias_expected.dart
@@ -1,9 +1,12 @@
 // ignore_for_file: camel_case_types, constant_identifier_names
-// ignore_for_file: non_constant_identifier_names
+// ignore_for_file: library_private_types_in_public_api
+// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis
 
 // ignore_for_file: no_leading_underscores_for_library_prefixes
 import 'dart:js_interop' as _i1;
 
+import '_tuples.dart' as _i2;
+
 typedef Shape2D = String;
 typedef PrismFromShape2D<S extends _i1.JSString> = _i1.JSArray<S>;
 @_i1.JS()
@@ -17,14 +20,15 @@
 typedef IsActive = bool;
 @_i1.JS()
 external String isUserActive(IsActive status);
+typedef NameAndAge = _i2.JSTuple2<_i1.JSString, _i1.JSNumber>;
 typedef Username = String;
 typedef Age = double;
 typedef Tags = _i1.JSArray<_i1.JSString>;
 typedef List<T extends _i1.JSAny?> = _i1.JSArray<T>;
 typedef Box<T extends _i1.JSAny?> = _i1.JSArray<_i1.JSArray<T>>;
 typedef Logger = LoggerType;
-typedef Direction = AnonymousUnion;
-typedef Method = AnonymousUnion$1;
+typedef Direction = AnonymousUnion_1008525;
+typedef Method = AnonymousUnion_1614079;
 typedef Planet = Space_Planet;
 @_i1.JS()
 external LoggerContainer<_i1.JSNumber> get loggerContainers;
@@ -53,27 +57,29 @@
 
   static const LoggerType Other = LoggerType._(4);
 }
-extension type const AnonymousUnion._(String _) {
-  static const AnonymousUnion N = AnonymousUnion._('N');
+extension type const AnonymousUnion_1008525._(String _) {
+  static const AnonymousUnion_1008525 N = AnonymousUnion_1008525._('N');
 
-  static const AnonymousUnion S = AnonymousUnion._('S');
+  static const AnonymousUnion_1008525 S = AnonymousUnion_1008525._('S');
 
-  static const AnonymousUnion E = AnonymousUnion._('E');
+  static const AnonymousUnion_1008525 E = AnonymousUnion_1008525._('E');
 
-  static const AnonymousUnion W = AnonymousUnion._('W');
+  static const AnonymousUnion_1008525 W = AnonymousUnion_1008525._('W');
 }
-extension type const AnonymousUnion$1._(String _) {
-  static const AnonymousUnion$1 GET = AnonymousUnion$1._('GET');
+extension type const AnonymousUnion_1614079._(String _) {
+  static const AnonymousUnion_1614079 GET = AnonymousUnion_1614079._('GET');
 
-  static const AnonymousUnion$1 POST = AnonymousUnion$1._('POST');
+  static const AnonymousUnion_1614079 POST = AnonymousUnion_1614079._('POST');
 
-  static const AnonymousUnion$1 PUT = AnonymousUnion$1._('PUT');
+  static const AnonymousUnion_1614079 PUT = AnonymousUnion_1614079._('PUT');
 
-  static const AnonymousUnion$1 DELETE = AnonymousUnion$1._('DELETE');
+  static const AnonymousUnion_1614079 DELETE =
+      AnonymousUnion_1614079._('DELETE');
 
-  static const AnonymousUnion$1 PATCH = AnonymousUnion$1._('PATCH');
+  static const AnonymousUnion_1614079 PATCH = AnonymousUnion_1614079._('PATCH');
 
-  static const AnonymousUnion$1 OPTIONS = AnonymousUnion$1._('OPTIONS');
+  static const AnonymousUnion_1614079 OPTIONS =
+      AnonymousUnion_1614079._('OPTIONS');
 }
 @_i1.JS('Space.Planet')
 extension type Space_Planet._(_i1.JSObject _) implements _i1.JSObject {
diff --git a/web_generator/test/integration/interop_gen/typealias_input.d.ts b/web_generator/test/integration/interop_gen/typealias_input.d.ts
index 52e2b7a..debe1e8 100644
--- a/web_generator/test/integration/interop_gen/typealias_input.d.ts
+++ b/web_generator/test/integration/interop_gen/typealias_input.d.ts
@@ -11,6 +11,7 @@
     }
     const earth: Planet;
 }
+export type NameAndAge = [string, number];
 export type Username = string;
 export type Age = number;
 export type IsActive = boolean;
diff --git a/web_generator/test/integration/interop_gen/web_types_expected.dart b/web_generator/test/integration/interop_gen/web_types_expected.dart
index 3de6a1d..d8b2915 100644
--- a/web_generator/test/integration/interop_gen/web_types_expected.dart
+++ b/web_generator/test/integration/interop_gen/web_types_expected.dart
@@ -1,4 +1,6 @@
-// ignore_for_file: constant_identifier_names, non_constant_identifier_names
+// ignore_for_file: camel_case_types, constant_identifier_names
+// ignore_for_file: library_private_types_in_public_api
+// ignore_for_file: non_constant_identifier_names, unnecessary_parenthesis
 
 // ignore_for_file: no_leading_underscores_for_library_prefixes
 import 'dart:js_interop' as _i1;
@@ -13,7 +15,8 @@
 external _i1.JSPromise<_i2.WebGLBuffer> convertToWebGL(
     _i1.JSArrayBuffer buffer);
 @_i1.JS()
-external String getHTMLElementContent<T extends _i2.HTMLElement>(T element);
+external AnonymousType_7051203<T>
+    getHTMLElementContent<T extends _i2.HTMLElement>(T element);
 @_i1.JS()
 external void handleButtonClick(_i2.MouseEvent event);
 @_i1.JS()
@@ -28,6 +31,15 @@
   _i2.Event event,
   _i1.JSArray<EventManipulationFunc> onCallbacks,
 );
+extension type ElementStamp<T extends _i2.HTMLElement>._(_i1.JSObject _)
+    implements _i1.JSObject {
+  external String id;
+
+  external AnonymousUnion_1506805 stampType;
+
+  external T get target;
+  external Date get stampedAt;
+}
 @_i1.JS()
 external _i2.CustomEvent get myCustomEvent;
 @_i1.JS()
@@ -36,6 +48,23 @@
 external _i2.HTMLButtonElement get button;
 @_i1.JS()
 external _i2.HTMLDivElement get output;
+extension type AnonymousType_7051203<T extends _i1.JSAny?>._(_i1.JSObject _)
+    implements _i1.JSObject {
+  external AnonymousType_7051203({
+    AnonymousUnion_1500406? ref,
+    _i2.HTMLElement parent,
+  });
+
+  external AnonymousUnion_1500406? ref;
+
+  external _i2.HTMLElement parent;
+}
+extension type AnonymousUnion_1500406<T extends _i1.JSAny?>._(_i1.JSAny _)
+    implements _i1.JSAny {
+  T get asT => (_ as T);
+
+  String get asString => (_ as _i1.JSString).toDart;
+}
 extension type HTMLTransformFunc<T extends _i2.HTMLElement,
     R extends _i2.HTMLElement>._(_i1.JSObject _) implements _i1.JSObject {
   external R call(T element);
@@ -43,3 +72,265 @@
 extension type EventManipulationFunc._(_i1.JSObject _) implements _i1.JSObject {
   external _i1.JSAny? call(_i2.Event event);
 }
+
+/// Enables basic storage and retrieval of dates and times.
+extension type Date._(_i1.JSObject _) implements _i1.JSObject {
+  /// Returns a string representation of a date. The format of the string
+  /// depends on the locale.
+  @_i1.JS('toString')
+  external String toString$();
+
+  /// Returns a date as a string value.
+  external String toDateString();
+
+  /// Returns a time as a string value.
+  external String toTimeString();
+
+  /// Returns a value as a string value appropriate to the host environment's
+  /// current locale.
+  /// Converts a date and time to a string by using the current or specified
+  /// locale.
+  /// - [locales]:  A locale string or array of locale strings that contain one
+  ///   or more language or locale tags. If you include more than one locale
+  ///   string, list them in descending order of priority so that the first
+  ///   entry is the preferred locale. If you omit this parameter, the default
+  ///   locale of the JavaScript runtime is used.
+  /// - [options]:  An object that contains one or more properties that specify
+  ///   comparison options.
+  external String toLocaleString();
+
+  /// Returns a date as a string value appropriate to the host environment's
+  /// current locale.
+  /// Converts a date to a string by using the current or specified locale.
+  /// - [locales]:  A locale string or array of locale strings that contain one
+  ///   or more language or locale tags. If you include more than one locale
+  ///   string, list them in descending order of priority so that the first
+  ///   entry is the preferred locale. If you omit this parameter, the default
+  ///   locale of the JavaScript runtime is used.
+  /// - [options]:  An object that contains one or more properties that specify
+  ///   comparison options.
+  external String toLocaleDateString();
+
+  /// Returns a time as a string value appropriate to the host environment's
+  /// current locale.
+  /// Converts a time to a string by using the current or specified locale.
+  /// - [locales]:  A locale string or array of locale strings that contain one
+  ///   or more language or locale tags. If you include more than one locale
+  ///   string, list them in descending order of priority so that the first
+  ///   entry is the preferred locale. If you omit this parameter, the default
+  ///   locale of the JavaScript runtime is used.
+  /// - [options]:  An object that contains one or more properties that specify
+  ///   comparison options.
+  external String toLocaleTimeString();
+
+  /// Returns the stored time value in milliseconds since midnight, January 1,
+  /// 1970 UTC.
+  external double valueOf();
+
+  /// Returns the stored time value in milliseconds since midnight, January 1,
+  /// 1970 UTC.
+  external double getTime();
+
+  /// Gets the year, using local time.
+  external double getFullYear();
+
+  /// Gets the year using Universal Coordinated Time (UTC).
+  external double getUTCFullYear();
+
+  /// Gets the month, using local time.
+  external double getMonth();
+
+  /// Gets the month of a Date object using Universal Coordinated Time (UTC).
+  external double getUTCMonth();
+
+  /// Gets the day-of-the-month, using local time.
+  external double getDate();
+
+  /// Gets the day-of-the-month, using Universal Coordinated Time (UTC).
+  external double getUTCDate();
+
+  /// Gets the day of the week, using local time.
+  external double getDay();
+
+  /// Gets the day of the week using Universal Coordinated Time (UTC).
+  external double getUTCDay();
+
+  /// Gets the hours in a date, using local time.
+  external double getHours();
+
+  /// Gets the hours value in a Date object using Universal Coordinated Time
+  /// (UTC).
+  external double getUTCHours();
+
+  /// Gets the minutes of a Date object, using local time.
+  external double getMinutes();
+
+  /// Gets the minutes of a Date object using Universal Coordinated Time (UTC).
+  external double getUTCMinutes();
+
+  /// Gets the seconds of a Date object, using local time.
+  external double getSeconds();
+
+  /// Gets the seconds of a Date object using Universal Coordinated Time (UTC).
+  external double getUTCSeconds();
+
+  /// Gets the milliseconds of a Date, using local time.
+  external double getMilliseconds();
+
+  /// Gets the milliseconds of a Date object using Universal Coordinated Time
+  /// (UTC).
+  external double getUTCMilliseconds();
+
+  /// Gets the difference in minutes between Universal Coordinated Time (UTC)
+  /// and the time on the local computer.
+  external double getTimezoneOffset();
+
+  /// Sets the date and time value in the Date object.
+  /// - [time]:  A numeric value representing the number of elapsed milliseconds
+  ///   since midnight, January 1, 1970 GMT.
+  external double setTime(num time);
+
+  /// Sets the milliseconds value in the Date object using local time.
+  /// - [ms]:  A numeric value equal to the millisecond value.
+  external double setMilliseconds(num ms);
+
+  /// Sets the milliseconds value in the Date object using Universal Coordinated
+  /// Time (UTC).
+  /// - [ms]:  A numeric value equal to the millisecond value.
+  external double setUTCMilliseconds(num ms);
+
+  /// Sets the seconds value in the Date object using local time.
+  /// - [sec]:  A numeric value equal to the seconds value.
+  /// - [ms]:  A numeric value equal to the milliseconds value.
+  external double setSeconds(
+    num sec, [
+    num? ms,
+  ]);
+
+  /// Sets the seconds value in the Date object using Universal Coordinated Time
+  /// (UTC).
+  /// - [sec]:  A numeric value equal to the seconds value.
+  /// - [ms]:  A numeric value equal to the milliseconds value.
+  external double setUTCSeconds(
+    num sec, [
+    num? ms,
+  ]);
+
+  /// Sets the minutes value in the Date object using local time.
+  /// - [min]:  A numeric value equal to the minutes value.
+  /// - [sec]:  A numeric value equal to the seconds value.
+  /// - [ms]:  A numeric value equal to the milliseconds value.
+  external double setMinutes(
+    num min, [
+    num? sec,
+    num? ms,
+  ]);
+
+  /// Sets the minutes value in the Date object using Universal Coordinated Time
+  /// (UTC).
+  /// - [min]:  A numeric value equal to the minutes value.
+  /// - [sec]:  A numeric value equal to the seconds value.
+  /// - [ms]:  A numeric value equal to the milliseconds value.
+  external double setUTCMinutes(
+    num min, [
+    num? sec,
+    num? ms,
+  ]);
+
+  /// Sets the hour value in the Date object using local time.
+  /// - [hours]:  A numeric value equal to the hours value.
+  /// - [min]:  A numeric value equal to the minutes value.
+  /// - [sec]:  A numeric value equal to the seconds value.
+  /// - [ms]:  A numeric value equal to the milliseconds value.
+  external double setHours(
+    num hours, [
+    num? min,
+    num? sec,
+    num? ms,
+  ]);
+
+  /// Sets the hours value in the Date object using Universal Coordinated Time
+  /// (UTC).
+  /// - [hours]:  A numeric value equal to the hours value.
+  /// - [min]:  A numeric value equal to the minutes value.
+  /// - [sec]:  A numeric value equal to the seconds value.
+  /// - [ms]:  A numeric value equal to the milliseconds value.
+  external double setUTCHours(
+    num hours, [
+    num? min,
+    num? sec,
+    num? ms,
+  ]);
+
+  /// Sets the numeric day-of-the-month value of the Date object using local
+  /// time.
+  /// - [date]:  A numeric value equal to the day of the month.
+  external double setDate(num date);
+
+  /// Sets the numeric day of the month in the Date object using Universal
+  /// Coordinated Time (UTC).
+  /// - [date]:  A numeric value equal to the day of the month.
+  external double setUTCDate(num date);
+
+  /// Sets the month value in the Date object using local time.
+  /// - [month]:  A numeric value equal to the month. The value for January is
+  ///   0, and other month values follow consecutively.
+  /// - [date]:  A numeric value representing the day of the month. If this
+  ///   value is not supplied, the value from a call to the getDate method is
+  ///   used.
+  external double setMonth(
+    num month, [
+    num? date,
+  ]);
+
+  /// Sets the month value in the Date object using Universal Coordinated Time
+  /// (UTC).
+  /// - [month]:  A numeric value equal to the month. The value for January is
+  ///   0, and other month values follow consecutively.
+  /// - [date]:  A numeric value representing the day of the month. If it is not
+  ///   supplied, the value from a call to the getUTCDate method is used.
+  external double setUTCMonth(
+    num month, [
+    num? date,
+  ]);
+
+  /// Sets the year of the Date object using local time.
+  /// - [year]:  A numeric value for the year.
+  /// - [month]:  A zero-based numeric value for the month (0 for January, 11
+  ///   for December). Must be specified if numDate is specified.
+  /// - [date]:  A numeric value equal for the day of the month.
+  external double setFullYear(
+    num year, [
+    num? month,
+    num? date,
+  ]);
+
+  /// Sets the year value in the Date object using Universal Coordinated Time
+  /// (UTC).
+  /// - [year]:  A numeric value equal to the year.
+  /// - [month]:  A numeric value equal to the month. The value for January is
+  ///   0, and other month values follow consecutively. Must be supplied if
+  ///   numDate is supplied.
+  /// - [date]:  A numeric value equal to the day of the month.
+  external double setUTCFullYear(
+    num year, [
+    num? month,
+    num? date,
+  ]);
+
+  /// Returns a date converted to a string using Universal Coordinated Time
+  /// (UTC).
+  external String toUTCString();
+
+  /// Returns a date as a string value in ISO format.
+  external String toISOString();
+
+  /// Used by the JSON.stringify method to enable the transformation of an
+  /// object's data for JavaScript Object Notation (JSON) serialization.
+  external String toJSON([_i1.JSAny? key]);
+}
+extension type const AnonymousUnion_1506805._(String _) {
+  static const AnonymousUnion_1506805 emit = AnonymousUnion_1506805._('emit');
+
+  static const AnonymousUnion_1506805 none = AnonymousUnion_1506805._('none');
+}
diff --git a/web_generator/test/integration/interop_gen/web_types_input.d.ts b/web_generator/test/integration/interop_gen/web_types_input.d.ts
index 366cf53..7caa6fa 100644
--- a/web_generator/test/integration/interop_gen/web_types_input.d.ts
+++ b/web_generator/test/integration/interop_gen/web_types_input.d.ts
@@ -4,7 +4,7 @@
 interface EventManipulationFunc {
     (event: Event): any;
 }
-interface ElementStamp<T extends HTMLElement> {
+export interface ElementStamp<T extends HTMLElement> {
     readonly target: T;
     readonly stampedAt: Date;
     id: string;
@@ -16,11 +16,14 @@
 export declare function handleMouseEvent(event: MouseEvent): void;
 export declare function generateUrl(path: string): URL;
 export declare function convertToWebGL(buffer: ArrayBuffer): Promise<WebGLBuffer>;
-export declare function getHTMLElementContent<T extends HTMLElement>(element: T): string;
+export declare function getHTMLElementContent<T extends HTMLElement>(element: T): {
+    ref: T | string | null;
+    parent: HTMLElement;
+};
 export declare const button: HTMLButtonElement;
 declare const input: HTMLInputElement;
 export declare const output: HTMLDivElement;
 export declare function handleButtonClick(event: MouseEvent): void;
 export declare function handleInputChange(event: Event): void;
-export declare function transformElements(el: HTMLElement[], transformer: HTMLTransformFunc<HTMLElement, HTMLElement>);
+export declare function transformElements(el: HTMLElement[], transformer: HTMLTransformFunc<HTMLElement, HTMLElement>): any;
 export declare function handleEvents(event: Event, onCallbacks: EventManipulationFunc[]): any;
diff --git a/web_generator/test/type_map_test.dart b/web_generator/test/type_map_test.dart
new file mode 100644
index 0000000..1d6f34d
--- /dev/null
+++ b/web_generator/test/type_map_test.dart
@@ -0,0 +1,343 @@
+// Copyright (c) 2025, 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.
+
+@TestOn('node')
+library;
+
+import 'package:test/test.dart';
+import 'package:web_generator/src/ast/base.dart';
+import 'package:web_generator/src/ast/builtin.dart';
+import 'package:web_generator/src/ast/declarations.dart';
+import 'package:web_generator/src/ast/types.dart';
+import 'package:web_generator/src/interop_gen/namer.dart';
+import 'package:web_generator/src/interop_gen/sub_type.dart';
+
+void main() {
+  group('Type Map Test', () {
+    setUp(clearTypeHierarchyCache);
+
+    test('Builtin Types Test', () {
+      final anyTypeMap = getTypeHierarchy(BuiltinType.anyType);
+      expect(anyTypeMap.nodes, isEmpty,
+          reason: 'JSAny should have no ancestors');
+
+      final booleanTypeMap =
+          getTypeHierarchy(BuiltinType.primitiveType(PrimitiveType.boolean));
+      expect(booleanTypeMap.nodes, isNotEmpty,
+          reason: 'JSBoolean inherits JSAny');
+      expect(booleanTypeMap.nodes.length, equals(1),
+          reason: 'JSBoolean only inherits JSAny');
+
+      final booleanToJSObjectLookup = booleanTypeMap.lookup('JSObject');
+      expect(booleanToJSObjectLookup, isNull,
+          reason: 'JSBoolean does not inherit JSObject');
+
+      final booleanToJSAnyLookup = booleanTypeMap.lookup('JSAny');
+      expect(booleanToJSAnyLookup, isNotNull,
+          reason: 'JSBoolean inherits JSAny');
+      final (level: booleanToJSAnyLevel, path: booleanToJSAnyPath) =
+          booleanToJSAnyLookup!;
+      expect(booleanToJSAnyLevel, equals(1));
+      expect(booleanToJSAnyPath, equals([0]));
+
+      final objectTypeMap =
+          getTypeHierarchy(BuiltinType.primitiveType(PrimitiveType.object));
+      expect(objectTypeMap.nodes.length, equals(1),
+          reason: 'JSObject only inherits JSAny');
+      final objectToObjectLookup = objectTypeMap.lookup('JSObject');
+      expect(objectToObjectLookup, isNotNull, reason: 'JSObject is JSObject');
+      final (level: objectToObjectLevel, path: objectToObjectPath) =
+          objectToObjectLookup!;
+      expect(objectToObjectLevel, equals(isZero),
+          reason: 'Lookup search on self');
+      expect(objectToObjectPath, isEmpty);
+
+      final arrayTypeMap = getTypeHierarchy(BuiltinType.primitiveType(
+          PrimitiveType.array,
+          typeParams: [BuiltinType.anyType]));
+      expect(arrayTypeMap.nodes.length, equals(1),
+          reason: 'JSArray only inherits JSObject');
+    });
+
+    test('Sub Type Primitive Test', () {
+      expect(
+          (getLowestCommonAncestorOfTypes(
+                  [BuiltinType.anyType, BuiltinType.anyType]) as NamedType)
+              .name,
+          equals('JSAny'));
+
+      final numStringSubType = getLowestCommonAncestorOfTypes([
+        BuiltinType.primitiveType(PrimitiveType.num),
+        BuiltinType.primitiveType(PrimitiveType.string)
+      ]);
+      expect((numStringSubType as NamedType).name, equals('JSAny'));
+    });
+
+    group('LCA Test (small)', () {
+      final uint16ArrType = BuiltinType.referred('Uint16Array')!;
+      final uint8ClampedArrType = BuiltinType.referred('Uint8ClampedArray')!;
+      final functionType = BuiltinType.referred('Function')!;
+      final arrayType = BuiltinType.primitiveType(PrimitiveType.array,
+          typeParams: [BuiltinType.anyType]);
+
+      final tupleType = TupleType(
+          types: [PrimitiveType.double, PrimitiveType.int]
+              .map(BuiltinType.primitiveType)
+              .toList(),
+          tupleDeclUrl: '_tuple.dart');
+      final unionType = UnionType(
+          types: [uint16ArrType, uint8ClampedArrType],
+          name: 'AnonymousUnion_123456');
+      final closureType = FunctionType(
+          name: 'AnonymousFun_123456',
+          id: const ID(type: 'type', name: 'AnonymousFun_123456'),
+          returnType: arrayType);
+
+      test('Sub Type Test', () {
+        final typedArrUnion = getLowestCommonAncestorOfTypes(
+            [uint8ClampedArrType, uint16ArrType]);
+        expect(typedArrUnion, isA<BuiltinType>());
+
+        final BuiltinType(name: typedArrayName) = typedArrUnion as BuiltinType;
+        expect(typedArrayName, equals('JSTypedArray'),
+            reason: 'JSUint16Array & JSUint8ClampedArray both inherit'
+                ' JSTypedArray');
+
+        final arrayUnion = getLowestCommonAncestorOfTypes(
+            [uint8ClampedArrType, uint16ArrType, arrayType]);
+        expect(arrayUnion, isA<BuiltinType>());
+        final BuiltinType(name: arrayUnionName) = arrayUnion as BuiltinType;
+        expect(arrayUnionName, equals('JSObject'),
+            reason: 'JSUint16Array and JSUint8ClampedArray both inherit '
+                "JSTypedArray but JSArray doesn't");
+
+        final tupleArrayUnionType =
+            getLowestCommonAncestorOfTypes([tupleType, arrayType]);
+        expect(tupleArrayUnionType, isA<BuiltinType>());
+        expect(tupleArrayUnionType, equals(arrayType),
+            reason: 'A Tuple is an array underneath');
+
+        final doubleUnionType =
+            getLowestCommonAncestorOfTypes([unionType, uint16ArrType]);
+        expect(doubleUnionType, equals(typedArrUnion),
+            reason: 'LCA for A | B and A will always be A | B');
+
+        final closureFunUnionType =
+            getLowestCommonAncestorOfTypes([closureType, functionType]);
+        expect(closureFunUnionType, equals(functionType),
+            reason: 'A closure is a JSFunction underneath');
+      });
+    });
+
+    group('LCA Test (medium)', () {
+      final a = InterfaceDeclaration(
+          name: 'A',
+          exported: true,
+          id: const ID(type: 'interface', name: 'A'));
+      final b = InterfaceDeclaration(
+          name: 'B',
+          exported: true,
+          id: const ID(type: 'interface', name: 'B'));
+      final c = InterfaceDeclaration(
+          name: 'C',
+          exported: true,
+          id: const ID(type: 'interface', name: 'C'),
+          extendedTypes: [a.asReferredType()]);
+      final d = InterfaceDeclaration(
+          name: 'D',
+          exported: true,
+          id: const ID(type: 'interface', name: 'D'),
+          extendedTypes: [a.asReferredType()]);
+      final e = InterfaceDeclaration(
+          name: 'E',
+          exported: true,
+          id: const ID(type: 'interface', name: 'E'),
+          extendedTypes: [a.asReferredType(), b.asReferredType()]);
+      final f = InterfaceDeclaration(
+          name: 'F',
+          exported: true,
+          id: const ID(type: 'interface', name: 'F'),
+          extendedTypes: [a.asReferredType(), c.asReferredType()]);
+      final g = InterfaceDeclaration(
+          name: 'G',
+          exported: true,
+          id: const ID(type: 'interface', name: 'G'),
+          extendedTypes: [
+            a.asReferredType(),
+            b.asReferredType(),
+            d.asReferredType()
+          ]);
+      final h = InterfaceDeclaration(
+          name: 'H',
+          exported: true,
+          id: const ID(type: 'interface', name: 'H'),
+          extendedTypes: [g.asReferredType(), f.asReferredType()]);
+
+      test('Topological List Test', () {
+        final abTopoMap = topologicalList([
+          a.asReferredType(),
+          b.asReferredType()
+        ].map(getTypeHierarchy).toList());
+
+        expect(abTopoMap.first, equals({'A', 'B'}),
+            reason: 'Root Values should be interface types');
+        expect(abTopoMap[1], equals({'JSObject'}),
+            reason: 'A and B inherit JSObject');
+        assert(
+            abTopoMap.last.single == 'JSAny',
+            'A and B must always inherit JSAny, '
+            'and should be last in graph chain');
+
+        final cfTopoMap = topologicalList([
+          c.asReferredType(),
+          f.asReferredType()
+        ].map(getTypeHierarchy).toList());
+
+        expect(cfTopoMap[1], contains(equals('A')),
+            reason: 'C and F inherit A');
+
+        final egTopoMap = topologicalList([
+          e.asReferredType(),
+          g.asReferredType()
+        ].map(getTypeHierarchy).toList());
+        expect(egTopoMap[1], containsAll(['A', 'B']),
+            reason: 'E and G both inherit from A and B');
+      });
+
+      test('Sub Type Test', () {
+        final aType = getLowestCommonAncestorOfTypes([a.asReferredType()]);
+        expect(aType, isA<ReferredType>(),
+            reason: 'Union of a single referred type is a referred typed');
+        expect(aType, equals(aType), reason: 'Union of just A is A');
+
+        final abType = getLowestCommonAncestorOfTypes(
+            [a.asReferredType(), b.asReferredType()]);
+        expect(abType, isA<NamedType>(),
+            reason: 'Union of A and B is a builtin type');
+        expect((abType as NamedType).name, equals('JSObject'));
+
+        final acType = getLowestCommonAncestorOfTypes(
+            [a.asReferredType(), c.asReferredType()]);
+        expect(acType, isA<ReferredType>());
+        expect((acType as ReferredType).declaration.name, equals('A'));
+
+        final acdType = getLowestCommonAncestorOfTypes(
+            [a.asReferredType(), c.asReferredType(), d.asReferredType()]);
+        expect(acdType, isA<ReferredType>());
+        expect((acdType as ReferredType).declaration.name, equals('A'));
+
+        final cfType = getLowestCommonAncestorOfTypes(
+            [c.asReferredType(), f.asReferredType()]);
+        expect(cfType, isA<ReferredType>());
+        expect((cfType as ReferredType).declaration.name, equals('C'));
+
+        final egType = getLowestCommonAncestorOfTypes(
+            [e.asReferredType(), g.asReferredType()]);
+        expect(egType, isA<UnionType>(),
+            reason: 'Common types between E and G are more than one');
+        final UnionType(types: egUnionTypes) = egType as UnionType;
+        expect(egUnionTypes.length, equals(2),
+            reason: 'Common types between E and G are two');
+        expect(
+            egUnionTypes.map((t) => (t as NamedType).name), equals(['A', 'B']),
+            reason: 'Common types between E and G are two: A and B');
+
+        final eghType = getLowestCommonAncestorOfTypes(
+            [e.asReferredType(), g.asReferredType(), h.asReferredType()]);
+        expect(eghType, isA<UnionType>(),
+            reason: 'Common types between E, G and H is more than one');
+        final UnionType(types: eghUnionTypes) = eghType as UnionType;
+        expect(eghUnionTypes.length, equals(2),
+            reason: 'Common types between E, G and H are two');
+        expect(
+            eghUnionTypes.map((t) => (t as NamedType).name), equals(['A', 'B']),
+            reason: 'Common types between E, G and H are two: A and B');
+      });
+    });
+
+    group('LCA Test (large)', () {
+      final a = InterfaceDeclaration(
+          name: 'A',
+          exported: true,
+          id: const ID(type: 'interface', name: 'A'));
+      final b = InterfaceDeclaration(
+          name: 'B',
+          exported: true,
+          id: const ID(type: 'interface', name: 'B'),
+          extendedTypes: [a.asReferredType()]);
+      final c = InterfaceDeclaration(
+          name: 'C',
+          exported: true,
+          id: const ID(type: 'interface', name: 'C'),
+          extendedTypes: [b.asReferredType()]);
+      final d = InterfaceDeclaration(
+          name: 'D',
+          exported: true,
+          id: const ID(type: 'interface', name: 'D'),
+          extendedTypes: [b.asReferredType()]);
+      final h = InterfaceDeclaration(
+          name: 'H',
+          exported: true,
+          id: const ID(type: 'interface', name: 'H'),
+          extendedTypes: [b.asReferredType()]);
+      final g = InterfaceDeclaration(
+          name: 'G',
+          exported: true,
+          id: const ID(type: 'interface', name: 'G'),
+          extendedTypes: [
+            a.asReferredType(),
+          ]);
+      final e = InterfaceDeclaration(
+          name: 'E',
+          exported: true,
+          id: const ID(type: 'interface', name: 'E'),
+          extendedTypes: [d.asReferredType(), g.asReferredType()]);
+      final f = InterfaceDeclaration(
+          name: 'F',
+          exported: true,
+          id: const ID(type: 'interface', name: 'F'),
+          extendedTypes: [h.asReferredType(), g.asReferredType()]);
+      final fun = InterfaceDeclaration(
+          name: 'Fun',
+          exported: true,
+          id: const ID(type: 'interface', name: 'Fun'),
+          extendedTypes: [
+            BuiltinType.referred('Function')!,
+            c.asReferredType()
+          ]);
+      final closure = FunctionType(
+          name: 'AnonymousFun_111111',
+          id: const ID(type: 'type', name: 'AnonymousFun_111111'),
+          returnType: f.asReferredType());
+
+      test('Sub Type Test', () {
+        final efUnion = getLowestCommonAncestorOfTypes(
+            [e.asReferredType(), f.asReferredType()]);
+        expect(efUnion, equals(g.asReferredType()),
+            reason: 'E and F both inherit G');
+
+        final cefUnion = getLowestCommonAncestorOfTypes(
+            [c.asReferredType(), e.asReferredType(), f.asReferredType()]);
+        expect(cefUnion, equals(b.asReferredType()),
+            reason: 'C, E and F share B in common');
+
+        final cUnionAndEFUnion =
+            getLowestCommonAncestorOfTypes([efUnion, c.asReferredType()]);
+        expect(cUnionAndEFUnion, equals(a.asReferredType()),
+            reason: 'C | E | F is not equal to C | (E | F)');
+
+        final funBUnion = getLowestCommonAncestorOfTypes(
+            [fun.asReferredType(), b.asReferredType()]);
+        expect(funBUnion, equals(b.asReferredType()));
+
+        final funFunUnion =
+            getLowestCommonAncestorOfTypes([fun.asReferredType(), closure]);
+        expect(funFunUnion, isA<BuiltinType>());
+        expect(funFunUnion, equals(BuiltinType.referred('Function')),
+            reason: 'Union of an interface type implementing JSFunction and a '
+                'closure should equal JSFunction');
+      });
+    });
+  });
+}