Skip to content

FormBuilder#group incorrectly infers type if union type is passed in #45912

@bgotink

Description

@bgotink

Which @angular/* package(s) are the source of the bug?

forms

Is this a regression?

No

Description

The group function expands passed-in union types, which it shouldn't do:

const groupControl = formBuilder.group({
    value: '' as string | number,
});

// Expected type:
//    FormControl<string | number>
// Actual type:
//    FormControl<string> | FormControl<number>
const valueControl = group.controls.value;

image

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/angular-ivy-ao4sne?file=src%2Fapp%2Fapp.component.ts

Please provide the exception or error you saw

n/a

Please provide the environment you discovered this bug in (run ng version)

Unable to run ng version in stackblitz

Happens in 14.0.0-next.16

Anything else?

This is caused by the type mapping in

/**
* FormBuilder accepts values in various container shapes, as well as raw values.
* Element returns the appropriate corresponding model class, given the container T.
* The flag N, if not never, makes the resulting `FormControl` have N in its type.
*/
export type ɵElement<T, N extends null> =
T extends FormControl<infer U> ? FormControl<U> :
T extends FormGroup<infer U> ? FormGroup<U> :
T extends FormArray<infer U> ? FormArray<U> :
T extends AbstractControl<infer U> ? AbstractControl<U> :
T extends FormControlState<infer U> ? FormControl<U|N> :
T extends ControlConfig<infer U> ? FormControl<U|N> :
// ControlConfig can be too much for the compiler to infer in the wrapped case. This is
// not surprising, since it's practically death-by-polymorphism (e.g. the optional validators
// members that might be arrays). Watch for ControlConfigs that might fall through.
T extends Array<infer U|ValidatorFn|ValidatorFn[]|AsyncValidatorFn|AsyncValidatorFn[]> ? FormControl<U|N> :
// Fallthough case: T is not a container type; use it directly as a value.
FormControl<T|N>;

Typescript expands the type union in the type map, because different parts of the type union might end up in different branches of the type map.
Here's a minimal example (playground)

type IsNumber<T> = T extends number ? true : false;

// false
type Test1 =  IsNumber<string>;

// true
type Test2 = IsNumber<number>;

// boolean
type Test3 = IsNumber<string | number>;

This can be solved by wrapping the type in the map with [] (playground):

type IsExactlyNumber<T> = [T] extends [number] ? true : false;

// false
type Test1 =  IsExactlyNumber<string>;

// true
type Test2 = IsExactlyNumber<number>;

// false
type Test3 = IsExactlyNumber<string | number>;

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions