Complex Types Using Interfaces

What's Covered

We're going to start off this chapter by introducing TypeScript interfaces. This chapter examines interfaces in the context of JavaScript "data" objects and their fields/properties. As many of you know, interfaces play an over-sized role in many common design patterns (think SOLID1). We will talk about interfaces in that context in Chapter 9.

TypeScript provides other more advanced typing support that you may have come across in C# and Java. This chapter covers some of them, including:

A Note About Generics
Generics offer a very powerful data typing capability. They look and act a lot like generics in C# and are a very effective tool helping you adhere to the Don't Repeat Yourself (DRY) principle. If you aren't familiar with DRY, here's one place you could start: http://deviq.com/don-t-repeat-yourself/
Although generics are part of the type system, the tend to go hand in hand with classes, so we'll hold off on describing them until chapter 10 after you've had a chance to read about and digest TypeScript classes.
  • Enumerations: Attach a human-friendly label to a number.
  • Unions: A variable can be a "number" or "string" or "MyBrandShinyNewObject" but not anything else.
  • Custom types: Think classes but without a constructor. (If you don't know about classes, don't worry, you'll learn a it about them in chapter 9).

Interfaces as Data Describers

Declare a TypeScript interface like this:

interface myInterface {
}

That code defines a new interface called "myInterface". It's an empty interface, but nonetheless valid2.

Variables can now declare their type as being that interface:

const myVariable: myInterface;

Although there are some use cases for empty interfaces, you'll normally use them this way to describe complex objects. Let's consider a business scenario and implement a supporting data structure in plain JavaScript.

Your client owns a book store and you're developing a simple app that lets your client's customers view a listing of all available books. In JavaScript object terms, a "book" has these properties:

  • Author
  • Title
  • Genre (e.g. biography, history, sci-fi)
  • Short Description
  • Total Pages
  • Condition (e.g. New, Great, OK, Not Great)

In pure JavaScript, we might model a book this way:

var bookModel = {
    Author,
    Title,
    Genre,
    ShortDescription,
    TotalPages,
    Condition
}

That's simple enough. We have an object called "bookModel." The developer's intent is pretty clear, although there's actually plenty of room for improvement. If you want to re-use bookModel in pure JavaScript, you could clone it3:

var aBookInstance = (JSON.parse(JSON.stringify(bookModel)));

In TypeScript, we can use interfaces to define a better shape and even self-document the model. Here is one way to do it:

interface BookModel {
    Author: string;
    Title: string;
    Genre: string;
    ShortDescription: string;
    TotalPages: number;
    Condition: string;
}

When we want an actual book instance, we define it like this in TypeScript:

let aBookInstance: BookModel;

This interface shows three immediate advantages TypeScript provides over JavaScript:

  1. The developer's intent is much clearer. You can tell that TotalPages is meant to hold numeric values while the rest are meant to hold strings4.
  2. Spot-on intellisense.
  3. It's really a model. It's not a JavaScript variable masquerading as model. In fact, when you compile a TypeScript interface, it produces no JavaScript at all. Only the compiler knows about the interface. There is no run-time artifact.

Let's assume you agree that TypeScript conveys the the dev's intent more clearly than the pure JS example5. Here's a short 40 second video showing VS Code intellisense at work:

(Depending on you're reading the book, that video may not appear. In that case, click this link or go directly to the YouTube video with this link: https://youtu.be/o_wxodLGT34).

Here are some key takeaways from the video:

  1. Once you define an interface, it becomes another candidate data type. Use it the same way as the built-in data types, such as string, boolean, number, etc.
  2. Once you define a variable with an interface data type, you must usually include all of the interface fields. NOTE: As you'll soon see, it's possible to define optional interface components as well.
  3. It's not enough to add all of the interface fields to the "aBook" variable. You must also add them with the correct type. In the video, I tried to assign a string value to "TotalPages" field but the IDE told me that was not allowed.

Refactoring with Interfaces

Interfaces give us even more meaningful information and it's particularly useful when we refactor our code.

Let's imagine that we need to change our book model. When we started, we didn't realize that many books have multiple authors. As a result, we need to refactor the model and make Author an array of strings, not just a scalar / single string.

In pure JS, we don't need to do anything special. We just start writing code like this:

var bookModel = {
    Author, // NOTE! On [such and such a date], this was converted to an array
    Title,
    Genre,
    ShortDescription,
    TotalPages,
    Condition
}

var aBookInstance = JSON.parse(JSON.stringify(bookModel));
//aBookInstance.Author = "Paul Galvin";
aBookInstance.Author = ["Paul Galvin"];

It's a very simple change to make, but it's quite difficult to find all the places where you need to make the change. You mostly have to do a global search in your IDE to find instances of "Author" and refactor where you find them.

Contrast this with TypeScript:

(Depending on how you're reading the book, you may not be able to see the video. In that case, try clicking here or use the following URL in your favorite web browser: https://youtu.be/fNtcCTeMAhQ)

When I changed Author from string to string[], I invalidated every instance of every book model in the code. I can't run a successful build until I fix it. I still have a potentially tricky refactoring task on my hands - after all, I still need to fix every place in the code that references Author. However, the compiler won't let me miss any of those changes. That is powerful stuff.

Nested Objects and Interfaces

Although BookModel is technically a complex object, it's not very complex. Let's spice things up and take another look at "Author." We've already refactored the model to account for multiple authors. Authors are normal people, just like the rest of us, and in the United States and elsewhere, they usually have both a first and last name. In addition, authors love feedback. To this end, we want the author's preferred email for feedback.

This next bit of code shows the new AuthorModel and refactors BookModel to use it.

interface AuthorModel {
    FirstName: string;
    LastName: string;
    PreferredEmail: string;
}

interface BookModel {
    Authors: AuthorModel[];
    Title: string;
    Genre: string;
    ShortDescription: string;
    TotalPages: number;
    Condition: string;
}

// Example 1: Create an author object first, then add it to the book instance
const FoodBookAuthor1: AuthorModel = {
    FirstName: "Paul",
    LastName: "Galvin",
    PreferredEmail: "[email protected]"
}

const FoodBookAuthor2: AuthorModel = {
    FirstName: "Kelly",
    LastName: "Smith",
    PreferredEmail: "[email protected]"
}

const foodBook: BookModel = {
    Authors: [FoodBookAuthor1, FoodBookAuthor2],
    Title: "Foods - The Right Food for the Right Meal",
    Genre: "Life Hacks",
    ShortDescription: "Eggs are not for dinner",
    TotalPages: 158,
    Condition: "Used - Good"
}

// Example 2: Create a book instance in one line.
const GotM: BookModel = {
    Authors: [{
        FirstName: "Steven",
        LastName: "Erikson",
        PreferredEmail: "[email protected]"
    }],
    Title: "Gardens of the Moon",
    Genre: "High Fantasy",
    ShortDescription: "Empress tries to conquer city, fails, but wins something better",
    TotalPages: 772,
    Condition: "New"
}

As you can see, TypeScript supports nested objects quite nicely.

If you're using VSCode or Visual Studio, try copying in the above code. Hover your mouse over the Authors field in either Example 1 or Example 2 and then press F12. This will bring you to the definition of the object. This is very handy when trying to understand the underlying definition of a given type/interface.

Interfaces - Mapping a REST Response

We'll wrap up the discussion on interfaces by reverse engineering a REST response. In this scenario, I'm making a call out to a SharePoint REST endpoint asking for a "user"6. When I make the call, I get back a lot of information, starting with the HTTP wrapper around what I really want:

"HTTP Wrapper"
Figure: HTTP Wrapper

The HTTP wrapper consists of:

  • config (complex object)
  • data (complex object)
  • headers (complex object)
  • status (number)
  • statusText (string)

We can define an interface that matches that:


interface httpResponse {
    config: any,
    headers: any,
    status: number,
    statusText: string;
}

The above example is a bit lazy - it's not trying to model the data underlying config or headers. I'm waving my hands in their general direction by using "any." I certainly could model those objects but I'm going to focus on data instead. You'll notice that "data" is missing from the interface. Lets link that in. But first we need to define an interface that models the data portion of the REST response. To start, I need to know what the REST response is giving me:

"Data Portion of REST Response"
Figure: Data Portion of REST Response

This interface maps things nicely:

interface userProfileRestModel {
    Attachments: boolean;
    AuthorId: number;
    BPBrands: string[];
    BPDescription: string;
    "odata.editLink": string;
    // and other user profile fields
}

Note the odata.editLink field in the response - if your object's name has otherwise invalid characters in it, you can still get and set its values when you reference it via its name this way.

Now it's time to link them in. Here's the code:

interface userProfileResponse extends httpResponse {
    data: {
        value: userProfileRestModel[]
    }
}

Notice the extends keyword. I'm defining a new interface, userProfileResponse by extending the previously defined httpResponse interface. The new userProfileResponse interface contains all the fields and structure of both.

Here's another 40 second video that shows this visually.

(Depending on how you're reading the book, you may not see the video. In that case, try clicking here or go to YouTube directly in your web browser: https://youtu.be/oK3MpqhrVOo.)

The last dozen seconds of the video show you that the IDE understands the structure of the new userProfileResponse interface.

Summarizing Interfaces

TypeScript interfaces are a very useful feature of the language:

  • They are very good at demonstrating the developer's intent
  • IDEs understand their structure and provide great intellisense support.
  • They are better at modeling content than pure JavaScript.
  • If you need to refactor one of your models, it's much more difficult to miss something since everywhere you use the interface breaks.

We're not finished with interfaces - they also play a role with classes. That's where a significant amount of their pattern-implementation power comes from. Before we get to that, we'll cover off several other great typing features - enums, unions and custom types.

Enumerations and Union Types

So far, we've covered primitive data types (numbers, boolean, etc.) and how you can model complex objects using these primitive types. You can, in fact, create deeply nested data models using interfaces themselves. TypeScript provides additional ways to describe data. We'll look at two more of them: enumerations and unions. Note that TypeScript provides even more types such as intersection types, generics and type aliases. Some of these (e.g. intersections) cater to tools writers more than the casual audience I have in mind for this book. Generics, on the other hand, deserve their own chapter and work best with classes and methods.

Enumerations

Enumerations allow you to connect a string label to a numeric value. This is best shown via example:

enum HttpStatusCodes {
    OK = 200,
    GENERAL_SERVER_ERROR = 500,
    RESOURCE_NOT_FOUND = 304,
    FORBIDDEN = 403
}

Use enumerations in your code like this:

function parseResult(resultDetails: SomeInterface, resultCode: HttpStatusCodes) {
    if (resultCode === HttpStatusCodes.OK) {
        processSuccessfulResponse(resultDetails);
    }
    else if (resultCode === HttpStatusCodes.FORBIDDEN) {
        login();
    }
    else {
        processOtherError(resultCode, resultDetails);
    }
}

Many languages provide a similar enum syntax and if you've worked with one (like C# or Java) this all looks very familiar.

As with everywhere else in TypeScript, a good IDE supports enumerations with intellisense.

The snippet above example shows that you can match a text label with an arbitrary integer value. Sometimes, you don't care about the value. You just want the convenience of a human-readable label to use in your code. In that case, you can define an initial value and the compiler will increment it for you behind the scenes:

enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}

In this case, Down, Left and Right are assigned the values 2, 3 and 4 respectively.

Mapping labels, such as "FORBIDDEN" to a number value "403" constitutes the main use case for enums. Used this way, they allow you to express yourself more clearly in code. You may be fully aware that an http 403 is a "forbidden" message but other, newer developers may not.

Enums As Objects, Or Not

Unlike interfaces, the TypeScript compiler generates code for enums by default.

Here's the TypeScript Code and the generated JS:

enum HttpStatus {
    OK = 200,
    GENERAL_SERVER_ERROR = 500,
    RESOURCE_NOT_FOUND = 304,
    FORBIDDEN = 403
}

function parseResult(resultCode: HttpStatus) {
    if (resultCode === HttpStatus.OK) {
        console.log("Success response");
    }
    else if (resultCode === HttpStatus.FORBIDDEN) {
        console.log("Forbidden response.");
    }
    else {
        console.log("Some other response");
    }
}

Here's the generated JavaScript:

var HttpStatus;
(function (HttpStatus) {
    HttpStatus[HttpStatus["OK"] = 200] = "OK";
    HttpStatus[HttpStatus["GENERAL_SERVER_ERROR"] = 500] = "GENERAL_SERVER_ERROR";
    HttpStatus[HttpStatus["RESOURCE_NOT_FOUND"] = 304] = "RESOURCE_NOT_FOUND";
    HttpStatus[HttpStatus["FORBIDDEN"] = 403] = "FORBIDDEN";
})(HttpStatus || (HttpStatus = {}));

function parseResult(resultCode) {
    if (resultCode === HttpStatus.OK) {
        console.log("Success response");
    }
    else if (resultCode === HttpStatus.FORBIDDEN) {
        console.log("Forbidden response.");
    }
    else {
        console.log("Some other response");
    }
}

As you can see, TypeScript wraps the enum inside its own Immediately Invoked Function Expression (IIFE) and lives on as a code artifact. Most of the time, this isn't useful. You can skip the code generation and instead declare the enum as const:

Prefer Const Enums

You should normally prefer to use const enums. There are probably some good use cases for non-const enums but you almost certainly won't encounter them in your first weeks and months with the language, if at all. Const enums generate less code and that generated code is as easy to understand as the non-const generated code.


This is also in keeping with the broader "use const first" rule. If you can adopt that habit you'll be taking some early steps toward a more functional programming style and significantly reduce the risk of unanticipated side effects in your code.

const enum constHttpStatus {
    OK = 200,
    GENERAL_SERVER_ERROR = 500,
    RESOURCE_NOT_FOUND = 304,
    FORBIDDEN = 403
}

function parseResult(resultCode: constHttpStatus) {
    if (resultCode === constHttpStatus.OK) {
        console.log("Success response");
    }
    else if (resultCode === constHttpStatus.FORBIDDEN) {
        console.log("Forbidden response.");
    }
    else {
        console.log("Some other response");
    }
}

This results in more compact JavaScript:

function parseResult(resultCode) {
    if (resultCode === 200 /* OK */) {
        console.log("Success response");
    }
    else if (resultCode === 403 /* FORBIDDEN */) {
        console.log("Forbidden response.");
    }
    else {
        console.log("Some other response");
    }
}

It even puts in some helpful comments describing the the meaning of "403" or "200" if you find yourself digging into the generated JS.

Union Types

Union Types allow you to create a define a new entity that is comprised of multiple types or even values. Here's a simple example:

function move(inDirection: "left" | "up" | "down" | "right") {
    console.log(`Moving ${inDirection}.`);
}

This bit of code defines a function, "move" that takes a single parameter, "inDirection." Intellisense ensures that you don't try to pass in an invalid direction, like "sideways." Here's a short video demonstrating that.

(If you can't see the video, try clicking here. Or, open your preferred web browser and go to it directly: https://youtu.be/lfAa1-b-sng)).

This isn't a particularly great example since in cases like this, you would probably use an enumeration instead or split it out into five functions (moveLeft, moveRight, moveUp, moveDown and lower level "move" function). For a better use case, let's consider legacy code. Let's say you have built a library of JavaScript utility functions and you want to start using that library with a TypeScript project. Your library has a function, calculateCollectionTotal. This function takes in an array of objects and as long as they share a common field in common, "Total", it will add them all up and return the result. Here's what that might look like:

function calculateCollectionTotal(itemCollection) {
    return itemCollection.reduce(function(prev, current) {
      return prev + current.Total;
    }, 0);
}

console.log("Invoice lines total:", calculateCollectionTotal(invoices));
console.log("Order lines total:", calculateCollectionTotal(orders));
console.log("Pick lines total:", calculateCollectionTotal(PickingSlips));

If you're converting this legacy code to TypeScript, The "correct" approach here is to refactor the code, starting with a look at your invoices, orders and picking slips objects. Find their common elements, define an interface or possibly an abstract base class7. Restructure all the objects and update the overall code base. However, that's a lot of work. Union types can help you right away without the need for so much refactoring. Here's what it could look like:

function calculateCollectionTotal(itemCollection: Invoice[] | Order[] | PickingSlip[]): number {
    return itemCollection.reduce(function(prev: number, current: Invoice | Order | PickingSlip) {
      return prev + current.Total;
    }, 0);
}

console.log("Invoice lines total:", calculateCollectionTotal(invoices));
console.log("Order lines total:", calculateCollectionTotal(orders));
console.log("Pick lines total:", calculateCollectionTotal(PickingSlips));

This bit of TypeScript does the same thing as its plain JS cousin. However, it adds in some type safety that your IDE's intellisense feature can use. It's also nicely self-documenting. With one look at the signature, it's plain to anyone that this function was designed to calculate totals on a specific set of objects and no other objects.

You'll read about a better way to accomplish this using generics but they would force you to make a bigger change to your code base.

Further Reading

The following articles provide alternative and/or a deeper dive into the topics discussed in this chapter:

Summary and Recap

This chapter introduced interfaces for the first time in their capacity as data describers. Interfaces may be empty, they can describe a collection of primitive values (number, string, etc.). You can create one interface and extend it to another. They can represent nested objects, including deeply nested objects such as overly complex SharePoint JSON payloads.

You also read about enumerations and union types. Both of these help make programmer intent clear and help you avoid making mistakes in your code by ensuring that only certain types of values or objects can be passed into functions.

We're going to pause from heady subjects and diverge into template strings next. It's a nice and simple subject before we get into classes.


1. If you aren't familiar with this SOLID acronym, it's probably worth your time checking it out. This scotch.io write-up is a good start (https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design).
2. Empty interfaces aren't typically all that useful, but this article on binary searches in TypeScript provides one: https://blog.hellojs.org/implement-binary-search-in-typescript-using-generics-with-useful-refactorings-a4bcda932d7. This one may be a little on the complex side given where we are in the book, but it's worth coming back to once you finish.
3. There are a ridiculous number of ways to clone JavaScript objects. The approach I used in these examples comes from this clever blog post: http://heyjavascript.com/4-creative-ways-to-clone-objects/
4. It's pretty obvious that a property named "TotalPages" would be numeric. However, as this chapter progresses, you'll see how interface show developer intent when describing a less obvious properties.
5. If you don't agree, then I don't know what else I can tell you :).
6. If you happen to know anything about SharePoint - I'm not retrieving an SPUser here, I'm retrieving an item from a custom list.
7. Abstract classes, along with interfaces, provide a solid basis for your SOLID programming efforts. The book covers abstract classes in chapter 9.

results matching ""

    No results matching ""