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.
- Union type
- Index signature
- Indexed access types
- Keyof Type operator
- 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:
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:
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:
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.
#1 Solution for Logs, Traces & Metrics
APM
Kubernetes
Logs
Synthetics
RUM
Serverless
Security
More