Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Events
Videos
Audiobooks
Packt Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Full-Stack React, TypeScript, and Node
Full-Stack React, TypeScript, and Node

Full-Stack React, TypeScript, and Node: Build scalable and cloud-ready web applications using modern React, TypeScript, and Docker , Second Edition

Arrow left icon
Profile Icon David Choi Profile Icon Cihan Yakar
Arrow right icon
$54.99
Paperback Jun 2026 626 pages 2nd Edition
eBook
$32.99 $43.99
Paperback
$54.99
Arrow left icon
Profile Icon David Choi Profile Icon Cihan Yakar
Arrow right icon
$54.99
Paperback Jun 2026 626 pages 2nd Edition
eBook
$32.99 $43.99
Paperback
$54.99
eBook
$32.99 $43.99
Paperback
$54.99

What do you get with Print?

Product feature icon Instant access to your digital copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Redeem a companion digital copy on all Print orders
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Modal Close icon
Payment Processing...
tick Completed

Shipping Address

Billing Address

Shipping Methods
Table of content icon View table of contents Preview book icon Preview Book

Full-Stack React, TypeScript, and Node

2

Exploring TypeScript

In this chapter, we'll dive deeper into the TypeScript language. We'll learn about TypeScript's explicit type declaration syntax, as well as the many built-in types in TypeScript and their purpose. We'll also learn how to create our own types.

By the end of this chapter, you will have a strong understanding of the TypeScript language, which will allow you to read and understand existing TypeScript code with ease.

In this chapter, we're going to cover the following main topics:

  • What are types?
  • Exploring TypeScript types
  • Understanding classes and interfaces
  • Understanding inheritance
  • Understanding polymorphism
  • Learning generics
  • Utility types

Technical requirements

To take full advantage of this chapter, you should be an intermediate developer experienced in coding with another type-safe language and platform. You'll also need to install Node and a JavaScript code editor, such as Visual Studio Code (VSCode).

You can find the GitHub repository for this chapter at https://github.com/PacktPublishing/Full-Stack-React-TypeScript-and-Node-2nd-Edition. Use the code in the Chap2 folder.

Before continuing, we will set up our base project similarly to how we did in Chapter 1, Understanding TypeScript:

  1. Go to your FullStackTypeScript folder and create a new folder called Chap2.
  2. Open VSCode and go to File | Open Folder, and then open the Chap2 folder you just created. Then, select View | Terminal and enable the terminal window within your VSCode window.
  3. Type the npm init command to initialize the project for npm, and accept all the defaults.
  4. This time around, let's install TypeScript into our project instead of globally. Type this command:
    npm install typescript
  5. As you can see, this time, we left out the –g parameter, and if you look inside the package.json file, you should see in the dependencies section TypeScript 5.5 or higher. The ‑g parameter means to install the package on the machine and make it available to all projects, as opposed to installing it in the project.
  6. Now, before we finish this time, we will also add a tsconfig.json file to the root of our Chap2 folder. There will be a dedicated section later in this chapter explaining the tsconfig.json file, which is responsible for configuring TypeScript's settings, but for now, just copy the tsconfig.json file from the Chap2 source code folder.

Now we're ready to get started.

What are types?

A type is a reusable set of rules that describes the shape of a value. A type may define fields and method signatures. It can be shared and reused repeatedly. When you use a type, you are describing what an object assigned to that type must look like. That object is an example of your type, with specific values for its fields. Instances, in the traditional sense, come from classes. In TypeScript, as the name implies, types are very important. They're the main reason why the language was created in the first place. Let's take a look at how types work in TypeScript.

How do types work?

JavaScript does have types. number, string, Boolean, array, and so on are all types in JavaScript. However, those types are not explicitly set during the declaration; they are only inferred at runtime based on the assigned value. For example, in JavaScript you write let count = 5; and the engine treats count as a number, but nothing stops you from later writing count = "five";. In TypeScript, types are normally set during declaration, like let count: number = 5;, and from that point on the compiler will reject count = "five";. It is possible to allow the compiler to infer your type, but this feature exists as a convenience, and once a variable's type is set, it cannot be changed. In general, you're going to be explicitly setting your types in TypeScript. Also, in addition to the types supported by JavaScript, TypeScript has its own unique types and allows you to create your own types, which typically describe the shape of objects.

Let's look at an example:

  1. Create a file called shape.ts and add the following code:
    class Person {
        name: string = "";
    }
    const jill: { name: string } = {
        name: "jill"
    };
    const person: Person = jill;
    console.log(person);

    The first thing you should notice is that we have two ways of declaring the same type. We have a class called Person with a field called name. Below that, you see that we have a variable called jill that is of type { name: string }. This second type is a little weird because this type declaration has no given name; it's more like a type definition. Nevertheless, these two types are effectively equivalent in TypeScript.

    Now, below those lines, you can see that we have another variable called person of the type Person, and we set that variable to jill. Again, the compiler does not complain, and everything seems OK.

    Even though both the person object and jill have the same name field, aren't they different? Something about this feels a little off, but let's continue and see whether we can figure this out.

  2. Let's compile this code and run it to see what happens. However, in this case, let's do things a bit differently. As you saw earlier in the chapter, we installed TypeScript directly into the project itself. We are therefore also going to call it from within our project dependencies. Open the Chap2 file called package.json and add this line to the scripts section just under "test" (make sure to add a comma after the "test" line):
    "build": "tsc"

    The scripts section allows us to add terminal commands to run various tasks. In this case, we use the standard name for creating a build, "build", and then that name is runnable by npm. This command we just created tells TypeScript to transpile all files on the root of the project. Let's compile and run our code by using these two commands, run separately:

    npm run build
    node shape

Notice that we don't need to give a file extension. Once you've run the commands, you should see the following:

Figure 2.1 – The shape.ts output

Figure 2.1 – The shape.ts output

As you can see, the code compiles and runs without issue and we get the expected output. In TypeScript, unlike most mainstream statically typed languages, the compiler looks at the shape or structure of a type and is not concerned with its name at all. This approach is often called structural typing (or informally, duck typing: if it walks like a duck and quacks like a duck, it's a duck). This is why the TypeScript compiler treats the type of person as compatible with the type of jill. You will see in later chapters, as we dig more deeply into TypeScript's type system, why it is so important to be aware of this behavior.

TypeScript uses all the base JavaScript types and provides type declarations that can be used in annotations. Let's learn about the base TypeScript types now.

Exploring TypeScript types

In this section, we'll look at some of the core types available in TypeScript. Using most of these types will give you error-checking and compiler warnings that can help improve your code. They will also provide information about your intent to other developers on your team. So, let's continue and see how these types work.

The any type

any is a type that opts out of static type checking and can be set to any other type. If you declare a variable to be of type any, this means that you can set it to anything and later reset it to anything. It is, in effect, no type because the compiler will not check the type on your behalf. There is a key fact to remember about the any type: the compiler will not intercede and warn you of issues at development time, and it will also allow any values to flow into other variables, silently disabling type safety wherever they go. Therefore, if possible, using the any type should be avoided. For this reason, most TypeScript projects enable the noImplicitAny compiler option, which turns accidental, unannotated any values into errors.

In a large application, it is not always possible for a developer to control the objects that are used in their code. For example, if a developer is relying on a web service API call to get data, that data's type may be controlled by some other team or even a different company entirely. It is also possible that an API result schema may change frequently. Situations such as these require type flexibility and an escape hatch from the type system. The any type can provide that escape hatch.

It is important not to abuse the any type. You should be careful to only use it when you have no other way of creating a type. There are, however, some alternatives to using the any type, and the unknown type is one of them. Unlike any, unknown forces you to check the type before using the value, making it a safer choice. We'll cover that type next.

The unknown type

unknown is a type released in TypeScript version 3. It is similar to any in that once a variable of this type is declared, a value of any type can be set to it. That value can subsequently be changed to any other type, just like the any type. However, unlike any, you cannot perform almost any operation on an unknown value, such as accessing its properties, calling it, or assigning it to another variable, without first confirming what type it really is. The only time you can set an unknown variable to another variable, without first checking its type, is when you set an unknown type to another unknown or an any type.

Let's take a look at an example of any, and then we'll see why the unknown type is preferable to using the any type (it is, in fact, recommended by the TypeScript team):

  1. First, let's take a look at an example of the issue with using any. Go to VSCode and create a file called any.ts, and then type the following code:
    let val: any = 22;
    val = "string value";
    val = new Array();
    val.push(33);
    console.log(val);

    First, the val variable is declared, set to the any type, and given a value of 22, a number. Then, that same variable is set to string. Then, it is reset into an empty Array. Finally, the Array has a push method called on it, which adds an element with a value of 33 to the end of the Array. If you run this code using the following commands, you will see this result:

    Figure 2.2 – Any run result

    Figure 2.2 – Any run result

  2. Since val is of the any type, we can set it to whatever we like. As you can see, we set the variable to multiple different types. But once it is set to an array, we call push on it, since push is a method of Array. However, this is obvious only because we, as developers, are aware that Array has a method called push on it. What if we accidentally called something that does not exist on Array? Let's replace the previous code with the following:
    let val: any = 22;
    val = "string value";
    val = new Array();
    val.doesnotexist(33);
    console.log(val);

    As you can see, the new code contains a call to a function called doesnotexist that is clearly not a valid Array function.

  3. Let's try running the TypeScript compiler again:
    npm run build

    The compiler succeeds with no errors, unfortunately, since making something of the any type causes the compiler to no longer check the type. Additionally, we also lost IntelliSense, the VSCode development-time code highlighter and error checker. When you hover your mouse over the doesnotexist function, all you see is the any type. Only when we try and run the code do we get any indication that there is a problem, which is never what we want. Let's see what the exact error is when we run the code:

    Figure 2.3 – Any failing

    Figure 2.3 – Any failing

In a complex application, it is an easy error to make, even if the mistake is simply mistyping something.

Let's see a similar example using unknown:

  1. First, comment out your code inside of any.ts and delete the any.js file (as we will use the same variable names, if you do not do this, it will cause conflict errors).

    We'll learn about something called namespaces later, which can eliminate these sorts of conflicts.

  2. Now, create a new file called unknown.ts and add the following code to it:
    let val: unknown = 22;
    val = "string value";
    val = new Array();
    val.push(33);
    console.log(val);
  3. You will notice that VSCode gives you an error immediately, complaining about the push function. This is weird since obviously, Array has a method called push in it. This behavior shows how the unknown type works. You can consider the unknown type to be sort of like a label more than a type, and underneath that label is the actual type. However, the compiler cannot figure out the type on its own, so we need to explicitly prove the type to the compiler ourselves.
  4. We use type guards to prove that val is of a certain type:
    let val: unknown = 22;
    val = "string value";
    val = new Array();
    if (val instanceof Array) {
        val.push(33);
    }
    console.log(val);

    As you can see, we've wrapped our push call with a test to see whether val is an instance of Array

    Note that we'll learn more about instanceof in Chapter 3, Building Better Apps with ES6+ Features.

  5. Once we've made this check, the call to push can proceed without error, as shown here:
    Figure 2.4 – Unknown

    Figure 2.4 – Unknown

This mechanism is a bit cumbersome since we always have to test the type before calling members. However, it is still preferable to using the any type and a lot safer since it is checked by the compiler.

Intersection and union types

Remember when we started this section by saying that the TypeScript compiler focuses on type shape and not the name? This structural approach makes it natural for TypeScript to support what are called intersection types. This means that TypeScript allows the developer to create new types by combining (or merging) multiple distinct types together. Note that, despite the name, an intersection type contains all the fields of the combined types, not just their common ones. The "intersection" refers to the set of values that satisfy both types at once. This is hard to imagine, so let me give you an example. If you look at the following code, you can see a variable called obj that has what looks like two types associated with it, with each type having only one field, name or age:

let obj: { name: string } & { age: number } = {
    name: 'tom',
    age: 25
}

What we are doing in this code is merging two distinct types into a new single type by using the & symbol. This is why the obj variable can be set to the value that has both the name and age fields.

Let's try running this code and displaying the result on the console. Create a new file called intersection.ts and add the following code to it:

let obj: { name: string } & { age: number } = {
    name: 'tom',
    age: 25
}
console.log(obj);

If you compile and run this code, you will see an object that contains both the name and age properties together:

Figure 2.5 – Intersection result

Figure 2.5 – Intersection result

As you can see, both IntelliSense and the compiler accept the code, and the final object has both fields. This is an intersection type.

Now there is another type that is somewhat similar in syntax to the intersection type, but opposite in meaning, and that is the union type. In the case of unions, instead of combining types, we are using them in an "or" fashion, where it's one type or another. Let's look at an example.

Create a new file called union.ts and add the following code to it:

let unionObj: null | { name: string } = null;
unionObj = { name: 'jon'};
console.log(unionObj);

The unionObj variable is declared to be of the null or { name: string } type, by using the | symbol. If you compile and run this code, you'll see that it compiles and accepts both type values, just not at the same time. This means that the type value can be either null or an object instance of the { name: string } type. Note that before you can safely use a union value (for example, accessing unionObj.name), TypeScript will require you to first narrow the type, typically with a check like if (unionObj !== null).

Literal types

Literal types let you constrain a variable to a specific hardcoded value, such as a particular string, number, boolean, or bigint. They are often combined with union types to allow a small, fixed set of values.

Let's create another file called literal.ts and add this simple example of string literal types combined in a union:

let literal: "tom" | "linda" | "jeff" | "sue" = "linda";
literal = "sue";
console.log(literal);

As you can see, we have a bunch of hardcoded strings as the type. This means that only values that are the same as any of these strings will be accepted for the literal variable.

The compiler is happy to receive any of the values on the list, and even have it reset. However, it will not allow the setting of a value that is not on the list. Doing that will give a compile error. Let's see an example of this. Update the code as shown by resetting the literal variable to john:

let literal: "tom" | "linda" | "jeff" | "sue" = "linda";
literal = "sue";
literal = "john";
console.log(literal);

Here, we set the literal variable to john, and compiling gives the following error:

Figure 2.6 – A literal error

Figure 2.6 – A literal error

This type is great when you have multiple possible values for a variable, but you want to make them specific and limited.

Type aliases

Type aliases are used very frequently in TypeScript. A type alias does not create a new type; it simply gives an existing type another name, and it is often used to provide a shorter, simpler name to some complex type, or to reuse the same type consistently in multiple places. Note that TypeScript also has interface, which can serve a similar purpose for object shapes; we'll look at the differences later. For example, here's one possible usage:

type Points = 20 | 30 | 40 | 50;
let score: Points = 20;
console.log(score);

In this code, we take a long numeric literal type and give it a shorter name of Points. Then, we declare score as the Points type and give it a value of 20, which is one of the possible values for Points. And, of course, if we tried to set the score to, let's say, 99, the compilation would fail.

Another example of type aliases would be for object literal type declarations:

type ComplexPerson = {
    name: string,
    age: number,
    birthday: Date,
    married: boolean,
    address: string
}

Since the type declaration is very long and does not have a name, like, for example, a class would, we use an alias instead. Type aliasing can be used for just about any type in TypeScript, including unions, intersections, tuples, functions, and generics, which we'll explore further later in the chapter.

Function return types

For completeness' sake, I wanted to show one example of a function return declaration. It's quite similar to a typical variable declaration. Create a new file called functionReturn.ts and add this code to it:

function runMore(distance: number): number {
    return distance + 10;
}
console.log(runMore(20));

The runMore function takes a parameter called distance of type number and returns a number. The parameter declaration is just like any variable declaration, but the function return comes after the parentheses and indicates what type is returned by the function. Declaring the return type explicitly is not just for readability; it also lets the compiler catch cases where the function body accidentally returns the wrong type. If you compile and run this function, it will, of course, display 30 in the terminal.

If a function returns nothing, then you can either omit the return type and let TypeScript infer it, or you can declare void to be more explicit. Note that void and undefined are not the same thing in TypeScript: void is a signal that the caller should not use the return value, even though at runtime such a function still produces undefined. Let's look at an example of returning void. Comment out the runMore function and console log, and then compile and run this code:

function eat(calories: number) {
    console.log("I ate " + calories + " calories");
}
function sleepIn(hours: number): void {
    console.log("I slept " + hours + " hours");
}
let ate = eat(100);
console.log(ate);
let slept = sleepIn(10);
console.log(slept);

The two functions both return void, but only the sleepIn function is explicit about that. Here's the output:

Figure 2.7 – Function void results

Figure 2.7 – Function void results

As you can see, their internal console.log statements do run and display messages. However, the two variables, ate and slept, which accept the function returns, are both undefined, since undefined is JavaScript's value for something that has no value. So, the function return type declaration is quite similar to variable declarations.

Now, if we use function types as parameter types, it looks a bit different. Let's take a look at that in the next section.

Functions as types

It may seem a bit odd at first, but in TypeScript, a type can also be an entire function signature. Since functions are first-class values in JavaScript, they need their own types too. In TypeScript, this signature can also act as a type for an object's fields or another function's parameters.

Let's take a look at an example of this. Create a new file called functionSignature.ts and add the following code to it:

type Run = (miles: number) => boolean;
let runner: Run = function (miles: number): boolean {
    if (miles > 10) {
        return true;
    }
    return false;
}
console.log(runner(9));

The first line shows us a function type that we will be using in this code. The Run type alias is there to give the function signature a reusable name. The actual function type is (miles: number) => boolean. This is TypeScript's arrow-style syntax for function types, and it should look familiar if you've used arrow functions in JavaScript. So, the only things needed then are the parentheses to indicate parameters, the => symbol, which indicates that this is a function, and then the return type. Note that the parameter name (miles) in the type is only for documentation; the compiler matches parameters by position and type, not by name, so an implementation is free to use a different parameter name.

In the code after the function definition line, you have the declaration of the runner variable, which is of the Run type, our function type. This function simply checks whether the person has run more than 10 miles and returns true if they have and false if they have not. Now, this means that our variable runner is actually a function, and we can call it like any other function by using parentheses wrapped around a parameter value, like this: runner(9). This call is passed directly into console.log, which writes out the result of the function call. Compile and run this code and you should see this:

Figure 2.8 – Function type result

Figure 2.8 – Function type result

Calling runner with a parameter of 9 would make the function return false, which is correct.

The never type

A type called never seems quite strange at first glance, so let's try and understand it. The never type is used as a return type for a function that never returns in the normal sense, either because it always throws an error or because it runs forever in an infinite loop. At first, this sounds like the void type. However, they are quite different. In void, a function does return, in the completed sense of the word; it just does not return any value (it returns undefined, which is effectively no value). In the case of never, the function does not finish at all. Now, this may seem totally useless, but it's actually quite powerful for indicating intent.

Let's look at an example. Create a file called never.ts and add the following code:

function oldEnough(age: number): never | boolean {
    if (age > 59) {
        throw Error("Too old!");
    }
    if (age <= 18) {
        return false;
    }
    return true;
}

As you can see, oldEnough function either throws (when age is over 59) or returns a boolean, which is what never | boolean expresses. Note the annotation is redundant, since never is absorbed in unions, so it is simply boolean. On its own, never is the return type of a function that never returns (always throws or loops forever), documenting that intent; the compiler also treats code after such a call as unreachable.

 

In this section, we learned about the many built-in types in TypeScript. We were able to see why using these types can improve our code quality and help us catch errors earlier in the coding cycle. In the next section, we'll learn how we can use TypeScript to create our own types and also follow object-oriented programming (OOP) principles.

Understanding classes and interfaces

We've already briefly looked at classes and interfaces in previous sections. Let's take a deeper look and see why these types can help us write better code. Once we complete this section, we will be better prepared to write more readable, reusable code with fewer bugs.

Classes

At a high level, classes in TypeScript look like classes in JavaScript. They are a container for a related set of fields and methods that can be instantiated and reused. Modern JavaScript also supports classes with features like private fields, but TypeScript adds extra compile-time features for encapsulation, such as public, private, protected, and readonly modifiers, which we'll see shortly. Let's take a look at a new example.

We will be creating many files in this project, and some of them will use the same name for some types. If you get errors about "duplicate identifiers," just go to the other file and comment out that code.

Create a new file called classes.ts and enter the following code:

class Person {
    constructor() {}
    msg: string = "";
    speak() {
        console.log(this.msg);
    }
}
const tom = new Person();
tom.msg = "hello";
tom.speak();

This example has a simple class called Person that is very similar to something you would see in JavaScript. Let's explain this code in bullet form to make it easier to follow:

  • Person: Firstly, we have a name for the class so that it can be easily reused.
  • constructor: Next, we have a method called constructor that can help build an instance of the class. Constructors are used to initialize any fields that the class might have and to do any other setup for the class instance, such as running functions (in this case, it does nothing).
  • msg: Then, we have a single field called msg, which is called using the this keyword.
  • this: Inside a class method, the this keyword represents the running instance of the class, in other words, an actual object instance of the Person class. This is a JavaScript feature and indicates that the field or method being referenced belongs to that object and that object alone. Note that the value of this in JavaScript depends on how a function is called, not where it is defined, so passing a method around as a standalone function can change what this refers to; for now, we'll just use it inside class methods where it behaves as described.
  • speak: Next, there is a method called speak that writes the msg value to the console. We then create an instance of our class. Finally, we set the msg field to a value of hello and call the speak method.

Now, let's look at how classes differ between TypeScript and JavaScript.

Access modifiers

We stated previously that one of the main principles of object-oriented development is encapsulation, or information hiding. Well, if we take a look at the code again, clearly, we are not hiding the msg variable as it is exposed and editable outside of the class. This is the default accessibility of all class members in TypeScript. If an explicit accessor is not set, that member is accessible and modifiable outside of the class. If you want this behavior explicitly, you can set the public accessor, but this shouldn't be necessary since again, it is the default.

Let's see what TypeScript allows us to do with accessors. Let's update the code like this:

class Person {
    constructor(private msg: string) {}
    
    speak() {
        console.log(this.msg);
    }
}
const tom = new Person("hello");
// tom.msg = "hello";
tom.speak();

As you can see, we updated the constructor with a parameter that uses a keyword called private. This syntax is called a parameter property, and it does two things in one line. Firstly, it tells the compiler that the class has a field called msg of the string type that should be private. Secondly, by adding this field to the constructor, we are saying that whenever we call the constructor, e.g., new Person("hello"), we want the msg field to be set to whatever the parameter is set to.

Now, what does setting something to private actually do? By setting the field to private, we make it inaccessible from outside the class. The result of this is that tom.msg = "hello" no longer works and causes an error. Try removing the comments, //, before tom.msg = "hello" and compiling. You should see this message:

Figure 2.9 – Classes error

Figure 2.9 – Classes error

As you can see, it complains that a private member, msg, cannot be accessed from outside of the class. Also, please note that we only applied our modifier to a field, but access modifiers can be applied to any member field or method.

Let's continue modifying this code. Update the Person type with this code:

class Person {
  private msg: string;

  constructor(msg: string) {
    this.msg = msg;
  }

  speak() {
    this.msg = "speak " + this.msg;
    console.log(this.msg);
  }
}

const tom = new Person("hello");
// tom.msg = "hello";
tom.speak();

As you can see, we've modified how the msg variable is being set and initialized. This code does the same thing as the code we just saw previously. However, it's a more verbose version.

Now, thus far, we've only been using the private accessor. However, keep in mind that as we discussed in Chapter 1, there is the alternative # symbol, which is part of JavaScript, for making members private. An important difference between the two is that TypeScript's private is only enforced at compile time and disappears in the emitted JavaScript, so the field is still reachable at runtime through tricks like bracket access, whereas JavaScript's # private fields are truly private at runtime. Once we begin building our application, I will utilize that symbol most of the time unless there is some particular capability that only the TypeScript-style accessor can provide:

class Person {
    constructor(private readonly msg: string) {}
    
    speak () {
        this.msg = "speak " + this.msg;
        console.log(this.msg);
    }
}
const tom = new Person("hello");
// tom.msg = "hello";
tom.speak();

Once you complete this update, VSCode IntelliSense complains because, in the speak function, we are attempting to change the value of msg even though it has already been set once through the constructor, which again is not allowed once you use readonly on a field.

The private and readonly access modifiers are not the only modifiers available in TypeScript. There are several other types of access modifiers. However, they will make more sense if we explain them in the context of inheritance later. Now, as we continue this chapter, we will be introducing more features of newer versions of JavaScript, which forces us to modify the TypeScript compiler configuration. So, let's take a short detour and discuss TypeScript configuration.

TypeScript configuration

TypeScript was first released in late 2012. Since then, multiple versions of ECMAScript, the official JavaScript standard, have been released. Therefore, to allow developers to target their desired JavaScript versions, and also to control various configuration settings around compilation and project setup, tsconfig.json was created. This configuration file is quite extensive and allows you to control many aspects of TypeScript configuration and transpilation, but for our purposes, we'll focus on a few of the most often-used settings. Note that starting with TypeScript 6.0 (released in March 2026), several of these defaults changed significantly, so the sections below reflect the modern defaults.

Let's learn about TypeScript configuration by adding a tsconfig.json file to the root of our Chap2 project:

As you'll recall from the beginning of this chapter, we copied over an existing tsconfig.json file. The settings we are about to create will be the same as that file, and so if you like, you can just follow along without having to recreate that file.

  1. First, open your terminal and enter this command:
    npx tsc --init

    (Make sure you use two dashes before init.) This code triggers the TypeScript compiler, tsc, and causes it to create a file called tsconfig.json in the root of our Chap2 folder. You'll also notice that, unlike our installation of TypeScript, we are not using npm. Although both command-line tools are related, npm is intended for installing dependencies, and npx is for executing them. In addition, npx allows the execution of some commands without first having to install them globally. Let's continue.

  2. Now that we've created our tsconfig.json file, let's take a look at some of the settings. Starting at the top, we can see a field called compilerOptions, and this is exactly what it seems. This section has various flags for setting compilation/transpilation and development-time IntelliSense. Here's a list of some of the more commonly used flags:
    • target: This flag is used to control which version of ECMAScript our code will be transpiled into. Remember, TypeScript is a development-time technology, and therefore, we need to select the desired version of ECMAScript that will actually run. Starting with TypeScript 6.0, the default value is ES2025; in TypeScript 5.x it was ES2016, and in earlier versions it was ES3. Note that ES5 and below are now deprecated targets, so the lowest supported target is ES2015. For our purposes, we want the absolute latest version, so write ESNext in your file, like this:
      "target": "ESNext",
    • lib: This setting will configure the ECMAScript version and the various APIs that are available during development. In other words, it will provide IntelliSense, which makes available ECMAScript capabilities and methods that are only found in those versions of ECMAScript and API that we select. As you can see, it is an array and can take multiple values. For our development, we will use the following:
      "lib": [
        "ESNext",
        "DOM",
        "DOM.Iterable"
      ] 
    • You already know what ESNext is. DOM means the API that allows us to interact with DOM nodes. Another way of saying DOM nodes is HTML elements. DOM.Iterable allows us to work with collections of DOM nodes, such as NodeList.
    • module: This flag allows us to control what type of module format we will use. A module is simply an encapsulated set of code, sort of like a class, where we can be selective about what to expose to the outside world. A module is always a single JavaScript or TypeScript file.
    • The modern form of creating modules uses ES6 syntax, which we will learn about soon. But there is an older form, called CommonJS, which is still heavily used in the Node.js ecosystem. TypeScript 6.0 changed the default for this flag from commonjs to esnext, reflecting the shift toward ESM across the ecosystem. Keep in mind that if you are targeting Node.js, using ESNext modules usually requires setting "type": "module" in your package.json. We will again use the latest format based on ESNext. Update the module flag like this:
      "module": "ESNext"
    • strict: This flag forces more type checks and stricter type rules enforcement. It is actually a flag that represents multiple related flags but exists as a single convenience flag, since many devs want the complete set of strictness rules. The sub-flags it turns on include noImplicitAny (which we mentioned earlier in the any section), strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, alwaysStrict, and useUnknownInCatchVariables. For example, the strictNullChecks sub-flag forces any variables that may get a value of null or undefined to be explicitly set to those types in their variable declaration. This is a pretty important flag since it prevents variables from unintentionally getting set as undefined and potentially causing exceptions at runtime. Another important sub-flag for OOP purposes is strictPropertyInitialization. This flag forces fields created within classes to be initialized either at declaration or in the constructor. Starting with TypeScript 6.0, strict defaults to true, whereas in earlier versions it defaulted to false and had to be enabled manually. We will leave the strict flag set to true for our project.

OK, we now have the ability to select our desired ECMAScript and API versions. So, let's continue learning about TypeScript class capabilities.

Getters and setters

Another feature of classes is actually available in both TypeScript and JavaScript: getters and setters:

  • Getter: A property that allows modification or validation of a related field before returning it
  • Setter: A property that allows modification or computation of a value before setting it to a related field

These are also known as property accessors. Let's look at an example. Create another file called getSet.ts and add the following code:

class Speaker {
  #message: string = "";
  constructor(private name: string) {}

  // getter setter
}

Our code is quickly getting more complicated. If you are new to JavaScript entirely, I provide a JavaScript refresher in Chapter 3Building Better Apps with ES6+ Features. In this chapter, I will focus on TypeScript.

The code is short, but there's a fair amount happening here, so let's go over it. First, near the very top, just after the class Speaker declaration starts, you can see that our message field is not set in the constructor but is set up as a private field, using the # symbol, and therefore it is not accessible directly from outside our class. In addition, that field is set immediately on declaration, because in TypeScript, with strict mode on, a field must have a value either on declaration or set in the constructor. The only initializer the constructor takes as a parameter is our name field.

Now, let's add our actual getter and setter and overwrite the // getter setter comment with this:

get Message() {
  if (!this.#message.includes(this.name)) {
    throw Error("message is missing speaker's name");
  }
  return this.#message;
}

set Message(val: string) {
  let tmpMessage = val;
  if (!val.includes(this.name)) {
    tmpMessage = this.name + " " + val;
  }
  this.#message = tmpMessage;
}

You can see that we start by declaring the get Message() property accessor. This is our getter. In the getter, we test to see whether our message field value has the speaker's name in it by using the JavaScript includes function. This function is like contains in other languages, and it searches for the given parameter as a substring of the original string. Now, if our if statement does not find the speaker's name (the ! symbol is a negation symbol), we throw an exception to indicate an unwanted situation. Notice also that our message field is called this.#message. This is because when we use the # symbol when declaring a field, we must always use it subsequently when calling the associated field. The setter, also called Message, is indicated by the set keyword, and this property receives a string and adds the speaker's name if needed by checking whether it is missing from the message field.

Note that although both getter and setter look like functions under the hood, syntactically they are accessed like fields, without parentheses. When they are used later in code, you read and assign to them as if they were regular properties.

Now then, after the definition of our Speaker class is finished, we have the actual instantiation and usage of our class, as shown here. Add this code below the setter:

const speaker = new Speaker("john");
speaker.Message = "hello";
console.log(speaker.Message);

The speaker object is instantiated as a new speaker with the name john and its Message property is set to hello. This mechanism allows us to set the message field without actually exposing it to the outside world. Thereafter, the message is written to the console.

Let's compile and run this code:

Figure 2.10 – getSet output

Figure 2.10 – getSet output

To drive the point home further, let's try switching the speaker.Message = "hello" line to speaker.message = "hello". If you compile, you should see this error:

Figure 2.11 – Message field error

Figure 2.11 – Message field error

This occurred because message is a private field and cannot be accessed from outside our class directly.

You may be wondering why I mentioned getters and setters here when they are available in regular JavaScript, too. The honest answer is that in this example nothing is strictly TypeScript-only, # private fields and get/set are both standard JavaScript features today, but getters and setters are such a core part of how we do encapsulation in class-based OOP that they belong in any serious discussion of classes. If you look at the example, you can see that the message field is private and the getter and setter properties are public. So, to allow good encapsulation, a common pattern is to hide our field and only expose it when needed via a getter and/or setter or some method that allows modification of the field. Also, remember that when deciding on an access level for your members, you want to start with the most restrictive capabilities first and then become less restrictive as needed.

More concretely, by allowing field access via getters and setters, we can run validation and transformation logic on the way in and on the way out, as we've done in our example where the setter prepends the speaker's name if it is missing and the getter refuses to return a message that doesn't contain it. This gives us much tighter control over what enters and leaves our class than a plain public field ever could.

Static properties and methods

Finally, let's discuss static properties and methods. When you mark something as static inside a class, you are saying that this member is a member of the class type and not of the class instance. In other words, you access it through the class name (e.g., ClassA.typeName) rather than through an instance (a.typeName). Note that, like get/set and # private fields, static is a standard JavaScript feature rather than something TypeScript adds on top.

Let's look at an example. Create a new file called staticMember.ts and add the following code:

class ClassA {
    static typeName: string;
    constructor(){}
    
    static getFullName() {
        return "ClassA " + ClassA.typeName;
    }
}
const a = new ClassA();
console.log(a.typeName);

If you attempt to compile this code, it will fail with a message along the lines of "Property 'typeName' does not exist on type 'ClassA'. Did you mean to access the static member 'ClassA.typeName' instead?" Again, static members must be accessed using the class name. Here is the fixed version of the code:

class ClassA {
    static typeName: string;
    constructor(){}
    
    static getFullName() {
        return "ClassA " + ClassA.typeName;
    }
}
const a = new ClassA();
console.log(ClassA.typeName);

As you can see, we reference typeName with the class name.

So then, the question is, why might you want to use a static member instead of an instance member? There are a few common reasons: you might want a utility or helper method that doesn't need any instance state (the getFullName method in the previous example is a simple case of this), you might want to define constants that belong to the class itself, or you might want to share data across class instances. For example, you might want to do something like this:

class Runner {    
    static lastRunTypeName: string;
    constructor(private typeName: string) {}
    
    run() {        
        Runner.lastRunTypeName = this.typeName;
    }
}
const a = new Runner("a");
const b = new Runner("b");
b.run();
a.run();
console.log(Runner.lastRunTypeName);

In the case of this example, I am trying to determine the last class instance that has called the run function at any given time. If you compile and run this code, you will see that the displayed value in the terminal will be a, because a's run method ran last. Note that this "last instance" pattern is fine as an illustration, but in real code it is shared mutable global state and is usually better avoided.

Another point to be aware of is that inside a class, static members can be accessed by both static and instance members. However, static members cannot access instance members, because when a static method runs there is no specific instance for this to refer to.

One last note: if you are using strict mode (which is the default from TypeScript 6.0 onward), you may notice that declaring static typeName: string; without an initializer works here even though strictPropertyInitialization normally requires fields to be initialized. That check applies to instance fields, not to static ones, so a static field can stay uninitialized until it is first assigned.

Now, we have learned about classes and their features in this section. This will help us design our code for encapsulation, which will enhance its quality. Next, we will learn about interfaces and contract-based coding.

Interfaces

In OOP design, another important principle is abstraction. The goal of abstraction is to reduce complexity and the tight coupling of code by not exposing the internal implementation (we already covered abstraction in Chapter 1Understanding TypeScript). One way of doing this is to use interfaces to show only the signature of a type, as opposed to its internal workings. An interface is also sometimes called a contract, since having specific types for parameters and return types enforces certain expectations between both the user and the creator of the interface. So, another way of thinking about interfaces is as strict rules about what can come out of and go into values of that type. Note that, as we hinted earlier in the type aliases section, a type alias describing an object shape can do much of the same job as an interface; the main practical differences are that interface declarations with the same name automatically merge, interfaces are conventionally used to describe the shape a class must implement, and interfaces cannot describe unions or other non-object types, whereas type aliases can.

Now, interfaces are just a set of rules. In order to have working code, we need an implementation of those rules to get anything done. So, let's show an example of an interface with implementation to get started. Create a new file called interfaces.ts and add the following interface definition:

interface Employee {
    name: string;
    id: number;
    isManager: boolean;
    getUniqueId: () => string;
}

This interface defines an Employee type that we will later use as the shape for concrete objects. As you can see, there is no implementation of the getUniqueId function, just its signature. The implementation comes later when we define it. Remember that an interface exists only at compile time; it does not emit any JavaScript, so you never new an interface the way you new a class.

Now, add the implementation to the interfaces.ts file. Insert the following code, which creates two instances of the Employee interface:

const linda: Employee = {
    name: "linda",
    id: 2,
    isManager: false,
    getUniqueId: (): string => {
        let uniqueId = linda.id + "-" + linda.name;
        if(!linda.isManager) {
            return "emp-" + uniqueId;
        }
        return uniqueId;
    }
}
console.log(linda.getUniqueId());
const pam: Employee = {
    name: "pam",
    id: 1,
    isManager: true,
    getUniqueId: (): string => {
        let uniqueId = pam.id + "-" + pam.name;
        if(pam.isManager) {
            return "mgr-" + uniqueId;
        }
        return uniqueId;
    }
}
console.log(pam.getUniqueId());

So, we create an object literal called linda that conforms to the Employee interface, setting the two field names, name and id, and then implementing the getUniqueId function. Later, we console log linda.getUniqueId call. After that, we create another object, called pam, that also conforms to the same interface. However, not only does it have different field values, but its implementation of getUniqueId is also different from the linda object. This is one of the main uses of interfaces: to allow for a single structure across objects but to enable different implementations. In practice, the more common way to provide that implementation is to have a class declare implementsEmployee and fill in the members there; the object literal form we are using here is just the simplest way to demonstrate the idea. In this way, we provide strict rules for the type structure, but also allow some flexibility in terms of how functions go about doing their work. Here's the output of our code:

Figure 2.12 – Employee interface results

Figure 2.12 – Employee interface results

Another possible use of interfaces is when using third-party APIs. Sometimes, the type information is not well documented, and all you're getting back is untyped JSON or the object type is extremely large and has many fields you will never use. It is quite tempting, under these circumstances, to just use any as the type and be done with it, but as we saw in the any section, that effectively disables type checking for those values. However, you should prefer providing a type declaration if at all possible.

What you can do under these circumstances is to create an interface that has only the fields that you know and care about. Then, you can declare your data type to be of this type. At development time, TypeScript will not be able to check the type since for API network calls, data will be coming in at runtime. Regardless, since TypeScript only cares about the shape of any given type, it will ignore the fields not mentioned in your type declaration, and as long as the data comes in with the fields you defined in your interface, the runtime will not complain and you will maintain development-time type safety. However, please do ensure you handle null and undefined fields appropriately, as they can cause exceptions during runtime.

In this section, we learned about interfaces, how they act as contracts for the shape of a value, and how they relate to type aliases and classes. We will be able to use interfaces to abstract away the implementation details of a class and, therefore, produce loose coupling between our code and, thus, better code quality. In the next section, we will learn about how classes and interfaces allow us to perform inheritance and, therefore, code reuse.

Understanding inheritance

In this section, we'll learn about inheritance. Inheritance in OOP is often described as a method for code reuse, though its deeper purpose is to model "is-a" relationships and enable polymorphism. When used well, it can shrink our application code size; generally, shorter code tends to have fewer bugs. So, this can improve our app quality once we get started building. Note that, like most of the class features we've seen so far, class, extends, and super are all standard JavaScript; TypeScript mainly adds the access modifiers and the implements keyword on top.

As stated, inheritance is primarily about allowing code reuse. Inheritance is also conceptually designed to be like real-life inheritance so that the logical flow of inheritance relationships can be intuitive and easier to understand. Let's look at an example of this now. Create a file called classInheritance.ts and add the following code:

class Vehicle {
    constructor(private wheelCount: number) {}
    showNumberOfWheels() {
        console.log(`wheels: ${this.wheelCount}`);
    }
}
class Motorcycle extends Vehicle {
    constructor() {
        super(2);
    }
}
class Automobile extends Vehicle {
    constructor() {
        super(4);
    }
}
const motorCycle = new Motorcycle();
motorCycle.showNumberOfWheels();
const autoMobile = new Automobile();
autoMobile.showNumberOfWheels();

A quick note if you've never seen backticks, ``, and ${} before: It's called string interpolation and is simply a quick and easy way to insert variable values inside strings.

As you can see, there is a base class, also known as a parent, called Vehicle. This class acts as the main container for source code that is being reused later by whatever classes inherit from it, also known as children or subclasses. The child classes inherit from Vehicle by using the extends keyword. One thing to notice is that in the constructor for each child class, you see a call to super. In a subclass constructor, you must call super before you access this; a common convention is to simply put it on the first line. In this case, super refers to the Vehicle class constructor. (If a subclass does not define its own constructor at all, JavaScript generates one that just forwards its arguments to super).

Now, as you can see, each child is passing a different number of wheels to the parent's wheelCount variable via the parent's constructor. Then, at the end of the code, an instance of each child, Motorcycle  and Automobile, is created and the showNumberOfWheels  method is called. That method call to showNumberOfWheels might seem a bit strange since it's defined inside the parent Vehicle class. However, that's the point of inheritance—that we can make use of code from base classes when we extend them. If we compile and run this code, we get the following:

Figure 2.13 – The classInheritance result

Figure 2.13 – The classInheritance result

So, then, each child provides a different number of wheels to the parent wheelCount variable, although they cannot access the variable directly because it's private. Now, let's say that there was a reason why the child classes would want to access the wheelCount variable of the parent directly. For example, let's say that if a flat tire occurred, an updated wheel count would be necessary. What could we do? Well, let's try creating a function unique to each child class that tries to update wheelCount. Let's see what happens. Update the code by adding a new function, updateWheelCount, to the Motorcycle class:

class Vehicle {
    constructor(private wheelCount: number) {}
    showNumberOfWheels() {
        console.log(`wheels: ${this.wheelCount}`);
    }
}
class Motorcycle extends Vehicle {
    constructor() {
        super(2);
    }
    updateWheelCount(newWheelCount: number){
        this.wheelCount = newWheelCount;
    }
}
class Automobile extends Vehicle {
    constructor() {
        super(4);
    }
}
const motorCycle = new Motorcycle();
motorCycle.showNumberOfWheels();
const autoMobile = new Automobile();
autoMobile.showNumberOfWheels();

As a test, we updated only the Motorcycle class and added an updateWheelCount method to it, and this gave us an error. Can you guess why? It's because we are trying to access a private member of the parent class. Even when child classes inherit their members from a parent, they still do not have access to that parent's private members. This is the right behavior, again, to promote encapsulation. So, then, what do we do? Well, let's try editing the code again to allow this:

class Vehicle {
    constructor(protected wheelCount: number) {}
    showNumberOfWheels() {
        console.log(`wheels: ${this.wheelCount}`);
    }
}
class Motorcycle extends Vehicle {
    constructor() {
        super(2);
    }
    updateWheelCount(newWheelCount: number){
        this.wheelCount = newWheelCount;
    }
}
class Automobile extends Vehicle {
    constructor() {
        super(4);
    }
}
const motorCycle = new Motorcycle();
motorCycle.showNumberOfWheels();
const autoMobile = new Automobile();
autoMobile.showNumberOfWheels();

Do you see the small change we made? We changed the wheelCount parameter on the Vehicle parent class constructor to be of the protected accessor type. The protected type allows the class and any inheriting classes to have access to the member.

Before we move on to the next topic, let's briefly mention the concept of namespaces. Namespaces are an older TypeScript feature that act as containers grouping related classes, functions, and variables under a single name, somewhat like a module. Here's a simple example. Create a new file called namespaces.ts and add the following code:

namespace A {
    class FirstClass {}
}
namespace B {
    class SecondClass {}
    const test = new FirstClass();
}

As you can see from this code, even before compiling, VSCode IntelliSense is already complaining that FirstClass cannot be found. This is because it is defined inside namespace A and is not visible from namespace B unless accessed as A.FirstClass. That said, in modern TypeScript code, namespaces are considered a legacy feature and ES modules (import/export across files) are the preferred way to organize code; we mention namespaces here mostly so you recognize them if you encounter them in older codebases.

In this section, we learned about inheriting from classes. Class inheritance is a very important tool for reusing code. In the next section, we'll look at using abstract classes, which is a more flexible way of doing inheritance.

Abstract classes

As mentioned previously, interfaces can be useful for defining contracts, but they contain no implementation, no state, and no constructors. Regular classes have working implementations, but sometimes only a signature is required for some members while others still need concrete code. For these types of scenarios, where you want a single type to mix abstract rules with real implementation and shared state, you would use an abstract class instead of either a class or an interface. Let's create a new file called abstractClass.ts and copy and paste our code from our classInheritance.ts file into it. If you do this, you might get some errors, since the two files both have the same class and variable names. The cleanest fix is to turn each file into a module (for example by adding export {} at the top), but you can also simply comment out the code in the file you are not using whenever this occurs.

So, in our new abstractClass.ts file, we are going to update it with namespaces and modify the Vehicle class to be abstract. Add the namespace and update the Vehicle class like this:

namespace AbstractNamespace {
    abstract class Vehicle {
        constructor(protected wheelCount: number) {}
        abstract updateWheelCount(newWheelCount: number): void;
        showNumberOfWheels() {
            console.log(`wheels: ${this.wheelCount}`);
        }
    }
  // the rest of our existing code
}

So, to start, we've wrapped all the code within a scope called namespace AbstractNamespace. This is merely a container that prevents the members of our abstractClass.ts file from bleeding into the global scope. As we noted in the previous section, namespaces are a legacy feature and modules are the modern way to handle scoping; we're using a namespace here only to keep this self-contained example short, and in real code you would turn the file into a module instead.

If you look at the new Vehicle code, we have a new keyword before the class called abstract. This is what indicates that the class will be an abstract one. You can also see that we have a new function called updateWheelCount. This function has an abstract keyword in front of it, which indicates that it will have no implementation within the Vehicle class and needs to be implemented by an inheriting class.

Now, after the Vehicleabstract class definition, we want our child classes to implement our Vehicle class. So, add the Motorcycle and Automobile classes below the Vehicle class:

    class Motorcycle extends Vehicle {
        constructor() {
            super(2);
        }

        updateWheelCount(newWheelCount: number){
            this.wheelCount = newWheelCount;
            console.log(`Motorcycle has ${this.wheelCount}`);
        }
    }

    class Automobile extends Vehicle {
        constructor() {
            super(4);
        }

        updateWheelCount(newWheelCount: number){
            this.wheelCount = newWheelCount;
            console.log(`Automobile has ${this.wheelCount}`);
        }

        showNumberOfWheels() {
            console.log(`wheels: ${this.wheelCount}`);
        }
    }
    // more code here

After adding the classes, we instantiate them and call their respective updateWheelCount methods, as shown:

  const motorCycle = new Motorcycle();
    motorCycle.updateWheelCount(1);

    const autoMobile = new Automobile();
    autoMobile.updateWheelCount(3);

As you can see, the implementation of the abstract member updateWheelCount is in the child classes. This is the capability that an abstract class provides. An abstract class can act both like a regular class, providing member implementations, and like an interface, providing only the rules to be implemented by a sub-class.

Since an abstract class can have abstract members, you cannot instantiate an abstract class.

If you review the code of the Automobile class, you can see that it has its own implementation of showNumberOfWheels, even though this function is not abstract. This demonstrates something called overriding, which is the ability of a child to create a unique implementation of the parent's member and use that implementation instead.

In this section, we learned about the different kinds of class-based inheritance, including how abstract classes let us combine contract-style rules with shared implementation in one place. In the next section, we'll learn about doing inheritance with interfaces and how it's different from class-based inheritance.

Interface inheritance

As explained earlier, interfaces are a way of setting rules for a type. This is also sometimes called a contract. Interfaces will allow us to separate implementation from definition and, therefore, provide abstraction. One thing worth noting up front is that, unlike classes, an interface can extend multiple other interfaces at once (interface C extends A, B {}), and a class can implement multiple interfaces at the same time (class X implements A, B, C {}). Let's learn how to use interfaces with inheritance.

Create a new file called interfaceInheritance.ts and add the following code:

namespace InterfaceNamespace {
    interface Thing {
        name: string;
        getFullName: () => string;
    }
    interface Vehicle extends Thing {
        wheelCount: number;
        updateWheelCount: (newWheelCount: number) => void;
        showNumberOfWheels: () => void;
    }

    // more code coming here
}

After the namespace, you can see that there is an interface called Thing, and after that, the Vehicle interface is defined, and it inherits from Thing using the extends keyword. (As in the previous section, the namespace here is only a quick way to keep the example self-contained; in real code you would make the file a module instead.) I put this into the example to show that interfaces can also inherit from other interfaces. The Thing interface has two members: name and getFullName—and as you can see, although Vehicle extends Thing, there is no mention of those members anywhere inside of Vehicle. This is because Vehicle is an interface and therefore cannot have any implementation.

Now, if you look at the following code, you will see that the Motorcycle class uses the implements keyword to define implementations of the Vehicle interface's members. Replace the previous code's comment with this:

    class Motorcycle implements Vehicle {
        name: string;
        wheelCount: number = 0;
        constructor(name: string) {
            // no super for interfaces
            this.name = name;
        }
        updateWheelCount(newWheelCount: number){
            this.wheelCount = newWheelCount;
            console.log(`Motorcycle has ${this.wheelCount}`);
        }
        showNumberOfWheels() {
            console.log(`Motorcycle has ${this.wheelCount} wheels`);
        }
        getFullName() {
            return "MC-" + this.name;
        }
    }
    const moto = new Motorcycle("beginner-cycle");
    console.log(moto.getFullName());
}

You may have noticed the // no super for interfaces comment in the constructor. Unlike extending a class, there is no super() call when implementing an interface, because an interface is purely compile-time: it does not emit any JavaScript, so there is no parent constructor to invoke. The implements keyword itself also only performs a compile-time check that the class has all the required members; at runtime, the interface has vanished entirely and nothing is enforced.

Also, notice how by implementing the Vehicle interface, our Motorcycle class must implement both the members of Vehicle and Thing. This occurs because Vehicle extends Thing. So, if we compile and run this code, we get the following:

Figure 2.14 – The interfaceInheritance result

Figure 2.14 – The interfaceInheritance result

Interfaces do not provide code reuse directly, since they have no implementation of their own. What they do provide is a stable, type-safe contract: a function can accept any object that conforms to an interface without caring about the concrete class behind it, which is the essence of polymorphism. Hiding the implementation behind an interface is also beneficial in terms of encapsulation and abstraction, which are also important principles of OOP.

In this section, we learned about inheritance and how it helps us model relationships between types and share behavior in a structured way. We learned about how to do inheritance with the three major building blocks of TypeScript's OOP: classes, abstract classes, and interfaces. In the next section, we will cover generics.

Understanding polymorphism

Polymorphism is a bit of an intimidating name (it comes from Greek: poly = many, morph = form), but it's actually quite powerful because it allows us to write code that works with a general type and lets the specific implementation vary at runtime, all while maintaining type safety. The key idea is that the caller doesn't need to know which concrete type it is working with.

Let's look at some code and see how this works. Create a file called polymorphism.ts and add the following code. This is going to be a fair bit of code, so let's go through it in pieces:

interface Animal {
  name: string;
  runMaxMiles(hours: number): number;
}

First, we've created an interface that shows an Animal type. This type has a name and a method signature of runMaxMiles. Obviously, since this is an interface, our intention is to implement this interface with specific capabilities, so let's do that now:

class Wolf implements Animal {
  name: string = "";
  runMaxMiles(hours: number): number {
    return hours * 45;
  }
}

class Cheetah implements Animal {
  name: string = "";
  runMaxMiles(hours: number): number {
    return hours * 75;
  }
}

Here, we've created two distinct implementations: Wolf and Cheetah. Each one has a certain distance it can cover within a given time period—in other words, miles per hour. Let's continue by adding the portion of our code that uses our Animal interface polymorphically. The function below selects an animal based on the duration, then calls runMaxMiles through the Animal interface without caring which concrete class is behind it:

const hours = 0.5;
function pickTheBestAnimalToRun(hours: number): number {
  let animal: Animal | undefined;
  if (hours >= 0.5) {
    animal = new Wolf();
    animal.name = "wolfie";
  } else {
    animal = new Cheetah();
    animal.name = "cheetos";
  }

  if (animal instanceof Wolf) {
    console.log("This is a wolf");
  }
  if (animal instanceof Cheetah) {
    console.log("This is a cheetah");
  }
  return animal.runMaxMiles(hours);
}

pickTheBestAnimalToRun(hours);

So, what have we done in our pickTheBestAnimalToRun function? First, we created an animal variable of the Animal type. Then, based on the hours needed, we select either a Wolf or a Cheetah. This selection part is really a factory pattern: picking which concrete class to create. The actual polymorphism happens at the bottom: animal.runMaxMiles(hours) works correctly regardless of whether animal is a Wolf or a Cheetah, because both conform to the Animal interface. If we later added a third class, say Horse implements Animal, this same line would work unchanged. That is the essence of polymorphism: existing code stays closed to modification but open to extension.

The example also includes instanceof checks that print which concrete type was selected. These are here just for demonstration logging; in real code, heavy use of instanceof to branch on concrete types is actually the opposite of polymorphism and usually means the interface should be doing the work instead:

Figure 2.15 – The polymorphism result

Figure 2.15 – The polymorphism result

As you can see, our code runs and the pickTheBestAnimalToRun function selects the wolf since our hours value was 0.5. The important takeaway is that the call to runMaxMiles worked through the Animal interface without the caller needing to know it was specifically a Wolf.

Note that TypeScript's structural typing, which we covered earlier, means that polymorphism here works by shape, not by name. A class with the right name and runMaxMiles members would be compatible with Animal even without writing implements Animal;. The implements keyword just makes the compiler verify the shape for you at the point of declaration.

Next, let's learn about generics, which provide another form of polymorphism called parametric polymorphism, where a type itself becomes a parameter.

Learning generics

Generics allow a type definition to include an associated type that can be chosen by the user of the generic type, instead of being dictated by the type creator. Think of as a parameter, but for types instead of values. In this way, there are structures and rules, but still some amount of flexibility. Generics will definitely come into play later when we code with React, so let's learn about them here.

Generics can be used for functions, classes, and interfaces. Let's look at an example of generics with functions. Create a file called functionGeneric.ts and add the following code:

function getLength<T>(arg: T): number {
    if(arg.hasOwnProperty("length")) {
        return arg["length"];
    }
    return 0;
}
console.log(getLength<number>(22));
console.log(getLength("Hello world."));

Note that this code has errors, but we'll work through those together. If we start at the top, we see a function called getLength<T>. This function uses a generic, <T>, that tells the compiler that wherever it sees the T symbol, it can expect some associated type. Now, our function implementation checks to see whether the arg parameter has a field called length, by using the hasOwnProperty function. This function is built into JavaScript, and as you can see, it checks whether a certain property exists. If it does not have a length value, it just returns 0. Finally, toward the bottom, you can see that the getLength function is called two times: once for a number and another time for a string. Additionally, you can see that for number, it explicitly has the <number> type indicator, whereas for string, it does not. These two examples are there only to show that you can be explicit about what the associated type is, but the compiler can usually figure out which type you meant based on the usage.

Now, the first issue with this code is the errors. When T is unconstrained, TypeScript treats it much like unknown: you cannot access any properties or call any methods on it, because the compiler has no guarantee about what T actually is. That is why both the hasOwnProperty call and the bracket notation arg["length"] produce errors. So, let's update this code to eliminate these errors.

First, comment out the code we just wrote and add the following new code below it:

interface HasLength {
    length: number;
}
function getLength<T extends HasLength>(arg: T): number {
    return arg.length;
}
console.log(getLength<number>(22));
console.log(getLength("Hello world."));

This code is quite similar, except we use a HasLength interface to constrain what types are allowed. Constraining generic types is done with the extends keyword. By writing T extends HasLength, we are telling the compiler that whatever T is, it must inherit from or be of the HasLength type, which effectively means that it must have the length property. Therefore, when the two previous getLength calls are made, it fails for number types, since they don't have a length property, but it works for string.

OK, now let's look at an example that uses interfaces and classes together. Let's create a file called classGeneric.ts and add the following code to it:

namespace GenericNamespace {
    interface Wheels {
        count: number;
        diameter: number;
    }
    interface Vehicle<T> {
        getName(): string;
        getWheelCount: () => T;
    }
    // more code coming here
}

So, we can see that we have an interface called Wheels, which provides wheel information such as count and diameter. We can also see that the Vehicle interface takes a generic of type T. You can probably guess what we're going to do, but let's continue by adding another type, Automobile, and replace the comment line with it:

  class Automobile implements Vehicle<Wheels> {
        constructor(private name: string, private wheelsWheels){}
        getName(): string {
            return this.name;
        }
        getWheelCount(): Wheels {
            return this.wheels;
        }
    }

We see that the Automobile class implements the Vehicle interface with the generic as the Wheels type, which gives an implementation to the getName and getWheelCount methods. Then, finally, let's add another class into the namespace just below our Automobile class called Chevy:

    class Chevy extends Automobile {
        constructor() {
            super("Chevy", { count: 4, diameter: 18 });
        }
    }

In the case of the Chevy class, we do not need to implement anything because we are directly inheriting from Automobile and, therefore, receiving its methods without having to also create them inside of Chevy. After all these types are defined, add this code after the Chevy class:

    const chevy = new Chevy();
    console.log("car name ", chevy.getName());
    console.log("wheels ", chevy.getWheelCount());

The last three lines create an instance of the Chevy class, and then its methods are called inside the console logs. If you compile and run, you should see this:

Figure 2.16 – The classGeneric.ts result

Figure 2.16 – The classGeneric.ts result

You can see that our inheritance hierarchy is several levels deep, but our code is able to successfully return a valid result.

In this section, we learned about using generics on both functions and class types. Generics are a foundational feature of TypeScript that you will encounter everywhere, from collections and utility types to API wrappers, and they are especially common in React development, as we'll see soon.

Utility types

Utility types are created by the TypeScript team and provide pre-built solutions for common use cases in TypeScript. In this section, we'll learn about a few of the more commonly used utility types.

ReturnType<Type>

ReturnType is a very useful utility type, especially when working with functions whose return types you don't control directly. As the name implies, ReturnType<Type> allows us to create a type from the type a function returns. Let's create an example. Create the returnType.ts file and add this code to it:

function getData() {
  return [
    {
      name: "jon",
      age: 24,
    },
    {
      name: "linda",
      age: 35,
    },
    {
      name: "tom",
      age: 21,
    },
  ];
}

As you can see, this function returns person data as an array. It does not have an explicit return type, so we want to type that explicitly. Let's add some more code to create that explicit type. Add this underneath the function:

type Result = ReturnType<typeof getData>;

Let's unpack this code. First, we start with a type alias called Result. Then, we make our ReturnType call and add a type in between the arrow brackets. The code typeof getData tells TypeScript to get the return type of the getData function and then associate it with ReturnType. The ReturnType is a generic. Note that you will learn more about typeof in Chapter 3, Building Better Apps with ES6+ Features. This line of code makes our Result type look like this:

{
  name: string;
  age: number;
}[]

Notice that it is an array type, as indicated by the square brackets. Next, let's add the code that will call our getData function and view its results:

const result: Result = getData();
console.log(result);

As you can see, we have a variable, result, of type Result that receives the getData function's return value, and then we log it on the console. If you compile and run this code, the result will look like this:

Figure 2.17 – The returnType.ts result

Figure 2.17 – The returnType.ts result

Pick<Type, Keys>

Sometimes, when dealing with things such as an API, we may find that the type has more information than we actually need. This may make dealing with that type unnecessarily complex. Therefore, this utility type allows us to be selective about which properties we care about and only populate our type with those properties.

Let's create an example. Create a new file, pick.ts, and add this code:

interface SuperComplexType {
  name: string;
  age: number;
  street: string;
  state: string;
  zip: string;
  employeeId: number;
  dateStarted: Date;
  favoriteFood: string;
  favoriteColor: string;
  favoriteSportsTeam: string;
  homeTown: string;
  corporateOfficeCity: string;
}

Now, let's pretend that the API we need to call is returning an object that has this type of information. Clearly, if we didn't need every single field here, it would be cumbersome to have to deal with such a type. So, let's create our own type with just the fields we need by using Pick. Add this code below SuperComplexType:

type SimpleType = Pick<
  SuperComplexType,
  "name" | "age" | "corporateOfficeCity"
>;

const adam: SimpleType = {
  name: "adam",
  age: 33,
  corporateOfficeCity: "New York",
};

Now, starting at the SimpleType line, you can see that we use the Pick utility type, pass our original SuperComplexType, and then select each field that we want in our new type. If you hover over this new SimpleType, you will see that it only includes the fields we selected. The adam variable was added just to give us confirmation that this new type works as expected. Note that Pick only affects the type at compile time; it does not filter out fields from the actual runtime data. If the API sends all twelve fields, they will still be there in memory, but TypeScript will only let you access the three you picked.

Omit<Type, Keys>

Now, if there's a type for including only the properties you want, you would think there's a type for removing the ones you don't. There is: Omit<Type, Keys>. Let's revisit the Pick example we just used and revise it for Omit. Create a file called omit.ts and add this code:

// copy SuperComplexType here
type SimpleTypeOmit = Omit<
  SuperComplexType,
  "name" | "age" | "corporateOfficeCity"
>;

Make sure to copy the SuperComplexType interface to the top of your omit.ts file. If you hover over the SimpleTypeOmit type, it will show you a list of all the properties inside of SuperComplexType except the three properties you included in the Omit type.

There are many utility types that you can choose from to provide helpers to improve and streamline your code. These utility types are part of TypeScript's standard library, well-tested, and widely used. Therefore, before rolling your own helper, you should check the documentation and see whether there is a suitable type that already exists and use that.

Summary

In this chapter, we learned about the TypeScript language. We learned about the many different types that exist in the language and also how to create our own types. We also learned how to use TypeScript to create object-oriented code. It was a large and complex chapter but will be useful knowledge for when we begin building our app.

In the next chapter, we will review some of the most important features of JavaScript. We will also learn about some of the newer features in the later versions of the language. Since TypeScript is a true superset of JavaScript, it is important to have an up-to-date understanding of JavaScript in order to make maximal use of TypeScript.

Get this book's PDF version and more

Scan the QR code (or go to packtpub.com/unlock). Search for this book by name, confirm the edition, and then follow the steps on the page.

Image

Image

Note: Keep your invoice handy. Purchases made directly from Packt don't require an invoice.

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • New Update: Chapters on monitoring, observability, and AI-assisted development
  • Master the architecture of React 19 and modern single-page applications (SPAs)
  • Containerize with Docker and Podman, automate workflows with CI/CD and deploy to the cloud

Description

In the fast-paced world of web development, React is a widely used library for building applications, while Node.js and Express support scalable server-side solutions and web services. TypeScript enhances JavaScript projects with robustness and maintainability, making it an essential tool for large-scale applications. This edition provides a hands-on guide to mastering these technologies, with new chapters and updated content that reflects current industry practices. Begin with a solid foundation in TypeScript to build high-quality web applications. Explore React 19, leveraging the Hooks API and Redux Toolkit for state management. Then transition to server-side development with Express, incorporating modern practices like JWT-based authentication and Prisma ORM for database management. A major focus of this edition is production readiness. Learn how to containerize your application with Docker and Podman, automate builds and tests with GitHub Actions, and deploy to the cloud. New chapters add monitoring and observability with OpenTelemetry and Grafana plus a hands-on guide to AI-assisted development with LLM coding agents. Other updates include Vitest for testing and expanded content on Postgres and Prisma ORM. By the end of this book, you will have built and deployed a comprehensive full-stack application, ready for production.

Who is this book for?

This book is designed for experienced developers who are new to full-stack web development. It is ideal for those with production programming experience in any language looking to transition into building full-stack applications using modern web technologies. A good understanding of JavaScript is required.

What you will learn

  • Build a full-stack application using React 19, TypeScript, and Node.js
  • Apply TypeScript's key features to improve code quality and maintainability
  • Build React apps with functional components, Hooks and Redux Toolkit
  • Test frontend and backend code with Vitest and React Testing Library
  • Set up an Express API with PostgreSQL and Prisma from scratch
  • Secure your API with JWT authentication and authorization
  • Containerize applications with Docker and Podman, then deploy to the cloud with CI/CD
  • Add monitoring and observability, and explore AI-assisted development
Estimated delivery fee Deliver to South Africa

Standard delivery 10 - 13 business days

$12.95

Premium delivery 3 - 6 business days

$34.95
(Includes tracking information)

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Jun 29, 2026
Length: 626 pages
Edition : 2nd
Language : English
ISBN-13 : 9781803235776
Languages :
Tools :

What do you get with Print?

Product feature icon Instant access to your digital copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Redeem a companion digital copy on all Print orders
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Modal Close icon
Payment Processing...
tick Completed

Shipping Address

Billing Address

Shipping Methods
Estimated delivery fee Deliver to South Africa

Standard delivery 10 - 13 business days

$12.95

Premium delivery 3 - 6 business days

$34.95
(Includes tracking information)

Product Details

Publication date : Jun 29, 2026
Length: 626 pages
Edition : 2nd
Language : English
ISBN-13 : 9781803235776
Languages :
Tools :

Packt Subscriptions

See our plans and pricing
Modal Close icon
$19.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
$199.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just $5 each
Feature tick icon Exclusive print discounts
$279.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just $5 each
Feature tick icon Exclusive print discounts

Table of Contents

18 Chapters
Chapter 1: Understanding TypeScript Chevron down icon Chevron up icon
Chapter 2: Exploring TypeScript Chevron down icon Chevron up icon
Chapter 3: Building Better Apps with ES6+ Features Chevron down icon Chevron up icon
Chapter 4: Learning Single Page Application Concepts with React Chevron down icon Chevron up icon
Chapter 5: React Development with Hooks Chevron down icon Chevron up icon
Chapter 6: Learning Redux, Context, and React Router Chevron down icon Chevron up icon
Chapter 7: Testing with Vitest and React Testing Library Chevron down icon Chevron up icon
Chapter 8: Understanding Node.js Chevron down icon Chevron up icon
Chapter 9: Learning Express Chevron down icon Chevron up icon
Chapter 10: Database and Persistence with PostgreSQL + Prisma Chevron down icon Chevron up icon
Chapter 11: Authentication and Authorization Chevron down icon Chevron up icon
Chapter 12: Building the Full-Stack Application Chevron down icon Chevron up icon
Chapter 13: Deploying Full-Stack Applications Chevron down icon Chevron up icon
Chapter 14: Monitoring and Observability Chevron down icon Chevron up icon
Chapter 15: AI-Assisted Development with LLM Coding Agents Chevron down icon Chevron up icon
Chapter 16: Unlock Your Exclusive Benefits Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon
Index Chevron down icon Chevron up icon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

What is the digital copy I get with my Print order? Chevron down icon Chevron up icon

When you buy any Print edition of our Books, you can redeem (for free) the eBook edition of the Print Book you’ve purchased. This gives you instant access to your book when you make an order via PDF, EPUB or our online Reader experience.

What is the delivery time and cost of print book? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela
What is custom duty/charge? Chevron down icon Chevron up icon

Customs duty are charges levied on goods when they cross international borders. It is a tax that is imposed on imported goods. These duties are charged by special authorities and bodies created by local governments and are meant to protect local industries, economies, and businesses.

Do I have to pay customs charges for the print book order? Chevron down icon Chevron up icon

The orders shipped to the countries that are listed under EU27 will not bear custom charges. They are paid by Packt as part of the order.

List of EU27 countries: www.gov.uk/eu-eea:

A custom duty or localized taxes may be applicable on the shipment and would be charged by the recipient country outside of the EU27 which should be paid by the customer and these duties are not included in the shipping charges been charged on the order.

How do I know my custom duty charges? Chevron down icon Chevron up icon

The amount of duty payable varies greatly depending on the imported goods, the country of origin and several other factors like the total invoice amount or dimensions like weight, and other such criteria applicable in your country.

For example:

  • If you live in Mexico, and the declared value of your ordered items is over $ 50, for you to receive a package, you will have to pay additional import tax of 19% which will be $ 9.50 to the courier service.
  • Whereas if you live in Turkey, and the declared value of your ordered items is over € 22, for you to receive a package, you will have to pay additional import tax of 18% which will be € 3.96 to the courier service.
How can I cancel my order? Chevron down icon Chevron up icon

Cancellation Policy for Published Printed Books:

You can cancel any order within 1 hour of placing the order. Simply contact customercare@packt.com with your order details or payment transaction id. If your order has already started the shipment process, we will do our best to stop it. However, if it is already on the way to you then when you receive it, you can contact us at customercare@packt.com using the returns and refund process.

Please understand that Packt Publishing cannot provide refunds or cancel any order except for the cases described in our Return Policy (i.e. Packt Publishing agrees to replace your printed book because it arrives damaged or material defect in book), Packt Publishing will not accept returns.

What is your returns and refunds policy? Chevron down icon Chevron up icon

Return Policy:

We want you to be happy with your purchase from Packtpub.com. We will not hassle you with returning print books to us. If the print book you receive from us is incorrect, damaged, doesn't work or is unacceptably late, please contact Customer Relations Team on customercare@packt.com with the order number and issue details as explained below:

  1. If you ordered (eBook, Video or Print Book) incorrectly or accidentally, please contact Customer Relations Team on customercare@packt.com within one hour of placing the order and we will replace/refund you the item cost.
  2. Sadly, if your eBook or Video file is faulty or a fault occurs during the eBook or Video being made available to you, i.e. during download then you should contact Customer Relations Team within 14 days of purchase on customercare@packt.com who will be able to resolve this issue for you.
  3. You will have a choice of replacement or refund of the problem items.(damaged, defective or incorrect)
  4. Once Customer Care Team confirms that you will be refunded, you should receive the refund within 10 to 12 working days.
  5. If you are only requesting a refund of one book from a multiple order, then we will refund you the appropriate single item.
  6. Where the items were shipped under a free shipping offer, there will be no shipping costs to refund.

On the off chance your printed book arrives damaged, with book material defect, contact our Customer Relation Team on customercare@packt.com within 14 days of receipt of the book with appropriate evidence of damage and we will work with you to secure a replacement copy, if necessary. Please note that each printed book you order from us is individually made by Packt's professional book-printing partner which is on a print-on-demand basis.

What tax is charged? Chevron down icon Chevron up icon

Currently, no tax is charged on the purchase of any print book (subject to change based on the laws and regulations). A localized VAT fee is charged only to our European and UK customers on eBooks, Video and subscriptions that they buy. GST is charged to Indian customers for eBooks and video purchases.

What payment methods can I use? Chevron down icon Chevron up icon

You can pay with the following card types:

  1. Visa Debit
  2. Visa Credit
  3. MasterCard
  4. PayPal
What is the delivery time and cost of print books? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela
Modal Close icon
Modal Close icon