Type Checking Classes and Interfaces

Let's learn about checking classes and interfaces in Typescript.

The preceding two sections are all leading up to the very important question of what exactly TypeScript checks when it checks a type.

For example, is the following legal?

class Hat {
  constructor(private size: number) {}
}

class Shirt {
  constructor(private size: number) {}
}

let x: Hat;
x = new Shirt()

In this code we have two classes, Hat and Shirt, that are not related to each other, but which have the same set of properties, namely an attribute called size, which is a number. Given that setup, can you declare a value of type Hat and then assign it a value of type Shirt?

In many typed languages, this sequence would be an error because the Hat and Shirt classes have no common ancestor. In TypeScript, though, this is very legal.

In TypeScript, types are compared only on their list of properties (the TypeScript docs call this “structural typing”). The basic rule is:

Given an assignment, such as left = right, for every property on the left (receiving) side of the assignment there must be a matching compatible property on the right side of the assignment.

In our above example, Hat has one property, a number named size. Shirt has a matching number property named size and so a variable of type Hat can be assigned a value of type Shirt.

This compatibility match does not necessarily go in both directions if the two classes have different property lists. Let’s add a property to Shirt:

class Hat {
  constructor(private size: number) {}
}

class Shirt {
  constructor(private size: number, private sleeves: string) {}
}

Now we’ve added a second property to shirt. Which of these is now legal?

let x: Hat = new Shirt()
let x: Shirt = new Hat()

The first line is still legal—Hat only has one property, and Shirt still shares it. But the second line is now a type error, because Shirt has a property, sleeves, that is not contained by Hat.

The same basic idea of compatibility holds when you need to determine if two functions are type compatible with each other—meaning whether two functions, as first-class items in the system, can be used in the same place.

There must be a parameter on the right side for every parameter in the function type on the left side of the assignment in the same order. The right side can have extra parameters, since passing extra parameters to a function and having them be ignored is not at all unusual in JavaScript.

In general, whether a parameter or property is optional or required does not make a difference as far as type compatibility is concerned.

If you are used to Java, Elm, or some other strictly typed language, the TypeScript rules here may seem odd, and in a way, they are. TypeScript is more permissive than type systems in those languages because they can allow objects of unrelated types to be assigned.

In fact, the TypeScript documentation discusses some edge cases where the compiler might allow code that turns out to be unsafe at run time we’re not going to worry about those cases right here, but if you are curious, check them out in the TypeScript Handbook.

The stated reason for managing the type system by structure and not relationships is that it turns out to be a very good fit for the somewhat free-wheeling approach to types that you see in a typical JavaScript program.

In particular, a lot of JavaScript code doesn’t make a strong distinction between objects that are just created with object literal syntax and class instances created with new. The TypeScript type system allows you to apply type safety no matter how your objects or functions are created, and no matter whether you have a class declaration for them.

A nice side effect of TypeScript’s approach to typing is that it allows types to be easily added together to form a combined type. And it turns out that this kind of type composition is also a nice fit for common JavaScript patterns.

Getting type knowledge to Typescript

There will often come a time where you will know more about the type information of data in your code than TypeScript will be able to infer. TypeScript provides a few different ways for you to refine the inferred types so as to allow more accurate typing.

TypeScript allows for typecasting with the keyword any. The any type is the default where TypeScript can’t infer a type, and it means that any value is legal there. You can explicitly use any in cases where you think that TypeScript’s expected inference is likely to be too constricting.

This is perhaps most helpful when dealing with data from libraries or modules that don’t use TypeScript:

let externalValue: any = SomeModule.someFunction()

The use of any prevents TypeScript from doing any type checking on the externalValue variable, which means that the return value of someFunction won’t be used to constrain other code.

In practice, using any allows you to gradually add type checking to existing code by explicitly stating what data is or is not type checked, as you add more type information, often the uses of any can be replaced with more specific types.

Conversely, there are times when you may know the type is more specific than TypeScript assumes, rather than more general.

We almost have an example of this in our existing code. We get our toggle buttons and targets using the querySelectorAll method. That method returns a value of type HTMLElement[], which is fine for our purposes because all we want to do is respond to clicks (for our toggle buttons) or show and hide (for our targets).

If, however, we knew that the target element being returned was a form element and we wanted to change its value as a result of the button being clicked, TypeScript would not allow that because value is not a property of HTMLElement, it’s a property of a subclass called HTMLInputElement.

When we know more about the type of the data than the compiler can know, TypeScript provides two ways to send that information to the compiler. The one we’ll be using in this book uses the keyword as:

let elements: HTMLElement[] = document.querySelectorAll(this.targetSelector)
let inputElements: HTMLInputElement[] = elements as HTMLInputElement[]

An alternate syntax, which we won’t use because it is confusing in React code, uses angle brackets:

let elements: HTMLElement[] = document.querySelectorAll(this.targetSelector)
let inputElements: HTMLInputElement[] = <HTMLInputElement[]>elements

An important fact about these type assertions is that they are for compile time only—they are only there to give the compiler more information about the properties available on that data. If the data is incorrect at run time, you’ll get a run-time error at the time you try to use a nonexistent property, not at the time of this type assertion. There is a way to do run-time type checks in TypeScript, which I’ll show in Validating Code with Advanced TypeScript.

There is one other place in which you might have more information than the compiler. If you have functions that return functions, TypeScript may not be able to infer the type of the this parameter in the internal function. To combat that, any TypeScript function definition can define this as the first parameter in an argument list. Defining this explicitly allows you to give this a type, but does not affect the parameters used in calling the function.

What’s next?

This chapter provided a basic introduction to how TypeScript works. It can get a lot more complicated, though, which you’ll see later on in Validating Code with Advanced TypeScript chapter. First, let’s talk about how TypeScript gets compiled and sent to the browser using webpack and Webpacker.

Get hands-on with 1200+ tech skills courses.