DEV Community

Cover image for Object Types (TypeScript)
Tomohiro Yoshida
Tomohiro Yoshida

Posted on • Updated on

Object Types (TypeScript)

What you should know about Object Types in TypeScript

I will mainly talk about these two topics in this article.

  1. Structural Typing
  2. Discriminated Unions

This article is inspired by Learning TypeScript written by Josh Goldberg. I highly recommend reading this book if you want to learn TypeScript more.

What are Object Types and how to use them

If you declare a new object, TypeScript will consider it a new object type.

In real-world development, it’s common to utilize Aliased Object Types.

I intentionally use “type” instead of “interface” in this article because I expect some readers of this article may not know about “interface” yet. But the type will be interchangeable in most cases if you’ve already been familiar with the interface.

type Vehicle = {
   wheels: number;
   plate: string;
   run: () => void;
}

const car: Vehicle = {
   wheels: 4,
   plate: "aa2-cc4",
   run: () => {console.log("This car runs very fast!")}
}
Enter fullscreen mode Exit fullscreen mode

If objects’ properties are not precisely the same when you initialize objects with Type annotation, TypeScript yells at you.

type Vehicle = {
   wheels: number;
   plate: string;
   run: () => void;
}

const runner: Vehicle = {
   run: () => {console.log("This runner runs very fast!")}
}
// Type '{ drive: () => void; }' is missing the following properties from type 'Vehicle': wheels, plate

const airplane: Vehicle = {
    wheels: 3,
    plate: "11b-334",
    run: () => {console.log("This plane runs to departure!”)},
    fly: () => {console.log("This plane flies very high!)}
}
// Type '{ wheels: number; plate: string; run: () => void; fly: () => void; }' is not assignable to type 'Vehicle'. Object literal may only specify known properties, and 'fly' does not exist in type 'Vehicle'.
Enter fullscreen mode Exit fullscreen mode

So far so good, right?

Okay, it’s a good time to dive into the TypeScript world deeply.

Structural Typing

Please guess what happens in the example below:

type Runner = {
    run: () => void;
}

type Vehicle = {
   wheels: number;
   plate: string;
   run: () => void;
}

const car: Vehicle = {
   wheels: 4,
   plate: "aa2-cc4",
   run: () => {console.log("This car runs very fast!")}
}

const startToRun = (arg: Runner) => {
    arg.run();
}

startToRun(car);
Enter fullscreen mode Exit fullscreen mode

Will TypeScript alert you? What do you think?

The answer is No.

You can execute the code without any error messages and see the log "This car runs very fast!" in the console.

It is the concept of Structural Typing, which is described in the Learning TypeScript book as follows:

TypeScript’s type system is structurally typed: meaning any value that happens to satisfy a type is allowed to be used as a value of that type.

In other words, TypeScript does not care if there are extra properties. But it matters if a given value can satisfy the properties of the type.

Let’s look back at the previous example again.

type Runner = {
    run: () => void;
}

type Vehicle = {
   wheels: number;
   plate: string;
   run: () => void;
}

const car: Vehicle = {
   wheels: 4,
   plate: "aa2-cc4",
   run: () => {console.log("This car runs very fast!")}
}

const startToRun = (arg: Runner) => {
    arg.run();
}

startToRun(car);
Enter fullscreen mode Exit fullscreen mode

In this example, the car variable is compatible to be an argument of the startToRun function because the Vehicle type can satisfy the run property in the Runner type.

Let me show one more example:

type Runner = {
    run: () => void;
}

type Vehicle = {
   wheels: number;
   plate: string;
   run: string;
}

const car: Vehicle = {
   wheels: 4,
   plate: "aa2-cc4",
   run: "boom"
}

const startToRun = (arg: Runner) => {
    console.log(arg.run());
}

startToRun(car);
// Argument of type 'Vehicle' is not assignable to parameter of type 'Runner'.
    // Types of property 'run' are incompatible.
    // Type 'string' is not assignable to type '() => void'.
Enter fullscreen mode Exit fullscreen mode

In the above example, the type of the run property in the Vehicle type is string that is a different type from the run property in the Runner type. Therefore, TypeScript says that Types of property 'run' are incompatible.

Discriminated Unions

Once you understand how to use Object Types, you will want to use them with Union.

Please take a look at an example:

type EBook = {
    title: string;
    megabyte: number;
}

type PaperBook = {
    title: string;
    pages: number;
}

type Book = EBook | PaperBook;

const book: Book = Math.random() > 0.5
    ? {title: 'Enjoy cooking', megabyte: 200}
    : {title: 'Learning History', pages: 300};

book.title; // No warning

book.megabyte
// Property 'megabyte' does not exist on type 'Book'.
//  Property 'megabyte' does not exist on type 'PaperBook'.

book.pages
// Property 'pages' does not exist on type 'Book'.
//  Property 'pages' does not exist on type 'EBook'.
Enter fullscreen mode Exit fullscreen mode

TypeScript warns when accessing the megabyte and pages properties because these properties will potentially not exist in the Book type.

If you want to access megabyte or pages, you should use the technique of “Narrowing” to narrow down the possibility of the existence of properties.

Here is an example:

if("megabyte" in book) {
    book.megabyte; // No type error
} else {
    book.pages; // No type error
}
Enter fullscreen mode Exit fullscreen mode

The logic for narrowing down types is the so-called type guard.

There is another solution for narrowing down Unions. You can add a property whose value indicates the object’s type to union-typed objects.

type EBook = {
    title: string;
    megabyte: number;
    format: 'digital';
}

type PaperBook = {
    title: string;
    pages: number;
    format: 'paper';
}

type Book = EBook | PaperBook;

const book: Book = Math.random() > 0.5
    ? {title: 'Enjoy cooking', megabyte: 200, format: 'digital'}
    : {title: 'Learning History', pages: 300, format: 'paper'};

if(book.format === 'digital') {
    book.megabyte; // No type error
} else {
    book.pages; // No type error
}
Enter fullscreen mode Exit fullscreen mode

Perfect! You can safely access the properties that will potentially not exist by this technique.

The union-typed object with this shape is called a discriminated union. And the property that is used as an identifier of union typed objects is a discriminant.

Are you wondering why string values can be a type?

Good point. Actually, 'digital' and 'paper' in this code is literal types that are more specific versions of primitive types. Therefore, you can consider 'digital' and 'paper' as types in the above example code.

Conclusion

Object Types are a powerful technique to take advantage of TypeScript features.

One important fact that you should keep in mind is that the type system in TypeScript is Structural Typing which means you can use any value satisfying a type as a value of that type. (It does not matter whether the value has extra properties and a different name of a type.)

When it comes to making a union of object types, you can add a discriminant to object types. And the shape of union typed objects that have discriminant is considered as a discriminated union.

Top comments (0)