Introduction to Object Types in TypeScript Pt1

Introduction to Object Types in TypeScript feature image

In JavaScript, objects are among the most popular ways to work with and pass around data. In TypeScript, there is a special type called “object type” created solely for objects. This tutorial will help you understand what object types in TypeScript are and how to work with them.

Object types in a brief

In JavaScript, there are basically two types of values. The first type are primitive data types. These are the eight basic data types, some of which you will work with quite often. These data types include string, number, boolean, null, symbol and so on. Aside to these primitive data types, there is the second type of values.

This second type of values are objects. In JavaScript, you can quickly distinguish between a primitive data type and an object by looking at the value. If the value itself has any properties, it is an object. Otherwise, it is one of the eight primitive data types. Each of these types has also corresponding type in TypeScript.

This applies to objects as well. In TypeScript, there is a new type called object type. This type applies to any value that has some properties, at least one. This new type aims to make working with objects, as well as annotating them, easier.

Anonymous object types

TypeScript allows you to define two types of object types. The first type is anonymous. This is when you define an object for a specific object without using type or an interface. One example of an anonymous object type can be a function parameter. Let’s say you have a function which accepts an object as a parameter.

If you want to define the object type for this object parameter as anonymous you will define it at the definition of the function. You define what properties the object is supposed to have. For each property, you also define what the type of the property value is.

// Define a function with anonymous object type:
function myFunc(user: { username: string, email: string }) {
  return `user: ${user.username}, email: ${user.email}`
}

In the example above, you defined the object parameter called the user. The anonymous object type of this parameter says that the object has two properties: username and email. Both properties are of type string and both are required.

Named object types

The second way to define an object types is by using a type alias or an interface. In this case, you use one of these two to define the shape of the object. When you want to annotate an object with this shape you reference the type alias or interface. TypeScript will use the alias or interface to infer types for all object properties.

// No.1: type alias
// Create a type alias for user object:
type User = {
  username: string;
  email: string;
}

// No.2: interface
// Create am interface for user object:
interface User {
  username: string;
  email: string;
}

// Use the type alias or interface to annotate user parameter:
function myFunc(user: User) {
  return `user: ${user.username}, email: ${user.email}`
}

The structure of the object type itself is the same. There are still two properties, of a type string. The difference is that now the object type is defined outside the function or place where it is used, independently if you want.

Named and anonymous object type and re-usability

Named object types have one big benefit that is re-usability of your code. When you define object types as named, you can use them as many times as you want. If you also export them, you can also use them anywhere you want. Write once, use anywhere, any time. You can’t do this with anonymous types.

// Define the type alias for Human object once:
type Human = {
    name: string;
    age: number;
}

// Use Human type alias for one function:
function getUser(user: Human) {
  return `name: ${user.name}, age: ${user.age}`
}

getUser({ name: 'Tim', age: 44 })
// Output:
// 'name: Tim, age: 44'

// Use Human type alias for another function:
function getUsers(users: Human[]) {
  const usersNames = users.map(user => user.name)

  return usersNames
}

getUsers([{
  name: 'Joe',
  age: 21
}, {
  name: 'Jack',
  age: 36
}, {
  name: 'Samantha',
  age: 27
}])
// Output:
// [ 'Joe', 'Jack', 'Samantha' ]

Since anonymous object type has no name you can’t reference it elsewhere in your code. If you want to re-use the shape it defines, you have to write it again. This is one reason TypeScript developers use named object types more often than anonymous. However, this doesn’t mean you should never use anonymous object type.

A good rule of thumb is to think about the object and what is the likelihood you will use its shape again. If it is likely you will work with its shape, or similar, it might be a good idea to create a type alias or an interface. Then, whenever you will work with that specific shape you will reference the alias or interface.

This will make it much easier to make changes as you work. You will have to change only one place, the alias or the interface. Once you make the change, it will propagate everywhere you use the alias or the interface. Compare this to searching for all occurrences of that specific shape in your code and updating them.

This will also help you keep the probability of bugs at the minimum. When you update the alias or interface TypeScript will be able to immediately warn you if you have to change some code so the code reflects the new shape. This will not happen with anonymous object type because there is no single source of truth TypeScript could use.

On the other hand, if you are not likely to work with that, or similar, shape again, anonymous object type will do the job.

Object type and property modifiers

When you define an object type, anonymous or named, all properties are required and changeable. TypeScript allows you to change this with the help of property modifiers.

Optional object properties

There is a difference between an object that may have some properties and an object that must have some properties. When you create an object type that specifies some properties, TypeScript expects to find these properties in the object you annotated with that object type.

If you forget to define all these properties in the object, TypeScript will complain. Along with this, TypeScript will also expect to find only those properties you defined. It will not expect any other. It will actually also complain if it finds some additional properties. There are two ways out of this.

The first way is to create multiple variations of the object type to cover various use cases. This might work for some cases, when you alter the shape of the object. However, creating new variant just to make one property optional is insane. What you can do instead is to tell TypeScript that some property is optional.

This will also tell TypeScript that the property may not be defined every time. And, if it is indeed not defined it should complain about it. Well, unless you actually try to use the property. You can achieve this, making some property optional, by putting a question mark symbol (?) right after the property name in the object type.

// Create object type with optional properties:
type Animal = {
  name: string;
  species: string;
  numberOfLegs?: number; // This property is optional (the '?' after the property name)
  wingSpan?: number; // This property is optional (the '?' after the property name)
  lengthOfTail?: number; // This property is optional (the '?' after the property name)
}

// This will work:
const dog: Animal = {
  name: 'Jack',
  species: 'dog',
  numberOfLegs: 4,
  lengthOfTail: 30
}

// This will work:
const bird: Animal = {
  name: 'Dorris',
  species: 'pelican',
  wingSpan: 1.83,
}

// This will work:
const fish: Animal = {
  name: 'Nemo',
  species: 'fish'
}

// This will not work:
const spider: Animal = {
  name: 'Joe'
  // The "species" property is required, but missing.
}
// TS error: Property 'species' is missing in type '{ name: string; }' but required in type 'Animal'.

Readonly object properties

The second property modifier is readonly. This modifier helps you specify properties which values should not be change after you initialize them. Note that this modifier works only in TypeScript. If you mark some property as readonly, and later try to change it, TypeScript will complain by throwing an error.

However, this will not prevent JavaScript from executing that change. For JavaScript, there is no such a thing as a readonly property, at least not now. You can specify a property as a readonly by putting the readonly keyword just before the property in the object type.

// Create object type with optional properties:
type User = {
  readonly name: string; // Make "name" readonly
  readonly email: string; // Make "email" readonly
  password: string;
  role: 'admin' | 'user' | 'guest';
}

// This will work:
const jack: User = {
  name: 'Jack',
  email: 'jack@jack.com',
  password: '1234_some_pass_to_test_56789',
  role: 'guest'
}

// This will work:
// Try to change value of property "role" on "jack" object:
jack.role = 'user'

// This will not work:
// Try to change value of readonly property "email" on "jack" object:
jack.email = 'jack@yo.ai'
// TS error: Cannot assign to 'email' because it is a read-only property.

Object types and index signatures

So far, we’ve worked with objects in which we knew all properties beforehand. This may not be true every time. You may find yourself in situations where you will know only what type of property and what type of a value to expect. However, you may not know the exact name of the property.

In TypeScript, this is not a problem thanks to index signatures. With index signatures, you can specify the type of a property you expect along with the type of its value. This gives you a lot of flexibility because as long as both types are correct TypeScript will not complain about anything.

When you want to use index signature you have to remember to use a slightly different syntax for defining properties. Normally, you would define some property “X”, add colon, and then add some type for its value. This tells TypeScript that there is specific property “X” in the object. Thing is, we don’t know this “X”.

To overcome this with index signature, you have to wrap the property with square brackets and add some type. This type says what type the property itself will be. Allowed types for index signatures are string and number. The rest is the same. What follows next are colons and some type for the value.

// Create object type with index signature:
type StringKey = {
  // The property will be a type of string:
  [key: string]: string;
}

// Create another object type with index signature:
type NumberKey = {
  // The property will be a type of number:
  [index: number]: string;
}

// This will work:
const user: StringKey = {
  // Property is always a string.
  firstName: 'Jack',
  lastName: 'Doe',
}

// This will work:
const bookshelf: StringKey = {
  // Property is always a number.
  1: 'Hackers and Painters',
  2: 'Blitzscaling',
}

// This will not work:
const languages: NumberKey = {
  // Properties are strings, not numbers.
  one: 'JavaScript',
  two: 'TypeScript',
}
// TS error: Type '{ one: string; two: string; }' is not assignable to type 'NumberKey'.
// Object literal may only specify known properties, and 'one' does not exist in type 'NumberKey'.

// This will also not work:
const pets: StringKey = {
  // Properties are strings,
  // but the values are numbers and not strings.
  dog: 1,
  cat: 2,
}
// TS error: Type 'number' is not assignable to type 'string'.

Readonly index signatures

Index signatures also allow you to use the readonly keyword to specify readonly properties.

// Create object type with index signature and readonly property:
type ReadonlyStringKey = {
  // The property will be a type of string and a readonly:
  readonly [key: string]: string | number;
}

// Create new object with shape of "ReadonlyStringKey":
const cat: ReadonlyStringKey = {
  name: 'Suzzy',
  breed: 'Abyssinian Cat',
  age: 2
}

// This will not work:
// Try to change the value of "name" property on "cat":
cat.name = 'Vicky'
// TS error: Index signature in type 'ReadonlyStringKey' only permits reading.

// Try to change the value of "age" property on "cat":
cat.age = 1
// TS error: Index signature in type 'ReadonlyStringKey' only permits reading.

Conclusion: Introduction to Object Types in TypeScript

Objects are fundamental part of JavaScript. TypeScript object types can also make them type safe. Object types can also make it easier to work with objects in general. I hope that this tutorial helped you learn what anonymous and named object types in TypeScript are and how to use property modifiers and index signatures.

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.