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'.`, + ); + }); }); });