Mastering Mapped Types in TypeScript

Typescript is an open-source and type-centric programming language that is becoming increasingly popular amongst web developers and software engineers. As an improvement to the existing JavaScript language, Typescript has been designed to make development easier, faster, and more efficient.

This functionality allows developers to write less repetitious, shorter, and reusable code. In this article, we will learn about advanced TypeScript types built from existing types to allow for code reuse.

One of the features of Typescript that makes it so powerful is the ability to use mapped types. These provide a way to create new types from existing types by transforming, filtering, or slicing their properties.

In this blog post, we will look at the basics of mapped types in Typescript and what makes them so useful for web development.

The following subjects will be covered in this article:

What are Mapped Types in TypeScript?

Mapped types in Typescript allow you to create new types based on an existing type by applying a transformation to each property in the original type. The transformation is specified using a mapping function that takes the original property as input and returns the new property.

The function of mapped types is to take one type and change it into another by changing each of its properties. New types are produced by mapping existing types of information into new types.

A "mapped type" is produced by accessing the properties of another type, using the keyof operator, and then altering the retrieved properties to produce a new type.

Syntax of Mapped types

The syntax for index signatures indicates the type of properties and is the foundation for mapped types.

{ [ P in K ] : T }

Here, P is a variable that represents a property key of the original type K. T is the type that the property will be transformed into. The resulting type will have the same properties as the original type, but with the values transformed into the specified type.

We'll go through some of TypeScript's essential features and the built-in mapped types that form the foundation of mapped types.

Built-in Mapped Types in TypeScript

There are a few built-in mapped types in TypeScript.

  1. Union type
  2. Index signature
  3. Indexed access types
  4. Keyof Type operator
  5. Utility types

1. Union type

A union type is used to define more than one data type for a variable or a function parameter, indicating that a variable has many types of values.

We define a union type as a type that is created from two or more other types and that represents values that can be of any type.

Syntax:

(type1 | type2 | type3 | .. | typeN)

Look at the following example:

let empId: string | number | boolean;
empId = 14;
empId = "all4";
empId = true;

Using the union type, the variable empId is declared by more than one type as string, number, and boolean. The | operator is used to represent the union type.

For example, let's say we have a function that takes a user ID, which can be either a string or a number:

function getUserById(id: string | number): void {
  // function logic here
}

This function can now be called with an argument that is either a string or a number:

getUserById('123'); // valid
getUserById(456); // valid
getUserById(true); // invalid - type error

By using a union type, we can make the getUserById function more flexible and allow it to accept both strings and numbers, while still ensuring that any other types are not accepted.

2. Index Signature

An index signature in TypeScript allows you to define the type of the values that an object can have when accessed with a certain key that is not defined in the object's interface.

Here's an example of how you can use an index signature to define the type of an object with dynamic keys:

interface User {
  name: string;
  age: number;
  [key: string]: string | number;
}

const user: User = {
  name: "John",
  age: 30,
  email: "john@example.com",
  phone: "123-456-7890",
  address: "123 Main St.",
};

In this example, we define an interface User that has two required properties, name and age, and an index signature that allows any additional properties with string or number values.

The key: string syntax in the index signature specifies that the property key must be a string, and the [key: string]: string | number syntax specifies that the property value can be either a string or a number.

We then create an object user that has the required name and age properties, as well as additional properties like email, phone, and address. The use of an index signature allows us to add any number of additional properties to the user object without having to update the User interface.

3. Indexed access type

An indexed access type is a powerful feature in TypeScript that allows you to access properties of an object dynamically using a string or a number.

Here's an example of how you can use indexed access type to create a function that retrieves a property value from an object:

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = {
  id: 1,
  name: 'John Doe',
  email: 'johndoe@example.com',
  age: 30
};

const getName = getProperty(user, 'name'); // valid
console.log(getName);

const getAge = getProperty(user, 'age'); // valid
console.log(getAge);

const salary = getProperty(user, 'salary'); // invalid - type error

In this example, we define an interface called User, which describes the properties of an object. We then define a function called getProperty that takes an object and a property name as arguments, and returns the value of the property.

The <T, K extends keyof T> syntax specifies that the getProperty function takes a generic type T that represents the type of the object, and a K type that extends the keys of T. This means that the key parameter can only be a key of the obj parameter.

By using an indexed access type, we can ensure that the property key passed to the getProperty function exists on the object, and return the type of that property.

4. Keyof type operator

The keyof type operator in TypeScript is used to create a union type of all the keys of a given type. It is often used in combination with mapped types to create new types that are based on existing types.

Syntax of the keyof type operator:

let keys: keyof ExistingType;

Here's an example of how keyof can be used in a mapped type:

interface User {
  id: number;
  name: string;
  email: string;
}

type UserKeys = keyof User;
// equivalent to:
// type UserKeys = "id" | "name" | "email"

In this example, we define an interface User with three properties. We then use the keyof operator to create a type UserKeys that is a union of the keys of the User interface. The resulting type is equivalent to the string literal type "id" | "name" | "email".

We can use UserKeys to create new types based on the keys of User. For example, we can create a type that represents a subset of the properties of User:

type UserSubset = {
  [K in UserKeys]?: User[K];
};

const userSubset: UserSubset = {
  name: "John",
  email: "john@example.com",
};

In this example, we use a mapped type to create a new type UserSubset that has optional properties corresponding to the keys of User. We use UserKeys to iterate over the keys of User and specify that each property in UserSubset should have the same type as the corresponding property in User.

The resulting type UserSubset allows us to create objects with a subset of the properties of User. We can then use UserSubset to type-check objects that have a subset of the properties of User.

5. Utility types

Utility types are predefined TypeScript types that provide common type transformations. They are used to simplify and streamline the creation of new types. There are plenty of utility types available in TypeScript.

Here, we can see some of these.

  • Partial
  • Required
  • Record
  • Omit
  • Pick
  • Readonly

a.) Partial

A partial type makes all of an object's properties optional. In this context, the word "optional" refers to a parameter that is not required to have a value or to be specified.

interface Point {
  a: number;
  b: number;
}

let pointPart: Partial<Point> = {}; // 'partial' allows a and b to be optional

pointPart.a = 10;
console.log(pointPart);

Output:

Here the point object has two properties, a and b. Partial type is used to make a and b optional, but we specify the value for a, so the output will be a:10.

b.) Required

Its primary role is to modify an object's properties so that they are all necessary.

interface Car {
  make: string;
  model: string;
  mileage?: number;
}
let myCar: Required<Car> = {
  make: "BMW",
  model: "X5",
  mileage: 12000, // ‘Required’  forces mileage to be defined
};
console.log(myCar);

Output:

In this example, the mileage property is declared as optional (? ), but using the required type, we set all properties of Car to be required. So the output also includes mileage.

c.) Record

Records serve as shorthand for the definition of object types with particular key and value types.

Record<string,number> is equivalent to {[key:string]:number}.

const nameAgeMap: Record<string, number> = {
  A: 21,
  B: 25,
};
console.log(nameAgeMap);

Output:

In this case, Record<string,number> indicates the type of the keys (A, B are string) and values (21, 25 are numbers).

d.) Omit

It eliminates keys from an object type.

interface Person {
  name: string;
  age: number;
  location?: string;
}
const details: Omit<Person, "age" | "location"> = {
  name: "XYZ",
  // "Omit" has removed age and location from the type and they can't be defined here
};
console.log(details);

Output:

e.) Pick

It eliminates all keys from an object type except for the ones we've specified and want to use.

interface Person {
  name: string;
  age: number;
  location?: string;
}
const details: Pick<Person, "name"> = {
  name: "XYZ",
  // "Pick" has only kept name, so age and location were removed from the type and they can't be defined here
};
console.log(details);

Output:

Pick TypeScript
Pick TypeScript

f.) Readonly

A property can be set to read-only using this method. Readonly is a user-friendly feature for this when developers don't want to modify the properties and its types in classes, types, or interfaces unnecessarily.

Outside of the class, readonly members can be accessed, but their values cannot be modified. They must therefore be initialised either at declaration time or inside the constructor.

class Employee {
  readonly empCode: number;
  empName: string;
  constructor(code: number, name: string) {
    this.empCode = code;
    this.empName = name;
  }
}
let emp = new Employee(10, "John");
emp.empCode = 20; // Compiler Error
emp.empName = "Bill";

In this case, we make empCode read-only and set the value to 10. However, we assigned empCode 20 outside of the Employee class. So it will show an error.

Generics

A technique for creating reusable coding components is called generics. It supports multiple data types rather than just one. Generics is a type that parametrizes the types. It ensures type safety.

Generics use the type variable <T>, which acts as a reference type with one or more type parameters. In generics, the standard (generic) type variable works with multiple data types instead of just one.

It stores the type (string, number) as a value. Users can construct generic classes, functions, methods, and interfaces in TypeScript.

Note: The use of type variable <T> is not limited to generics; some type variables are also available in generics.

To further explain generics, consider the example below:

const toArrayGeneric = <T> (x: T, y: T, z: T) => {
  return [x, y, z];
};

let genericArray = toArrayGeneric<number>(1, 2, 3);
console.log(genericArray);

The above code defines a function named toArrayGeneric which takes in three arguments of the same type T and returns an array of type T with those three arguments.

The toArrayGeneric function is generic as it uses the generic type parameter , which allows the caller to specify the type of the arguments at the function call site.

Declare a variable named genericArray of type T[] and initialize it with the result of calling toArrayGeneric with the arguments 1, 2, and 3. The T type is specified as a number by calling the function with .

Output:

Generics

Additionally, we employ generics with the typeof type operator for multiple types. Let's look at the example below.

// Multiple Types

let printValues = <X,Y,Z> (a:X, b:Y, c:Z) => {
	console.log(`a is ${typeof a}, b is ${typeof b}, c is ${typeof c}`)
}

printValues("one",true,1)

Output:

Multiple Types in Generics
Multiple Types in Generics

Tuples

A typed array with a predetermined length and types for each index is known as a tuple. Each one of the array's elements is described by its type. The tuple type should be set to read-only because the initial values of tuples have strongly defined types.

//Tuple

let userTuple:[string,number,boolean] = [‘John’,30,true]
console.log(userTuple)

Output:

[ 'John', 30, true ]

In this case, userTuple is indexed as each array element's predefined types (string, number, boolean).

Why should Tuples be set to read-only?

Consider the following program,

let userTuple:[string, number, boolean] = ["John", 30, true]

userTuple[0] = "Test"
userTuple.push(4)
console.log(userTuple)

In this, we created a typed array [string, number, boolean] = ["John", 30, true] using tuples. This coding gives more flexibility to users to modify the typed array.

Here userTuple[0] is used to modify the 0th index of an array, which means ‘John’ will be replaced by ‘Test’ and userTuple. push(4) is used to add the 4th element in an array.

In TypeScript, push() is used to add elements to an array. To avoid these unwanted modifications in an array, we should code tuples as readonly.

Output:

[ 'Test', 30, true, 4 ]

Let's look at an example of a read-only type

//readonly

let userTuple: readonly [string, number, boolean] = ['John', 30, true];
userTuple.push(4);

Here we set tuples as readonly and used the push() function to add further elements.

Output:

index.ts:2:11 - error TS2339: Property 'push' does not exist on type 'readonly [string, number, boolean]'.

Tuple has two types:

1. Named tuples

In a typed array, it provides a variable name for each index.

type Person = [firstName: string, lastName: string, age: number];

const person: Person = ["John", "Doe", 30];

console.log(person[0]); // "John"
console.log(person.firstName); // "John"
console.log(person.age); // 30

In this example, a type alias Person is defined as a tuple with three elements, each with a name and a type. A variable person of type Person is declared and initialized with a tuple containing three values.

The first element of the tuple can be accessed using array index notation or by using the name firstName. Similarly, the second and third elements can be accessed using array index notation or using the names lastName and age, respectively.

2. Destructing tuples

It is nothing more than the coding structure being broken down to make it easier to understand.

type Point = [number, number];

const point: Point = [1, 2];

const [x, y] = point;

console.log(x); // 1
console.log(y); // 2

In this example, a type alias Point is defined as a tuple with two elements, both of which are of type number. A variable point of type Point is declared and initialized with a tuple containing two numbers.

The const [x, y] = point syntax uses destructuring to extract the two values from the tuple into separate variables x and y. The variables x and y are then logged to the console. This is a convenient way to extract values from a tuple without having to use array index notation.

In this example, a type alias Point is defined as a tuple with two elements, both of which are of type number. Then, a variable point of type Point is declared and initialized with a tuple containing two numbers.

The const [x, y] = point syntax uses destructuring to extract the two values from the tuple into variables x and y. The variables x and y are then logged into the console. This is a convenient way to extract values from a tuple without using array index notation.

Conclusion

With mapped types, TypeScript gives you a lot of versatility. For example, you can change a type's name or perform string interpolation on keys.

In this blog post, we have covered the basics of mapped types in TypeScript and some advanced use cases. We have seen how to use mapped types to create new types that are based on existing types, and how to use key and value types in mapped types.

We have also covered some more advanced topics such as partial and required types, conditional types, and inference with mapped types. These techniques can be used to create highly customized and flexible types that meet your specific needs.

By mastering mapped types in TypeScript, you can create more robust and maintainable code, reduce the risk of errors, and increase productivity. Whether you're working on a small or large TypeScript project, mapped types can be an invaluable tool for creating and manipulating types.


Atatus Real User Monitoring

Atatus is a scalable end-user experience monitoring system that allows you to see which areas of your website are underperforming and affecting your users. Understand the causes of your front-end performance issues and how to improve the user experience.

By understanding the complicated frontend performance issues that develop due to slow page loads, route modifications, delayed static assets, poor XMLHttpRequest, JS errors, core web vitals and more, you can discover and fix poor end-user performance with Real User Monitoring (RUM).

You can get a detailed view of each page-load event to quickly detect and fix frontend performance issues affecting actual users. With filterable data by URL, connection type, device, country, and more, you examine a detailed complete resource waterfall view to see which assets are slowing down your pages.

Try your 14-day free trial of Atatus.

Vaishnavi

Vaishnavi

CMO at Atatus.
Chennai

Monitor your entire software stack

Gain end-to-end visibility of every business transaction and see how each layer of your software stack affects your customer experience.