TypeScript’s utility types are powerful tools that allow developers to manipulate and transform types in a concise and reusable way. These types operate at compile time, enabling TypeScript to catch potential errors before the code is executed. By leveraging utility types, we can enhance type safety, improve code readability, and reduce redundancy.

Why Use Utility Types?

  1. Code Reusability: Build new types from existing ones without rewriting structures.
  2. Type Safety: Ensure consistency and correctness across your application.
  3. Compile-Time Benefits: Errors are caught during development, reducing runtime issues.

This guide explores common utility types with practical examples and highlights how they differ from manually defining types.


Common Utility Types with Examples

1. Partial<T>

The Partial<T> utility type makes all properties of a type optional.

Example:
interface User {
  id: number;
  name: string;
  email: string;
}

// Using Partial to create an object with optional properties
const userUpdates: Partial<User> = { name: "Alice" };

// Without Partial, you'd need to define a new type manually:
type UserUpdates = {
  id?: number;
  name?: string;
  email?: string;
};

const manualUpdates: UserUpdates = { name: "Alice" }; // Same result

Difference: Partial<T> dynamically makes all properties optional, avoiding the need to manually redefine the type.


2. Required<T>

The Required<T> utility type makes all properties of a type mandatory.

Example:
interface User {
  id: number;
  name?: string;
  email?: string;
}

// Using Required to enforce all properties
const fullUser: Required<User> = {
  id: 1,
  name: "Bob",
  email: "bob@example.com",
};

// Without Required, you'd need to redefine the type:
type FullUser = {
  id: number;
  name: string;
  email: string;
};

const manualFullUser: FullUser = {
  id: 1,
  name: "Bob",
  email: "bob@example.com",
};

Difference: Required<T> ensures all properties are mandatory without manually redefining the type.


3. Readonly<T>

The Readonly<T> utility type prevents modification of properties.

Example:
interface User {
  id: number;
  name: string;
}

// Using Readonly to make properties immutable
const user: Readonly<User> = { id: 1, name: "Charlie" };
// user.name = "Dave"; // Error: Cannot assign to 'name' because it is a read-only property

// Without Readonly, you'd need to enforce immutability manually:
type ImmutableUser = {
  readonly id: number;
  readonly name: string;
};

const manualUser: ImmutableUser = { id: 1, name: "Charlie" };

Difference: Readonly<T> simplifies immutability by applying readonly to all properties dynamically.


4. Record<K, T>

The Record<K, T> utility type creates an object type with keys K and values T.

Example:
type UserRoles = "admin" | "user" | "guest";

// Using Record to define a map of roles to permissions
const permissions: Record<UserRoles, boolean> = {
  admin: true,
  user: false,
  guest: false,
};

// Without Record, you'd need to manually define the type:
type Permissions = {
  admin: boolean;
  user: boolean;
  guest: boolean;
};

const manualPermissions: Permissions = {
  admin: true,
  user: false,
  guest: false,
};

Difference: Record<K, T> dynamically maps keys to values, reducing redundant type definitions.


5. Pick<T, K>

The Pick<T, K> utility type selects specific properties from a type.

Example:
interface User {
  id: number;
  name: string;
  email: string;
}

// Using Pick to create a type with selected properties
type UserSummary = Pick<User, "id" | "name">;

const summary: UserSummary = { id: 1, name: "Eve" };
const summary2: Pick<User, "id" | "name"> = { id: 1, name: "Eve",  email:brice@nguenkam.com }; // Error: 'email' does not exist in type 'Pick<User, "id" | "name">'

// Without Pick, you'd need to manually define the type:
type ManualUserSummary = {
  id: number;
  name: string;
};

const manualSummary: ManualUserSummary = { id: 1, name: "Eve" };

Difference: Pick<T, K> simplifies creating subsets of types by automatically selecting properties.


6. Omit<T, K>

The Omit<T, K> utility type removes specific properties from a type.

Example:
interface User {
  id: number;
  name: string;
  email: string;
}

// Using Omit to exclude the 'email' property
type UserWithoutEmail = Omit<User, "email">;

const userWithoutEmail: UserWithoutEmail = { id: 2, name: "Frank" };

// Without Omit, you'd need to manually define the type:
type ManualUserWithoutEmail = {
  id: number;
  name: string;
};

const manualUserWithoutEmail: ManualUserWithoutEmail = { id: 2, name: "Frank" };

Difference: Omit<T, K> dynamically excludes properties, reducing manual effort.


7. Exclude<T, U>

The Exclude<T, U> utility type removes types in U from T.

Example:
type Roles = "admin" | "user" | "guest";

// Using Exclude to filter out 'admin'
type NonAdminRoles = Exclude<Roles, "admin">;

const role: NonAdminRoles = "user"; // Valid
// const invalidRole: NonAdminRoles = "admin"; // Error

// Without Exclude, you'd need to manually define the type:
type ManualNonAdminRoles = "user" | "guest";

const manualRole: ManualNonAdminRoles = "user";

Difference: Exclude<T, U> dynamically filters types, avoiding manual enumeration.


8. Extract<T, U>

The Extract<T, U> utility type selects types in U from T.

Example:
type Roles = "admin" | "user" | "guest";

// Using Extract to isolate 'admin'
type AdminRoles = Extract<Roles, "admin">;

const role: AdminRoles = "admin"; // Valid

// Without Extract, you'd need to manually define the type:
type ManualAdminRoles = "admin";

const manualRole: ManualAdminRoles = "admin";

Difference: Extract<T, U> dynamically isolates types, simplifying type definitions.


9. ReturnType<T>

The ReturnType<T> utility type extracts the return type of a function.

Example:
const getUser = () => ({
  id: 1,
  name: "Grace",
});

// Using ReturnType to infer the return type
type UserType = ReturnType<typeof getUser>;

const user: UserType = { id: 1, name: "Grace" };

// Without ReturnType, you'd need to manually define the return type:
type ManualUserType = {
  id: number;
  name: string;
};

const manualUser: ManualUserType = { id: 1, name: "Grace" };

Difference: ReturnType<T> dynamically infers return types, reducing redundancy.


Notes :

Because TypeScript is a superset of JavaScript, utility types exist only at compile time to enforce type safety and do not affect runtime output. The final JavaScript output is just plain JavaScript objects with no TypeScript type constraints.


Conclusion

TypeScript’s utility types are essential for creating flexible, reusable, and maintainable code. By dynamically transforming types, these utilities save time, reduce errors, and improve readability.

By Shabazz

Software Engineer, MCSD, Web developer & Angular specialist

Leave a Reply

Your email address will not be published. Required fields are marked *