S
SolidJS•15mo ago
zishh

Best Practices for Unrepresentable Intermediate State

Hello, I am looking for some opinions regarding best practice in the following situation. Lets say I have a model which contains properties and some of those properties can only exist in tandem e.g. blood pressure which are usually two values. To encode this in the model the two properties are grouped into an optional property.
interface SomeModel {
a: string;
b?: {
b1: string;
b2: string;
};
}
interface SomeModel {
a: string;
b?: {
b1: string;
b2: string;
};
}
If I want to create a custom component which can be used to create such a model instance, I want to have three input elements. The component props would look like this:
interface SomeComponentProps {
value: SomeModel;
onChange: (value: SomeModel | undefined) => void;
}
interface SomeComponentProps {
value: SomeModel;
onChange: (value: SomeModel | undefined) => void;
}
As soon as I input b1 or b2 I run in a case in which the model is invalid because only one of the two b-values exist. That's fine as an intermediate state but how do I represent this? I could add an extra callback onValidationFailure and call that. But then I have this weird state in which the state in the application does not reflect the state of the UI. So I would assume that I should also call onChange(undefined) because we do not have a valid instance at this point in time. The issue is that this would break the two-way data binding because the value is updated with undefined and all input elements are cleared. The other option would be to use two different models. So I have one for persistency and one which can represent all intermediate states. This results in a lot of boilerplate. I hope my issue became kind of clear. How is this invalid intermediate state, which can not be represented by the "normal" model, handled in the components?
14 Replies
Max
Max•15mo ago
hi, if I undertand the issue, it's not really different to any form validation. There's tracking the values, and validating final state. If you had a simple for m
type ExpectedFormValues={
readonly name:string;
readonly email:string;
}
type ExpectedFormValues={
readonly name:string;
readonly email:string;
}
and you can track current values like
type CurrentValues=Partial<ExpectedFormValues>
type CurrentValues=Partial<ExpectedFormValues>
its the same issue in a sense, unless you just start with empty string values and use just one model but then any further validation should fail I think to accurately capture what's really going on you should go with your approach of using 2 different models, but it doesnt really have to be a lot of boilerplate. One type just represents the objects that's keeping track of value, and the other the expected value. And can be abstracted quite easily Alternatively just use one model but make it lose enough to implement value level validation and that's where you can also keep track of errors, requirements, etc..
zishh
zishhOP•15mo ago
Hey thanks for your response. Do I understand your first approach correctly that you suggest to use Partial<T> for the current state of the form? If I think about it this is kind of the second model without all the boilerplate? It probably is the same as any other form validation - But I could not find a good article / blog post / whatever which describes validation from a design perspective.
Max
Max•15mo ago
yea pretty much, if you just want to declare it once, you can define the expected type for a completed form, and it follows that pretty much (~ component level initial values can be there or not) all fields are undefined at some point so partial or some deep partial type is a decent solution to broadly capture everything. basically, the issue you outline with b in your interface and b1 and b2 being required for b to be valid is a similar argument that you could make for b and a to be both required for SomeModel to be valid, unless its actually possible for b to be undefined and SomeModel still valid. Valid in the sense that you can do something actually useful with it, like submit for use elsewhere In terms of design there's so much you can do it really depends on what you want, but it should not matter too much, you can probably tell if it starts to suck cause using it sucks. If you see, even lots of form libraries have some issues and it shows. But you can build values against a partial type as we discussed, you can decouple it by having an array of tuples that correspond to the property and expected value type Values = {[K in keyof Model]:readonly [K,Model[K]]}[keyof Model][] and at that point which values are in what state is still suitable for the type, and it expected logic level validation which may required runtime objects as well as types, but depends on what you wanna do with it.
zishh
zishhOP•15mo ago
Okay - I will try this strategy and see how it goes. Regarding the second paragraph: Yes I have the case that b is actually optional in the model. So it is fine to completely leave it undefined. Its just that if I want to define it I have to fully define it. I guess the issue is that I am usually not a web developer. So it is a bit hard to get a feeling - But yes I asked the question because the solutions I came up with looked ugly 🙂 I am not sure what this Values type would be. I guess I have to read up on typescript generics to decipher it. Thanks for your help!
Max
Max•15mo ago
yea of course nice, have a look what you come up with if it helps after can also make an example just to put it together
zishh
zishhOP•15mo ago
Do you mean that you or me create an example? I understood the Values type now. It's just an array of property-names + values tuples. I am not sure in what circumstances this is better than DeepPartial<Model> but its probably some kind of confusion on my side because I have not tried to implement this yet.
Max
Max•15mo ago
yea if you think can help I can just put it as code Its not better its just a different way of doing it, in a way its simpler as it removes the expectation of all fields being present and delegates that to logic level validation, but probably equally as good with partial / deeppartial in most cases, if using some sort of js validation it would't matter
zishh
zishhOP•15mo ago
Hey @maxoucsgo - I finally got some time to try it out. Unfortunately I did not get very far. If you still have time it would probably help me. How would you change this example and make it less ugly? It's basically the example from the beginning - Either both numbers are given or none.
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
Max
Max•15mo ago
yea for sure think you have to create a share link tho cause that seems its just the playground
zishh
zishhOP•15mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
zishh
zishhOP•15mo ago
@maxoucsgo Hm i could also go the zod route and create a small library which automatically generates a relaxed model - I was not able to model the behavior I want with zod. Still think I am missing something completely.
GitHub
TypeScript-first schema validation with static type inference
TypeScript-first schema validation with static type inference
Max
Max•15mo ago
yea nice, I'll make an example but basically that's the idea, whether or not you decide to go for zod, or do something simpler that works for that case the idea is the same of relying on runtime for validation and having the typescript represent the schema essentialy basically something like
type Model = {
x?: {
foo: number;
bar: number;
};
}
type Model = {
x?: {
foo: number;
bar: number;
};
}
can become something like
type Model = {
type: "object";
properties: {
x: {
type: "object";
required: false;
properties: {
foo: { type: "number"; required: true };
bar: { type: "number"; required: true };
};
};
};
};
type Model = {
type: "object";
properties: {
x: {
type: "object";
required: false;
properties: {
foo: { type: "number"; required: true };
bar: { type: "number"; required: true };
};
};
};
};
or
type Model = {
type: "object";
properties: {
x: {
type: "object";
properties: {
foo: { type: "number"};
bar: { type: "number"};
};
required:['foo','bar']//or all, or true
};
required:[]
};
};
type Model = {
type: "object";
properties: {
x: {
type: "object";
properties: {
foo: { type: "number"};
bar: { type: "number"};
};
required:['foo','bar']//or all, or true
};
required:[]
};
};
the type is for the model and not specifially the values had a try mocking up something to demonstrate the concept, it's a bit of overhead, especially some of the typing and there not great, but could be interesting start but then using it is fairly nice
Max
Max•15mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
Max
Max•15mo ago
the approach is basically around schemas and stuff so probably you could find it better to just use something that already exists but this might be intersting to see the idea behind using schemas for forms
Want results from more Discord servers?
Add your server