Add Strong Typing and Proper Error Handling to a Form Validator
The first thing we need to do is capture generics in type arguments for required
, min-length
, and email
.
const createFormValidator = makeFormValidatorFactory({ required: (value) => { if (value === "") { return "Required" } }, minLength: (value) => { if (v
Transcript
0:00 Let's rock and roll. I'm quite excited about this one. We have some generics we need to capture in type arguments. We need to capture required, minLength, and email. The results of these functions are not generic. They're basically either going to be string or void, is what we're returning. We can either return Required, or we can return nothing, which is void in TypeScript.
0:24 We need to capture required, minLength, and email. Let's do that. I know we need to capture those because we need to make these ones type-safe afterwards. Really important to capture them. We've got then TValidator.
0:38 I have a choice. I can either capture the entire object, or I can capture just the keys. In this case, because the entire object is not generic, I just want to capture the keys, I think, but let me show you what it looks like to capture the whole object.
0:51 TValidators extends. Then I'm going to make it a Record string and then a function where it returns string or void. Then this validators is going to jump in there. This is giving me an error. Why is it giving me an error? Type value any Required or undefined is not assignable to type no parameters string void. I've missed out this value, any.
1:18 I need to basically say this value is going to be a string here. What we get then is in this slot, we get required is value string to...Oh, it's actually capturing the literal type there. I wasn't expecting that.
1:36 The issue I'm having with this is there's a lot of stuff that's inside the type argument that doesn't need to be there. Really, all we need in here is required, minLength, and email because all of this stuff, we actually don't care about the result of. What I'm going to change this to is TValidatorsKeys extends a string. string, there we go.
1:59 Then validators is going to be a record of TValidator keys. Then this stuff in here is just going to be the description of the validators. We've got a Record TValidatorKeys. value string goes to string or void.
2:16 Now, inside here, in the type argument, we're just getting required or minLength or email. Very good. Everything here is still strongly typed. Great. Now, validateUser createFormValidator config unknown. We need to strongly type this.
2:30 We're going to need to capture some stuff from here too. We're going to need to capture id, username, and email. Same as we had before. We can either capture this as an object or just the keys. I think I'm going to do the keys.
2:43 I'm going to say TObjKeys extends string. Then config is going to be a Record of TObjKeys. Then each member of that is going to be an array of TValidatorKeys. Whoa. Now what we have is we now have -- wow -- email, id, username. Then config is a record of email, id, and username. Each one is an array of required, minLength, or email.
3:16 That means I'm going to get autocomplete in this slot here. Super-duper nice. If I add a new one, if I say, "newValidator," for instance, which is just going to always return void, then this gets added here, newValidator. So pretty. Lovely, lovely, lovely.
3:34 Now though, these errors are not being properly inferred. This error down here is still breaking for us. This validator function, it's expecting you to only be able to call it with an id here. This validator is still values unknown.
3:50 Where's the unknown? Here it is, values unknown. This now needs to be a Record of TObjKeys string. The TObjKeys, that's going to be the things that we add into this createFormValidator function. Then that gets put into validateUser.
4:09 Now you have to pass in the thing that you specify when you create the form validator. This will work, but this will not work -- blah, blah, blah, blah, blah, blah -- until we add name required here. So nice. No type annotations, but we're still getting this beautiful, beautiful inference.
4:30 We're not getting the errors in the right shape though, yet. This errors any is still being typed there. That's because, inside this function, we're doing a little bit of a hack. We're saying this errors is going to be an empty object as any.
4:45 The reason we're doing that is because...I mean, I guess we could do this. Type Extract TObjKeys string. Let me give this a go. The type that we want to give it is going to be, basically, this type down here. For each key in the object, we want it to be either string or undefined.
5:05 We can say, "as Record TObjKeys string undefined." Ooh. That seemed to work. The reason we have to use as there is if we do this...Is this going to work? No, it's not going to work because type empty object is not assignable to that type.
5:23 We need to do an as because, in the stage down here, we actually build the object out. Even though the object is empty at this point, by the end here, it will be a fully fleshed-out thing, which is really good.
5:39 This, I think, means that the errors here are exactly what we want them to be. If we add a new one here -- we say, "wow required" -- then we have to pass that into here. Then we can even say...Oh yeah. typeof errors. Type does not satisfy the constraint because we've now got an extra addition into the errors. Fantabulous. That is so good.
6:02 I think then there's still a little bit of stuff to clean up here. Optionally, you could clean up this validators and extract this out into a key here. Probably this is going to live in like a utils folder in your app or going to be its own package.
6:18 You'd probably want to extract this out, basically so it's available for other people to extend or use in abstractions they want to create, but I'm pretty happy with how this turned out. This is just gorgeous.