How to Create Your First Web Application with Typescript

You’ve probably heard all the buzz and hype around Typescript by now, but you were too stubborn to learn it because you hated and didn’t want to support Microsoft (I get it).

JavaScript is amazing but there are certain pain points that come with it such as refactoring large codebases or trying to avoid putting console logs everywhere only to realise that the thing you thought was defined was actually coming back as null and other similar issues.

One of the tools that is trying to help with these pain points and has been growing in popularity is Typescript.

The first thing to keep in mind is that TypeScript is a typed superset of JavaScript that compiles down to plain old JavaScript, which also supports modern ES2015+ features. Transforming TypeScript works out of the box without any additional configuration.

Alright, reading about it and trying it out are 2 different things, so let’s roll up our sleeves and get our hands dirty.

In this tutorial, we are going to build a shopping list application in pure vanilla typescript with no dependencies - that’s right, you heard correctly - no react, no vue, nothing!

We are going to write the application as an ES6 Class to keep everything encapsulated and our aim is to keep it nimble and fast.

If you want to skip ahead of this blog post and see the final application, you can either:

I'd still encourage you to read on as you can get a break down of the thought process involved and maybe learn a trick or two :)

Getting Started

Before getting started, make sure you have installed Node and NPM accordingly. Alternatively, you can follow my guide on installing Node Version Manager (NVM).

  1. Setup the folder structure
  2. Configure the build tool (Parcel JS)
  3. Setup the HTML
  4. Define the Class
  5. Setup the SCSS
  6. Setup the Interface
  7. Update the Render Method
  8. Bind Submit Event Listener
  9. Update AddItem Method
  10. Bind Input Event Listener
  11. Update the UpdateItem Method
  12. Bind Click Event Listener
  13. Update RemoveItem Method
  14. Update loadLocalStorage Method
  15. Update saveLocalStorage Method
  16. Update Init Method
  17. The final stretch

Setup the folder structure

The first thing you want to do is setup a basic skeleton folder structure for your website application.

Use the bash commands below to flesh out your skeleton application.

mkdir my-shopping-list && \
cd my-shopping-list && \
npm init -y && \
touch tsconfig.json && \
mkdir src && cd src && \
mkdir ts && cd ts && \
touch index.ts && cd .. && \
mkdir scss && cd scss && \
touch index.scss && cd .. && \
touch index.html && cd ..

Once you have executed the above commands, you should have a basic structure like so:

|- /
|- tsconfig.json
|-- src/
|---- scss/
|------ index.scss
|---- ts/
|------ index.ts
|---- index.html

Setup the build tool (Parcel JS)

For our build tool, we will be using Parcel JS as there is zero configration required. Parcel is a build job that will be responsible for the following tasks:

  1. Compiling SCSS/SASS and outputting the files into a dist directory
  2. Compiling Typescript code and outputting the files into a dist directory
  3. Starting up a web server so you can preview your web application
  4. Watching your files for changes and automatically reloading the web server so you get instant feedback of your updates

Today is your lucky day as there is already a setup we can use so we can rock and roll. All you need to do is pop open the package.json file and put in the following contents:

package.json

{
  "name": "my-shopping-list",
  "version": "1.0.0",
  "description": "A quick and nimble shopping list application for everyday people.",
  "scripts": {
    "build": "NODE_ENV=production rm -rf dist && tsc --noEmit && parcel build src/index.html",
    "clean": "rm -rf dist",
    "lint": "tslint --project \".\"",
    "start": "NODE_ENV=development parcel src/index.html --open"
  },
  "private": true,
  "devDependencies": {
    "parcel-bundler": "^1.12.4",
    "sass": "^1.26.2",
    "tslint": "^6.1.1",
    "typescript": "^3.8.3"
  }
}

You will also need to edit the tsconfig.json file and add in the following contents:

tsconfig.json

Setup the HTML

Now that you have your build configured, it’s time to write some code.

Let’s start off by editing the index.html file and adding in the following skeleton structure:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Shopping List</title>
  <meta name="description" content="A quick and nimble shopping list application for everyday people.">
  <link rel="stylesheet" href="scss/index.scss">
</head>
<body>
  <div id="app"></div>
  <script src="ts/index.ts"></script>
</body>
</html>

You’ll notice our HTML structure is pretty barebones but we have one key component here. The <div id="app"> element. This will be our mount point for our application and the Javascript will produce most of the DOM and functionality.

Define the Class

The first thing I like to do when working on a new web application is to flesh out the flow and the type of methods I would potentially require. This gives me a basic flow in my mind as to how the application will work and play out.

So let’s define a basic set of rules of how our application will function:

  1. When I open up the application, I want to be presented with a textbox where I can input a shopping list item
  2. After hitting the enter key, pressing the add button or submitting the form (form submit will handle all 3 scenarios), I want to see the new item added to a list
  3. I can add multiple items to the list
  4. I want the ability to update items as you type (autosave)
  5. I want the ability to remove items by clicking a delete button
  6. Lastly, if I reload the page, I want all my items to remain saved (autosave/autoload functionality)

Now that we have established our common rules, let’s start fleshing out our application. Pop open src/index.ts and start building out a basic Class like below.

index.ts

export class MyShoppingList {

  private host: HTMLElement;

  constructor(host: HTMLElement) {
    this.host = host;
  }

  public init(): void {
    this.render();
    this.bindEvents();
  }

  public render(): void {
    this.host.insertAdjacentHTML('afterbegin', `
      <div class="container">
        <h1 class="logo">My Shopping List</h1>
        <div class="items"></div>
      </div>
    `);
  }

  public bindEvents(): void {

  }

  public addItem(): void {

  }

  public updateItem(): void {

  }

  public removeItem(): void {

  }

  public updateLocalStorageItems(): void {

  }

  public loadLocalStorageItems(): void {

  }
}

// Initialise the class to run
const app = document.querySelector('#app') as HTMLElement;
if (app !== null) {
  const application = new MyShoppingList(app as HTMLElement);
  application.init();
}

As you can see above, we have constructed a Class with a set of empty methods (excluding the render method). The render method is very basic and will append a title to <div id="app">.

To test this code out, open your terminal and run npm start.

If you see a blank page with a big heading My Shopping List, then this is great! Your application is working and we can proceed to build it out further.

Setup the SCSS

Let’s quickly get the styling out of the way. Open up src/index.scss and add in the following styles:

*, :after, :before {
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
}
body {
  font-family: sans-serif;
  margin: 0;
  background-color: #111317;
  color: #f1f1f1;
}

.container {
  max-width: 600px;
  padding-left: 1rem;
  padding-right: 1rem;
  margin: 1rem auto;
}
.row {
  margin: 0.5rem 0;
  position: relative;
}
.logo {
  font-size: 1.25rem;
  line-height: 1.5rem;
  text-align: center;
  display: block;
  padding: 1rem;
}
.delete-button:before {
  content: "+";
  display: block;
  transform: rotate(45deg);
}

input,
button {
  font-family: sans-serif;
  font-size: 16px;
  line-height: 22px;
  border: 1px solid #aaa;
  color: #f1f1f1;
  background-color: transparent;
  transition: .3s all;
}
input[type="text"] {
  width: 100%;
  display: block;
  padding: 0.5rem 1rem;
}

::placeholder {
  color: #aaa;
}

.add-button,
.delete-button {
  font-size: 1.25rem;
  position: absolute;
  top: 0;
  right: 0;
  width: 50px;
  height: 100%;
  border: 0;
}
.add-button {
  font-size: 1rem;
}
.delete-button {
  color: red;
}

Now that you knocked out the styling fairly quickly, let’s work out the crux of this application.

Setup the Interface

There are 2 main pieces:

  1. A global array which holds our shopping list items
  2. A localStorage object which saves a copy of this list

With this in mind, let’s start by setting up the interface for our shopping list along with the Class properties.

Create a new file within src/ts/ called interfaces.ts

In this file, put in the following code:

export interface Item {
  id: string;
  value: string;
}

Now in index.ts - import the Items interface:

import { Item } from './interfaces';

Add a new private property below host

private items: Array<Item>;

Update the constructor to look like the following:

constructor(host: HTMLElement) {
  this.host = host;
  this.items = [];
}

Update the Render Method

Now that our interfaces and properties are setup, let’s update the render method to add a textbox for adding items along with showing all current items.

public render(): void {
  this.host.insertAdjacentHTML('afterbegin', `
    <div class="container">
      <h1 class="logo">My Shopping List</h1>
      <div class="items"></div>
    </div>
  `);
  const itemsEl = this.host.querySelector('.items') as HTMLElement;
  if (itemsEl !== null) {
    this.items.map((item: Item) => {
      itemsEl.insertAdjacentHTML('beforeend', `
        <form autocomplete="off" id="item-${item.id}" class="row edit-item">
          <input class="edit-input" data-id="${item.id}" type="text" name="item" value="${item.value}" />
          <button
            type="button"
            class="delete-button delete-item"
            data-id="${item.id}"
          ></button>
        </form>
      `);
  });
  itemsEl.insertAdjacentHTML('beforeend', `
    <form autocomplete="off" class="row add-item">
      <input class="add" type="text" name="item" placeholder="Add item..." />
      <input class="add-button add" type="submit" value="+" />
    </form>
  `);
  }
}

Ok great, now your application should have a basic textbox with an add button.

Bind Submit Event Listener

With our basic view in place, we can now wire up a submit event listener for the <form id="add-item">. Let’s start by populating the bindEvents method with the following code:

public bindEvents(): void {
  this.host.addEventListener('submit', (event: Event) => {
    event.preventDefault();
    const targetEl = event.target as HTMLElement;
    if (targetEl.closest('.add-item') !== null) {
      const inputEl = targetEl.querySelector('input[type="text"]') as HTMLInputElement;
      this.addItem(inputEl);
      inputEl.value = '';
    }
  });
}

What the above code will do is bind an event listener of submit to the <div id="app"> element (this is done for easy clean up down the track) then match which form was submitted (in our case, <form class="add-item">). Then based on that match, it will run the addItem method passing in a parameter of the input element and then resetting the input element’s value.

Right now, nothing will happen as our addItem method doesn’t do anything. So let’s update this method to handle our data.

Update AddItem Method

First we need to pass in a parameter to the addItem method then wire it up to push that value into this.items and update the DOM to show the new item in the list.

Update the addItem method to look like below:

public addItem(inputEl: HTMLInputElement): void {
  const addFormEl = this.host.querySelector('.add-item') as HTMLFormElement;
  // Generate Random ID
  // Try to avoid having duplicate IDs as they may clash
  const id = 'i-' + Math.random().toString(36).substr(2, 8);
  const value = inputEl.value;
  this.items.push({id, value});
  addFormEl.insertAdjacentHTML('beforebegin', `
    <form autocomplete="off" id="item-${id}" class="row edit-item">
      <input class="edit-input" data-id="${id}" type="text" name="item" value="${inputEl.value}" />
      <button
        type="button"
        class="delete-button delete-item"
        data-id="${id}"
      >
      </button>
    </form>
  `);
}

As you can see above, it will push in a new object into this.items which contains a unique id and the inputted value.

In addition to this, we inject some DOM that contains another form (<form class="edit-form">) which has the value of the item and a button to delete it.

Give your application a try. You should be able to add items by typing in a value and hitting the Enter key or tabbing to the + button and hitting the Enter key or using the mouse to explicitly click the + button. This is an excellent start as we have a working application built around accessibility and this will help us in the long run.

Can you feel the energy?! - this should feel very exciting!

The next part is to wire up the ability to update the items. Let’s do something groovy and simplify this flow.

Rather than hitting the Enter key or submitted the form again, let’s make it so as you start typing into an existing item, it will automatically update for every key press.

Bind Input Event Listener

Update the bindEvents method with a new listener on input (add this below the submit event listener). Use the code below:

this.host.addEventListener('input', (event: Event) => {
  const targetEl = event.target as HTMLInputElement;
  if (targetEl.closest('.edit-input') !== null) {
    this.updateItem(targetEl);
  }
});

What this code will do is listen for input change and each keystroke will fire the event matching to the <input class="edit-input"> element you are typing into. Each time this event fires, it will call the updateItem method and pass in the input element.

Update the UpdateItem Method

Let’s update the method to look like below:

public updateItem(inputEl: HTMLInputElement): void {
  const id = inputEl.getAttribute('data-id');
  const value = inputEl.value;
  const updateItems = this.items.map((item: Item) => {
    if (item.id === id) {
      return { id, value };
    }
    return item;
  });
  this.items = updateItems;
  console.log(this.items);
}

What this code does is it grabs the item id as a reference, then maps over this.items to find the matching item.id and updates that particular item with our new value. The ouput of this const will return a new array with the updated item. We then update this.items to the new item list.

Alright, now go back to your browser and pop open the dev tools (CTRL + SHIFT + I for Mac) or F12 for Windows/Linux.

Go to the Console tab or hit the ESC key to pop it open.

Now add some items to your shopping list and then update some of them. You’ll notice for each keypress, a console.log will come up showing our entire list.

Expand the list to ensure your changes are being updated into this.items.

Once you have confirmed it’s all working, remove the console.log and let’s proceed with wiring up the removeItem method.

Bind Click Event Listener

Add a final click event listener below the input event listener:

this.host.addEventListener('click', (event: Event) => {
  const targetEl = event.target as HTMLButtonElement;
  if (targetEl.closest('.delete-item') !== null) {
    this.removeItem(targetEl);
  }
});

Update RemoveItem Method

Let’s update this method to look like below:

public removeItem(buttonEl: HTMLButtonElement): void {
  const id = buttonEl.getAttribute('data-id');
  const updateItems = this.items.filter((item: Item) => item.id !== id);
  if (buttonEl.closest('.edit-item') !== null) {
    buttonEl.closest('.edit-item')?.remove();
  }
  this.items = updateItems;
}

This method will iterate over this.items and filter out or remove the item which matches against the id of the item we want to remove.

Once found, we return a new array the excludes the deleted item and update this.items to our new array.

We also find the corresponding DOM item and remove it.

Now your app should be working like a shopping list. You can add items, update items and even delete them - how cool is that!

But wait, you accidently reloaded the application and BAM all your items got cleared! facepalm!

Let’s fix this. We will use our friend localStorage to tackle this problem.

We can update the methods loadLocalStorageItems and updateLocalStorageItems to save this data to localStorage for us and retrieve it on page load (or refresh).

First we need to setup a name space for the localStorage item. Let’s create a new file within src/ts/ called constants.ts and add the following line:

export const APP_NAME = 'MY_SHOPPING_LIST';

Now in index.ts add the import to your APP_NAME constant (add it below the interfaces import):

import { APP_NAME } from './constants';

Update loadLocalStorage Method

Update the loadLocalStorage method with the following code:

public loadLocalStorageItems(): Promise<void> {
  return new Promise((resolve) => {
    const localStorageObj = localStorage.getItem(APP_NAME);
    if (localStorageObj !== null) {
      this.items = JSON.parse(localStorageObj);
    } else {
      localStorage.setItem(APP_NAME, '[]');
    }
    resolve();
  });
}

In this method, we return a promise which checks if a localStorage instance exists.

If it does, we hydrate this.items with the parsed localStorage contents (because local storage stores values as a string, we need to unstringify it - parse it).

If the item doesn’t exist, we create a new localStorage object with the contents of an empty array [].

Update saveLocalStorage Method

Before we wire up the loadLocalStorage method, let’s update the updateLocalStorage method with the following code:

public updateLocalStorageItems(items: Array<Item>): void {
  const localStorageObj = localStorage.getItem(APP_NAME);
  if (localStorageObj !== null) {
    localStorage.setItem(APP_NAME, JSON.stringify(items));
  }
}

With this method, it converts this.items to a string and stores it in the localStorage object (because local storage only accepts a string value).

Update Init Method

Ok, we are so close. We need to update the init function to run the loadLocalStorage method first and once it’s completed, it will fire a callback function to run both methods: render and bindEvents.

Update the init method to look like below:

public init(): void {
  this.loadLocalStorageItems().then(() => {
    this.render();
    this.bindEvents();
  });
}

The Final Stretch

You made it to the final step. Let’s wire up our new localStorage methods after each add, update and remove method.

Add the following line at the end of each of these methods: addItem, updateItem and removeItem:

this.updateLocalStorageItems(this.items);

After all that copying and pasting (or maybe you wrote everything out from scratch), the end result of your code in index.ts should look like below.

import { Item } from './interfaces';
import { APP_NAME } from './constants';

export class MyShoppingList {

  private host: HTMLElement;
  private items: Array<Item>;

  constructor(host: HTMLElement) {
    this.host = host;
    this.items = [];
  }

  public init(): void {
    this.loadLocalStorageItems().then(() => {
      this.render();
      this.bindEvents();
    });
  }

  public render(): void {
    this.host.insertAdjacentHTML('afterbegin', `
      <div class="container">
        <h1 class="logo">My Shopping List</h1>
        <div class="items"></div>
      </div>
    `);
    const itemsEl = this.host.querySelector('.items') as HTMLElement;
    if (itemsEl !== null) {
      this.items.map((item: Item) => {
        itemsEl.insertAdjacentHTML('beforeend', `
          <form autocomplete="off" id="item-${item.id}" class="row edit-item">
            <input class="edit-input" data-id="${item.id}" type="text" name="item" value="${item.value}" />
            <button
              type="button"
              class="delete-button delete-item"
              data-id="${item.id}"
            ></button>
          </form>
        `);
    });
    itemsEl.insertAdjacentHTML('beforeend', `
      <form autocomplete="off" class="row add-item">
        <input class="add" type="text" name="item" placeholder="Add item..." />
        <input class="add-button add" type="submit" value="+" />
      </form>
    `);
    }
  }

  public bindEvents(): void {
    this.host.addEventListener('submit', (event: Event) => {
      event.preventDefault();
      const targetEl = event.target as HTMLElement;
      if (targetEl.closest('.add-item') !== null) {
        const inputEl = targetEl.querySelector('input[type="text"]') as HTMLInputElement;
        this.addItem(inputEl);
        inputEl.value = '';
      }
    });
    this.host.addEventListener('input', (event: Event) => {
      const targetEl = event.target as HTMLInputElement;
      if (targetEl.closest('.edit-input') !== null) {
        this.updateItem(targetEl);
      }
    });
    this.host.addEventListener('click', (event: Event) => {
      const targetEl = event.target as HTMLButtonElement;
      if (targetEl.closest('.delete-item') !== null) {
        this.removeItem(targetEl);
      }
    });
  }

  public addItem(inputEl: HTMLInputElement): void {
    const addFormEl = this.host.querySelector('.add-item') as HTMLFormElement;
    // Generate Random ID
    // Try to avoid having duplicate IDs as they may clash
    const id = 'i-' + Math.random().toString(36).substr(2, 8);
    const value = inputEl.value;
    this.items.push({id, value});
    addFormEl.insertAdjacentHTML('beforebegin', `
      <form autocomplete="off" id="item-${id}" class="row edit-item">
        <input class="edit-input" data-id="${id}" type="text" name="item" value="${inputEl.value}" />
        <button
          type="button"
          class="delete-button delete-item"
          data-id="${id}"
        >
        </button>
      </form>
    `);
    this.updateLocalStorageItems(this.items);
  }

  public updateItem(inputEl: HTMLInputElement): void {
    const id = inputEl.getAttribute('data-id');
    const value = inputEl.value;
    const updateItems = this.items.map((item: Item) => {
      if (item.id === id) {
        return { id, value };
      }
      return item;
    });
    this.items = updateItems;
    this.updateLocalStorageItems(this.items);
  }

  public removeItem(buttonEl: HTMLButtonElement): void {
    const id = buttonEl.getAttribute('data-id');
    const updateItems = this.items.filter((item: Item) => item.id !== id);
    if (buttonEl.closest('.edit-item') !== null) {
      buttonEl.closest('.edit-item')?.remove();
    }
    this.items = updateItems;
    this.updateLocalStorageItems(this.items);
  }

  public updateLocalStorageItems(items: Array<Item>): void {
    const localStorageObj = localStorage.getItem(APP_NAME);
    if (localStorageObj !== null) {
      localStorage.setItem(APP_NAME, JSON.stringify(items));
    }
  }

  public loadLocalStorageItems(): Promise<void> {
    return new Promise((resolve) => {
      const localStorageObj = localStorage.getItem(APP_NAME);
      if (localStorageObj !== null) {
        this.items = JSON.parse(localStorageObj);
      } else {
        localStorage.setItem(APP_NAME, '[]');
      }
      resolve();
    });
  }
}

// Initialise the class to run
const app = document.querySelector('#app') as HTMLElement;
if (app !== null) {
  const application = new MyShoppingList(app as HTMLElement);
  application.init();
}

Now test your app and see if you can add items, update items and delete items.

Reload the page and if your items are still persisted, you should pat yourself on the back, because now you are officially a Typescript developer.