Engineering Blog

                            

Utility Types in TypeScript

What are utility Types?

In TypeScript, utility types are built-in types that allow you to manipulate and transform existing types. They can be used to extract, exclude, or modify the properties of an existing type, or to create a new type based on an existing one. TypeScript provides several utility types to facilitate common type transformations. These utilities are available globally.

we will discuss a few of them below.

Pick<Type, Keys>

It Constructs a type by picking the set of properties Keys (string literal or union of string literals) from Type. let us assume , we happen to have a record of employee id and corresponding id’s employee name and department mapped to the id and we want to write a function to print out the details in the record based on the employee id.

here is one way of doing it (anti-pattern)

type Employee={
    id:number,
    address:string,
    age:number,
    salary:number,
    DOB:Date,
    DOJ:Date,
}


// a program to save personal details into a file and display to std o/p

type EmpRecord={
    [id:number]:{name:string,dept:string}
}
const empoyeeRecord:EmpRecord ={
    1:{name:"John", dept:"Sales"},
    2:{name:"David",dept:"Engineering"},
    3:{name:'Scott',dept:'Security'},
    4:{name:'Mary',dept:"Engineering"}
}



const findDept=(employee:Employee)=>{
    console.log(`${empoyeeRecord[employee.id]["name"]}  works in ${empoyeeRecord[employee.id]["dept"]} deptartment`)
}

findDept({id:2,
    address:"newYork",
    age:21,
    salary:200000,
    DOB:new Date('1997-01-01'),
    DOJ:new Date('1997-01-01'),
})

In the example above , it is clear that the findDept function only requires the id field in the input employee object,. Since we have defined the type of the input object to be of Employee , now we need to pass all the fields required by the Employee type so that the input object satisfies the contract of the function’s argument. A better way to tackle this would be extracting only the id filed into a type and using it as a contract to the function. For example:


const findDept=(employee:Pick<Employee,"id">)=>{
    console.log(`${empoyeeRecord[employee.id]["name"]}  works in ${empoyeeRecord[employee.id]["dept"]} deptartment`)
}

findDept({id:2})

Omit<Type, Keys>

The Omit utility type is the opposite of Pick. Instead of stating what properties to keep, Keys refers to the set of properties keys to be omitted. It is more useful when you only want to get rid of certain properties from an object and keep the others.

interface Employee {
  id: number;
  name: string;
  address: string;
  age: number;
  salary: number;
  DOB: Date;
  DOJ: Date;
};

type PersonalInfo=Omit<Employee,"id"|"salary"|"DOJ">


const getPersonalInfo = (emp: Employee):PersonalInfo => {
  return { name: emp.name, address: emp.address, age: emp.age, DOB: emp.DOB };
};

const personalInfo=getPersonalInfo({
  id: 2,
  name: "John",
  address: "newYork",
  age: 21,
  salary: 200000,
  DOB: new Date("1997-01-01"),
  DOJ: new Date("1997-01-01"),
});

console.log(personalInfo)

// outputs 

{
  name: 'John',
  address: 'newYork',
  age: 21,
  DOB: 1997-01-01T00:00:00.000Z
}

In the example above , we run a function that takes in an input object of type Employee and expects to return an object of type that leaves out id , DOJ and salary fields from output object. Instead of creating a new type for output we can simply derive a type from the the input type .

Partial<Type>

Constructs a type with all properties of Type set to optional. This utility will return a type that represents all subsets of a given type. It may become very useful while writing the update logic of an object.


interface Student{
    name:string;
    age:number;
    address:string;
    gender:'male'|'female',
    class:number,
    roll:number
}

let student1: Student = {
  name: "suman",
  age: 21,
  address: "kathmandu",
  gender: "male",
  class: 10,
  roll: 181,
};

const updateStudentInfo=(student:Student,updateInfo:Partial<Student>):Student=>{

    return {...student,...updateInfo}
}

student1=updateStudentInfo(student1,{gender:'female',address:'lalitpur'})
console.log(student1)

// outputs
{
  name: 'suman',
  age: 21,
  address: 'lalitpur',
  gender: 'female',
  class: 10,
  roll: 181
}

Required<Type>

Constructs a type consisting of all properties of Type set to required. The opposite of Partial.

Let us suppose we have built an e-commerce app where users can signup with name and email . But the premium members of this e-commerce app are eligible to SMS notification about the best deals in their city and therefore it is necessary to collect their phone number and city during upgrade to premium which they had left out during signup process. using Required utility type we can make sure that we collect these info.

interface User {
  name: string;
  email: string;
  phoneNo?: number;
  billingAddress?: string;
}

let user: User = { name: "John", email: "JohnDoe@email.com" };

const upgradeToPremiumUser = (
  normalUser: User,
  updateInfo: Required<Pick<User, "phoneNo" | "billingAddress">>
): Required<User> => {

    return {...normalUser,...updateInfo}
};

const newPremiumUser=upgradeToPremiumUser(user,{phoneNo:91987654321,billingAddress:'kolkata'})

console.log(newPremiumUser)

//outputs
{
  name: 'John',
  email: 'JohnDoe@email.com',
  phoneNo: 91987654321,
  billingAddress: 'kolkata'
}

Readonly<Type>

Constructs a type with all properties of Type set to Readonly, meaning the properties of the constructed type cannot be reassigned.

consider a use case where we want to fetch the client(front-end) a config file , which is not meant to be changed in any form before it is sent to the client to consume and therefore we type the config file as read only on our back-end so that when if accidentally we are writing a code that mutates the config file it fails and throws a warning .

// define config type
 interface Configuration {
     apiUrl: string;
     colorScheme: 'light' | 'dark';
     enableLogging: boolean;
     features: {
       comments: boolean;
       likes: boolean;
       recommendations: boolean;
    };
     i18n: {
       defaultLanguage: string;
       supportedLanguages: string[];
    };
  }
  type ConfigObject=Readonly<Configuration>

  // init a config type
const config: ConfigObject = {
    apiUrl: 'https://api.example.com',
    colorScheme: 'light',
    enableLogging: true,
    features: {
      comments: true,
      likes: true,
      recommendations: true
    },
    i18n: {
      defaultLanguage: 'en',
      supportedLanguages: ['en', 'fr', 'de']
    }
  };

  // some more lines of related code

  config.apiUrl='' // uninteded code (throws waring and doesn't  compile)

Readonly type can be very handy to avoid such unintended bugs when we are dealing with critical data.

Generics

In TypeScript, generics are a way to create reusable types and functions that can work with a variety of different types. Generics are indicated by placing a type parameter within angle brackets (<>).

Here is an example of a generic function that takes an argument of type T and returns an array of that type:

function createArray<T>(item: T, count: number): T[] {
    return new Array(count).fill(item);
  }
  
  const numbers = createArray<number>(0, 5); 
  const strings = createArray<string>('hello', 3); 
  console.log(numbers) // [0, 0, 0, 0, 0]
  console.log(strings) // ['hello', 'hello', 'hello']

References

Previous Post
Next Post