-
Notifications
You must be signed in to change notification settings - Fork 24.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FormBuilder#group incorrectly infers type if union type is passed in #45912
Comments
(removed, working on a patch to fix this) |
…ypes. Previously, using `FormBuilder` with a union type would produce unions of *controls*: ``` // `foo` has type `FormControl<string>|FormControl<number>`. const c = fb.group({foo: 'bar' as string | number}); ``` This actually works in many cases, due to how sophisticated Typescript's distributed types are (e.g. `value` still has type `string|number`), but it is subtly incorrect. Here is a code example that exposes the reason the inference is incorrect: ``` let fc = c.controls.foo; // Error: Type 'FormControl<string | number>' is not assignable to type 'FormControl<string> | FormControl<number>'.ts fc = new FormControl<string|number>(''); ``` Instead, we want the union to apply to the *values*: ``` // `foo` should have type `FormControl<string|number>`. const c = fb.group({foo: 'bar' as string | number}); ``` Essentially, we want to prevent Typescript from distributing the type. [As specified in the handbook](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types): > Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets. This PR applies this suggestion to `FormBuilder`'s type inference. Fixes angular#45912.
…ypes. Previously, using `FormBuilder` with a union type would produce unions of *controls*: ``` // `foo` has type `FormControl<string>|FormControl<number>`. const c = fb.nonNullable.group({foo: 'bar' as string | number}); ``` This actually works in many cases, due to how extraordinarily powerful Typescript's distributive types are (e.g. `value` still has type `string|number`), but it is subtly incorrect. Here is a code example that exposes the reason the inference is incorrect. It exploits the fact that Typescript will not "un-distribute" a type, producing an obviously spurious error: ``` // fc gets an inferred distributive union type `FormControl<string> | FormControl<number>` let fc = c.controls.foo; // Error: Type 'FormControl<string | number>' is not assignable to type 'FormControl<string> | FormControl<number>'. fc = new FormControl<string|number>('', {initialValueIsDefault: true}); ``` Instead, we want the union to apply to the *values*: ``` // `foo` should have type `FormControl<string|number>`. const c = fb.nonNullable.group({foo: 'bar' as string | number}); ``` Essentially, we want to prevent Typescript from distributing the type. [As specified in the handbook](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types): > Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets. This PR applies this suggestion to `FormBuilder`'s type inference. Fixes angular#45912.
I believe #45942 should fix the problem. Thanks for catching this. Typescript's support for unifying distributive types is shockingly good. Even with the previous bug, this still compiled:
because Typescript will perform the distribution when assigning to
|
…ypes. (#45942) Previously, using `FormBuilder` with a union type would produce unions of *controls*: ``` // `foo` has type `FormControl<string>|FormControl<number>`. const c = fb.nonNullable.group({foo: 'bar' as string | number}); ``` This actually works in many cases, due to how extraordinarily powerful Typescript's distributive types are (e.g. `value` still has type `string|number`), but it is subtly incorrect. Here is a code example that exposes the reason the inference is incorrect. It exploits the fact that Typescript will not "un-distribute" a type, producing an obviously spurious error: ``` // fc gets an inferred distributive union type `FormControl<string> | FormControl<number>` let fc = c.controls.foo; // Error: Type 'FormControl<string | number>' is not assignable to type 'FormControl<string> | FormControl<number>'. fc = new FormControl<string|number>('', {initialValueIsDefault: true}); ``` Instead, we want the union to apply to the *values*: ``` // `foo` should have type `FormControl<string|number>`. const c = fb.nonNullable.group({foo: 'bar' as string | number}); ``` Essentially, we want to prevent Typescript from distributing the type. [As specified in the handbook](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types): > Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets. This PR applies this suggestion to `FormBuilder`'s type inference. Fixes #45912. PR Close #45942
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
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: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
Please provide the environment you discovered this bug in (run
ng version
)Anything else?
This is caused by the type mapping in
angular/packages/forms/src/form_builder.ts
Lines 45 to 62 in 3f3812e
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)
This can be solved by wrapping the type in the map with
[]
(playground):The text was updated successfully, but these errors were encountered: