Introduction to Object Types in TypeScript Pt2

Introduction to Object Types in TypeScript Pt2 feature image

TypeScript introduces a new type called “object type”. This type is created specifically for objects and makes it working with them easier. In this tutorial, you will learn about how to extend object types and how to use intersection types. You will also learn how to create object types using generics.

Extending object types

As you work with objects, you will often have to handle types that are different, but only slightly. For example, you might create one object that contains a number of properties. So, you will create an object type for it. Then, you will create another object that is slightly different.

Since this second object is slightly different, you can’t use the previous object type. Now, you have two options. The first option is to create new object type for this second object. This is okay, but since those two object are similar, part of this type will be duplicating code of the first type.

The second option is also about creating new object type. However, this time, you will build it on top of the previous. This means that the second object type will inherit properties defined in the first object type. Then, it will add some additional, those you need just for the second object.

This process of building on top of existing interfaces is called extending. It is very similar to extending JavaScript classes. In this case, you are extending object types. The result is the same, the combination of new interface you are extending and the old you are extending with.

You can extend object types, or interfaces, with the extends keyword. This keyword follows after the object type name. It is then followed by the existing object type you want to extend the new type with. The rest is like defining new object type with an interface. You add curly braces and add additional properties inside the body.

// Create some default object type/interface:
interface Guest {
  username: string;
  email: string;
  password: string;
}

// Create new object type by extending Guest:
interface User extends Guest {
  canPost: boolean;
}
// Translates to:
// interface User {
//   username: string;
//   email: string;
//   password: string;
//   canPost: boolean;
// }

// Create another object type by extending User:
interface Admin extends User {
  role: 'admin';
  securityClearance: 'low' | 'medium' | 'high';
}
// Translates to:
// interface Admin {
//   username: string;
//   email: string;
//   password: string;
//   canPost: boolean;
//   role: 'admin';
//   securityClearance: 'low' | 'medium' | 'high';
// }

// Create new object of type Guest:
const guest: Guest = {
  username: 'joe0001',
  email: 'joe@joe.co',
  password: 'joe_joe_12345'
}

// Create new object of type User:
const user: User = {
  username: 'tom0001',
  email: 'tom@tom.eu',
  password: 'tom_tom_12345',
  canPost: true
}

// Create new object of type Admin:
const admin: Admin = {
  username: 'dick0001',
  email: 'dick@dick.bz',
  password: 'dick_dick_12345',
  canPost: true,
  role: 'admin',
  securityClearance: 'medium'
}

One important thing to mention. You can extend object types defined with interfaces only when you define them. You can’t extend them when you use them to annotate some object. This will not work.

Combining type aliases and interfaces, or intersection types

In the previous part, you’ve learned about type aliases. You’ve learned what they are and how to use them to define object types, as an alternative to interfaces. One thing you should know about type aliases is that you can combine them. You can take one alias, combine it with another, or multiple, and create a new type alias.

This also works with interfaces. This means that you are not limited to extending interfaces. You don’t have to create one by building on top of another. You can also combine two or more existing interfaces. This can be useful when you have two existing object types and need properties that match their intersection or combination.

You can combine type aliases or interfaces by using the & operator. The result of this is a TypeScript construct called “intersection type”. It is a new type that combines all type aliases or interfaces you’ve specified. When you define new intersection type, you have to declare it staring with type keyword, similarly to aliases.

// Create first interface:
interface BasicSet {
  monitorModel: string;
  keyboardModel: string;
  mouseModel: string;
}

// Create second interface:
interface AdvancedSet {
  headphonesModel: string;
  gameControllerModel: string;
}

// Create intersection type based on Square and Cube:
type GamerSet = BasicSet & AdvancedSet
// Translates to:
// type GamerSet = {
//   monitorModel: string;
//   keyboardModel: string;
//   mouseModel: string;
//   headphonesModel: string;
//   gameControllerModel: string;
// }

Intersection types have an advantage over extending. You can use them when you define new object types as well as when you annotate object with existing types. The syntax in both cases is the same.

// Create first interface:
interface BasicObject {
  width: number;
  height: number;
  depth: number;
}

// Create second interface:
interface Book {
  title: string;
  author: string;
  numberOfPages: number;
}

// Annotate object with intersection type
// of BasicObject and Book:
const hardcover: BasicObject & Book = {
  width: 7.5,
  height: 9.3,
  depth: 0.9,
  title: 'The Four Steps to the Epiphany',
  author: 'Steve Blank',
  numberOfPages: 384
}

Intersection types also allow you to combine interfaces with type aliases and vice versa.

// Create an interface:
interface BasicBook {
  sizeInMB: number;
  numberOfPages: number;
}

// Create a type alias:
type Book = {
  title: string;
  author: string;
}

// Annotate object with intersection type
// of BasicObject interface and Book type:
const ebook: BasicBook & Book = {
  sizeInMB: 5,
  numberOfPages: 400,
  title: 'Good to Great',
  author: 'Jim Collins'
}

Object types and generics

Basic type aliases and interfaces work well when you know what type this or that property will be. This may not apply to all situations. It can happen that at the time you define an object type you will know only that some property will be expected. However, you will not know what type this property will be, not at that moment.

You could solve this problem by using multiple possible types. You could also use any or unknown, but neither of these will be really helpful. TypeScript gives you third option you can use in situations like these. When you define a new object type, you can also specify any parameters it accepts.

When you specify a parameter for an object type, you can then reference it inside the body of the object type. You can think about this basically as about a function. If you define a function and specify a parameter you can then work with that parameter inside the function itself.

What’s more, when you define a parameter, you don’t have to know its value at that very moment. You will know it when you call the function and pass the value as an argument. Object types allow you to do the same thing. In case of object types, interfaces, type aliases and types in general, these constructs are called “generic”.

This name may sound strange and cryptic. Stripped to the essentials, generics are basically just types that work with parameters. You can define a parameter for interface or type aliases using angle brackets notation. These angle brackets follow after the name of the type and contain all parameters the type accepts.

If you have multiple parameters, you separate them by commas. The important thing comes when you want to use that type. When you define a parameter for a type and you want to use it, you have to specify some value for the parameter and pass it as an argument. This allows you specify types at the moment of annotation, not declaration.

// Example no.1: Interfaces
// Create a generic interface with one parameter called "Type":
interface Robot<Type> {
  name: Type; // Referencing the "Type" parameter
}

// Specify "Type" parameter to be type of number:
const factoryBot: Robot<number> = {
  name: 26598
}
// The interface for "factoryBot" will be:
// interface Robot<number> {
//   name: number;
// }

// Specify "Type" parameter to be type of string:
const humanoid: Robot<string> = {
  name: 'Joe'
}
// The interface for "humanoid" will be:
// interface Robot<string> {
//   name: string;
// }


// Example no.2: Type aliases
type Book<BookType> = {
  title: string;
  author: string;
  type: BookType;
}

// Specify "BookType" parameter to be type of 'electronic':
const ebook: Book<'electronic'> = {
  title: 'A Game of Thrones',
  author: 'George R. R. Martin',
  bookType: 'electronic'
}
// The type for "ebook" will be:
// type Book<'electronic'> = {
//   title: string;
//   author: string;
//   bookType: 'electronic';
// }

Generics beyond simple type parameters

When you work with generic, you can also use parameters to pass another types or interfaces. This can be useful when you have complex objects where some parts will change. For example, different types of payload. One type of a payload might be object of one shape. Another type of a payload might be object of a different shape.

You could solve this by using multiple variants of types or interfaces. This would lead to duplicate code, even if you used extending. Another solution is to create one generic type or interface and use the parameter to specify the payload type.

// Create a generic interface with "Payload" parameter:
interface ServerResponse<Payload> {
  code: string;
  message: string;
  payload: Payload;
}

// Create interface for the first type of payload:
interface PayloadUser {
  name: string;
  email: string;
}

// Create interface for the second type of payload:
interface PayloadPost {
  title: string;
  author: string;
  slug: string;
  date: string;
}

// Create interface for the third type of payload:
interface PayloadImage {
  title: string;
  size: number;
  fileType: string;
}

// Create object for response with payload of type user:
const serverResponseWithUser: ServerResponse<PayloadUser> = {
  code: '200',
  message: 'Success',
  payload: {
    name: 'Sandy Jones',
    email: 'sandy@jones.com',
  }
}

// Create object for response with payload of type post:
const serverResponseWithPost: ServerResponse<PayloadPost> = {
  code: '200',
  message: 'Success',
  payload: {
    title: 'Hello world',
    author: 'Anonymous',
    slug: 'Hello world',
    date: 'January 1, 1970 00:00:00'
  }
}

// Create object for response with payload of type image:
const serverResponseWithImage: ServerResponse<PayloadImage> = {
  code: '200',
  message: 'Success',
  payload: {
    title: 'cat',
    size: 1,
    fileType: 'jpg'
  }
}

Conclusion: Introduction to object types in TypeScript pt2

With the help of extending interfaces and intersection types, work with even complex objects becomes easier. When you add generics, even temporarily unknown types are no longer a problem. It is my hope that this tutorial helped you understand all these concepts, how they work and how to use them.

If you liked this article, please subscribe so you don't miss any future post.

If you'd like to support me and this blog, you can become a patron, or you can buy me a coffee 🙂

By Alex Devero

I'm Founder/CEO of DEVERO Corporation. Entrepreneur, designer, developer. My mission and MTP is to accelerate the development of humankind through technology.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.