From de431b841423a69fb8991273acde9156ab45a746 Mon Sep 17 00:00:00 2001 From: Nike Okoronkwo Date: Tue, 9 Sep 2025 09:20:49 -0500 Subject: [PATCH] [interop] Add support for Conditional and Predicate Types Fixes #464 Fixes #463 This PR adds support for transforming conditional types (`t extends a ? b : c`) and predicate types (`t is a`), and generates Dart alternatives to these types. Predicate types are converted to booleans, while conditional types are converted to unions, since the result can either be one or the other. There currently isn't a way (afaik) to introspect into parameter values to give a specific result depending on the param result. We could be smart, however, and wrap such methods with other methods to do the casting for us in the case of conditional types, and could _try_ to do similar in predicate types (via `assert` checks and more). Signed-off-by: Nike Okoronkwo --- web_generator/lib/src/ast/helpers.dart | 2 + web_generator/lib/src/ast/types.dart | 76 +++++++++---------- .../interop_gen/transform/transformer.dart | 36 +++++++++ .../lib/src/js/typescript.types.dart | 20 +++++ .../interop_gen/ts_typing_expected.dart | 45 +++++++---- .../interop_gen/ts_typing_input.d.ts | 5 +- 6 files changed, 128 insertions(+), 56 deletions(-) diff --git a/web_generator/lib/src/ast/helpers.dart b/web_generator/lib/src/ast/helpers.dart index dade60cc..fe17b5b0 100644 --- a/web_generator/lib/src/ast/helpers.dart +++ b/web_generator/lib/src/ast/helpers.dart @@ -259,6 +259,8 @@ Type desugarTypeAliases(Type t) { if (t case final ReferredType ref when ref.declaration is TypeAliasDeclaration) { return desugarTypeAliases((ref.declaration as TypeAliasDeclaration).type); + } else if (t case ReferredDeclarationType(type: final actualType)) { + return desugarTypeAliases(actualType); } return t; } diff --git a/web_generator/lib/src/ast/types.dart b/web_generator/lib/src/ast/types.dart index 17ff2537..19b9c586 100644 --- a/web_generator/lib/src/ast/types.dart +++ b/web_generator/lib/src/ast/types.dart @@ -673,48 +673,48 @@ sealed class _UnionOrIntersectionDeclaration extends NamedDeclaration dartTypeName ?? typeName, _ => t.dartName ?? t.id.name }; + final Expression body; + if (desugarTypeAliases(t) == repType) { + body = refer('_'); + } else if (jsTypeAlt.id == t.id) { + body = refer('_').asA(type); + } else { + body = switch (t) { + BuiltinType(name: final n) when n == 'int' => refer('_') + .asA(jsTypeAlt.emit(options?.toTypeOptions())) + .property('toDartInt'), + BuiltinType(name: final n) when n == 'double' || n == 'num' => + refer('_') + .asA(jsTypeAlt.emit(options?.toTypeOptions())) + .property('toDartDouble'), + BuiltinType() => refer('_') + .asA(jsTypeAlt.emit(options?.toTypeOptions())) + .property('toDart'), + 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') + ]), + _ => refer('_').asA(jsTypeAlt.emit(options?.toTypeOptions())) + }; + } 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 - }; + ..body = body.code; }); }))); } diff --git a/web_generator/lib/src/interop_gen/transform/transformer.dart b/web_generator/lib/src/interop_gen/transform/transformer.dart index a5c82801..3237e4ee 100644 --- a/web_generator/lib/src/interop_gen/transform/transformer.dart +++ b/web_generator/lib/src/interop_gen/transform/transformer.dart @@ -1113,6 +1113,42 @@ class Transformer { return _getTypeFromTypeNode(refType, typeArg: typeArg, isNullable: isNullable ?? false); + case TSSyntaxKind.TypePredicate: + // in the future, we can be smarter about this + // but for now, we just have this as a boolean + return BuiltinType.primitiveType(PrimitiveType.boolean, + isNullable: isNullable); + case TSSyntaxKind.ConditionalType: + final conditionalType = type as TSConditionalTypeNode; + final trueType = _transformType(conditionalType.trueType); + final falseType = _transformType(conditionalType.falseType); + + final types = [trueType, falseType] + .sorted((a, b) => a.id.toString().compareTo(b.id.toString())); + + final expectedID = ID(type: 'type', name: types.join('|')); + + if (typeMap.containsKey(expectedID.toString())) { + return (typeMap[expectedID.toString()] as UnionType) + ..isNullable = (isNullable ?? false); + } + + final trueTypeName = trueType is NamedType + ? trueType.name + : trueType.dartName ?? trueType.id.name; + + final falseTypeName = falseType is NamedType + ? falseType.name + : falseType.dartName ?? falseType.id.name; + final conditionalName = '${trueTypeName}Or$falseTypeName'; + + final un = UnionType(types: types, name: conditionalName); + final unType = typeMap.putIfAbsent(expectedID.toString(), () { + namer.markUsed(conditionalName); + return un; + }); + + return unType..isNullable = (isNullable ?? false); case TSSyntaxKind.TypeLiteral: // type literal final typeLiteralNode = type as TSTypeLiteralNode; diff --git a/web_generator/lib/src/js/typescript.types.dart b/web_generator/lib/src/js/typescript.types.dart index 5335c8d0..2a81435a 100644 --- a/web_generator/lib/src/js/typescript.types.dart +++ b/web_generator/lib/src/js/typescript.types.dart @@ -100,6 +100,8 @@ extension type const TSSyntaxKind._(num _) { static const TSSyntaxKind FunctionType = TSSyntaxKind._(184); static const TSSyntaxKind ConstructorType = TSSyntaxKind._(185); static const TSSyntaxKind TypeOperator = TSSyntaxKind._(198); + static const TSSyntaxKind TypePredicate = TSSyntaxKind._(182); + static const TSSyntaxKind ConditionalType = TSSyntaxKind._(194); // Other static const TSSyntaxKind Identifier = TSSyntaxKind._(80); @@ -202,6 +204,24 @@ extension type TSParenthesizedTypeNode._(JSObject _) implements TSTypeNode { external TSTypeNode get type; } +@JS('TypePredicateNode') +extension type TSTypePredicateNode._(JSObject _) implements TSTypeNode { + @redeclare + TSSyntaxKind get kind => TSSyntaxKind.TypePredicate; + external TSIdentifier get parameterName; + external TSTypeNode? get type; +} + +@JS('ConditionalTypeNode') +extension type TSConditionalTypeNode._(JSObject _) implements TSTypeNode { + @redeclare + TSSyntaxKind get kind => TSSyntaxKind.ConditionalType; + external TSTypeNode get checkType; + external TSTypeNode get extendsType; + external TSTypeNode get trueType; + external TSTypeNode get falseType; +} + @JS('TupleTypeNode') extension type TSTupleTypeNode._(JSObject _) implements TSTypeNode { external TSNodeArray get elements; 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 a9d185bd..91048bba 100644 --- a/web_generator/test/integration/interop_gen/ts_typing_expected.dart +++ b/web_generator/test/integration/interop_gen/ts_typing_expected.dart @@ -15,6 +15,15 @@ external String myFunction(String param); @_i1.JS() external String myEnclosingFunction(_i1.JSFunction func); @_i1.JS() +external bool objectIsProduct(_i1.JSObject obj); +@_i1.JS() +external AnonymousType_2194029 get randomNonTypedProduct; +@_i1.JS() +external ProductOrrandomNonTypedProduct objectAsProduct( + _i1.JSObject obj, + bool structured, +); +@_i1.JS() external _i1.JSArray> indexedArray(_i1.JSArray arr); @_i1.JS() @@ -84,8 +93,6 @@ external _i2.JSTuple4<_i1.JSString, _i1.JSNumber, _i1.JSBoolean, _i1.JSSymbol> @_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( @@ -125,6 +132,26 @@ external _i1.JSAny? get someIntersection; external AnonymousIntersection_4895242 get myThirdIntersection; @_i1.JS() external AnonymousIntersection_1711585 get myTypeGymnastic; +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; +} +typedef Product = AnonymousType_2194029; +extension type ProductOrrandomNonTypedProduct._(AnonymousType_2194029 _) + implements AnonymousType_2194029 { + Product get asProduct => _; + + AnonymousType_2194029 get asRandomNonTypedProduct => _; +} extension type AnonymousType_9143117._(_i1.JSObject _) implements _i1.JSObject { external AnonymousType_9143117({ @@ -210,19 +237,6 @@ extension type AnonymousUnion_7503220._(_i1.JSTypedArray _) _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, @@ -233,7 +247,6 @@ extension type AnonymousType_1358595._(_i1.JSObject _) implements _i1.JSObject { external double taxRate; } -typedef Product = AnonymousType_2194029; extension type AnonymousType_2773310._(_i1.JSObject _) implements _i1.JSObject { external AnonymousType_2773310({ String id, 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 b5697fec..5e2b5b8f 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 @@ -29,7 +29,6 @@ export declare const myEnumValue2: typeof MyEnum; export declare function myFunction(param: string): string; export declare let myFunctionAlias: typeof myFunction; export declare let myFunctionAlias2: typeof myFunctionAlias; -// 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; @@ -65,7 +64,9 @@ export declare class MyProduct implements Product { price: number; constructor(id: number, name: string, price: number); } -export function indexedArray(arr: T[]): { id: number, value: T }[]; +export declare function objectIsProduct(obj: object): obj is Product; +export declare function objectAsProduct(obj: object, structured: boolean): (typeof structured) extends true ? Product : typeof randomNonTypedProduct; +export declare function indexedArray(arr: T[]): { id: number, value: T }[]; export const responseObject: { id: string; value: any;