The tic tac toe board
A tic tac toe board consists of 9 separate cells, which are identified based on their position. The cell names derive from their column and row position. We use
C for the columns, and
3 for the rows. This is the structure we are going to use to play our game:
Each cell on the board can be filled with three different values: an
"O" or a blank cell (
" "). In TypeScript, we can create a union of string literals to define a type that supports these values. A string literal is a string limited to a specific string. A union is the choice between different types.
First, we need to create an empty playing board by using our first advanced concept. We are going to map our basic board to an empty board with the help of a mapped type.
We’ll iterate through all keys of
board, which is a union with all the cells, and we’ll set the value of all cells to (
" "). By doing so, we can start the game with an empty board.
Finding empty cells
To ensure a fair game, we should only allow sets on an empty cell. Below you see the definition how to get all the empty cells as a union of string literals.
In the above example, we have created a type alias with one generic parameter. The parameter
board has a constraint: it should at least have the properties of a board defined before, to know the type you are working with. To explain the rest of the type we’ll reduce the code in steps. Imagine that our current board looks like the following:
The first step is to transform it into a new object with the help of the following rule:
This results in the following:
This iterates through all the fields in the object. With the help of a conditional type, we’ll check if every field has the value
(" "). So when we give the field name as type literal, every non-empty field will be set to the type
never is a special type in TypeScript for non-existing types. It is used a lot for unreachable code. In the next step, the index operator is used to get all the values of the object.
This works because the object type has exactly the same field names as the newly created object. It literally says “give me the value types of the following fields”. Because the field names are known, we can replace it in the following way:
This results in a union of all the value types in this case:
In a union type, all value types will be reduced to a unique set, so the following is the same:
never is a special case that stands for a non-existing value type, it will be taken away when there are other types in the same union. Thus, the find type that the compiler will produce is the following, which is the type of the empty cells:
Making a move
Now that we know which cells are unoccupied, we can make a move. To do this, we’ll create a type alias that accepts three parameters: the current board, the value you want to use (
"X" | "O") and in which cell you want to do this.
To make sure that the chosen cell is available, we’ll use the type
AvailableCells that we have created before as a constraint on the chosen cell. We’ll remap the board so everything is the same, except for the chosen cell. This cell has been changed to
"O" depending on what was chosen. It returns the new created type, but be aware that this is a new type: it is only possible to create new types, instead of mutating already existing types.
Checking if someone has won
The last thing we have to do before we can start playing is to be able to check if a player has already won. There are only 8 winning combinations, so we can check the ones that could have won by using the index operator, for example:
For a whole column we can do:
Here we use an intersection type. An intersection type is made by using the
& operator. It says that the new type must have the properties of both sides of the operator. Just like the union operator, we can remove all elements that are not unique. For our above example it means we can reduce it to the following:
If we had a winning combination, it would look like the following:
So, if we want to know if
X has won, we just have to check if the type is
"X". For all combinations that looks like this:
If the type of one of these combinations matches on the char of
c, we say that this char has won by returning
true, otherwise we return
false. Make sure you use this order: when you change the order of the extends around, it is not working correctly because
"X" & "O" extends "X" -> true and
"X" extends "X" & "O" -> false which is the behavior that we want.
The final step is to make a type that checks if any of the players has won. This looks like the following:
X has won, we return
"X has won". If
O has won, we return
"O has won". Otherwise, we return “nobody has won”. Now we are ready to play!
As a graduate intern, I researched using advanced types in TypeScript to validate a configuration for a tool used here at Hoppinger. I have learned a lot about the possibilities of advanced types in TypeScript, but even more about the shortcomings of the type system and the implementation in the TypeScript compiler. I am proud to say that since this month, I am officially a fulltime Software Engineer at Hoppinger. In this article, I introduced you to some of the advanced types in TypeScript and a way to use them. If you want to learn more on these advanced types, the best advice to give is: practice! Further improvements of the code in this article could include:
- Tracking the state of the game and which player’s turn it is.
Have fun playing tic tac toe!