This blog post makes a deep dive into TypeScript's object type system, exploring interfaces and types, difference between them with various examples to help you master these types.
What Are Interfaces and Types In TypeScript
Interfaces and types belong to the object types in TypeScript.
Object types in TypeScript groups a set of properties under a single type.
Object types can be defined either using type
alias:
typescripttype User = { name: string; age: number; email: string; }; const user: User = { name: "Anton", age: 30, email: "[email protected]" };
Or interface
keyword:
typescriptinterface User { name: string; age: number; email: string; } const user: User = { name: "Anton", age: 30, email: "[email protected]" };
Interfaces in TypeScript
Interfaces are extendable and can inherit from other interfaces using the extends
keyword:
typescriptinterface Employee extends User { employeeId: number; salary: number; } const employee: Employee = { name: "Jack Sparrow", age: 40, email: "[email protected]", employeeId: 1, salary: 2000 };
Classes in TypeScript can inherit from interfaces and implement them using the
extends
keyword.
You can also extend interfaces by declaring the same interface multiple times:
typescriptinterface User { name: string; age: number; email: string; } interface User { phone: string; } const user: User = { name: "Anton", age: 30, email: "[email protected]", phone: "1234567890" };
In TypeScript interfaces, you can use property modifiers to define optional and readonly properties.
Optional Properties
These properties are not mandatory when creating an object:
typescriptinterface User { name: string; age: number; email?: string; // Optional property } const user: User = { name: "Anton", age: 30 };
Readonly Properties
These properties cannot be modified after the object is created:
typescriptinterface User { name: string; readonly age: number; email: string; } const user: User = { name: "Anton", age: 30, email: "[email protected]" }; // Error: Cannot assign to 'age' because it is a read-only property. // user.age = 31;
Index Signatures
Index signatures allow you to define the type of keys and values that an object can have.
Sometimes you may not know all the names of the properties during compile time, but you do know the shape of the values. In such use cases you can use an index signature to define the properties:
typescriptinterface Names { [index: number]: string; } const names: Names = ["Anton", "Jack"];
You can also define dictionaries by using index signatures:
typescriptinterface NameDictionary { [key: string]: string; } const dictionary: NameDictionary = { name: "Anton", email: "[email protected]" };
There is a limitation when working with index signatures: you can't have named properties of another type:
typescriptinterface NumberDictionary { [index: string]: number; length: number; // Error: Property 'name' of type 'string' is not assignable to 'string' index type 'number'. name: string; }
You can bypass this limitation my using Unions:
typescriptinterface Dictionary { [index: string]: number | string; length: number; name: string; } const dictionary: Dictionary = { name: "Anton", email: "[email protected]", length: 5 };
It might be useful to make index signatures readonly to prevent assignment to their indexes:
typescriptinterface ReadonlyDictionary { readonly [index: number]: string; } const dictionary: ReadonlyDictionary = { 0: "Anton" }; dictionary[0] = "John"; // Error: Index signature in type 'ReadonlyDictionary' is readonly.
Types in TypeScript
The type in TypeScript is similar to the interfaces but you can't extend existing types or add new properties to them.
typescripttype User = { name: string; age: number; email: string; }; type User = { phone: string; }; // Error: Duplicate identifier 'User'.
Combining Interfaces and Types with Intersection Types
Intersection types in TypeScript allow you to combine multiple types into one.
An intersection type can be created by combining multiple interfaces or types using &
operator:
typescriptinterface Person { name: string; } interface ContactDetails { email: string; phone: string; } type Customer = Person & ContactDetails; const customer: Customer = { name: "Anton", email: "[email protected]", phone: "1234567890" };
In these examples a Customer
type is an intersection type that combines all properties from Person
and ContactDetails
interfaces.
It's important to note that you can only declare an Intersection Type with a type keyword. You can't declare an interface type that holds an Intersection.
Intersections are not limited just to interfaces and types, they can be used with other types, including primitives, unions, and other intersections.
Combining Interfaces and Types with Union Types
Union type in TypeScript is a type that is formed from two or more other types. Union types are also called Discriminated Unions.
A variable of union type can have one of the types from the union.
Let's explore an example of geometrics shapes, that have common and different properties:
typescriptinterface Circle { type: "circle"; radius: number; } interface Square { type: "square"; square: string; } type Shape = Circle | Square;
Let's create a function that calculates a square for each shape:
typescriptfunction getSquare(shape: Shape) { if (shape.type === "circle") { return Math.PI * shape.radius * shape.radius; } if (shape.type === "square") { return shape.square; } } const circle: Circle = { type: "circle", radius: 10 }; const square: Square = { type: "square", size: 5 }; console.log("Circle square: ", getSquare(circle)); console.log("Circle square: ", getSquare(square));
Here a getSquare
function accepts a shape parameter that can be one of 2 types: Circle
or Square
.
We need to define a property that will allow to distinguish types from each other.
In our example it's a type
property.
It's important to note that you can only create a Union Type with a type keyword. You can't declare an interface type that holds a Union Type.
Difference Between Interfaces and Types
Interfaces:
- can inherit from other interfaces
- can merge declarations, which is useful for extending existing objects
- can't represent more complex structures, like unions and intersections
Types:
- doesn't support inheritance
- doesn't allow extending existing objects
- can represent more complex structures, like unions and intersections
Summary
Both Interfaces and Types in TypeScript are quite the same but have some differences. Interfaces allow extension of existing objects, while types - not. On the other hand Types can represent unions and intersections.
Use Interfaces when you need to define the structure of an object and take advantage of declaration merging and extension. Use Types when you need you don't need them to be extended or to define complex types, such as unions or intersections.
There is no right choice whether to prefer Interfaces to Types or vice versa. You can choose based on the needs and personal preference, the most important part is to select a single approach that will be consistent in the project. My personal choice is Types as they can't be accidentally re-declared and extended, unless I need to do some fancy object-oriented stuff with interfaces and classes.
P.S.: you can find an amazing Cheat Sheets on Types and Interfaces from the Official TypeScript website.
Hope you find this blog post useful. Happy coding!