Mastering mapped types in TypeScript






TypeScript is a type-centric programming language, as is well known. This is further supported by the fact that TypeScript features allow users to construct new types from existing types for better programming.


With the use of this functionality, developers can write less repetitious, shorter, and more reusable code. We will learn about advanced TypeScript types in this article that are built from existing types to allow for code reuse.


The following subjects will be covered in this article:

  • Mapped types in TypeScript

  • Generics

  • Tuples

  • Keyof type operator

  • Indexed access types

  • Index signature

  • Union type

  • Utility types

  • Mapped types with other built-in mapped types

  • Recap



Mapped types in TypeScript


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


A mapped type maps existing properties of an existing type to the properties in a new type. Sometimes a type needs to be based on another type since you don't want to keep saying the same thing.


 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.


The syntax for index signatures, which is used to indicate the type of properties, is the foundation for mapped types.


Syntax of mapped types : { [ P in K ] : T }


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




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 denotes types.To work with multiple data types, generics use the common (generic) type variable <T> to access multiple data types rather than just one.  <T> is a type variable which acts as a reference type and  has one or more type parameters. 


Type variables in generics store the type (string, number) as a value. Users have the option to 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.

In order to further explain generics, consider the example below.

// Generics

const toArrayGeneric = <T> (x:T,y:T,z:T) => {

return [x,y,z]

}

let genericArray = toArrayGeneric<number> (1,2,3)

console.log(genericArray)


 Look at the above example, type variable<T> is created as a common(generic) type and assigned to parameters (x,y,z) which means parameterizing types. This common type <T> accessing and mapping types and values to x,y,z from genericArray. This output will be 1,2,3.


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)


Here, we used the typeof type operator. In coding, the typeof type operator is used to indicate the type of variables or properties. Here typeof will deliver the output  as a is string which is type of a variable, b is boolean which is type of b and c is number. All of them are carried out using generic type variables <X,Y,Z>.


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)


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


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. The output will be Test, 30, True, and 4. 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. 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() feature to add further elements. Let's see what the compiler will say; it will throw an error.

 

Tuple itself has two types

  1. Named tuples

  2. Destructing tuples


  1. Named tuples

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

//Named

let userTuple: readonly [userName:string,age:number,isEng:boolean] = [‘John’,30,true]


We give variable names such as userName, age, and isEng to each index of an array. The output will be userName:John, age:30, and isEng:true.

  1. Destructing tuples

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


//Accessing Named Tuple/Destructuring

let [userName,age,isEng] = userTuple

console.log(userName)


Here, userName, age, and isEng variable names are destructed from UserTuple, and you access the types and values of these variables through userTuple.

Now we see some built-in mapped types in TypeScript.

  • Union type

  • Index signature

  • Indexed access types

  • Keyof Type operator

  • Utility types


  • 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.


  • Index signature type

When we are unsure of the exact properties an interface possesses but know at least what types those properties should take, the index signature type can be useful.



interface User {

[key: string]: string

}

const user: User = {

name: “Jason”,

occupation: “teacher”

}


Here, interface User is not sure about its properties but knows its type is "string".

Look at the below example. When we mention properties with an unspecified type in the index signature, it will throw an error.


interface User {

[key: string]: string

age: number

// Property ‘age’ of type ‘number’ is not assignable to string index type ‘string’.

}


 In the below example, we use the union type to accept the age property, so it doesn’t              throw any errors.

interface User {

[key: string]: string | number

age: number

}

const user: User = {

name: “Jason”,

occupation: “teacher”,

age:29

}



  • Indexed access type

Indexed access type reuse the type of a property of another type.

Consider the below program, 

interface User {

id: number;

name: string;

address: {

street: string;

city: string;

country: string;

};

}

function updateAddress(

id: User[‘id’],

address: User[‘address’]

) {}

Here, id and address properties are reused in the updateAddress function from the interface User.

  • Keyof type operator

The keyof type operator takes an object type and returns a string or a numeric literal union of its keys.It is used to retrieve user values. Keyof type operator uses a format of a union operator and is primarily used in generics.

To extract the keys (variables) from an object in a type annotation, use the keyof type operatorThe keyof type operator, which queries the type named after it to extract all of its keys and then builds a union string literal type from the keys, is also known as an index query operator.


Syntax of the keyof type operator:  let keys: keyof ExistingType;

Let's look at few keyof type operator instances:


type Point = { x: number, y: number };

type P = keyof Point;


When you move your cursor on type P, it will show like  type P = “x”  | “y”.

Here, keyof type operator extracted the keys (variables) x and y and built a union of its keys "x" | "y".


If the type has a string or number index signature, keyof will return those types instead:

type Arrayish = { [n: number]: unknown };

type A = keyof Arrayish; // type A indicates like type A = number

type Mapish = { [k: string]: boolean };

type M = keyof Mapish; // type M indicates as type M = string  | number


M in this case is a string | number because JavaScript object keys are always converted to a string; obj[0] and obj["0"] are synonymous.


  • Utility types

creating new types by altering old ones.There are plenty of utility types available in TypeScript. 

Here, we can see some of these.

  • Partial

  • Required 

  • Record

  • Omit

  • Pick

  • Readonly

  • 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.

  • 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.


  • 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).

  • 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:

  • 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:


  • 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.


Mapped types with other built-in mapped  types


 type Properties = ‘a’ | ‘b’ | ‘c’ ;


A basic mapped type might resemble this.


type T = { [ P in Properties]: boolean };

// type T = {

// a: boolean;

// b: boolean;

// c: boolean;

// }


All it does is generate a boolean property by iterating through every conceivable string value.

This is not that helpful by itself, however including generics will greatly enhance it. It enables the definition of mapped types with all of their properties set to optional.


type Partial<T> = { [ P in keyof T ]?: T[P]; };

type IPartial = Partial<I>;  // ‘I’ is the interface defined on top 

// type IPartial {

//    a?: string;

//    b?: string;

// }


Although it appears a little more sophisticated, it follows the same format as the earlier, simpler definition. Here, the main distinction is that the properties of an existing type are modified.


In the beginning, it makes a literal string union of all property names using keyof (keyof T). The question mark(optional) is then added to each one as it iterates through them all ([P in keyof T]). The newly formed type is given the same type as the property on the provided type via the indexed access operator (T[P]).


Making properties optional is not the only option. It is possible to utilise any modifier or type. Even using the original property type is not required. For instance, making each property a number.


type ToNumber<T> = { [ P in keyof T ]: number };


or into a Promise


type ToPromise<T> = { [ P in keyof T ]: Promise<T[P]> };


Modifiers can even be eliminated by placing a - in front of them. For instance, removing the readonly modifier from every type's property.


type RemoveReadonly<T> = { -readonly [ P in keyof T ]: T[P] };


The optional marker is removed in the same way, thereby marking the property as required:


type RemoveOptional<T> = { [ P in keyof T ]-?: T[P] };


Recap


In TypeScript, changing one type into another type by applying a transformation to each of its properties is known as mapping. A mapped type "maps" properties in an existing type to the new properties of a new type. By iterating over a set of properties, a mapped type enables us to generate new types.


Mapped types are a helpful TypeScript feature for maintaining their types DRY ("Don't Repeat Yourself"). Mapped types are beneficial, when you have multiple types with the same keys but various value types.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.


Finally, these are the topics I wanted to discuss with you regarding TypeScript's mapped types and advanced types.


I hope you enjoy reading! Check you later!



Comments

Popular posts from this blog

DataDog vs. AWS CloudWatch: Choosing the Best Observability Tool

Redis Tutorial: Exploring Data Types, Architecture, and Key Features