-
Notifications
You must be signed in to change notification settings - Fork 33
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
Stricter types #147
Comments
@maxime1992 how is the hook different to regular validation? |
@zakhenry regular validation doesn't apply any kind of type checking. Therefore, if you forget to apply a required somewhere chances are it'll trigger bugs upstream (trying to access a property of a null value, etc). Whereas the hook there would bring some nice type safety. BUT writing this I realize that the hook definition above wouldn't really help to make sure all the properties are matching. It should rather be: protected checkFormResourceBeforeSending(
resource: NullableObject<OneListing>
): undefined | OneListing {
// do the check
} At the end of the function it'd return the |
Eh I really don't think we should be going to this level of enforcement of type safety - form interface validity should be controlled with validation, data output validation should be enforced by the type system. Given the developer has to write a mapping function anyway if it is a remap component, they have to get the type right. If it is not remapped, then the validation should manage it. |
I'm not convinced, I think we should. Maybe there's another way of applying that but as of today I don't think we're strict enough and it can easily lead to some bugs.
Well, it's a bit like saying let's code in JS and have a lot of tests instead of using Typescript + doing some tests 😛 (yes, I do exaggerate here 👼). Within our internal app we do have a lot of forms and most of them are built with ngx-sub-form. But if the API changes at some point and some properties previously not required would become required for example, while the build would still be fine, we'd end up with runtime errors if we're not super careful about the changes (which is not ideal, it should be a no brainer for that case at least). I'm trying here to find a solution to close the gap between validation and type safety.
Totally agree. But it's not the case today. You can totally end up returning an object with all his props set to null today and it'd be fine (for TS), while it shouldn't. Because you could reset a value, remove a child sub form which would set its own value in the parent to null, etc. We need runtime checks to guarantee this safety IMO.
The remap is not here for that I think and I'd like to be able to have that kind of type safety without implementing remap (it would mix multiple concepts and makes things harder IMO). |
What I’m getting at is we should come up with a solution that is not
requiring NullableProps. I think that should be considered an antipattern
and we may need to find better ways around that for dealing with oneOf
types. It may be the case that it is incorrect to have child components
create values when presented with null values?
On Wed, 26 Feb 2020 at 7:20 pm, Maxime ***@***.***> wrote:
Eh I really don't think we should be going to this level of enforcement of
type safety
I'm not convinced, I think we should. Maybe there's another way of
applying that but as of today I don't think we're strict enough and it can
easily lead to some bugs.
form interface validity should be controlled with validation
Well, it's a bit like saying let's code in JS and have a lot of tests
instead of using Typescript + doing some tests 😛 (yes, I do exaggerate
here 👼).
Within our internal app we do have a lot of forms and most of them are
built with ngx-sub-form. But if the API changes at some point and some
properties previously not required would become required for example, while
the build would still be fine, we'd end up with runtime errors if we're not
super careful about the changes (which is not ideal, it should be a no
brainer for that case at least).
*I'm trying here to find a solution to close the gap between validation
and type safety.*
data output validation should be enforced by the type system
Totally agree. But it's not the case today. You can totally end up
returning an object with all his props set to null today and it'd be fine
(for TS), while it shouldn't. Because you could reset a value, remove a
child sub form which would set its own value in the parent to null, etc. We
need runtime checks to guarantee this safety IMO.
Given the developer has to write a mapping function anyway if it is a
remap component, they have to get the type right
The remap is not here for that I think and I'd like to be able to have
that kind of type safety without implementing remap (it would mix multiple
concepts and makes things harder IMO).
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#147?email_source=notifications&email_token=AAFQE2KVUCA5B3VC5AK3NMLRE26GZA5CNFSM4K3PHJ3KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOENBQ6CA#issuecomment-591597320>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAFQE2OLDDRUDM73GSM45RTRE26GZANCNFSM4K3PHJ3A>
.
--
Zak Henry
|
Why? I do feel like allowing internally the object to be of type
Just to be clear, I'm not saying this issue is related to
I don't understand what you mean here? |
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 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 |
Not following up on last comment just sharing another use case for stricter types. The current definition of the protected transformToFormGroup(obj: ControlInterface | null, defaultValues: Partial<FormInterface> | null): FormInterface | null; Note that if (!obj) {
return {
prop1: null,
prop2: null,
// ....
prop49: null,
prop50: null,
}
} We can simply do instead: if (!obj) {
return null;
} Either way, all the values of the form can end up null. So my point is:
protected transformToFormGroup(obj: ThreadMillStepOperation | null): NullableProps<ThreadMillStepOperation> | null {
|
I still think we should drop the use of if (!object) {
return this.getDefaultValues();
} that way the interface can be very strongly trusted to be the correct value. If it is valid for some reason to have nullable props, then that should be declared on the form type |
As we may not always have default values to provide, protected getDefaultValues(): Partial<FormInterface> | null; So it's pretty much the same except that instead of having Let's take a simple example: A user. What would you use as default values for You want to set those values to null initially then guarantee that before sending the new object to the parent, the "outside" interface is fulfilled. I think we can think of forms as a contract 📝
I feel like enforcing that kind of behavior makes it clear for everyone (parents and also internally for the form). With strict types turned on, it'd also enforce better safety in TS or HTML when working with the interface Animal {
id: string;
name: string;
}
interface Person {
id: string;
name: string;
age: number;
favouriteAnimal: Animal;
} If you're working on a person form and accessing If you try to do: console.log(formGroupValues.favouriteAnimal.name) You could get an error if the |
but not at runtime. The typesystem should be used to ensure correctness of contract, not the runtime. With your example, if the requirement is that an animal must have the fields set, then the developer should do class AnimalForm extends NgxSubForm<Animal, NullableProps<Animal>> {} The default should probably just become I realise I'm describing a pretty big breaking change, but I really don't want to have dangerous (thrown error) bugs only appear at runtime. |
I disagree here as the form could be patched from anywhere really. And it should be perfectly fine patching the form with null values to reset it while you expect the value to match the interface before it gets send to the parent.
Nop all the keys can be nillable really. And the form will always be defined no matter what so I dont think
You'd just ensure that an object is matching a type. It'd be pretty much equivalent to a type guard. |
can we focus on this? I'm saying that it should be possible to completely eliminate this scenario. I think that is where all of the complexity is creeping in, and if it can be eliminated we will save so much complexity all over the place.
why? maybe it is acceptable for a parent form to pass |
Discussed IRL. Not sure what to do about it but here are some notes: The issues we have currently
Different implementations for different use casesIdea 1: Only make sure that a form has its required values setThis 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 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 Idea 2: Make sure that everything in the form matches the interfaceThis 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, @zakhenry did I forget anything? |
Context
This is taking over #118 as this issue much bigger than just for adding new values into
FormArrays
.Having a look into #146 and the stricter settings, I do realize that here:
We're defining the
listing$
asObservable<NullableObject<OneListing>>
.and here:
We're passing that into the root form. But the root form takes as a parameter a
OneListing
, not aNullableObject<OneListing>
:https://github.com/cloudnc/ngx-sub-form/blob/v5.0.1/src/app/main/listing/listing-form/listing-form.component.ts#L39
(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 asnull
.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:
And for a component instead of having:
It'd be
Besides the friendlier syntax,
DataInput
usesNullableObject
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 theOutput
we still want aOneListing
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 aValidators.required
for example).Demo:
if
checkFormResourceBeforeSending
would returnfalse
then we should internally throw an error (as it should never happen).Random thoughts
I wonder if:
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 TSThe text was updated successfully, but these errors were encountered: