🚀 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
-
MakeOptionalprovides granular control by making only specified keysKof a typeToptional, unlikePartialwhich affects all properties, by combiningOmitandPartial>. -
RequireAtLeastOneenforces that at least one property from a specific set of keysKmust be present in an objectT, solving complex “either/or” typing scenarios through a union of valid type combinations. -
ObjectValuesautomatically derives a union type from the values of an object type usingT[keyof T], ensuring types remain synchronized with runtime constants and preventing maintenance burdens, especially when the constant is definedas 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>orRequired<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>>;
This works by creating an intersection of two types:
-
Omit<T, K>: Takes the original typeTand removes the keysKthat we want to make optional. The remaining properties are still required. -
Partial<Pick<T, K>>: First, itPicks only the keysKfromT, and then it wraps them inPartialto 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 },
});
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];
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 });
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';
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];
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');
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.
👉 Read the original article on TechResolve.blog
☕ Support my work
If this article helped you, you can buy me a coffee:

Top comments (0)