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 tofalse
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
ornumber
.
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 classPerson
and an interfacePerson
, then the finalPerson
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.