DEV Community

Cover image for Tic Tac Toe with TypeScript - Part 1

Tic Tac Toe with TypeScript - Part 1

Borna Šepić
・5 min read

Why TypeScript?

If you're like me and Javascript is the only programing language you've ever learned you might be a bit repulsed to get into Typescript, at the end of the day your apps work just fine, why would you need to add another layer of complexity to it?

Well, the short answer is... It makes you a better developer.
It can also drastically reduce the number of run time bugs you encounter and make the developer experience far better and more efficient (once you get into it).

As always there is a bit of a learning curve to it, and it can really be frustrating sometimes to have your trusted IDE scream at you on every save. But it's a worthwhile tradeoff in the long run.

So without further ado, let's convert a small app from regular Javascript into its typed superset that is Typescript 🚀

The setup

For our application, we'll use the Tic Tac Toe we've written in the last article.

If you don't have it already you can grab it from Github here.

First things first, we'll need to install Typescript.
You'll first want to position your terminal at the root of the project and run npm init -y. This will create our package.json file (without asking too many questions 😇) and allow us to install typescript via NPM.

Next up we'll run npm i typescript to actually install Typescript and all it needs.

I'd recommend moving our project files (index.html, styles.css, script.js) into a new folder, just to keep things nice and clean, I've named the folder src but that's totally up to you.

This is how the project should look like at this point:
Alt Text

You'll also want to run tsc --init. This will generate our tsconfig.json file to allow us to have more control over the TS compiler.

Before continuing you'll want to change the // "lib": [], line in the config file (line 7) and replace it with "lib": ["es6", "dom", "es2017"],. This will allow us to use some more advanced features of JavaScript in our code.

To actually get started all we need to do is change our script.js into script.ts. And run tsc script.ts (this will compile our TypeScript file into good old regular JavaScript).
You've probably gotten an error compiling your script.ts file, but that's expected.

Please note we are still only including the script.js file in our index.html. Since "TypeScript is a typed superset of JavaScript", your browser will never actually run TypeScript. So it a nutshell your users won't notice in any way whether your app is written in TypeScript or not (except for the lack of bugs, and a 😃 on your face).

Actual TypeScript

Now let's get to the fun part and write ourselves some TypeScript! We'll go through the script line by line and convert what we can to TypeScript.
To keep things nice and "short", for this article we'll just go through the initial variables and finish the app in another one.

In the previous tutorial, we've created some variables that are storing our game state. Let's first take a look at them.

const statusDisplay = document.querySelector('.game--status');

let gameActive = true;
let currentPlayer = "X";
let gameState = ["", "", "", "", "", "", "", "", ""];

const winningMessage = () => `Player ${currentPlayer} has won!`;
const drawMessage = () => `Game ended in a draw!`;
const currentPlayerTurn = () => `It's ${currentPlayer}'s turn`;

We first have a document.querySelector method that returns an element with the class of 'game--status'. By doing a quick search on MDN we can see that the .querySelector returns an Element.
So we'll add a type to our statusDisplay variable to let TS know it should contain and Elemenet, like this:

const statusDisplay: Element = document.querySelector('.game--status');

You should be getting an error warning here saying type 'Element | null' is not assignable to type 'Element'.

When you think about it this error makes sense, we have no guarantee that the element with a class of "game--status" exists in our DOM. If this was a bigger app we might want to handle this case just to future proof our code but since it's a small application and we know that that element will always be there and we can tell TS that it will never return null by adding an exclamation point to the end, like this:

const statusDisplay: Element = document.querySelector('.game--status')!;

Next up we have our gameActive variable. Since we know this will only contain a boolean value (either true or false) we can assign the type of boolean to our variable.

let gameActive: boolean = true;

After that we have the currentPlayer variable. This technically does only contain a string, and there would be nothing wrong with just writing something like:

let currentPlayer: string = "X";

But because we have only two distinct cases here (the variable can only be "X" or "O", we can use a more appropriate functionality of TypeScript here called Enum. So the end product should look something like this:

enum PlayerSigns {
    X = "X",
    O = "O"
let currentPlayer: PlayerSigns = PlayerSigns.X;

We have created an Enum that will hold our player signs, and assigned the value of our currentPlayer variable to that enum.

After that we have our gameState variable, where... we hold our game state (😎).

let gameState = ["", "", "", "", "", "", "", "", ""];

We can see that this will always be an array of strings, so we can pass that on to our comipler like this:

let gameState: string[] = ["", "", "", "", "", "", "", "", ""];

And lastly, we have our three functions that return our game status messages:

const winningMessage = () => `Player ${currentPlayer} has won!`;
const drawMessage = () => `Game ended in a draw!`;
const currentPlayerTurn = () => `It's ${currentPlayer}'s turn`;

Since they are all simple functions, without any inputs, that return strings we can use the same types for all of them.

const winningMessage: () => string = () => `Player ${currentPlayer} has won!`;
const drawMessage: () => string = () => `Game ended in a draw!`;
const currentPlayerTurn: () => string = () => `It's ${currentPlayer}'s turn`;

It can seem a bit annoying at times to have to write all the types yourself, but it's another one of those things that become second nature after a brief adjustment period.

Hopefully, by the end of this series, you'll be convinced on the benefits of using TypeScript for your project.

As always, thanks for reading, and until the next one ✌️

Discussion (3)

curtisfenner profile image
Curtis Fenner

I don't think you need to have a type declaration for any of those variables; TypeScript's inference is very good, and you generally only need to write type declarations for function parameters.

I haven't written a lot of TypeScript, but I don't think it's idiomatic to include types like string[] when you're clearly assigning a literal oh that type. (Also, that's not nearly specific enough of a type for gameState -- I'm guessing only a few values are valid; maybe it should be (PlayerSign | null)[]; possibly even the tuple so that the length is fixed.

bornasepic profile image
Borna Šepić Author

Hey Curtis, thanks for writing back.
As with a lot of things, it really comes down to personal preference.
I've used TypeScript for almost two years now, both at work and on pet projects and I've found it much preferable to just write types for everything rather than have the cognitive load of thinking "this probably doesn't need a type".

It also has an added benefit of improving your IDE experience by offering you better auto completes.

Also, you raise a valid point about the gameState type. To be honest, I wanted to avoid confusing people by rewriting some of the existing logic and just stick to writing types, but this is an excellent place to put the tuples to use. I'll make sure to update it in part two!

karataev profile image
Eugene Karataev

It also has an added benefit of improving your IDE experience by offering you better auto completes.

Well, for TS lines below are equal. There is no difference in IDE experience (autocomplete, e.t.c).

let gameActive: boolean = true;
let gameActive = true;

I've found it much preferable to just write types for everything rather than have the cognitive load of thinking "this probably doesn't need a type".

Just turn on noImplicitAny option in your tsconfig.json and TS will yell at you where a type can't be inferred. No extra cognitive thinking is required 😂