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 the Person 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 the Person interface)
  • An error will be reported for the object John because even though it has the two expected keys, the value for the key age is not a number but rather a string ("26" as opposed to 26)

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 of Employee and nothing else
  • An error will be thrown for Joe because while it does have the single property declared in Employee, it is missing the properties inherited from Person

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 and age in any object typed as Person 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 because age isn't in the initial Person 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!