DEV Community

Cover image for 12 Must-Have TypeScript Utility Types with Uses and Examples
Rajat Kaushik
Rajat Kaushik

Posted on

12 Must-Have TypeScript Utility Types with Uses and Examples

Introduction of TypeScript and its static typing capabilities has had a profound impact on the developer experience in the JavaScript world, making it now the industry-standard for most large-scale codebases.

However, TypeScript's value does not stop at static typing. It showcases its prowess through a collection of several utility types – predefined constructs that allow developers to seamlessly modify and utilize existing types: from Partial for crafting optional properties to ReturnType for inferring function returns, streamline complex type manipulations, saving developers both time and mental bandwidth.

In this article, we'll be talking about 12 underrated utility types that you should be using more in your codebase. These TypeScript utility types are like tools in your coding toolbox, each serving a unique purpose to help you shape your code with precision and clarity.

Table of Contents

Object Manipulation Types

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

Union Manipulation Types

  • Exclude
  • NonNullable
  • Extract

Function Types

  • Parameters
  • Return Type
  • Awaited

A note on interfaces vs types

We will be using both interfaces and types in this article.

In TypeScript, the choice between using types and interfaces depends on the specific scenario. Interfaces are suited for describing the shape of objects, defining their properties, methods, and events. On the other hand, types are more versatile, allowing you to define primitive type aliases, tuples, unions, and work with functions, complex types, and mapped types. Ultimately, the choice between types and interfaces depends on the specific use case and the developer's judgment.

1. Partial

Partial allows you to create a type where all properties of type T are made optional. This is like transforming a strict "must-have" checklist into a more relaxed "choose what you want."

When it's useful: When dealing with forms or data that might not have all fields filled in.

interface Pizza {
  size: string;
  toppings: string[];
  delivery: boolean;
}

type PartialPizza = Partial<Pizza>;

const customPizza: PartialPizza = {
  size: "Large",
  toppings: ["Pepperoni", "Mushrooms"],
};
Enter fullscreen mode Exit fullscreen mode

2. Required

Required enforces that all properties of type T must be present. It's like making sure you have all the key ingredients for your favorite dish.

When it's useful: When you want to ensure certain properties are always included in your data.

type RequiredPizza = Required<PartialPizza>;

const completePizza: RequiredPizza = {
  size: "Medium",
  toppings: ["Cheese", "Tomato"],
  delivery: true,
};
Enter fullscreen mode Exit fullscreen mode

3. Readonly

Readonly creates a type where all properties of type T are read-only. Think of it like a "no-touch" policy for your object properties.

When it's useful: When you want to prevent accidental changes to your data.

type ReadonlyPizza = Readonly<Pizza>;

const classicPizza: ReadonlyPizza = {
  size: "Small",
  toppings: ["Olives", "Onions"],
  delivery: false,
};
Enter fullscreen mode Exit fullscreen mode

4. Pick

Pick helps you extract a type containing only the specified properties from type T. Think of it as creating a custom mini-version of an object.

When it's useful: When you want to narrow down your object's properties to just the essentials.

type EssentialPizza = Pick<Pizza, 'size' | 'toppings'>;

const simplePizza: EssentialPizza = {
  size: "Medium",
  toppings: ["Tomatoes", "Cheese"],
};
Enter fullscreen mode Exit fullscreen mode

5. Record

Record creates a type with specified keys of type Keys and values of type Value. Imagine building your own dictionary of words and their meanings.

When it's useful: When you need to create a structured mapping between keys and values of specific type.

type FruitDictionary = Record<string, string>;

const fruits: FruitDictionary = {
  mango: "Yellow and delicious",
  apple: "Red and round",
};
Enter fullscreen mode Exit fullscreen mode

6. Omit

Omit creates a type by excluding specific properties listed in Keys from type T. It's like editing out parts of a picture you don't need.

When it's useful: When you want to remove certain properties from an object type.

type PizzaWithoutDelivery = Omit<Pizza, 'delivery'>;

const inPersonPizza: PizzaWithoutDelivery = {
  size: "Large",
  toppings: ["Bacon", "Jalapeno"],
};
Enter fullscreen mode Exit fullscreen mode

7. Exclude

Exclude filters out specific types from a union that are assignable to the excluded type. It's like saying "I only want one type of snack."

When it's useful: When you want to narrow down the possible values of a union type.

type SnackOptions = "Cookies" | "Fruit";

// Excluding "Cookies" for a guest with dietary restrictions
type GuestSnackOptions = Exclude<SnackOptions, "Cookies">;

const guest1: GuestSnackOptions = "Fruit"; // Valid
const guest2: GuestSnackOptions = "Cookies";   // Error: 'Chips' is excluded

Enter fullscreen mode Exit fullscreen mode

8. NonNullable

NonNullable ensures a value is not null or undefined within a union. It's your way of saying "I want the real thing!"

When it's useful: When you need to ensure a value isn't missing in a union type.

type NullableValue = string | null | undefined;
type SureValue = NonNullable<NullableValue>;
//same as type SureValue = string;

const certainValue: SureValue = "Absolutely!";
Enter fullscreen mode Exit fullscreen mode

9. Extract

Extract retains only the types from a union that are assignable to the extracted type. It's like fishing out your favorite items from a mixed bag of snacks.

When it's useful: When you want to extract specific types from a union type.

type SweetFruit = Extract<string | number | boolean, string>;
//same as type SweetFruit = string;

const favoriteFruit: SweetFruit = "Mango"; // Delicious choice!
Enter fullscreen mode Exit fullscreen mode

10. Parameters

Parameters captures the parameter types of a function.

When it's useful: When you need to work with the types of function parameters.

function cookDinner(mainCourse: string, sideDish: string): void {
  console.log(`Tonight's menu: ${mainCourse} with a side of ${sideDish}`);
}

type DinnerIngredients = Parameters<typeof cookDinner>;

const tonight: DinnerIngredients = ["Spaghetti", "Salad"];
Enter fullscreen mode Exit fullscreen mode

11. ReturnType

ReturnType captures the return type of a function. It's like knowing what you're getting as the final dish.

When it's useful: When you want to know what type of value a function will return.

function bakeCake(): string {
  return "A delicious cake!";
}

type CakeType = ReturnType<typeof bakeCake>;

const cake: CakeType = "Chocolate"; // Yum, cake time!
Enter fullscreen mode Exit fullscreen mode

12. Awaited

Awaited extracts the resolved type of a promise returned by a function. It's like getting the actual meal after waiting for your order.

When it's useful: When you're working with promises and want to know what they will resolve to.

async function fetchMeal(): Promise<string> {
  const response = await fetch("https://api.example.com/meal");
  return response.text();
}

type DeliveredMeal = Awaited<typeof fetchMeal>;

async function enjoyMeal(): Promise<void> {
  const meal: DeliveredMeal = await fetchMeal();
  console.log(`Enjoying a delicious meal: ${meal}`);
}
Enter fullscreen mode Exit fullscreen mode

Bonus: Mutable

Mutable creates a type where all properties of type T are mutable. TypeScript does not offer this type out-of-the-box, so we have to create it ourselves.

When it's useful: When you've got a read-only object but need to make changes to it.

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

type MutablePizza = Mutable<ReadonlyPizza>;

const dynamicPizza: MutablePizza = {
  size: "Extra Large",
  toppings: ["Jalapenos", "Tomatoes"],
  delivery: true,
};
Enter fullscreen mode Exit fullscreen mode

Combination Example

Let's explore some examples by integrating various utility types in TypeScript. We'll adopt a theme of assembling a superhero team with diverse abilities. Using utility types, we'll model different facets of this scenario.

// Step 1: Define the types of superhero abilities
type Ability = "Flight" | "Strength" | "Invisibility" | "Telekinesis";

// Step 2: Create utility types for different types of superheroes
type SuperheroBase = {
  name: string;
  abilities: Ability[];
};

type FlyingSuperhero = Required<Pick<SuperheroBase, "name">> & {
  abilities: ["Flight"];
};

type StrongSuperhero = Required<Pick<SuperheroBase, "name">> & {
  abilities: ["Strength"];
};

type InvisibleSuperhero = Required<Pick<SuperheroBase, "name">> & {
  abilities: ["Invisibility"];
};

type TelekineticSuperhero = Required<Pick<SuperheroBase, "name">> & {
  abilities: ["Telekinesis"];
};

// Step 3: Create some superhero instances using the utility types
const superman: FlyingSuperhero = {
  name: "Superman",
  abilities: ["Flight"],
};

const hulk: StrongSuperhero = {
  name: "Hulk",
  abilities: ["Strength"],
};

const invisibleWoman: InvisibleSuperhero = {
  name: "Invisible Woman",
  abilities: ["Invisibility"],
};

const jeanGrey: TelekineticSuperhero = {
  name: "Jean Grey",
  abilities: ["Telekinesis"],
};

// Step 4: Create a superhero team with various abilities
type SuperheroTeam = FlyingSuperhero | StrongSuperhero | InvisibleSuperhero | TelekineticSuperhero;

const mySuperheroTeam: SuperheroTeam[] = [superman, hulk, invisibleWoman, jeanGrey];

// Step 5: Define an async action that our superheroes can perform
async function performHeroicAction(hero: SuperheroTeam): Promise<string> {
  switch (hero.abilities[0]) {
    case "Flight":
      return "Rescued people from a tall building.";
    case "Strength":
      return "Lifted a fallen bridge to save civilians.";
    case "Invisibility":
      return "Sneaked past the enemy to gather information.";
    case "Telekinesis":
      return "Stopped a speeding car using telekinetic power.";
    default:
      return "Performed a mysterious heroic feat.";
  }
}

// Step 6: Let our superheroes perform actions and communicate asynchronously
async function superheroTeamAssemble(team: SuperheroTeam[]): Promise<void> {
  for (const hero of team) {
    const actionResult: Awaited<ReturnType<typeof performHeroicAction>> = await performHeroicAction(hero);
    const introduction = `${hero.name}: "${actionResult}"`;
    console.log(introduction);
  }
}

// Step 7: Assemble the superhero team and let them perform heroic actions
superheroTeamAssemble(mySuperheroTeam);
Enter fullscreen mode Exit fullscreen mode

It's important to note that the use of Awaited and ReturnType here is done explicitly to show you how they can be used together. TypeScript is usually smart enough to recognize the type of a promise's resolved value and apply that type inference on it's own.

This example demonstrates how you can leverage TypeScript utility types to model more complex scenarios involving asynchronous communication, asynchronous actions, and dynamic type analysis. It also adds an extra layer of fun and playfulness to our superhero team example! πŸ¦Έβ€β™‚οΈπŸ¦Έβ€β™€οΈπŸš€

Conclusion

TypeScript's utility types are built-in type manipulators that facilitate easy alterations and actions on established types. I hope you found these TypeScript utility types examples informative. This isn't the end; you can leverage these utility types to craft your own custom types. Using utility types can significantly enhance your code, making it more reusable, concise, and clean.

For more information about how you can use Cosmic in your application with Typescript support out-of-the-box, visit our documentation or get started with Cosmic.

Top comments (2)

Collapse
 
optimbro profile image
Rahul

Nice read, thanks for sharing!

Collapse
 
devjotaa profile image
Victor

This was really nice, thanks for sharing.