Skip to content

[interop] Add Support for Typealiases #407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions web_generator/lib/src/ast/declarations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,40 @@ class EnumMember {

String? dartName;
}

class TypeAliasDeclaration extends NamedDeclaration
implements ExportableDeclaration {
@override
final String name;

final List<GenericType> typeParameters;

final Type type;

@override
final String? dartName;

@override
bool exported;

@override
ID get id => ID(type: 'typealias', name: name);

TypeAliasDeclaration(
{required this.name,
this.typeParameters = const [],
required this.type,
required this.exported})
: dartName = null;

@override
TypeDef emit([DeclarationOptions? options]) {
options ??= DeclarationOptions();

return TypeDef((t) => t
..name = name
..types
.addAll(typeParameters.map((t) => t.emit(options?.toTypeOptions())))
..definition = type.emit(options?.toTypeOptions()));
}
}
10 changes: 10 additions & 0 deletions web_generator/lib/src/ast/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:code_builder/code_builder.dart';
import 'base.dart';
import 'builtin.dart';
import 'declarations.dart';
import 'types.dart';

BuiltinType? getSupportedType(String name, [List<Type> typeParams = const []]) {
final type = switch (name) {
Expand Down Expand Up @@ -38,6 +39,15 @@ Type getJSTypeAlternative(Type type) {
if (primitiveType == null) return BuiltinType.anyType;

return BuiltinType.primitiveType(primitiveType, shouldEmitJsType: true);
} else if (type is ReferredType) {
switch (type.declaration) {
case TypeAliasDeclaration(type: final t):
case EnumDeclaration(baseType: final t):
final jsTypeT = getJSTypeAlternative(t);
return jsTypeT == t ? type : jsTypeT;
default:
return type;
}
}
return type;
}
Expand Down
4 changes: 2 additions & 2 deletions web_generator/lib/src/interop_gen/transform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ class TransformResult {
// (namespaces + functions, multiple interfaces, etc)
Map<String, String> generate() {
final emitter = DartEmitter.scoped(useNullSafetySyntax: true);
final formatter =
DartFormatter(languageVersion: DartFormatter.latestLanguageVersion);
final formatter = DartFormatter(
languageVersion: DartFormatter.latestShortStyleLanguageVersion);
return programMap.map((file, declMap) {
final specs = declMap.decls.values.map((d) {
return switch (d) {
Expand Down
60 changes: 53 additions & 7 deletions web_generator/lib/src/interop_gen/transform/transformer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class Transformer {
_transformFunction(node as TSFunctionDeclaration),
TSSyntaxKind.EnumDeclaration =>
_transformEnum(node as TSEnumDeclaration),
TSSyntaxKind.TypeAliasDeclaration =>
_transformTypeAlias(node as TSTypeAliasDeclaration),
_ => throw Exception('Unsupported Declaration Kind: ${node.kind}')
};
// ignore: dead_code This line will not be dead in future decl additions
Expand Down Expand Up @@ -177,6 +179,30 @@ class Transformer {
return declarations?.toDart.first;
}

TypeAliasDeclaration _transformTypeAlias(TSTypeAliasDeclaration typealias) {
final name = typealias.name.text;

final modifiers = typealias.modifiers?.toDart;
final isExported = modifiers?.any((m) {
return m.kind == TSSyntaxKind.ExportKeyword;
}) ??
false;

final typeParams = typealias.typeParameters?.toDart;

final type = typealias.type;

return TypeAliasDeclaration(
name: name,
// TODO: Can we find a way not to make the types be JS types
// by default if possible. Leaving this for now,
// so that using such typealiases in generics does not break
type: _transformType(type),
typeParameters:
typeParams?.map(_transformTypeParamDeclaration).toList() ?? [],
exported: isExported);
}

FunctionDeclaration _transformFunction(TSFunctionDeclaration function) {
final name = function.name.text;

Expand Down Expand Up @@ -228,18 +254,20 @@ class Transformer {

GenericType _transformTypeParamDeclaration(
TSTypeParameterDeclaration typeParam) {
final constraint = typeParam.constraint == null
? BuiltinType.anyType
: _transformType(typeParam.constraint!, typeArg: true);
return GenericType(
name: typeParam.name.text,
constraint: typeParam.constraint == null
? BuiltinType.anyType
: _transformType(typeParam.constraint!));
constraint: getJSTypeAlternative(constraint));
}

/// Parses the type
///
/// TODO(https://github.com/dart-lang/web/issues/384): Add support for literals (i.e individual booleans and `null`)
/// TODO(https://github.com/dart-lang/web/issues/383): Add support for `typeof` types
Type _transformType(TSTypeNode type, {bool parameter = false}) {
Type _transformType(TSTypeNode type,
{bool parameter = false, bool typeArg = false}) {
switch (type.kind) {
case TSSyntaxKind.TypeReference:
final refType = type as TSTypeReferenceNode;
Expand All @@ -255,7 +283,10 @@ class Transformer {
// for this, and adding support for "supported declarations"
// (also a better name for that)
final supportedType = getSupportedType(
name, (typeArguments ?? []).map(_transformType).toList());
name,
(typeArguments ?? [])
.map((type) => _transformType(type, typeArg: true))
.toList());
if (supportedType != null) {
return supportedType;
}
Expand All @@ -280,8 +311,19 @@ class Transformer {
final firstNode =
declarationsMatching.whereType<NamedDeclaration>().first;

// For Typealiases, we can either return the type itself
// or the JS Alternative (if its underlying type isn't a JS type)
switch (firstNode) {
case TypeAliasDeclaration(type: final t):
case EnumDeclaration(baseType: final t):
final jsType = getJSTypeAlternative(t);
if (jsType != t && typeArg) return jsType;
}

return firstNode.asReferredType(
(typeArguments ?? []).map(_transformType).toList(),
(typeArguments ?? [])
.map((type) => _transformType(type, typeArg: true))
.toList(),
);
// TODO: Union types are also anonymous by design
// Unless we are making typedefs for them, we should
Expand Down Expand Up @@ -381,7 +423,8 @@ class Transformer {
'The given type with kind ${type.kind} is not supported yet')
};

return BuiltinType.primitiveType(primitiveType);
return BuiltinType.primitiveType(primitiveType,
shouldEmitJsType: typeArg ? true : null);
}
}

Expand Down Expand Up @@ -445,6 +488,9 @@ class Transformer {
break;
case final EnumDeclaration _:
break;
case final TypeAliasDeclaration t:
if (decl.type is! BuiltinType) filteredDeclarations.add(t.type);
break;
// TODO: We can make (DeclarationAssociatedType) and use that
// rather than individual type names
case final HomogenousEnumType hu:
Expand Down
10 changes: 10 additions & 0 deletions web_generator/lib/src/js/typescript.types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ extension type const TSSyntaxKind._(num _) {
static const TSSyntaxKind InterfaceDeclaration = TSSyntaxKind._(264);
static const TSSyntaxKind FunctionDeclaration = TSSyntaxKind._(262);
static const TSSyntaxKind ExportDeclaration = TSSyntaxKind._(278);
static const TSSyntaxKind TypeAliasDeclaration = TSSyntaxKind._(265);
static const TSSyntaxKind Parameter = TSSyntaxKind._(169);
static const TSSyntaxKind EnumDeclaration = TSSyntaxKind._(266);

Expand Down Expand Up @@ -178,6 +179,15 @@ extension type TSFunctionDeclaration._(JSObject _) implements TSDeclaration {
external TSNodeArray<TSNode> get modifiers;
}

@JS('TypeAliasDeclaration')
extension type TSTypeAliasDeclaration._(JSObject _)
implements TSDeclaration, TSStatement {
external TSNodeArray<TSNode>? get modifiers;
external TSNodeArray<TSTypeParameterDeclaration>? get typeParameters;
external TSIdentifier get name;
external TSTypeNode get type;
}

@JS('ParameterDeclaration')
extension type TSParameterDeclaration._(JSObject _) implements TSDeclaration {
external TSNode get name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ extension type const Permissions._(int _) {
static const Permissions All = Permissions._(7);
}
@_i1.JS()
external bool hasPermission(Permissions perm, Permissions flag);
external bool hasPermission(
Permissions perm,
Permissions flag,
);
@_i1.JS()
external Permissions get userPermissions;
@_i1.JS()
Expand Down
19 changes: 14 additions & 5 deletions web_generator/test/integration/interop_gen/functions_expected.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,29 @@ external void logMessages(
_i1.JSArray<_i1.JSString> messages4,
]);
@_i1.JS()
external _i1.JSPromise<U> delay<U extends _i1.JSAny?>(num ms, [U? returnValue]);
external _i1.JSPromise<U> delay<U extends _i1.JSAny?>(
num ms, [
U? returnValue,
]);
@_i1.JS()
external _i1.JSArray<_i1.JSNumber> toArray(num a);
@_i1.JS()
external double square(num a);
@_i1.JS()
external double pow(num a);
@_i1.JS('pow')
external double pow$1(num a, num power);
external double pow$1(
num a,
num power,
);
@_i1.JS('toArray')
external _i1.JSArray<_i1.JSString> toArray$1(String a);
@_i1.JS()
external _i1.JSObject createUser(String name, [num? age, String? role]);
external _i1.JSObject createUser(
String name, [
num? age,
String? role,
]);
@_i1.JS()
external T firstElement<T extends _i1.JSAny?>(_i1.JSArray<T> arr);
@_i1.JS()
Expand All @@ -38,8 +48,7 @@ external T identity<T extends _i1.JSAny?>(T value);
external void someFunction<A extends _i1.JSAny?>(_i1.JSArray<A> arr);
@_i1.JS('someFunction')
external B someFunction$1<A extends _i1.JSAny?, B extends _i1.JSAny?>(
_i1.JSArray<A> arr,
);
_i1.JSArray<A> arr);
@_i1.JS()
external T logTuple<T extends _i1.JSArray<_i1.JSAny?>>(
T args, [
Expand Down
76 changes: 76 additions & 0 deletions web_generator/test/integration/interop_gen/typealias_expected.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// ignore_for_file: constant_identifier_names, non_constant_identifier_names

// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:js_interop' as _i1;

typedef Username = String;
typedef Age = double;
typedef IsActive = bool;
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 PromisedArray<U extends _i1.JSAny?, T extends _i1.JSArray<U>>
= _i1.JSPromise<T>;
typedef Shape2D = String;
typedef PrismFromShape2D<S extends _i1.JSString> = _i1.JSArray<S>;
typedef Logger = LoggerType;
typedef Direction = AnonymousUnion;
typedef Method = AnonymousUnion$1;
@_i1.JS()
external LoggerContainer<_i1.JSNumber> get loggerContainers;
@_i1.JS()
external Logger myLogger;
@_i1.JS()
external Method get requestMethod;
@_i1.JS()
external Username get username;
@_i1.JS()
external Age get age;
@_i1.JS()
external _i1.JSArray<Tags> get tagArray;
@_i1.JS()
external List<_i1.JSString> get users;
@_i1.JS()
external Box<_i1.JSNumber> get matrix;
@_i1.JS()
external PrismFromShape2D<_i1.JSString> makePrism(Shape2D shape);
@_i1.JS('makePrism')
external PrismFromShape2D<S> makePrism$1<S extends _i1.JSString>(S shape);
@_i1.JS()
external PromisedArray<_i1.JSString, _i1.JSArray<_i1.JSString>> fetchNames();
@_i1.JS()
external String isUserActive(IsActive status);
extension type const LoggerType._(int _) {
static const LoggerType Noop = LoggerType._(0);

static const LoggerType Stdout = LoggerType._(1);

static const LoggerType Stderr = LoggerType._(2);

static const LoggerType File = LoggerType._(3);

static const LoggerType Other = LoggerType._(4);
}
extension type const AnonymousUnion._(String _) {
static const AnonymousUnion N = AnonymousUnion._('N');

static const AnonymousUnion S = AnonymousUnion._('S');

static const AnonymousUnion E = AnonymousUnion._('E');

static const AnonymousUnion W = AnonymousUnion._('W');
}
extension type const AnonymousUnion$1._(String _) {
static const AnonymousUnion$1 GET = AnonymousUnion$1._('GET');

static const AnonymousUnion$1 POST = AnonymousUnion$1._('POST');

static const AnonymousUnion$1 PUT = AnonymousUnion$1._('PUT');

static const AnonymousUnion$1 DELETE = AnonymousUnion$1._('DELETE');

static const AnonymousUnion$1 PATCH = AnonymousUnion$1._('PATCH');

static const AnonymousUnion$1 OPTIONS = AnonymousUnion$1._('OPTIONS');
}
typedef LoggerContainer<N extends _i1.JSNumber> = _i1.JSArray<N>;
32 changes: 32 additions & 0 deletions web_generator/test/integration/interop_gen/typealias_input.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
declare enum LoggerType {
Noop = 0,
Stdout = 1,
Stderr = 2,
File = 3,
Other = 4
}
export type Username = string;
export type Age = number;
export type IsActive = boolean;
export type Tags = string[];
export type List<T> = T[];
export type Box<T> = Array<Array<T>>;
export type PromisedArray<U, T extends Array<U>> = Promise<T>;
export type Shape2D = string;
export type PrismFromShape2D<S extends Shape2D> = Array<S>;
export type Logger = LoggerType;
export type Direction = "N" | "S" | "E" | "W";
export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
type LoggerContainer<N extends number> = N[];
export declare const loggerContainers: LoggerContainer<Logger>;
export declare let myLogger: Logger;
export declare const requestMethod: Method;
export declare const username: Username;
export declare const age: Age;
export declare const tagArray: Tags[];
export declare const users: List<Username>;
export declare const matrix: Box<number>;
export declare function makePrism(shape: Shape2D): PrismFromShape2D<Shape2D>;
export declare function makePrism<S extends Shape2D>(shape: S): PrismFromShape2D<S>;
export declare function fetchNames(): PromisedArray<string, string[]>;
export declare function isUserActive(status: IsActive): string;
Loading