What you should know about Object Types in TypeScript
I will mainly talk about these two topics in this article.
- Structural Typing
- 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!")}
}
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'.
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);
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);
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'.
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'.
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
}
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
}
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)