fix(compiler-cli): expand type for native controls with a dynamic type

We recently allowed users to have a dynamic input `type` with signal forms, but the logic that infers the value type falls back to `string` even though in theory it can be any of the other types.

These changes expand the inferred type to `string | number | boolean | Date | null` if we detect a dynamic `type` binding.

(cherry picked from commit 8a3f3a91cf)
This commit is contained in:
Kristiyan Kostadinov 2025-12-09 10:29:52 +01:00 committed by Alex Rickabaugh
parent 6d7475582f
commit 65297c6201
3 changed files with 60 additions and 0 deletions

View file

@ -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);
}

View file

@ -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('<input [type]="expr" [field]="f"/>', [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('<input [attr.type]="expr" [field]="f"/>', [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'},

View file

@ -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: '<input [type]="type" [field]="f"/>',
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'.`,
);
});
});
});