Skip to Main Content
Js typewriter

TypeScript - PHP style error checking in JS

Intro

Today we will be learning all about TypeScript. If this language has been on your radar for awhile where you've heard it mentioned, you're just not sure what it is, or you've had some exposure but don't really understand it, then today is your lucky day because we will be doing a deep dive. The aim is to explain the scenarios you would use this language and there are examples sprinkled in where we eventually convert a simple sites JS completely to use TS. So brew yourself a coffee, pull up a comfy chair and lets get started!

Background

JavaScript is Stupid

No really, this isn't a joke, JavaScript can genuinely seem very stupid at times, here are a few examples to explain why I have begun to believe this is the case. JS thinks that an empty string and the number zero are the same value when being compared. We also often see that 2 + 1 = 21 instead of 3 because it is treating integers as strings, but 2 - 1 = 1 as it suddenly realises that they are actually numbers. The cause of this behaviour can be narrowed down to the fact that the + sign is used both for addition and string concatenation. JavaScript’s equality operator (==) coerces its operands, leading to unexpected behavior:

if ("" == 0) {
 // It is! But why??
}
if (1 < x < 3) {
 // True for *any* value of x!
}

JavaScript also allows accessing properties which aren’t present, instead of throwing an error it just returns that the result of this equation is not a number. Whilst technically correct, it isn't the true error here and leads to a lot of time spent looking at the wrong issue, this is not a helpful output for us:

const obj = { width: 10, height: 15 };
// Why is this NaN? Spelling is hard!
const area = obj.width * obj.heigth;

How does TypeScript help

It's time to introduce TypeScript now that we have found this flaw in how JS handles these errors. TypeScript checks a program for errors before execution, eliminating the chance that these kinds of errors will make it to production. When compiling, TypeScript will throw the following error for the above example:

Property 'heigth' does not exist on type '{ width: number; height: number; }'. Did you mean 'height'?

TypeScript is essentially JavaScript at its core so you can take any working JavaScript code and put it into a TypeScript file without worrying about the syntax of how it is written. You can then debug it all and return it back to vanilla JS if desired, or keep it compiling this way for future error checking. The compiled output file is pure JS and has no undesirable effects, so you may as well keep it in place and use it. It can also be handy to quickly check small code blocks where projects haven't been configured to use TypeScript yet by just copy pasting them into a TS build and compiling them.

So what is it

TypeScript is a typed superset, meaning that it adds rules about how different kinds of values can be used. You can run TypeScript as a compiler once it has been installed via npm, which we will explain the steps for in a moment. The above example error about obj.heigth was not a syntax error: it is an error of using a kind of value (a type) in an incorrect way.

This is common in PHP with version 8.0 onwards thanks to changes around defining types for variables and functions, which helps us to more consistently write error free code. This results in a more programming styled object oriented language as a result, and it looks like it can finally be applied to JS in a similar way here to help flag errors before our code makes it to the server.

Setting up TypeScript

Standalone NPM Install

Add the following into your package.json file under dependencies:

"typescript": "^5.8.2"

Once this is done you can run npm install locally and it will pull in the typescript package for you. You can then run npx tsc to run the typescript compiler, by default it will give you a lot of available options for this command which means that it has installed successfully. The next step is to start configuring a tsconfig.json file to proceed.

Using with Laravel Mix

Instead of installing typescript via npm, we install ts-loader, this is a wrapper for typescript which works with both webpack and laravel mix. You do this with the following in your package.json file under dependencies:

"ts-loader": "^9.5.2"

Run npm install and we are ready to configure using a tsconfig.json file. The version number may be different for you, this was the latest compatible version available to us at the time of writing this article.

Configuring TypeScript

tsconfig.json

This new file will go into the root level of the project at the same level as your package.json file and webpack.mix.js file. Create the new file, name it tsconfig.json and paste the following json object into it:

{
 "compilerOptions": {
   "module": "commonjs",
   "target": "es5",
   "noImplicitAny": false,
   "sourceMap": false
 },
 "include": ["src/js/*"]
}

webpack.mix.js

A webpack.mix.js file should already exist within your project, what we need to do here is add in an additional part to the mix command, you will likely already have mix.js().vue().sass() etc, just add it into this chain before .version(); is called at the end. Paste the following code into your file:

.webpackConfig({
   module: {
      rules: [
         {
            test: /\.tsx?$/,
            loader: "ts-loader",
            options: { appendTsSuffixTo: [/\.vue$/] },
            exclude: /node_modules/
         }
      ]
   },
   resolve: {
      extensions: ["*", ".js", ".jsx", ".vue", ".ts", ".tsx"]
   }
})

The options part within these rules means that typescript will be applied to any .vue component files we have used, this is of course optional and based on your flavours of js you have applied to your project. To get the vue compatibility to run, all you have to do is add lang=”ts” on any script tags within the .vue files.

hello-world.ts

This is a new file which we shall create at the same level as our app.js file. So for us that would be within the src/js folder. This may be differently for you based on your projects folder structure. For now we are going to write a very simple console log command to show a hello world message in our browser to test that everything has been set up correctly.

export function helloWorld(): string {
   return "Hello world!";
}

app.js

Finally we will edit our existing app.js file in our project under src/js/app.js, this sits next to our newly created hello-world.ts file. Again, this folder structure may be a little different for your project so just tweak it where needed to match your scenario. Currently we are just testing that this simple command compiles correctly, to do this paste in the following code to call in our new TypeScript file at either the very top or very bottom of the app.js file.

const helloWorld = require("./hello-world").helloWorld();
console.log(helloWorld);

Compiling TypeScript

Now that all of the above steps have been completed, go back into your package.json file and run either the dev or prod tasks to recompile the js and css in your project. You may have different automations in place here but just run anything which you usually use to compile your js. This should alter the output generated in public/assets/js/app.js, assuming there are no TypeScript errors when compiling, you can now upload the changes to your test server. When you reload the site you should see a message within the browser console saying “Hello World!” which confirms that our compilation has worked. We have now successfully written our first bit of TypeScript and compiled it back into minified vanilla js.

Compilation Tests

We can now compile TypeScript, but you might be forgiven for thinking, "why would we want to do this, can't I just write better code"? Fear not, I’ve explored a few scenarios and included code examples and outputs below to demonstrate why using TypeScript can be beneficial to everyone, even the most seasoned of coders, so let’s get into it!

First let’s try our 3 examples from earlier. If I add the code into the app.js file for each of them, then we can still compile successfully. This however is not good because we know that the code will cause errors in the browser and as a result will cause knock-on effects to any code using those variables / statements. Below you can see the errors present in the code and the completed compilation without any of them being flagged.

Ts 1

What this means is that we have errora which have not been picked up and these will eventually be reported as a bug within a production environment.

So how do we avoid this? Well now that we have TypeScript configured and available to us, let's see what happens when we include those code examples within our TypeScript file and compile it into the JS. To do this we just move the code out of one file and into the other.

Below are the exact same code examples but because they exist within our TypeScript file they are being fully evaluated on the fly by PHPStorm now. If you are using another IDE which doesn’t evaluate in realtime such as Nova, Sublime, or any of the other options available out there, then fear not because it is still picked up further down when it has been compiled.

Ts 2
Ts 3
Ts 4

As you can see above, all of our errors have been flagged on the fly before we have compiled the code, it has given us recommendations on what to do to fix these errors and we can ensure that we are writing bug free code before it is even compiled. As mentioned before though, these bugs are also picked up by our compiler now. Run any of the js compilation scripts (dev and prod for us), and you will get the following output.

Ts 5

Webpack has compiled with 4 errors, all of our errors have been raised and flagged when we run the compiler. Each error also contains a recommended fix along with the file name and line numbers. This is fantastic because it means most people would be able to follow those instructions to find the error and at least try the suggested fix without needing to be a JS expert themselves to resolve the issue.

To further test this and put it through its paces, I devided I would then alter the hello world example to expect a number to be returned instead of a string, here is what happened:

Ts 6
Ts 7

As you can see we have exactly the same error logging behaviour as our other examples. This means that we can resolve the errors and be certain that our JS doesn’t contain any issues before we deploy it to a live website. We are also cutting down the possibility of missing these errors when checking the fucntionality manually in the browser because the compiler has already caught them without us having to test every possible scenario on a given page. Setting expected return types for functions is something which we often do now in custom PHP, this ensures that the type of response we are getting back is correct and can confidently use it without having issues instead of just presuming it will be correct.

A common issue in JS is to see NaN reported which means that it was expecting a number when dealing with a variable, but the variables value is not a number, or NaN for short. Using TypeScript should completely eliminate these kinds of errors by telling us exactly where it could creep in and allowing us to fix it before it happens.

Converting a full project

This last part took me the longest to figure out. Apparently naming a TypeScript file and a JavaScript file exactly the same (minus the file extension) causes some serious problems! I’ve taken our internal boilerplate site and spun it up in a new testing environment, I’ve then renamed the app.js file and called it app.ts but oh good lord this caused some headaches! Don't do this unless you want to see just how much it complains. Let’s name it functions.ts instead to get around all of these issues. If you have a functions.js file already, then choose a different name which you don't already have a file for.

Now for some code conversion. Everything needs to be within a function which is exported and callable, we can’t have any loose code. Here are some examples, the top code block is how it looked in vanilla js, and the bottom code block is how it now looks in the typescript file. You can see that instead of using const to define variables or functions, we are now exporting functions with the same name instead. Everything must be within a function.

const mobileMenu = () => {
const dropdownNav = () => {

export function mobileMenu() {
export function dropdownNav() {

Any code outside of a function needs to be wrapped inside of a function, which we can then export and call from other files. This also led to a new function called criticalCss to be created for us as we had some loose unstructured code handling this. You will find that as well as converting existing fucntions you end up creating a lot of new functions also.

Finally we shall create a fresh new app.js file which our compiler will target. The app.js file requires our functions.ts file in the same way in which we loaded our hello world file in our previous example.

const app = require("./functions");

app.criticalCss();

document.addEventListener("DOMContentLoaded", () => {
   app.mobileMenu();
   app.dropdownNav();
});

This now means our entire projects js is being compiled by typescript and is being checked for errors. This boilerplate site of ours doesn't have much JS so it was a fairly quick process, but this is how you would work through larger files and multiple files using these same methods.

TypeScript immediately flagged a few things when compiling, mainly around e.target not being an element as it was instead being detected as a node, this was easily fixed though by doing the following:

var elem = <Element>e.target;

Once this is added it treats elem as an Element correctly. An advantage by doing this here is that we also get full autocomplete within our code editor on the elem variable which we can use throughout our code. This also means that it always expects an element, and when it isn’t an element it’ll throw an error and tell you. Anytime you use cloneNode, please also make sure that you call <Element> at the start of the node you are cloning, otherwise it treats it as a node instead of an element. Here is an example of this:

var criticalCSS = <Element>styles[i].cloneNode(true);

Alternate Syntax

There are many ways to invoke our TypeScript functions and here are a couple which return the same results and don't change the output of the compilation process. This is just a case of finding a preference for whichever method you personally would prefer to use.

const app = require("./functions");

const helloWorld = require("./hello-world").helloWorld();
console.log(helloWorld);

app.criticalCss();

document.addEventListener("DOMContentLoaded", () => {
   app.mobileMenu();
   app.dropdownNav();
});

import * as app from "./functions";

import { helloWorld } from "./hello-world";
console.log(helloWorld());

app.criticalCss();

document.addEventListener("DOMContentLoaded", () => {
   app.mobileMenu();
   app.dropdownNav();
});

My personal preference is the latter of those two example as it resembles how we use vuejs more closely. Consistency is important when handing across to other members in your team so everyone can get up to speed quickly in understanding how the codebase has been structured. In my opinion the second example using the import statement also allows you finer control to import only a selection of functions from a file instead of the entire file. The first example just includes the entire file each time with no ability to restrict this.

Closing Thoughts

Error checking has been present within php long enough now within my code editor that I have come to really enjoy using it to help me ship stable code, it's a good sanity check. To see this available and now applied to JS makes me happy because it effectively eliminates another area where support tickets may have originated from. If we can provide website visitors with a bug free experience then this is a positive outcome for everyone.

TypeScript may not be a tool which fits your exact coding environment or specific scenarios, but it is beneficial to be aware of it as a potential tool in our box as developers. We need to build up a range of tools which don't just make our lives easier, but also make experiences on the internet better and more fluid. Lets apply prevention rather than cure when it comes to these bugs, we won't catch all of them but minimising them is an extremely good practice which impacts everybody.

Useful Resources

https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html

https://webpack.js.org/guides/typescript/

https://sebastiandedeyne.com/typescript-with-laravel-mix

421899024 10160014929187201 3541990577107788219 n 2024 06 19 120204 sylo

Matt develops custom plugins for Craft CMS to extend the core functionality. He also focusses on pagespeed improvements and server environments.

Learn more about us: