TypeScript’s Type Manipulations

Explore Popular Utility Types for Type Transformations

Irene Smolchenko
Bits and Pieces

--

A display of colorful leaves on a string, suggesting transformations.
Photo by Chris Lawton on Unsplash

TypeScript’s type manipulations are powerful tools for transforming and manipulating types. They allow you to create new types, perform conditional type checks, transform union types, and more.

This article is inspired by TypeScript’s documentation and aims to explain type manipulations in a user-friendly way. We’ll explore some commonly used utility types, providing practical examples to illustrate their usage.

Most Used Type Manipulations

Pick

Pick<Type, Keys>Selects a subset of properties from an existing type. In the example below, a new type that captures only the chosen properties is created, enabling a more focused and specific type definition.

interface Todo {
title: string;
description: string;
completed: boolean;
}

// The new type TodoPreview includes only the specific properties "title" and "completed" from the Todo interface
type TodoPreview = Pick<Todo, "title" | "completed">;

// The variable 'todo' of type TodoPreview should have a "title" property of type string and a "completed" property of type boolean
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};

todo;

Omit

Omit<Type, Keys> The opposite of Pick. Omit creates a type by excluding specific properties from an existing type. In the below example, the Omit type takes two parameters: the original type (Todo) and the properties to be omitted ("description").

interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}

// Create a new type based on the original Todo type, excluding the description property
type TodoPreview = Omit<Todo, "description">;

// The todo variable is assigned an object that conforms to the TodoPreview type
const todo: TodoPreview = {
title: "Clean room",
completed: false,
createdAt: 1615544252770,
};

todo;
// Create a new type by omitting multiple properties: "completed" and "createdAt"
type TodoInfo = Omit<Todo, "completed" | "createdAt">;

// The todoInfo variable is assigned an object conforming to the TodoInfo type
const todoInfo: TodoInfo = {
title: "Pick up kids",
description: "Kindergarten closes at 5pm",
};

todoInfo;

Partial

Partial<Type>Makes all properties of a type optional. In the example below, the Partial type manipulation is used to create a new type called Partial<Todo>. This type allows for optional properties within the Todo interface.
By using the spread syntax, the original todo object is combined with the updated properties specified in fieldsToUpdate, resulting in a new object that includes all the properties from both sources.

interface Todo {
title: string;
description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
// 'Partial<Todo>' means that the properties of Todo, can be present or absent in objects of the Partial<Todo> type.
// The fieldsToUpdate parameter allows for selectively updating properties of the todo object.
return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
title: "organize desk",
description: "clear clutter",
};

// The todo2 object is created by merging the properties of todo1 with the updated "description" property
const todo2 = updateTodo(todo1, {
description: "throw out trash",
});

Required

Required<Type> The opposite of Partial, makes all properties of a type mandatory. In the given example, the Required type manipulation is used to create a new type called Required<Props>. This type ensures that all properties within the Props interface are required and cannot be optional or undefined.

// Required<Props> makes both a and b properties mandatory, regardless of the optional syntax used in the Props interface
interface Props {
a?: number;
b?: string;
}

const obj: Props = { a: 5 };

const obj2: Required<Props> = { a: 5 };
// Error: Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'

Exclude

Exclude<UnionTypes, ExcludedMembers>Removes types from a union type based on a condition.

type T0 = Exclude<"a" | "b" | "c", "a">;
// T0 excludes the type "a" from the union type, resulting in the type "b" | "c"

type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
// T1 excludes both "a" and "b" from the original union type, resulting in the type "c"

type T2 = Exclude<string | number | (() => void), Function>;
// T2 excludes the type Function from the union type string | number | (() => void, and evaluates to the type string | number


type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };

type T3 = Exclude<Shape, { kind: "circle" }>
// T3 excludes the shape { kind: "circle"; radius: number } from the Shape union type,
// resulting in a new type that includes the remaining shapes { kind: "square"; x: number } and { kind: "triangle"; x: number; y: number }

Extract

Extract<Type, Union> Takes two parameters: the original union type and the type to be extracted, and creates a new type by selecting types from a union that match a specific condition.

type T0 = Extract<"a" | "b" | "c", "a" | "f">;
// The type "a" is common to both the original union and the extracted type, so it is returned as the result

type T1 = Extract<string | number | (() => void), Function>;
// The () => void type is assignable to Function, so it is returned as the result

type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };

type T2 = Extract<Shape, { kind: "circle" }>
// The shape { kind: "circle"; radius: number } matches the extracted shape, so it is returned as the result

ReturnType

ReturnType<Type>A type manipulation that provides the ability to extract and represent the return type of a given function type.

declare function f1(): { a: number; b: string };

type T0 = ReturnType<() => string>;
// ReturnType takes a function type as its parameter and returns the type of the value it returns, T0 is inferred as string

type T1 = ReturnType<(s: string) => void>;
// The parameter type that the function recieves is irrelevant, T1 is inferred as void

type T2 = ReturnType<<T>() => T>;
// Since T is not constrained or specified, the return type of the generic function is inferred as unknown

type T3 = ReturnType<<T extends U, U extends number[]>() => T>;
// The function type represents a generic function that takes a type T that extends U, where U is an array of numbers.
// The function returns a value of type T that is inferred as number[], so T3 is number[]

type T4 = ReturnType<typeof f1>;
// The typeof f1 retrieves the type of the function f1, which is { a: number; b: string }.
// T4 represents the return type of f1, which is { a: number; b: string }

I found the examples below interesting, so I have separated them.

  • ReturnType<any> Since any is a very broad type, TypeScript allows it to be used as the parameter for ReturnType. The return type of any is any itself.
  • ReturnType<never> Since never type represents values that will never occur, it indicates that a function will never have a return value. Therefore, ReturnType<never> results in the never type.
  • ReturnType<string> The string type represents the primitive type for strings, not a function type. Since ReturnType expects a valid function type as its parameter, providing string leads to a compilation error.
  • ReturnType<Function> The Function is a built-in TypeScript interface representing the base type for all JavaScript functions. However, it is not a specific function type. Since Function is not a valid function type, providing it as the parameter for ReturnType results in an error.
type T5 = ReturnType<any>;
// The any type indicates that the return type can be any type.

type T6 = ReturnType<never>;
// The never type represents a type that never has a value.

type T7 = ReturnType<string>;
// Type 'string' does not satisfy the constraint '(...args: any) => any'.

type T8 = ReturnType<Function>;
// Type 'Function' does not satisfy the constraint '(...args: any) => any'

💡 What if you needed to reuse these types across all your projects? An open-source toolchain such as Bit can help you share types across multiple projects, considerably minimizing boilerplate.

Learn more:

Conclusion

TypeScript offers a wide range of type manipulations, with certain utility types being more popular than others. In this article, we have provided an overview of the commonly used types, accompanied by examples.

If you would like explanations of other utility types, please don’t hesitate to mention them in the comments below!

Feel free to explore my other articles as well if you are interested in diving deeper into TypeScript.

Resource

By buying me a virtual croissant on Buy Me a Coffee, you can directly support my creative journey. Your contribution helps me continue creating high-quality content. Thank you for your support!

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--

🍴🛌🏻 👩🏻‍💻 🔁 Front End Web Developer | Troubleshooter | In-depth Tech Writer | 🦉📚 Duolingo Streak Master | ⚛️ React | 🗣️🤝 Interviews Prep