Skip to content

Commit 47a1211

Browse files
Brooooooklynclaude
andcommitted
fix(jit): align union type resolution with Angular's typeReferenceToExpression
Angular's reference filters only `null` literal types from union types and requires exactly one non-null type to remain. Previously we iterated all union members and returned the first resolvable one, which is more permissive than Angular's behavior. With Angular-aligned semantics: - `T | null` → resolves to T (1 non-null type remains) - `undefined | T` → unresolvable (2 non-null types remain) - `null | undefined | T` → unresolvable (2 non-null types remain) Unresolvable types emit `{ type: undefined }` matching Angular's `paramType || ts.factory.createIdentifier('undefined')` fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 61b518f commit 47a1211

File tree

3 files changed

+41
-23
lines changed

3 files changed

+41
-23
lines changed

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -929,16 +929,20 @@ fn extract_type_name_from_annotation(type_annotation: &oxc_ast::ast::TSType<'_>)
929929
}
930930
}
931931
oxc_ast::ast::TSType::TSUnionType(union) => {
932-
// For union types like `T | null`, `undefined | T`, `null | undefined | T`,
933-
// iterate all members and return the first that resolves to a name.
934-
// Using try-each-and-continue handles TSNullKeyword, TSUndefinedKeyword,
935-
// and any other non-reference type gracefully.
936-
for t in &union.types {
937-
if let Some(name) = extract_type_name_from_annotation(t) {
938-
return Some(name);
939-
}
932+
// Match Angular's typeReferenceToExpression behavior:
933+
// filter out only `null` literal types, and if exactly one type remains,
934+
// resolve that type. Otherwise, return None (unresolvable).
935+
// See: angular/packages/compiler-cli/src/ngtsc/transform/jit/src/downlevel_decorators_transform.ts
936+
let non_null: std::vec::Vec<_> = union
937+
.types
938+
.iter()
939+
.filter(|t| !matches!(t, oxc_ast::ast::TSType::TSNullKeyword(_)))
940+
.collect();
941+
if non_null.len() == 1 {
942+
extract_type_name_from_annotation(non_null[0])
943+
} else {
944+
None
940945
}
941-
None
942946
}
943947
_ => None,
944948
}

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6307,20 +6307,28 @@ export class HighlightDirective {
63076307

63086308
#[test]
63096309
fn test_jit_union_type_ctor_params() {
6310-
// Bug fix: union types like `undefined | SomeService` or `null | undefined | T`
6311-
// must correctly extract the type name for ctorParameters.
6312-
// Previously only TSNullKeyword was skipped, causing TSUndefinedKeyword to short-circuit.
6310+
// Angular-aligned union type behavior for ctorParameters.
6311+
// Angular's typeReferenceToExpression filters ONLY `null` literal types.
6312+
// If exactly one non-null type remains, it resolves; otherwise unresolvable.
6313+
//
6314+
// `T | null` → resolves to T (1 non-null type)
6315+
// `undefined | T` → unresolvable (2 non-null types: undefined + T)
6316+
// `null | undefined | T` → unresolvable (2 non-null types: undefined + T)
6317+
//
6318+
// See: angular/packages/compiler-cli/src/ngtsc/transform/jit/src/downlevel_decorators_transform.ts
63136319
let allocator = Allocator::default();
63146320
let source = r#"
63156321
import { Component } from '@angular/core';
63166322
import { ServiceA } from './a.service';
63176323
import { ServiceB } from './b.service';
6324+
import { ServiceC } from './c.service';
63186325
63196326
@Component({ selector: 'test', template: '' })
63206327
export class TestComponent {
63216328
constructor(
63226329
svcA: undefined | ServiceA,
63236330
svcB: null | undefined | ServiceB,
6331+
svcC: ServiceC | null,
63246332
) {}
63256333
}
63266334
"#;
@@ -6329,22 +6337,23 @@ export class TestComponent {
63296337
let result = transform_angular_file(&allocator, "test.component.ts", source, &options, None);
63306338
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
63316339

6332-
// Both types must be correctly extracted despite union with undefined/null
6340+
// `ServiceC | null` resolves correctly (1 non-null type)
63336341
assert!(
6334-
result.code.contains("type: ServiceA"),
6335-
"ctorParameters should resolve 'undefined | ServiceA' to ServiceA. Got:\n{}",
6342+
result.code.contains("type: ServiceC"),
6343+
"ctorParameters should resolve 'ServiceC | null' to ServiceC. Got:\n{}",
63366344
result.code
63376345
);
6346+
6347+
// `undefined | ServiceA` and `null | undefined | ServiceB` are unresolvable per Angular spec
6348+
// (2 non-null types remain after filtering null)
63386349
assert!(
6339-
result.code.contains("type: ServiceB"),
6340-
"ctorParameters should resolve 'null | undefined | ServiceB' to ServiceB. Got:\n{}",
6350+
!result.code.contains("type: ServiceA"),
6351+
"ctorParameters must not resolve 'undefined | ServiceA' (2 non-null types). Got:\n{}",
63416352
result.code
63426353
);
6343-
6344-
// Should NOT emit 'type: undefined' for either
63456354
assert!(
6346-
!result.code.contains("type: undefined"),
6347-
"ctorParameters must not emit 'type: undefined' for resolvable union types. Got:\n{}",
6355+
!result.code.contains("type: ServiceB"),
6356+
"ctorParameters must not resolve 'null | undefined | ServiceB' (2 non-null types). Got:\n{}",
63486357
result.code
63496358
);
63506359

crates/oxc_angular_compiler/tests/snapshots/integration_test__jit_union_type_ctor_params.snap

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
---
22
source: crates/oxc_angular_compiler/tests/integration_test.rs
3+
assertion_line: 6360
34
expression: result.code
45
---
6+
57
import { Component } from '@angular/core';
68
import { ServiceA } from './a.service';
79
import { ServiceB } from './b.service';
10+
import { ServiceC } from './c.service';
811
import { __decorate } from "tslib";
912

1013
let TestComponent = class TestComponent {
1114
constructor(
1215
svcA: undefined | ServiceA,
1316
svcB: null | undefined | ServiceB,
17+
svcC: ServiceC | null,
1418
) {}
1519

1620
static ctorParameters = () => [
17-
{ type: ServiceA },
18-
{ type: ServiceB }
21+
{ type: undefined },
22+
{ type: undefined },
23+
{ type: ServiceC }
1924
];
2025
};
2126
TestComponent = __decorate([

0 commit comments

Comments
 (0)