Skip to main content

TS Ep 33: Built-in Utility Types (`Partial`, `Required`, `Readonly`)

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 33: This Article
Instead of manually creating duplicate interfaces to make properties optional, required, or read-only, TypeScript provides global Utility Types. Powered by homomorphic mapped types under the hood, Partial<T>, Required<T>, and Readonly<T> transform object shapes instantly.

1. Partial<T> (Making All Properties Optional)
#

Partial<T> constructs a new object type where every property of T is marked as optional (?).

Production Use Case: Patch/Update API Payloads
#

When updating an entity via a REST PATCH endpoint, the client can supply a subset of fields without re-sending the entire object:

interface UserProfile {
  id: string;
  username: string;
  email: string;
  bio: string;
  avatarUrl: string;
}

// 🟢 Partial<UserProfile> makes ALL properties optional!
type UpdateProfileDto = Partial<UserProfile>;
/* Inferred Type:
{
  id?: string;
  username?: string;
  email?: string;
  bio?: string;
  avatarUrl?: string;
}
*/

function updateProfile(userId: string, patch: Partial<UserProfile>) {
  console.log(`Updating User ${userId} with keys:`, Object.keys(patch));
}

// Valid calls:
updateProfile("usr_101", { bio: "Senior Engineer" });
updateProfile("usr_101", { email: "new@acme.com", avatarUrl: "https://img.com/a.png" });

How Partial<T> is Defined Under the Hood:
#

type CustomPartial<T> = {
  [P in keyof T]?: T[P];
};

2. Required<T> (Making All Properties Mandatory)
#

Required<T> does the exact opposite of Partial<T>. It removes optional modifiers (?) from every property in T, making all properties strictly mandatory.

Production Use Case: Resolving Config Defaults
#

When an application accepts an optional user configuration object, a resolution function merges default settings to guarantee that every property is populated:

interface AppOptions {
  theme?: "light" | "dark";
  logLevel?: "info" | "debug" | "error";
  retries?: number;
}

// 🟢 Required<AppOptions> removes '?' from ALL keys!
type FullyResolvedOptions = Required<AppOptions>;
/* Inferred Type:
{
  theme: "light" | "dark";
  logLevel: "info" | "debug" | "error";
  retries: number;
}
*/

const defaultOptions: FullyResolvedOptions = {
  theme: "dark",
  logLevel: "info",
  retries: 3,
};

function initApp(userOpts: AppOptions): FullyResolvedOptions {
  return {
    ...defaultOptions,
    ...userOpts,
  };
}

const activeConfig = initApp({ theme: "light" });
// Both 'logLevel' and 'retries' are guaranteed to exist on activeConfig!
console.log(activeConfig.logLevel.toUpperCase()); // Safe!

How Required<T> is Defined Under the Hood:
#

TypeScript uses the -? Mapping Modifier to explicitly strip optionality:

type CustomRequired<T> = {
  [P in keyof T]-?: T[P];
};

3. Readonly<T> (Freezing Property Assignments)
#

Readonly<T> constructs a type where every property of T receives the readonly modifier, preventing property reassignment at compile time.

Production Use Case: Immutability in State Stores
#

interface AppState {
  currentUser: { id: string; name: string };
  isOnline: boolean;
}

function freezeState(state: AppState): Readonly<AppState> {
  return Object.freeze(state);
}

const state = freezeState({
  currentUser: { id: "101", name: "Alice" },
  isOnline: true,
});

// ❌ Compiler Error: Cannot assign to 'isOnline' because it is a read-only property.
// state.isOnline = false;

How Readonly<T> is Defined Under the Hood:
#

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

4. The Shallow Limitation of Readonly<T>
#

Caution

Shallow Warning: Readonly<T> is strictly shallow. It only adds readonly to the top-level keys of T. Nested object properties remain mutable unless wrapped recursively!

interface NestedState {
  user: {
    name: string; // Nested property!
  };
}

const state: Readonly<NestedState> = {
  user: { name: "Alice" },
};

// ❌ Top-level property assignment is BLOCKED:
// state.user = { name: "Bob" }; // Error! Readonly property.

// 🟢 WORKED! Nested property assignment is STILL ALLOWED by TS:
state.user.name = "Bob"; // No compiler error!

(Note: In Module 4 Ep 50, we will build a custom recursive DeepReadonly<T> type to solve this exact limitation).


Summary & Next Steps
#

In this episode:

  • We analyzed Partial<T> and used it for PATCH/Update DTO payloads.
  • We analyzed Required<T> and used -? to enforce fully-resolved config objects.
  • We analyzed Readonly<T> for state container immutability.
  • We investigated the Shallow Limit of Readonly<T> on nested objects.
  • We revealed the mapped type implementations (?, -?, readonly) powering these utilities.

In Episode 34: Utility Types (Pick and Omit), we will explore how to construct sub-interfaces by selecting or excluding specific key subsets!

typescript - This article is part of a series.
Part 33: This Article