Tyler Sengia

Notes for Starting Typescript

TypeScript is key to building maintainable applications for the web, but often lacks the easy start that vanilla JavaScript enjoys.

Here are some noteworthy things that you may find useful to know when developing in TypeScript.

1. Strict Type Checking

The null value is often referred to as the billion-dollar mistake. In a strictly typed language, programmers often assume that a variable will only contain a value matching the declared datatype. But this assumption overlooks the fact that null or other null-ish values can also be assigned to the variable. This leads to many runtime errors such as null pointer exceptions that cause bugs and crashes. Strict type systems were supposed to prevent such runtime errors in the first place, but adding a null value makes the program vulnerable to a new class of errors.

JavaScript has two null-ish primitive values: null and undefined. It can be a bit confusing that JavaScript has both of these primitive values, but they are distinct and not equivalent. The undefined value is returned if you try to access a non-existent property of an object, and null is a valid value you can assign to variable.

let a = {
    foo: 1,
    zub: null
};

console.log(a.foo); // Prints: 1
console.log(a.bar); // Prints: undefined
console.log(a.zub); // Prints: null

The goal of TypeScript is to provide a stricter type system for JavaScript in hopes of making development and maintenance easier.

Sadly, TypeScript allows values of any type to be set to null or left undefined. For example, most programmers would expect a boolean variable to be either true or false, but really it can also be null, or undefined! Because of this, default TypeScript is still vulnerable to the billion-dollar mistake.

Thankfully, TypeScript can be configured to perform “strict null checking” on your code to ensure that you handle all the cases when your code handles a value that may be undefined.

If you are starting out a new TypeScript project, I highly recommend enabling all available strict type checks, and not just strict null checking.

To enable strict type checking, add the following line to your tsconfig.json file:

{
    "strict": true,
}

Adding "strict": true to your tsconfig.json file will enable strict null checking as well as other type checks such as strict property initialization.

At first, working with strictly checked TypeScript will seem difficult. The compiler will produce more errors than you’re used to. After a while, you will find that the compiler is finding issues and edge cases you were not considering, and development will become smoother.

The value of strict null checking is so great that the developers of VS Code undertook a multi-year-long effort to gradually refactor all of VS Code’s TypeScript to be strict null checked. You can read more about it on their blog (archived).

2. Parsing and Serializing JSON

JSON.stringify(object) will serialize your JavaScript object into a JSON string. The inverse, JSON.parse(your_string_here) will return a plain JavaScript object. If you’re writing in TypeScript, this can be a problem. You might expect the deserialized object to be an instance of a class and have the methods available from that class. Instead, you have a plain JavaScript object with none of your helpful class methods.

Typescript does not have a built-in serializer/deserializer, and this has become such a large problem that an entire repository has been created to petition the TypeScript authors to add support for serialization.

In the meantime, to convert a plain JavaScript object into an instance of a class, you have a couple of options:

  1. Use only plain JavaScript objects and never use classes for deserialized data
  2. Write a custom deserialization method for your class that copies the values/references from the object into an actual instance of the class
  3. Use a package from NPM to perform option #2

Option #1 essentially means that using TypeScript is pointless in the first place.

Option #2 might be tedious for large applications with lots of different classes to deserialize.

Option #3 is more palatable, but you have to find a secure and reliable package in npm, which might be difficult nowadays.

Code for Option #2 could look something like this:

class Foo {
    bar: boolean;
    fizz: number;

    doSomething(): boolean {
        console.log("Fizz = " + this.fizz);
        return this.bar;
    }

    // Copies value from obj into member variables, returns true on success
    deserialize(obj: any): boolean {
        // First, check that all of the needed fields in `obj` are set
        if (obj.bar === undefined || obj.fizz === undefined) {
            // A field was missing from obj!
            return false;
        }

        this.bar = obj.bar;
        this.fizz = obj.fizz;
            
        // All fields successfully copied from obj!
        return true;
    }

    constructor() {
        // Must initialize values in constructor if using `strict` checks!
        this.bar = false;
        this.fizz = 0;
    }
}

// This JSON will correctly deserialize into a Foo object
let parsed_good_json: object = JSON.parse("{\"bar\":true,\"fizz\":6}");

// This JSON is missing the `fizz` field and will fail deserialization
let parsed_bad_json: object = JSON.parse("{\"bar\":false}");

let foo_instance: Foo = new Foo();
// Attempt to deserialize both objects into a Foo object
for (const a of [parsed_good_json, parsed_bad_json]) {
    if (foo_instance.deserialize(a)) {
        console.log("Serialized parsed JSON into an instance of Foo!");

        // Now perform the class method
        foo_instance.doSomething();
    }
    else {
        console.error("Failed to deserialize JSON into Foo instance!");
    }
}

Run the above code in the TypeScript playground!

Phew, that’s a lot of code! And sadly, it’s still not done. The deserialize() method does not check that the types of fizz and bar match with the expected types for the class, and it also does not provide any error messages explaining why deserialization failed. For smaller projects, that might be acceptable. But if you have a larger application, then you’re going to need a more complete feature set.

This is why I recommend utilizing Option #3: Use a package from NPM.

There are a number of packages in NPM that may satisfy your needs:

3. VS Code integration with Yarn

This part is not about TypeScript, but you may find it useful while learning TypeScript/JavaScript development.

Yarn is a fantastic package manager, and I recommend that you switch to it immediately.

The default package manager, npm, is slow and leaves a node_modules folder with a mass equal to the sun. Yarn v3 in Plug and Play (PnP) mode utilizes compressed packages and installs dependencies in a fraction of the time that npm does.

However, if you use Yarn v3 in PnP mode, you will find that VS Code does not provide correct type hinting by default. VS Code will throw errors about being unable to find installed modules. This problem also occurs for other IDEs too, so you can take a look at this page in the Yarn documentation for instructions to set up Yarn to work with your favorite IDE.

To resolve this issue, you will need to download and run the @yarnpkg/sdks package using the below command:

yarn dlx @yarnpkg/sdks vscode

VS Code will then display a notification asking you to activate some custom TypeScript settings. Activating the TypeScript settings will then fix VS Code being unable to locate installed modules.

Conclusion

TypeScript’s typing system gives me more confidence in the code I write and catches bugs before they hit production, but can have a larger learning curve than vanilla JavaScript. I hope that these tips help reduce that learning curve and enable you to use the strictly typed power of TypeScript.