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"],
};
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,
};
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,
};
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"],
};
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",
};
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"],
};
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
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!";
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!
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"];
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!
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}`);
}
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,
};
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);
It's important to note that the use of
Awaited
andReturnType
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)
Nice read, thanks for sharing!
This was really nice, thanks for sharing.