· Pantelis Theodosiou · Programming  Â· 6 min read ·

0 views

Understanding TypeScript Utility Types

A comprehensive guide to TypeScript utility types including Partial, Required, Readonly, Pick, Omit, Record, and ReturnType. Learn practical examples, common pitfalls, and how to write DRY, type-safe code.

A comprehensive guide to TypeScript utility types including Partial, Required, Readonly, Pick, Omit, Record, and ReturnType. Learn practical examples, common pitfalls, and how to write DRY, type-safe code.

Why Utility Types Exist (and Why You Should Care)

If you’ve been using TypeScript for a while, you’ve probably hit that point where your types start repeating themselves. You’ve got an interface for User, another for UpdateUser, one for ReadonlyUser, and somehow they all look the same except for a few tweaks. That’s where utility types step in. They’re TypeScript’s way of saying, “You don’t have to reinvent that type, I’ve got you.” Utility types let you transform existing types - make every field optional, remove keys, pick specific ones, and more - without rewriting the whole thing. They’re small helpers that can save you from typing fatigue and type drift (you know, when one type changes and you forget to update all the others). Let’s break down the most useful ones you’ll actually use day to day.


Partial<T>

Turns all properties of a type into optional ones.

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

// Useful for PATCH endpoints or partial updates
function updateUser(id: number, data: Partial<User>) {
  // data can include any subset of User props
}

updateUser(1, { name: 'Pantelis' });

Without Partial, you’d need to define another interface like UserUpdate, repeating the same fields but with question marks everywhere. This single utility saves you from that duplication.


Required<T>

The opposite of Partial. It makes every property mandatory, even if they were originally optional.

interface Config {
  cache?: boolean;
  retries?: number;
}

const fullConfig: Required<Config> = {
  cache: true,
  retries: 3,
};

You don’t use it as often, but it’s perfect for cases where you need to ensure that all optional fields are now present - like after some normalization step.


Readonly<T>

Locks down an object type so its fields can’t be reassigned.

interface Settings {
  theme: string;
  language: string;
}

const appSettings: Readonly<Settings> = {
  theme: 'dark',
  language: 'en',
};

appSettings.theme = 'light'; // Error: cannot assign to 'theme' because it is a read-only property

This is a simple but powerful safeguard - especially when you’re passing objects into functions you don’t fully trust not to mutate things.


Pick<T, K>

Extracts a subset of keys from a type. Think of it like destructuring for types.

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

type PublicUser = Pick<User, 'id' | 'name' | 'email'>;

Now PublicUser only includes those three fields. Super useful when exposing only safe fields to a client or UI layer.


Omit<T, K>

The inverse of Pick. Removes specific keys from a type.

type SafeUser = Omit<User, 'password'>;

Now you get a User without the password property. This one’s a life-saver when working with APIs - you can reuse your model but strip out sensitive stuff before sending it anywhere.


Record<K, T>

Creates an object type with a set of keys of type K and values of type T.

type Role = 'admin' | 'editor' | 'viewer';

const permissions: Record<Role, string[]> = {
  admin: ['read', 'write', 'delete'],
  editor: ['read', 'write'],
  viewer: ['read'],
};

You’ll often see Record used for maps, dictionaries, or configs. It’s basically a type-safe replacement for a plain { [key: string]: any } mess.


ReturnType<T>

Extracts the return type of a function.

function createUser() {
  return { id: 1, name: 'Pantelis', email: 'dev@ptheodosiou.com' };
}

type UserReturn = ReturnType<typeof createUser>;
// same as { id: number; name: string; email: string; }

Great when you want to reuse the output type of a function without duplicating it manually. It helps keep your types consistent with the implementation - no outdated interfaces lying around.


Common Pitfalls & Gotchas

Let’s be real, even experienced devs trip over these sometimes.

  1. Partial isn’t recursive Partial only makes top-level properties optional. If your object has nested types, those inner fields stay strict.
interface Profile {
  user: { name: string; email: string };
  theme: string;
}

type LooseProfile = Partial<Profile>;
// user is still required, only 'user' and 'theme' are optional

// Solution:
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
  1. Readonly doesn’t freeze objects at runtime It’s compile-time only. Someone can still mutate the object in plain JS. If you truly need immutability, use Object.freeze() or an immutable library

  2. Don’t overdo Pick and Omit Chaining them too much can make your types unreadable. If you find yourself writing something like Omit<Pick<Something, 'a' | 'b'>, 'b'>, it’s probably time to rethink your base interface.

  3. Record<string, any> is a trap It’s basically the same as { [key: string]: any }. If you want type safety, be explicit about the keys or values. Record<'admin' | 'user', User> is fine; Record<string, any> defeats the purpose of TypeScript.

  4. ReturnType doesn’t work well with overloaded functions If a function has multiple overloads, ReturnType might not infer what you expect. Sometimes it’ll just return the union of all overloads. Be aware of that when typing APIs or utility wrappers.


TL;DR

Utility TypeWhat It DoesExample Use Case
Partial<T>Makes all props optionalPatch updates
Required<T>Makes all props requiredData validation
Readonly<T>Prevents mutationImmutable configs
Pick<T, K>Selects keys from a typeExpose safe fields
Omit<T, K>Removes keys from a typeHide sensitive data
Record<K, T>Key-value map typeRole-based config
ReturnType<T>Grabs a function’s output typeReuse inferred types

Final Thoughts

Utility types are one of those features that look small but completely change how you structure code in TypeScript. They keep your types DRY, flexible, and consistent with your actual data models. Once you start using them, you’ll notice your type definitions shrink while your code safety goes up. And that’s exactly the point - writing less TypeScript, but with more confidence.

Back to Blog