A simple guide to interface data type in TypeScript

In this article, we are going to learn about the interface type to enforce restrictions on the shape of objects.

An interface is a shape of an object. A standard JavaScript object is a map of key:value pairs. JavaScript object keys in almost all the cases are strings and their values are any supported JavaScript values (primitive or abstract).

An interface tells the TypeScript compiler about property names an object can have and their corresponding value types. Therefore, interface is a type and is an abstract type since it is composed of primitive types.

When we define an object with properties (keys) and values, TypeScript creates an implicit interface by looking at the property names and data type of their values in the object. This happens because of the type inference.

In the above example, we have created an object student with firstName, lastName, age and getSalary fields and assigned some initial values. Using this information, TypeScript creates an implicit interface type for student.

{
  firstName: string;
  lastName: string;
  age: number;
  getSalary: (base: number) => number;
}

An interface is just like an object but it only contains the information about object properties and their types. We can also create an interface type and give it a name so that we can use it to annotate object values but here, this interface doesn’t have a name since it was created implicitly. You can compare this with the function type in the previous lesson which was created implicitly at first and then we created a function type explicitly using type alias.

Let’s try to mess with the object properties after it was defined.

As you can see from the above example, TypeScript remembers the shape of an object since the type of ross is the implicit interface. If we try to override the value of a property with a value of different type other than what’s specified in the interface or try to add a new property which isn’t specified in the interface, the TypeScript compiler won’t compile the program.

If you want an object to basically have any property, then you can explicitly mark a value any and the TypeScript compiler won’t infer the type from the assigned object value. There are other better ways to achieve exactly this and we will go through them in this article.

Interface Declaration

Though the implicit interface we have seen so far is technically a type, but it wasn’t defined explicitly. As discussed, an interface is nothing but the shape an object can take. If you have a function that accepts an argument that should be an object but of a particular shape, then we need to annotate that argument (parameter) with an interface type.

In the above example, we have defined a function getPersonInfo which accepts an object argument that has firstName, lastName, age and getSalary fields of specified data types. Notice that we have used an object that contains property names and their corresponding types as a type using :<type> annotation. This is an example of an anonymous interface since the interface doesn’t have a name, it was used inline.

This all seems a little complicated to handle. If the ross object gets more complicated and it needs to be used in multiple places, TypeScript just seems a thing that you liked initially but now just a tough thing to deal with. To solve this problem, we define an interface type using interface keyword.

In the example above, we have defined an interface Person that describes the shape of an object, but this time, we have a name we can use to refer to this type. We have used this type to annotate ross variable as well as the person argument of the getPersonIfo function. This will inform TypeScript to validate these entities against the shape of Person.

Why use an interface?

Interface type can be important to enforce a particular shape. Typically in JavaScript, we put blind faith at runtime that an object will always contain a particular property and that property will always have a value of a particular type such as {age: 21, ...} as an example.

When we actually start to perform operations on that property without first checking if that property exists on the object or if its value is what we expected, things can go wrong and it may leave your application unusable afterward. For example, {age: '21', ...}, here age value is a string.

Interfaces provide a safe mechanism to deal with such scenarios at compile time. If you are accidentally using a property on an object that doesn’t exist or using the value of a property in the illegal operation, the TypeScript compiler won’t compile your program. Let’s see an example.

In the above example, we are trying to use name property of the _student argument inside the printStudent function. Since the _student argument is a type of Student interface, the TypeScript compiler throws an error during compilation since this property doesn’t exist in the Student interface.

Similarly, 100 - _student.firstName is not a valid operation since firstName property is a type of string and last time I checked, you can’t subtract a string from a number is JavaScript (results in NaN).

In the above example, we have used the traditional way of writing function type for the getSalary field. However, you can also use function syntax without the body for the same, which is generally used in interfaces.

interface Student {
  firstName: string;
  lastName: string;
  age: number;
  getSalary(base: number): number;
}

Optional Properties

Sometimes, you need an object to have a property that holds data of particular data type but it is not mandatory to have that property on the object. This is similar to the optional function parameters we learned in the previous lesson.

Such properties are called optional properties. An interface can contain optional properties and we use ?:Type annotation to represent them, just like the optional function parameters.

In the above example, the Student interface has the age property which is optional. However, if the age property is provided, it must have a value of the type number.

In the case of the ross object which is a type of Student interface, we have not provided the value for the age property which is legal, however, in case of monica, we have provided the age property but its value is string which is not legal. Hence the TypeScript compiler throws an error.

The error might seem weird but it actually makes sense. If the age property doesn’t exist on an object, the object.age will return undefined which is a type of undefined. If it does exist, then the value must be of the type number.

Hence the age property value can either be of the type undefined or number which in TypeScript is represented using union syntax number | undefined.

💡 We will learn type unions in an Type System lesson.

However, optional properties pose serious problems during the program execution. Let’s imagine if we are using age property in an arithmetic operation but its value is undefined. This is a kind of a serious problem.

But the good thing is, the TypeScript compiler doesn’t allow performing illegal operations on an optional property since its value can be undefined.

In the above example, we are performing an arithmetic operation on age property which is illegal because the value of this property can be number or undefined in the runtime. Performing arithmetic operations on undefined results in NaN (not a number).

💡 However, for above program, we had tp set --strictNullChecks flag to false which is a TypeScript compiler flag. If we do provide this option, the above program compiles just fine.

To avoid this error or warning, we need to explicitly tell TypeScript compiler that this property is a type of number and not the number or undefined. For this, we use type assertion (AKA type conversion or typecasting).

In the above program, we have used (_student.age as number) which converts the type of _student.age from number | undefined to number. This is a way to tell TypeScript compiler, “Hey, this is a number”. But a better way to handle this would be to also check if _student.age is undefined at runtime and then perform the arithmetic operation.

💡 We will learn about type assertions in an Type System lesson.

Function type using an interface

Not only the shape of a plain object, but an interface can also describe the signature of a function. In the previous lesson, we used type alias to describe a function type but interfaces can also do that.

interface InterfaceName {
  (param: Type): Type;
}

The syntax to declare an interface as a function type is similar to the function signature itself. As you can see from the example above, the body of the interface contains the exact signature of an anonymous function, without the body of course. Here parameter names do not matter.

In the example above, we have defined IsSumOdd interface which defines a function type that accepts two arguments of type number and returns a boolean value. Now you can use this type to describe a function because the IsSumOdd interface type is equivalent to function type (x: number, y: number) => boolean.

An interface with an anonymous method signature describes a function. But a function in the JavaScript realm is also an object, which means you can add properties to a function value just like an object. Therefore it is perfectly legal you can define any properties on an interface of the function type.

In the example above, we have added type and calculate properties on the IsSumOdd interface which describes a function. Using Object.assign method, we are merging type and calculate properties with a function value.

Interfaces of the function type can be helpful to describe constructor functions. A constructor function is similar to a class whose job is to create objects (instances). We only had constructor functions up until ES5 to mimic a class in JavaScript. Therefore, TypeScript compiles classes to constructor functions if you are targeting ES5 or below.

💡 If you want to learn more about constructor function, follow this article.

If we put new keyword before anonymous function signature in the interface, it makes the function constructible. That means the function can only be invoked using new keyword to generate objects and not using a regular function call. A sample constructor function looks like below.

function Animal(_name) {
  this.name = _name;
}

var dog = new Animal("Tommy");
console.log(dog.name); // Tommy

Fortunately, we don’t have to work with constructor functions since TypeScript provides class keyword to create a class that is much easier to work with than a constructor function, trust me. In fact, a class deep down is a constructor function in JavaScript. Try the below example.

class Animal {
  constructor(_name) {
    this.name = _name;
  }
}

console.log(typeof Animal); // "function"

A class and a constructor function are one and the same thing. The only difference is that the class gives us rich OOP syntax to work with. Hence, an interface of a constructor function type represents a class.

In the above example, we have defined the Animal class with a constructor function that accepts an argument of type string. You can consider this as a constructor function that has a similar signature of the Animal constructor.

The AnimalInterface defines a constructor function since it has anonymous function prefixed with the new keyword. This means the Animal class qualifies to be a type of AnimalInterface. Here, AnimalInterface interface type is equivalent to the function type new (sound: string) => any.

The createAnimal function accepts ctor argument of AnimalInterface type, hence we can pass Animal class as the argument value. We won’t be able to add getSound method signature of the Animal class in AnimalInterface and the reason is explained in the Classes lesson.

Indexable Types

An indexable object is an object whose properties can be accessed using an index signature like obj[ 'property' ]. This is the default way to access an array element but we can also do this for the object.

var a = [1, 2, 3];
var o = { one: 1, two: 2, three: 3 };
console.log(a[0]); // 1
console.log(a["one"]); // 1 (same as `a.one`)

At times, your object can have an arbitrary number of properties without any definite shape. In that case, you can just use object type. However, this object type defines any value which not number, string, boolean, symbol, null, or undefined as discussed in the basic types lesson.

If we need to strictly check if a value is a plain JavaScript object then we might have a problem. This can be solved using an interface type with an index signature for the property name.

interface SimpleObject {
  [key: string]: any;
}

The SimpleObject interface defines the shape of an object with string keys whose values can be any data type. Here, the key property name is just used for placeholder since it is enclosed in square brackets.

In the above example, we have defined ross and monica object of type SimpleObject interface. Since these objects contain string keys and values of any data type, it is perfectly legal.

If you are confused about the key 1 in the monica which is a type of number, this is legal since object or array items in JavaScript can be indexed using number or string keys, as shown below.

var o = { 0: "Zero", 1: "One" };
var a = ["Zero", "One"];
console.log(o["0"]); // Zero
console.log(o[1]); // One
console.log(a[1]); // One
console.log(a["1"]); // One

If we need to be more precise about the type of keys and their values, we can surely do that as well. For example, we can define an indexable interface type with keys of type number and values of type number if we want.

💡 An index signature key type must be either string or number.

In the example above, we have defined a LapTimes interface that can contain property names of type number and values of type number. This interface can represent a data structure that can be indexed using number keys hence array ross and objects monica and joey are legal.

However, the rachel object does not comply with the shape of LapTimes since key one is a string and it can only be accessed using string such as rachel[ 'one' ] and nothing else. Hence the TypeScript compiler will throw an error as shown above.

It is possible to have some properties required and some optional in an indexable interface type. This can be quite useful when we need an object to have a certain shape but it really doesn’t matter if we get extra and unwanted properties in the object.

In the example above, we have defined an interface LapTimes which must contain property name with string value and optional property age with number value. An object of type LapTimes can also have arbitrary properties whose keys must be number and whose values should also be number.

The object ross is a valid LapTimes object even though it doesn’t have age property since it is optional. However, monica does have the age property but is value is string hence it doesn’t comply with the LapTimes interface.

The joey object also doesn’t comply with the LapTimes interface since it has a gender property which is a type of string. The rachel object doesn’t have name property which is required in the LapTimes interface.

💡 There are some gotchas that we need to look out for while using indexable types. These are mentioned in this documentation.

Extending Interface

Like classes, an interface can inherit properties from other interfaces. However, unlike classes in JavaScript, an interface can inherit from multiple interfaces. We use extends keyword to inherit an interface.

By extending an interface, the child interface gets all the properties of the parent interface. This is quite useful when multiple interfaces have a common structure and we want to avoid code duplication by taking the common properties out into a common interface that can be later inherited.

In the above example, we have created a Student interface that inherits properties from the Person and Player interface.

Multiple Interface Declarations

In the previous section, we learned how an interface can inherit the properties of another interface. This was done using the extend keyword. However, when interfaces with the same name are declared within the same module (file), TypeScript merges their properties together as long as they have distinct property names or their conflicting property types are the same.

In the above example, we have declared Person interface several times. This will result in a single Person interface declaration by merging the properties of all the Person interface declarations.

💡 In the Classes lesson, we have learned that a class implicitly declares an interface and an interface can extend that interface. So if a program has a class Person and an interface Person, then the final Person type (interface) will have merged properties between the class and the interface.

Nested Interfaces

An interface can have deeply nested structures. In the example below, the info field of the Student interface defines the shape of an object with firstName and lastName properties.

Likewise, it is perfectly legal for a field of an interface to have the type of another interface. In the following example, the info field of the Student interface has the type of Person interface.

#typescript #interfaces