typescript
How to Declare Object Types in TypeScript
author
Luis Paredes
published
Aug 7, 2023
TypeScript is a typed superset of JavaScript that adds type safety to the language. This makes it easier to write code that is more reliable and less error-prone.
One of the features of TypeScript that makes it so powerful is its ability to declare object types. Object types allow you to specify the type of each property in an object, which can help to prevent errors and make your code more readable.
In this blog post, I will show you how to declare object types in TypeScript alongside real world examples that will help you apply the knowledge effectively.
Let's dive in!
Basic Object Types
An object type in TypeScript is a type that represents an object. An object can have any number of properties, and each property can have a different type.
To declare an object type, you can use the interface
keyword with the {}
syntax. For example, the following code declares an object type with two properties:
interface Person {
name: string;
age: number;
};
In the example, we describe an object named Person
that has two entries: name
, which is a string
and age
which is a number
. If we were to define a few Person
objects as follows:
const Lau: Person = {
name: "Lau",
age: 30,
};
const Mary: Person = {
name: "Mary",
};
const Peter: Person = {
name: "Peter",
age: 38,
favoriteColor: "red",
};
const John: Person = {
name: "John",
age: "26",
};
TypeScript will check the inner structure of each object and after comparing the structure of the objects with the structure of the Person
interface, the following results will be obtained:
- No errors will be reported for object
Lau
since its structure matches exactly thePerson
interface - An error will be reported for the object
Mary
because it lacks one of the entries described in the interface (age
is missing) - An error will be reported for the object
Person
because it has an extra entry that isn't declared in the interface (favoriteColor
is not part of thePerson
interface) - An error will be reported for the object
John
because even though it has the two expected keys, the value for the keyage
is not a number but rather a string ("26"
as opposed to26
)
How to declare nested objects?
A common occurrence in real world applications is finding objects that contain other objects inside them. For example, we could have a product object that contains information about the aggregated reviews in a nested object:
const exampleProduct: Product = {
name: "Example Product",
price: 29.99,
description: "This is a sample product description.",
image: "example.jpg",
reviews: {
count: 15,
averageRating: 4.5,
},
};
For cases like this, all we need to do if to recreate the same nested structure in the interface
declaration. For the example given above we would proceed as follows:
interface Product {
name: string;
price: number;
description: string;
image: string;
reviews: {
count: number;
averageRating: number;
};
};
Notice that, as we essentially just define another interface inside the initial one, the inline definition can be refactored into a separate interface for improved robustness and readability:
interface Reviews {
count: number;
averageRating: number;
};
interface Product {
name: string;
price: number;
description: string;
image: string;
reviews: Reviews;
};
How to reuse object types declarations using inheritance?
Inheritance is a powerful feature of TypeScript that allows you to reuse the properties and methods of one type in another type. This can be useful for code reuse, thus making the codebase more maintainable.
To reuse object types declarations using inheritance, you can use the extends
keyword. For instance, the following code defines an interface called Person
and a interface called Employee
that inherits from the Person interface:
interface Person {
name: string;
age: number;
};
interface Employee extends Person {
company: string;
};
The Employee
interface inherits the name
and age
properties from the Person
interface, and it also has an additional property called company
.
Now, you can create an Employee object and TypeScript will check for both the properties defined in the Employee
interface itself and the properties inherited from the Person
interface. For example, if we had something like this:
const Jane: Employee = {
age: 22,
name: "Jane",
company: "Acme",
};
const Joe: Employee = {
company: "Acme",
};
- No errors will be thrown for
Jane
since it has all the expected properties ofEmployee
and nothing else - An error will be thrown for
Joe
because while it does have the single property declared inEmployee
, it is missing the properties inherited fromPerson
How to choose whether to use type
or interface
when declaring object types?
For those of you who are familiar with other TypeScript features, you may be aware that the type
keyword can also be used to declare object types and would like to know when to use that keyword instead of the interface
keyword.
Simply put, when it comes to declaring object types, in most cases you'll be fine using either, the only important exception being wanting to merge object type declarations, which is only available for interface
:
- This will check for both
name
andage
in any object typed asPerson
because the two interfaces will be automagically merged:interface Person { name: string; }; interface Person { age: number; }; const Amy: Person = { name: "Amy", age: 40, };
- This will throw an error because the
Person
type is declared twice and another error becauseage
isn't in the initialPerson
type declaration:type Person = { name: string; }; type Person = { age: number; }; const Amy: Person = { name: "Amy", age: 40, };
I'll be using interface
in the examples of this article, however, keep in mind that type
declarations can be used unless stated otherwise.
Advanced Object Types
So far we've covered how to declare and use object types in most of the basic scenarios we might encounter. However, there are some specific use cases that are worth knowing in spite of not being as common as the cases described above. In this section we'll delve into these type of cases.
Union types
Union types allow you to define a type that can be one of two or more types. For example, the following code defines a union type called Address
that can be either a vanilla string
or an IAddress
interface:
interface IAddress {
street: string;
city: string;
state: string;
postalCode: string;
country: string;
};
type Address = string | IAddress;
This means that a variable of type Address
can be assigned a value that is either a string
or an IAddress
, therefore, both of these assignments will be valid:
// Address as a string
const simpleAddress: Address = "Main St, Cityville, Stateland, 12345, Ceeland";
// Address as an object following IAddress interface
const fullAddress: Address = {
street: "456 Elm Avenue",
city: "Townsville",
state: "Provinceville",
postalCode: "67890",
country: "Nationland",
};
Notice that union types can involve both simple types and object types, you can add as many cases to the disjunction as needed. For more information on union types, make sure to check out the official documentation.
Intersection types
TypeScript also offers an alternative way to combine two object types without using the extends
keyword. The resulting type that we get using this alternative approach is called an intersection type.
An intersection type is defined using the &
operator. For example, the Employee
declaration we created earlier can be rewritten like this:
interface Person {
name: string;
age: number;
}
type Employee = Person & {
company: string;
};
How to use generics in object types declarations?
Generics in TypeScript allow you to create reusable and flexible code by defining types that can work with various data types. When it comes to object type declarations, you can use generics to create types that are parameterized and can adapt to different shapes of objects. Here's how you can use generics in object type declarations:
interface MyObject<T> {
data: T;
timestamp: number;
};
const stringObject: MyObject<string> {
data: "Hello, generics!",
timestamp: Date.now(),
};
const numberObject: MyObject<number> {
data: 42,
timestamp: Date.now(),
};
In the example above, we define a generic type MyObject<T>
that takes a type parameter T
. This type parameter is used to specify the type of the data property in the object. Then, we create instances of MyObject
by providing different types as the type parameter.
Generics provide flexibility because they allow you to create types that can adapt to different data types without sacrificing type safety. This can be especially useful when you want to create reusable components or functions that work with a variety of data.
How to omit some properties in object types?
In TypeScript, managing object types with precision is essential for building robust and flexible code. There are scenarios where you might need to omit certain properties from an object type or create a type with some optional properties. TypeScript offers three handy utility types (namely: Partial
, Omit
and Pick
) to address these cases.
Using Partial
to create types with optional properties
The Partial utility allows you to create a new type by marking all properties of an existing type as optional. This is particularly useful when you want to define an object type with some properties that may or may not be present.
In the following example, the PartialUser
type is created using the Partial
utility, which makes all properties of User
optional. This enables you to create objects that have some or all of the properties defined in the original User
type.
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
const user: PartialUser = {
id: 123,
name: "John Doe",
};
Using Omit
to exclude specific properties
The Omit
utility type enables you to create a new type by excluding specific properties from an existing type. This is helpful when you want to derive a type that has all properties of the original type except for a few. Here's an example to illustrate the concept in a real-world scenario:
interface Person = {
name: string;
lastName: string;
age: number;
address: string;
};
type ReservedPerson = Omit<Person, "age" | "address">;
const Jane: ReservedPerson = {
name: "Jane",
lastName: "Doe",
};
As shown in the given scenario, the ReservedPerson
type is defined using the Omit
utility, excluding the age
property from the original Person
interface. This abstraction simplifies our code by focusing solely on the relevant properties for this particular use case.
Using Pick
to select specific properties
The Pick
utility type allows you to create a new type by selecting specific properties from an existing type. This is valuable when you want to create a type that includes only a subset of properties from a larger type.
In the following scenario, we'll explore how the Pick
utility works by creating a specialized type that captures essential contact details from a Contact
interface:
interface Contact {
name: string;
email: string;
phone: string;
address: string;
}
type BasicContact = Pick<Contact, "name" | "email">;
const importantContact: BasicContact = {
name: "Alice",
email: "alice@example.com",
};
In this scenario, the BasicContact
type is formed using the Pick
utility, extracting the name
and email
properties from the original Contact
interface. This enables us to focus explicitly on the specific information we need for a particular context.
Conclusion
Mastering object type declarations in TypeScript opens the door to writing more robust and maintainable code. By defining precise types for your objects, you enhance the clarity of your codebase and prevent potential errors.
With a solid grasp of these concepts, you're equipped to navigate various real-world scenarios effectively and produce cleaner, safer code. Your journey to harnessing the full potential of TypeScript object types has just begun!