Stricter types
See original GitHub issueContext
This is taking over https://github.com/cloudnc/ngx-sub-form/issues/118 as this issue much bigger than just for adding new values into FormArrays
.
Having a look into https://github.com/cloudnc/ngx-sub-form/pull/146 and the stricter settings, I do realize that here:
public listing$: Observable<NullableObject<OneListing>> = this.route.paramMap.pipe(
map(params => params.get('listingId')),
switchMap(listingId => {
if (listingId === 'new' || !listingId) {
return of(null);
}
return this.listingService.getOneListing(listingId);
}),
map(listing => (listing ? listing : this.emptyListing())),
);
We’re defining the listing$
as Observable<NullableObject<OneListing>>
.
and here:
<app-listing-form
[disabled]="readonlyFormControl.value"
[listing]="listing$ | async"
<----------------------------------------------
(listingUpdated)="upsertListing($event)"
></app-listing-form>
We’re passing that into the root form. But the root form takes as a parameter a OneListing
, not a NullableObject<OneListing>
:
@Component({
selector: 'app-listing-form',
templateUrl: './listing-form.component.html',
styleUrls: ['./listing-form.component.scss'],
})
export class ListingFormComponent extends NgxRootFormComponent<OneListing, OneListingForm>
(which now throw a TS error with strict mode ON 🙌)
Issue
In a world where we’d just make edits, we could skip the NullableObject
because we’d only receive objects of the exact type. But in reality we also want to have the possibility to create new objects (therefore they’d have all or some properties set as null when they’re passed as input).
A good example of that is the one above with listing$: Observable<NullableObject<OneListing>>
. We generate a new ID and pass all the other props as null
.
Other example, if the form is being reset (without the default values). They’ll all be set to null
.
Question
Should we always provide an API that would make the input required + nullable props + nil
and offer a new hook to run a strict check to make sure all the non nillables values are defined?
Example of a new API
We could have a new type:
export type DataInput<ControlInterface> =
| NullableObject<Required<ControlInterface>>
| null
| undefined;
And for a component instead of having:
@DataInput()
@Input('listing')
public dataInput: Required<OneListing> | null | undefined;
It’d be
@DataInput()
@Input('listing')
public dataInput: DataInput<OneListing>;
Besides the friendlier syntax, DataInput
uses NullableObject
which is what I want to focus on here. This means that we’d be able to pass null properties on the object (but they should still all be defined), and as in the Output
we still want a OneListing
we could have a hook that’d check for the null values and throw an error if needed. This hook would be useful to fill up the gap between what we want and the checks ran on the FormGroup (in case we forget to add a Validators.required
for example).
Demo:
export class ListingFormComponent extends NgxRootFormComponent<
OneListing,
OneListingForm
> {
@DataInput()
@Input('listing')
public dataInput: DataInput<OneListing>;
@Output('listingUpdated')
public dataOutput: EventEmitter<OneListing> = new EventEmitter();
// classic methods here...
protected checkFormResourceBeforeSending(
resource: NullableObject<OneListing>
): resource is OneListing {
// do the check
}
}
if checkFormResourceBeforeSending
would return false
then we should internally throw an error (as it should never happen).
Random thoughts
I wonder if:
- We always want that to be true or sometimes be able to skip the creation and enforce to pass only values of the type itself (without nullable props)
checkFormResourceBeforeSending
should be mandatory (could make things a bit more verbose…) or maybe just optional and would require to implement an interface. This may help at first and at some point we could potentially make it required? Also not sure how it’d work for people who are not using strict mode in TS
Issue Analytics
- State:
- Created 4 years ago
- Comments:14 (7 by maintainers)
Top GitHub Comments
That’s where the disagreement is then I think. My opinion is that a form control should declare the type that it is expecting in the input and the type that it will emit in the output. For simplicity they should be the same type. If there is some need for form reasons (therefore within the sub-components owned concerns) for that type to be
NullableProps
, then it should be doing a remap to handle that type mismatch. The common case ought to be that remap is not needed, and the full interface is what is passed by the parent.Within this set of principles, I’m sure we will be able to come up with a simpler (& safer) solution than what we have now, rather than a more complex one
Discussed IRL. Not sure what to do about it but here are some notes:
The issues we have currently
formControl
to contain a number, nothing prevents you from doing<input type="text" formControlName="yourProp">
and get a string instead as a value (ngx-sub-form is not helping, yet?)NullableProps<FormInterface> | null
so that we can pass some properties to initialise a form with some and set the rest to null values?Different implementations for different use cases
Idea 1: Only make sure that a form has its required values set
This would be the “light” one. It would only make sure at a compiler + runtime level that the values in the form are fulfilling the interface in a (TS) “strict” way. By TS strict way I’m referring to make sure that a type
T
for a value doesn’t end up beingNillable<T>
.The changes required for that would be easy to implement. Devs would have to define something in their forms like the following:
The
RequiredKeys
interface would be type safe and force devs to list the required properties. Behind the scenes, right before the value would be sent to the parent, it’d loop over the required properties and make sure that the corresponding form values are not null or undefined.Idea 2: Make sure that everything in the form matches the interface
This idea would be much more “intrusive” and require a more changes from the devs. But it’d also fill a much bigger gap: The gap between TS and HTML that is not type safe. In the example I talked about at the beginning of this post (input of type text which should be of type number) would be caught here.
Here’s how it could look like:
This would help us ensure that whether we’re updating a resource (easy case) or creating a resource (which can have null values), the output will always be of the expected type (in that case,
User
).@zakhenry did I forget anything?