Book Image

The TypeScript Workshop

By : Ben Grynhaus, Jordan Hudgens, Rayon Hunte, Matt Morgan, Vekoslav Stefanovski
5 (1)
Book Image

The TypeScript Workshop

5 (1)
By: Ben Grynhaus, Jordan Hudgens, Rayon Hunte, Matt Morgan, Vekoslav Stefanovski

Overview of this book

By learning TypeScript, you can start writing cleaner, more readable code that’s easier to understand and less likely to contain bugs. What’s not to like? It’s certainly an appealing prospect, but learning a new language can be challenging, and it’s not always easy to know where to begin. This book is the perfect place to start. It provides the ideal platform for JavaScript programmers to practice writing eloquent, productive TypeScript code. Unlike many theory-heavy books, The TypeScript Workshop balances clear explanations with opportunities for hands-on practice. You’ll quickly be up and running building functional websites, without having to wade through pages and pages of history and dull, dry fluff. Guided exercises clearly demonstrate how key concepts are used in the real world, and each chapter is rounded off with an activity that challenges you to apply your new knowledge in the context of a realistic scenario. Whether you’re a hobbyist eager to get cracking on your next project, or a professional developer looking to unlock your next promotion, pick up a copy and make a start! Whatever your motivation, by the end of this book, you’ll have the confidence and understanding to make it happen with TypeScript.
Table of Contents (16 chapters)
Preface

Declaration Files

Anytime we're asked to write additional boilerplate code, our first question is: why is it important to do this? With that in mind, before we walk through creating and managing declaration files, let's first analyze the role of declaration files in the development process.

The entire reason why we use TypeScript in the first place is to give our applications a specified structure based on types. Declaration files extend this functionality by allowing us to define the shape of our programs.

In this section, we will walk through two ways to work with declaration files. The first approach will be to create our own declaration files from scratch. This is a great place to start since it provides insight into how the declaration process works. In the second part, we will see how we can integrate types into third-party NPM libraries.

Note

Declaration files are not a new concept in the programming world. The same principle has been used for decades in older programming languages such as Java, C, and C++.

Before we get into this chapter's example project, let's look at the core elements that comprise a declaration file in TypeScript. Consider the following code, which assigns a string value to a variable:

firstName = "Kristine";

The preceding code in TypeScript will generate a compiler warning that says Cannot find name 'firstName', which can be seen in the following screenshot:

Figure 2.1: Compiler error when TypeScript cannot find a variable declaration

Figure 2.1: Compiler error when TypeScript cannot find a variable declaration

This error is shown because whenever we attempt to assign a value to a variable, TypeScript looks for where a variable name is defined. We can fix this by utilizing the declare keyword. The following code will correct the error that we encountered in the previous case:

declare let firstName: string;
firstName = "Kristine";

As you can see in the following screenshot, the compiler warning disappeared with the use of the declare keyword:

Figure 2.2: Example of a variable being defined in TypeScript

Figure 2.2: Example of a variable being defined in TypeScript

Now, that may not seem like a big deal, because we could accomplish the same goal by simply defining a let variable, such as the following:

let firstName: string;
firstName = "Kristine"

The preceding code would not generate an error when viewed in the Visual Studio Code editor.

So, what is the point of using declare? As we build out complex modules, the declare process allows us to describe the complete shape of our modules in a way that cannot be done by simply defining a variable. Now that you know the role of declaration files along with the basic syntax, let's walk through the full workflow of creating a declaration file from scratch in the following exercise.

Exercise 2.01: Creating a Declaration File from Scratch

In this exercise, we'll create a declaration file from scratch. We'll declare file conventions, import, and then use declared files. Consider that you are developing a web app that requires users to register themselves with credentials such as email, user roles, and passwords. The data types of these credentials will be stated in the declaration file that we'll be creating. A user won't be allowed to log in if they fail to enter the correct credentials.

Note

The code files for this exercise can be found here: https://packt.link/bBzat.

Perform the following steps to implement this exercise:

  1. Open the Visual Studio Code editor.
  2. Create a new directory and then create a file named user.ts.
  3. Start the TypeScript compiler and have it watch for changes to the file with the following terminal compile command:
    tsc user.ts ––watch

    The following screenshot shows how the command appears inside the terminal:

    Figure 2.3: Running the TypeScript compiler with the watch flag

    Figure 2.3: Running the TypeScript compiler with the watch flag

    It's fine to leave this file empty for now. We'll start building out our implementation shortly. Now let's create our declaration file.

  4. Create a directory called types/ at the root of our program and then create a file inside it called AuthTypes.d.ts.

    Our project's directory should now look like this:

    Figure 2.4: AuthTypes file structure

    Figure 2.4: AuthTypes file structure

    Note

    Traditionally, declaration files are kept in their own directory called types/ and are then imported by the modules that they are defining. It's also the standard convention to use the file extension of .d.ts instead of .ts for your declaration files.

  5. Within the new declaration file, define the shape of our AuthTypes module. Use the declare keyword at the top of the file. This tells TypeScript that we are about to describe how the AuthTypes module should be structured:
    declare module "AuthTypes" {
        export interface User {
            email: string;
            roles: Array<string>;
        }
    }

    In the preceding code, another bit of syntax that might be different than what you're used to writing is that we wrap the module name in quotation marks. When we implement the program, you'll see that if we remove the quotation marks, we won't be able to import the module. Inside the module, we can place any number of exports that we want the module to have. One of the most important concepts to keep in mind is that declaration files do not have any implementation code; they simply describe the types and structure for the elements used in the module. The following screenshot gives a visual representation of the code:

    Figure 2.5: AuthTypes interface

    Figure 2.5: AuthTypes interface

    The compiler messages suggest that the import should happen successfully as there have not been any errors up to this point.

    In this step, we're exporting a user interface that defines two data points: email and roles. As far as the data types are concerned, the email attribute needs to be a string, and roles needs to be an array filled with strings. Such type definitions will ensure that anyone using this module will be informed immediately if they attempt to use the incorrect data structure.

    Now that we have defined the AuthTypes module, we need to import it into our TypeScript file so that we can use it. We're going to use the reference import process to bring the file into our program.

  6. Go to the user.ts file and add the following two lines of code:
    /// <reference path = "./types/AuthTypes.d.ts" />
    import auth = require("AuthTypes");

    The code in the editor will look something like this:

    Figure 2.6: Importing a declaration file

    Figure 2.6: Importing a declaration file

    The first line in the preceding code will make AuthTypes.d.ts available to our program, and the second line imports the module itself. Obviously, you can use any variable name for the import statement that you prefer. In this code, we're importing the AuthTypes module and storing it in the auth keyword.

    With our module imported, we can now start building the implementation for our program. We'll start out by defining a variable and assigning it to our user interface type that we defined in the declaration files.

  7. Add the following code to the user.ts file:
    let jon: auth.User;

    The updated code of user.ts file will look something like this:

    /// <reference path = "./types/AuthTypes.d.ts" />
    import auth = require("AuthTypes");
    let jon: auth.User;

    What we've done here is quite impressive. We've essentially created our own type/interface in a separate file, imported it, and told the TypeScript compiler that our new variable is going to be of the User type.

  8. Add the actual values of email and roles for the jon variable with the help of the following code:
    jon = {
        email: "[email protected]",
        roles: ["admin"]
    };

    With the required shape in place, the program compiles properly, and you can perform any tasks that you need to do.

  9. Create another User and see how we can work with optional attributes. Add the following code to add details of the user alice:
    let alice: auth.User;
    alice = {
        email: "[email protected]",
        roles: ["super_admin"]
    };

    Now, let's imagine that we sometimes keep track of how a user found our application. Not all users will have this attribute though, so we'll need to make it optional without breaking the other user accounts. You can mark an attribute as optional by adding a question mark before the colon.

  10. Add a source attribute to the declaration file:
    declare module "AuthTypes" {
        export interface User {
            email: string;
            roles: Array<string>;
            source?: string;
        }
    }
  11. Update our alice user with a source value of facebook:
    /// <reference path = "./types/AuthTypes.d.ts" />
    import auth = require("AuthTypes");
    let jon: auth.User;
    jon = {
        email: "[email protected]",
        roles: ["admin"]
    };
    let alice: auth.User;
    alice = {
        email: "[email protected]",
        roles: ["super_admin"],
        source: "facebook"
    }

    Notice that the jon variable still works perfectly fine, even without the source value. This helps us to build flexible interfaces for our programs that define both optional and required data points.

  12. Open the terminal and run the following command to generate a JavaScript file:
    tsc user.ts

    Let's now look at the generated user.js file, which can be seen in the following screenshot:

Figure 2.7: Declaration file rules not added to the generated JavaScript code

Figure 2.7: Declaration file rules not added to the generated JavaScript code

Well, that's interesting. There is literally not a single mention of the declaration file in the generated JavaScript code. This brings up a very important piece of knowledge to know when it comes to declaration files and TypeScript in general: declaration files are used solely for the benefit of the developer and are only utilized by the IDE.

Declaration files are completely bypassed when it comes to what is rendered in the program. And with this in mind, hopefully the goal of declaration files is becoming clearer. The better your declaration files are, the easier it will be for the IDE to understand your program and for yourself and other developers to work with your code.

Exceptions

Let's see what happens when we don't follow the rules of our interface. Remember in the previous exercise that our interface required two data elements (email and roles) and that they need to be of the string and Array<string> types. So, watch what happens when we don't implement the proper data type with the following code:

jon = {
    email: 123
}

This will generate the following compiler error, as shown in the following screenshot:

Figure 2.8: TypeScript showing the required data types for an object

Figure 2.8: TypeScript showing the required data types for an object

That is incredibly helpful. Imagine that you are working with a library that you've never used before. If you were using vanilla JavaScript, this implementation would silently fail and would force you to dig through the library's source code to see what structure it required.

This compiler error makes sense, and in a real-life application, such as a React or an Angular app, the application wouldn't even load until the issue was fixed. If we update the data structure to match the declaration file for AuthTypes with the following code:

jon = {
    email: "[email protected]"
}

We can see that the compiler will move the error message up to the jon variable name. If you hover over it, or look at the terminal output, you'll see the error shown in the following screenshot:

Figure 2.9: TypeScript showing the required attributes for an object

Figure 2.9: TypeScript showing the required attributes for an object

This is an incredibly useful functionality. If you're new to development, this may not seem like a very big deal. However, this type of information is the exact reason why TypeScript continues to grow in popularity. Error messages such as this instantly provide the information that we need in order to fix the bug and work with the program. In the preceding screenshot, the message is telling us that the program won't compile as we are missing a required value, namely, roles.

Now that we have built out our own declaration file from scratch, it's time to move on and see how declaration files are utilized by other libraries.