diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts
index 39ac8e3fe07..884a92d579f 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/signal_forms.ts
@@ -157,6 +157,25 @@ export class TcbNativeFieldOp extends TcbOp {
]);
}
+ const hasDynamicType =
+ this.inputType === null &&
+ this.node.inputs.some(
+ (input) =>
+ (input.type === BindingType.Property || input.type === BindingType.Attribute) &&
+ input.name === 'type',
+ );
+
+ // If the type is dynamic, check it as if it can be any of the types above.
+ if (hasDynamicType) {
+ return ts.factory.createUnionTypeNode([
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword),
+ ts.factory.createTypeReferenceNode('Date'),
+ ts.factory.createLiteralTypeNode(ts.factory.createNull()),
+ ]);
+ }
+
// Fall back to string if we couldn't map the type.
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
}
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
index 464e3b81b21..c131e6b2fe0 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
@@ -2603,6 +2603,22 @@ describe('type check blocks', () => {
expect(block).toContain('_t2.field = (((this).f));');
});
+ it('should generate all possible type for input with a dynamic `type`', () => {
+ const block = tcb('', [FieldMock]);
+ expect(block).toContain('var _t1 = null! as string | number | boolean | Date | null;');
+ expect(block).toContain('_t1 = ((this).f)().value();');
+ expect(block).toContain('var _t2 = null! as i0.Field;');
+ expect(block).toContain('_t2.field = (((this).f));');
+ });
+
+ it('should generate all possible type for input with a dynamic attribute `type` binding', () => {
+ const block = tcb('', [FieldMock]);
+ expect(block).toContain('var _t1 = null! as string | number | boolean | Date | null;');
+ expect(block).toContain('_t1 = ((this).f)().value();');
+ expect(block).toContain('var _t2 = null! as i0.Field;');
+ expect(block).toContain('_t2.field = (((this).f));');
+ });
+
[
{inputType: 'text', expectedType: 'string'},
{inputType: 'radio', expectedType: 'string'},
diff --git a/packages/compiler-cli/test/ngtsc/signal_forms_spec.ts b/packages/compiler-cli/test/ngtsc/signal_forms_spec.ts
index e2d48e9aaa0..652c4ae83b1 100644
--- a/packages/compiler-cli/test/ngtsc/signal_forms_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/signal_forms_spec.ts
@@ -601,5 +601,30 @@ runInEachFileSystem(() => {
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
+
+ it('should infer an input with a dynamic `type` as being any of the other types', () => {
+ env.write(
+ 'test.ts',
+ `
+ import {Component, signal} from '@angular/core';
+ import {Field, form} from '@angular/forms/signals';
+
+ @Component({
+ template: '',
+ imports: [Field]
+ })
+ export class Comp {
+ type = '';
+ f = form(signal({test: true}));
+ }
+ `,
+ );
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(1);
+ expect(extractMessage(diags[0])).toBe(
+ `Type '{ test: boolean; }' is not assignable to type 'string | number | boolean | Date | null'.`,
+ );
+ });
});
});