DEV Community

Cover image for Solved: Helpful TypeScript Utility Types I’ve hand rolled over time, enjoy
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Helpful TypeScript Utility Types I’ve hand rolled over time, enjoy

🚀 Executive Summary

TL;DR: The article addresses common TypeScript challenges such as inflexible built-in utilities, complex rule enforcement, and type-constant drift. It introduces custom utility types like MakeOptional, RequireAtLeastOne, and ObjectValues to provide granular control, enforce ‘at least one’ conditions, and automatically synchronize types with constants, leading to more robust and maintainable code.

🎯 Key Takeaways

  • MakeOptional provides granular control by making only specified keys K of a type T optional, unlike Partial which affects all properties, by combining Omit and Partial>.
  • RequireAtLeastOne enforces that at least one property from a specific set of keys K must be present in an object T, solving complex “either/or” typing scenarios through a union of valid type combinations.
  • ObjectValues automatically derives a union type from the values of an object type using T[keyof T], ensuring types remain synchronized with runtime constants and preventing maintenance burdens, especially when the constant is defined as const.

Discover how to enhance your TypeScript projects with powerful, custom utility types. This guide provides reusable solutions to common typing challenges, helping you write cleaner, safer, and more maintainable code by moving beyond built-in defaults.

The Symptoms: Repetitive and Inflexible TypeScript Patterns

As applications grow in complexity, you may find yourself wrestling with TypeScript’s type system to accurately model your data structures and business logic. If you’ve encountered the following symptoms, it’s a sign that you could benefit from a custom utility type toolkit:

  • You frequently create one-off types that are just slight variations of existing interfaces, leading to boilerplate and maintenance overhead.
  • Built-in utility types like Partial<T> or Required<T> feel too blunt, applying their logic to every property when you only need to modify a few.
  • You struggle to enforce complex validation rules at the type level, such as requiring “at least one of these specific fields” to be present.
  • Your types and runtime constants (like configuration objects) drift out of sync, forcing you to update them in two separate places and introducing the risk of errors.

These challenges indicate a need for more precise and reusable type definitions. Let’s explore three hand-rolled utility types that solve these exact problems.

Solution 1: Granular Control with MakeOptional<T, K>

The Problem: Partial<T> is All or Nothing

TypeScript’s built-in Partial<T> is useful, but it makes every single property of a type optional. This is often not what we need. For instance, when updating a user profile, you might allow the bio and profilePictureUrl to be optional in the payload, but the user’s id is always required to identify them. Using Partial<User> would incorrectly make the id optional too.

The Utility Type: MakeOptional<T, K>

This utility type allows you to pick specific keys from a type T and make only them optional, leaving the rest untouched. It provides the granularity that Partial<T> lacks.

type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
Enter fullscreen mode Exit fullscreen mode

This works by creating an intersection of two types:

  1. Omit<T, K>: Takes the original type T and removes the keys K that we want to make optional. The remaining properties are still required.
  2. Partial<Pick<T, K>>: First, it Picks only the keys K from T, and then it wraps them in Partial to make them all optional.

The & operator combines them, resulting in a new type where the specified keys K are optional and all other keys from T remain as they were.

Real-World Example: Updating a User Configuration

Imagine you have a strict AppConfig interface. You want to create a function that allows overriding just the theme or notifications settings.

interface AppConfig {
  readonly appId: string;
  readonly apiEndpoint: string;
  theme: 'dark' | 'light';
  notifications: {
    email: boolean;
    push: boolean;
  };
}

// Our custom utility type
type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// Define the type for our update payload
type ConfigUpdatePayload = MakeOptional<AppConfig, 'theme' | 'notifications'>;

/*
The resulting type is:
{
  readonly appId: string;
  readonly apiEndpoint: string;
  theme?: 'dark' | 'light';       // Now optional
  notifications?: {               // Now optional
    email: boolean;
    push: boolean;
  };
}
*/

function updateConfig(update: ConfigUpdatePayload) {
  // ... implementation
}

// VALID: We provide all required fields and one optional one.
updateConfig({
  appId: 'abc-123',
  apiEndpoint: 'https://api.example.com',
  theme: 'dark',
});

// INVALID: Missing a required field 'apiEndpoint'
// TypeScript Error: Property 'apiEndpoint' is missing in type...
updateConfig({
  appId: 'abc-123',
  notifications: { email: true, push: false },
});
Enter fullscreen mode Exit fullscreen mode

Comparison: MakeOptional vs. Partial

Feature Partial<T> MakeOptional<T, K>
Granularity All-or-nothing. Affects all properties of T. Selective. Affects only the specified keys K.
Common Use Case Mocking objects for tests where most fields are irrelevant. Typing API PATCH request payloads or update functions.
Flexibility Low. Cannot preserve required status for any fields. High. Can precisely model which fields can be omitted.

Solution 2: Enforcing Complex Rules with RequireAtLeastOne<T, K>

The Problem: Typing “Either/Or” Scenarios

Standard interfaces are great for defining shapes where all properties are independent. But what if your business logic requires that *at least one* property from a specific set must be provided? For example, a function to find a user might accept a userId, an email, or a username. It needs at least one, but a consumer could provide more. A standard interface can’t enforce this “at least one” rule.

The Utility Type: RequireAtLeastOne<T, K>

This advanced utility type ensures that at least one of the specified keys K exists on the object.

type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
    Pick<T, Exclude<keyof T, Keys>> 
    & {
        [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
    }[Keys];
Enter fullscreen mode Exit fullscreen mode

This type is more complex, but its core logic is to create a union of all possible valid combinations. For each key in Keys, it creates a type where that specific key is required and the other keys in the set are optional. The union [Keys] at the end means the final type must match one of these valid combinations.

Real-World Example: Flexible Search Parameters

Let’s model a search function that requires either a document id or a slug to locate a document.

interface DocumentIdentifiers {
  id: string;
  slug: string;
  authorId: number; // This field is independent and always optional
}

// Our utility type
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
    Pick<T, Exclude<keyof T, Keys>> 
    & {
        [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
    }[Keys];

// We need at least 'id' or 'slug'
type FindDocumentParams = RequireAtLeastOne<Partial<DocumentIdentifiers>, 'id' | 'slug'>;

function findDocument(params: FindDocumentParams) {
  // ... implementation
}

// VALID calls
findDocument({ id: 'doc-123' });
findDocument({ slug: 'my-first-post' });
findDocument({ id: 'doc-123', slug: 'my-first-post' });
findDocument({ id: 'doc-123', authorId: 42 }); // other optional fields are fine

// INVALID: Missing both 'id' and 'slug'
// TypeScript Error: Type '{ authorId: number; }' has no properties in common 
// with type 'Required<Pick<...>> & ...'.
findDocument({ authorId: 42 });
Enter fullscreen mode Exit fullscreen mode

Solution 3: Deriving Types from Constants with ObjectValues<T>

The Problem: Keeping Types and Constants in Sync

A common pattern is to define a set of string constants in an object for things like event names, status codes, or user roles. You then create a separate union type by hand to represent those values. This creates a maintenance burden: if you add a new role to the constant object, you must remember to update the union type as well. Forgetting to do so can lead to subtle bugs that TypeScript won’t catch.

// The error-prone manual approach
export const USER_ROLES = {
  ADMIN: 'admin',
  EDITOR: 'editor',
  VIEWER: 'viewer',
  // If we add a new role here...
};

// ...we have to remember to update the type here!
export type UserRole = 'admin' | 'editor' | 'viewer';
Enter fullscreen mode Exit fullscreen mode

The Utility Type: ObjectValues<T>

This simple but powerful utility creates a union type from the values of a given object type. It ensures your types are always derived directly from your constants, eliminating drift.

type ObjectValues<T> = T[keyof T];
Enter fullscreen mode Exit fullscreen mode

This works by using the indexed access type T[keyof T]. keyof T creates a union of all the keys of T. When used as an index, T[…] looks up the types of all those keys and combines them into a new union type.

Real-World Example: Defining Application Statuses

Let’s define a single source of truth for deployment statuses and derive the type automatically.

// Our single source of truth for statuses.
// 'as const' is crucial here! It tells TypeScript to infer the most specific
// type possible (e.g., 'PENDING' instead of just 'string').
export const DEPLOYMENT_STATUS = {
  PENDING: 'pending',
  IN_PROGRESS: 'in_progress',
  SUCCESS: 'success',
  FAILED: 'failed',
} as const;

// Our utility type
type ObjectValues<T> = T[keyof T];

// The derived type. It is now automatically synced with the constant.
// Result: type DeploymentStatus = "pending" | "in_progress" | "success" | "failed"
export type DeploymentStatus = ObjectValues<typeof DEPLOYMENT_STATUS>;

function setDeploymentStatus(id: string, status: DeploymentStatus) {
  console.log(`Setting deployment ${id} to status: ${status}`);
}

// VALID
setDeploymentStatus('deploy-abc', DEPLOYMENT_STATUS.IN_PROGRESS);

// INVALID
// TypeScript Error: Argument of type '"error"' is not assignable to 
// parameter of type 'DeploymentStatus'.
setDeploymentStatus('deploy-xyz', 'error');
Enter fullscreen mode Exit fullscreen mode

Conclusion: Building Your Own Type Toolkit

While TypeScript’s built-in utility types are a great starting point, the real power comes from composing them to solve the specific, recurring problems in your codebase. By creating your own small, focused utility types like MakeOptional, RequireAtLeastOne, and ObjectValues, you can:

  • Reduce Boilerplate: Stop writing one-off types and create reusable, descriptive solutions.
  • Increase Type Safety: Model complex business rules directly in the type system, catching errors at compile time instead of runtime.
  • Improve Maintainability: Create a single source of truth for your types and constants, making your code easier to refactor and understand.

Start looking for repetitive type patterns in your projects today. Chances are, you can abstract them into a helpful utility type and begin building a robust type toolkit that will pay dividends across your entire application.


Darian Vance

👉 Read the original article on TechResolve.blog


Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)