question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Context

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>:

https://github.com/cloudnc/ngx-sub-form/blob/v5.0.1/src/app/main/listing/listing-form/listing-form.component.ts#L39

@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:open
  • Created 4 years ago
  • Comments:14 (7 by maintainers)

github_iconTop GitHub Comments

1reaction
zakhenrycommented, Feb 27, 2020

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

0reactions
maxime1992commented, Mar 10, 2020

Discussed IRL. Not sure what to do about it but here are some notes:

The issues we have currently

  • Angular forms are not type safe (ngx-sub-form helps a bit)
  • Angular forms are bind to the view and if you declare a 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?)
  • How to have strict type safe interfaces (and therefore checks?) for forms without bloating our code within a simple sub form? Should the data input be 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 being Nillable<T>.

The changes required for that would be easy to implement. Devs would have to define something in their forms like the following:

export interface User {
  id: string;
  name: string;
  age: number;
  carModel?:string;
}

export class MyForm extends NgxSubForm<User> {
  // ...
  
  protected requiredKeys: RequiredKeys<User> = ['id', 'name', 'age'];

  // ...
}

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:

export type ValueType<FormInterface> = Record<keyof FormInterface, Unknown>;

export class MyForm extends NgxSubForm<User> {
  // ...
  
  protected validateOutput(value: ValueType<User>) value is User {
    // here check everything. 
    // that required values are defined, that strings are string, numbers are numbers, etc
  }
}

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?

Read more comments on GitHub >

github_iconTop Results From Across the Web

strict - TSConfig Option - TypeScript
All of the common types in TypeScript ... How to provide a type shape to JavaScript objects ... How TypeScript infers types based...
Read more >
What do strict types do in PHP? - Stack Overflow
In strict mode, only a variable of exact type of the type declaration will be accepted, or a TypeError will be thrown. The...
Read more >
Stricter types for `JSON` · Issue #51003 · microsoft/TypeScript
My point was, your types are excluding .myself on the basis that it could be a self-reference. That's way too strict. As for...
Read more >
Strict Types: Typescript, Flow, Javascript — to be or not to be?
Strictly typed languages like TypeScript makes a perfect choice if you or your teammates are already familiar with strongly typed languages ...
Read more >
Strict mode - Angular
Angular CLI creates all new workspaces and projects with strict mode enabled. ... anyComponentStyle budget types by 75% compared to the previous defaults....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found