I have created a new custom command for myself editBoard
.
This one, as the name states, is going to edit a board.
I haven't passed anything into this command, and it's already saying that I should be passing at least 1 argument, but I got 0.
Let's go ahead and take a look into our custom command.
Right now, it looks very similar to our addBoard
command that we talked about in the previous chapter, but I want to make it type safe, and actually use types from my Board
interface that I already have in my application.
Since I want the body
to be of type Board
, I can just go ahead and pass it.
Then when I take a look into my spec
file, I can start typing an object, and I have all of these auto suggestions of what I want to type, but my API actually does not allow me to use everything.
For example, I cannot change the user
or I cannot change the created
date when that board was created.
This means that I cannot use my interface as it is currently defined.
What can I do? There are multiple options that I can choose.
The bare minimum I can do is to use something that's called utility types.
One of those utility types is something called Record
.
This enables us to define what kind of keys we want to type, and what kind of parameters those keys should have.
If I type something like <string, any>
, this will mean that inside editBoard
, I can just pass any object.
Anything that will have a parameter and a value will be good enough.
I can even pass an empty object, and my editBoard
function would still not complain.
Now, this is less than ideal.
We need to be more specific, but there are some cases where we don't really care what kind of object we pass, as long as it's an object.
Let's take a look into another utility type, and that's called Pick
.
This enables us to pick any attributes that we want from an existing interface.
I'm going to use <Board, 'id'>
.
I paste it into my custom command, and when I now save my file and go to my spec
file, you can see that my TypeScript compiler is complaining.
I simply cannot pass an empty object.
What I need to do is pass an id
.
If I do that, the errors disappear.
Now, if I take a look into my custom command and what it actually does, I can see that it will call a PATCH
request to /api/boards/
with a certain id, and then we are going to be passing the body
that will contain all of the changes that we want to make.
For example, if we take a look into this interface, we might want to change the name
of our board, or maybe even the starred
attribute.
If we were to pass any of this into our command - for example, name: 'my board'
, the TypeScript compiler is going to complain about that.
The reason for that is that we simply picked only the id
.
If we want to add multiple keys that we want to include in this object, we can add a pipe character |
and then type the other attribute that we want to add.
As you can see, the errors have already disappeared.
But also, we might want to change the starred
attributes, so let's add that one as well.
When I add this, the test is going to complain, but I'm going to add it and pass starred: false
.
All of these keys are type checked, so in my interface, the starred
attribute is a boolean
, which means that if I change this to a string
, I'm immediately getting an error.
I'll change it back and go back to my editBoard
command.
This Pick
utility type is quite useful, but I can choose an opposite approach, and just pick all of the keys from my board interface, but leave out some of them.
To do this, I can use the utility type called Omit, and this is going to do the exact opposite.
Instead of requiring id
, name
, and starred
, it's going to require all of the others - so that would be user
and created
.
If we want to keep our function working, we need to add those into our Omit
utility type - so that's going to be created | user
.
Now everything is looking good.
The only problem might be that whenever we call editBoard
, we are required to call both name
and the starred
attribute, which is something we might not always want to do.
What if we just want to change the name or just want to change the starred attribute?
In that case, we can go ahead and define our attributes explicitly.
What I'm going to do is I'm going to define an object, and I'm going to say that there will be a key of id
that's going to be a type of Board['id]
.
Then, I'm going to say that there's also an attribute of name
with the type of Board['name']
and an attribute starred
, which will be a type of Board['starred']
.
The TypeScript compiler is now satisfied, but it still didn't solve the problem.
If I delete my starred
attribute, the compiler is going to complain about the fact that I haven't included the starred
attribute.
What I can do now is say that the name
attribute and the starred
attribute are optional.
To do that, I'm going to type this question mark ?
to both of them.
This means that I am required to use the id
, but I don't really have to add the name
or the starred
attribute.
If I were to decide that all of my attributes were optional, I could add the question mark to all of them, but also I could use another utility type that's called Partial
.
With this one, I can say that the argument of this editBoard
function can be a Partial<Board>
interface.
This means that if I start typing, I get these suggestions for different attributes, but none of them are required.
If I take a look into what editBoard
actually does, it seems that it is always going to require body.id
.
We basically always need to pass it if we want our API call to work.
What I can do is that I can actually combine these utility types.
I'm going to keep the Partial<Board>
, but then I'm going to add to this type another type that's called Required<>
.
In this one, I need to define which types are required, but I'm actually going to pass a Pick
utility type, and from my Board
, I'm going to pick the 'id'
.
Now in editBoard
, I am required to use the id
, which will be a number
.
But I also have the optional keys, so if I want to pass the name
, I can pass it, or I can pass the starred
say it's, for example, false
.
One more thing that is kind of annoying about this is that I have two different types in my editBoard
type definition and in the function itself.
Usually, I need to type everything twice to include that both in my custom command, and also in the type definition.
There is a different way of how I can do this.
Basically, I'm going to copy the whole function and define it as a function of its own.
I'm going to call it editBoard
, and paste the function in here.
Now, the thing that I'm going to do here is that I'll use this function as an argument into my Cypress.Commands.add
function.
Now that I have defined my custom command as a standard JavaScript function, what I can do here is say that this editBoard()
function will actually be a typeof
our editBoard
.
What this means is that TypeScript is going to infer all of the types that are required for this function from this standard JavaScript function, and then they're going to be passed on to the Cypress API, and then the whole function is going to be passed on into the Cypress API.
Everything still works as before.
If I delete my id
, it's going to complain.
I get the auto-completion, but in my custom command, I just need to write everything once.