Building Custom TypeScript Utility Types: A Beginner's Guide to Advanced Patterns

July 25, 2025

☕️ Support Us
Your support will help us to continue to provide quality content.👉 Buy Me a Coffee

After mastering the essential utility types like Partial and Pick, you might find yourself thinking: "These are great, but what if I need something more specific for my project?" Maybe you need a utility that makes only certain properties optional, or one that transforms property names in a specific way.

Here's where things get exciting. You can build your own utility types from scratch. But how do these type transformations actually work under the hood? What if we could peek inside Partial<T> to see the machinery that makes all properties optional?

The answer lies in understanding three powerful TypeScript features: mapped types, template literal types, and conditional types. These aren't just academic concepts. They're practical tools that let you create custom solutions for your specific development challenges.

Understanding the Foundation: Mapped Types

Have you ever wondered what happens when you write Partial<User>? How does TypeScript know to make every property optional? The secret lies in something called mapped types, which work like a transformation recipe that gets applied to every property in your type.

Think of mapped types as instructions you give to TypeScript: "Take this type, look at each property, and transform it according to my rules." It's similar to how you might use a for loop to transform every item in an array, except we're transforming type properties instead of array values.

Here's the basic pattern that makes this magic happen:

type TransformType<T> = {
  [PropertyName in keyof T]: DoSomethingWith<T[PropertyName]>
}

Let's break this down piece by piece. The [PropertyName in keyof T] part tells TypeScript to loop through each property name in type T. Then T[PropertyName] gives us the type of that specific property, which we can transform however we want.

Building Your First Custom Utility Types

Now that we understand the basic pattern, let's build our very own version of Partial<T> from scratch. What if we opened up the TypeScript source code and looked at how Partial actually works?

// This is essentially how Partial<T> is implemented
type MyPartial<T> = {
  [PropertyName in keyof T]?: T[PropertyName]
}

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

type PartialUser = MyPartial<User>;
// Result: { id?: string; name?: string; email?: string; }

Notice that tiny ? after the property name? That's the magic ingredient that makes each property optional. Our mapped type visits every property in the User type and adds that optional modifier.

But what if we want to go the opposite direction? Sometimes we start with optional properties and need to make them all required. Here's how we can build our own Required<T>:

type MyRequired<T> = {
  [PropertyName in keyof T]-?: T[PropertyName]
}

interface PartialSettings {
  theme?: string;
  notifications?: boolean;
  language?: string;
}

type CompleteSettings = MyRequired<PartialSettings>;
// Result: { theme: string; notifications: boolean; language: string; }

The -? syntax is like an eraser that removes the optional modifier. We're telling TypeScript to visit each property and strip away its optionality, making everything required.

Creating More Advanced Custom Types

Once you've mastered the basics, you might wonder: "What if I only want to transform specific properties instead of all of them?" This is where things get more interesting. Let's explore how Pick<T, K> works and then build something even more useful.

Here's how Pick<T, K> selects only the properties we want:

type MyPick<T, K extends keyof T> = {
  [PropertyName in K]: T[PropertyName]
}

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

// Only grab the safe-to-display properties
type PublicUser = MyPick<User, 'id' | 'name' | 'email'>;
// Result: { id: string; name: string; email: string; }

The key insight here is K extends keyof T. This constraint ensures that K can only contain property names that actually exist in T. If you try to pick a property that doesn't exist, TypeScript will show you an error immediately.

But what if we want to exclude properties instead of selecting them? Here's how we can build our own Omit<T, K>:

type MyOmit<T, K extends keyof T> = {
  [PropertyName in keyof T as PropertyName extends K ? never : PropertyName]: T[PropertyName]
}

// Remove sensitive data from our User type
type SafeUser = MyOmit<User, 'password'>;
// Result: { id: string; name: string; email: string; }

The as PropertyName extends K ? never : PropertyName part is doing conditional logic at the type level. If the property name is one we want to omit, we map it to never (which removes it). Otherwise, we keep the original property name.

Working with String Patterns and Smart Type Logic

What if you could tell TypeScript exactly what format a string should follow? Maybe you want to ensure all your CSS class names start with "btn-", or that API endpoints follow a specific pattern. This is where template literal types become incredibly useful.

Template literal types let you create string patterns that TypeScript can enforce. Think of them as templates for strings, similar to how you might use template strings in regular JavaScript, but for type checking.

type ButtonClass = `btn-${string}`;

// These work perfectly
const primaryButton: ButtonClass = "btn-primary";
const dangerButton: ButtonClass = "btn-danger";

// This would cause a TypeScript error
// const invalidButton: ButtonClass = "primary-btn"; // Error!

Now let's create something more practical. Imagine you're building an API and want to ensure all routes follow a consistent format:

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIVersion = 'v1' | 'v2';
type Resource = 'users' | 'posts' | 'comments';

// This creates all possible combinations automatically
type APIRoute = `/${APIVersion}/${Resource}`;
// Result: "/v1/users" | "/v1/posts" | "/v1/comments" | "/v2/users" | "/v2/posts" | "/v2/comments"

function callAPI(route: APIRoute, method: HTTPMethod) {
  return fetch(`https://api.example.com${route}`, { method });
}

// TypeScript knows these are valid
callAPI('/v1/users', 'GET');
callAPI('/v2/posts', 'POST');

// This would cause an error
// callAPI('/v3/users', 'GET'); // Error: "/v3/users" is not assignable to APIRoute

Here's something really cool: TypeScript includes built-in utilities that can transform the casing of strings at the type level. Let's see how these work:

type EventName = 'click' | 'focus' | 'blur';

// Make the first letter uppercase
type CapitalizedEvents = Capitalize<EventName>;
// Result: "Click" | "Focus" | "Blur"

// Convert everything to uppercase
type UpperEvents = Uppercase<EventName>;
// Result: "CLICK" | "FOCUS" | "BLUR"

// Convert everything to lowercase
type LowerEvents = Lowercase<EventName>;
// Result: "click" | "focus" | "blur"

But here's where things get really practical. Let's combine template literals with mapped types to automatically generate event handler properties. Have you ever noticed how React components use "onClick", "onFocus", etc.? We can generate these automatically:

type EventMap = {
  click: MouseEvent;
  focus: FocusEvent;
  input: InputEvent;
};

// Transform "click" into "onClick", "focus" into "onFocus", etc.
type EventHandlers = {
  [EventName in keyof EventMap as `on${Capitalize<string & EventName>}`]?: 
    (event: EventMap[EventName]) => void;
};

// This automatically creates:
// {
//   onClick?: (event: MouseEvent) => void;
//   onFocus?: (event: FocusEvent) => void;
//   onInput?: (event: InputEvent) => void;
// }

Now when you add a new event to the EventMap, the corresponding handler property gets created automatically. No more manually typing out every single event handler!

Advanced Mapped Type Patterns

Once you've mastered basic mapped types, you might wonder about more sophisticated transformations. What if you want to modify property names themselves, or create types that transform both keys and values simultaneously? These advanced patterns unlock powerful capabilities for custom utility types.

Let's start with key remapping, one of the most useful advanced patterns. Sometimes you need to transform not just the values of properties, but their names too. TypeScript's as clause in mapped types lets you do exactly this:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

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

type UserGetters = Getters<User>;
// Result: {
//   getName: () => string;
//   getAge: () => number;
//   getEmail: () => string;
// }

The as clause transforms each property name by adding "get" and capitalizing the first letter. This pattern is incredibly useful for generating consistent APIs from existing types.

Here's another powerful pattern: filtering properties based on their types. What if you want to create a type that only includes string properties, or only function properties?

type StringPropsOnly<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K]
}

interface MixedType {
  id: string;
  name: string;
  age: number;
  isActive: boolean;
  updateName: (name: string) => void;
}

type StringProps = StringPropsOnly<MixedType>;
// Result: { id: string; name: string; }

When we map a key to never, TypeScript removes it from the resulting type. This gives us a clean way to filter properties based on any criteria we want.

Let's combine these patterns to create something really practical. Imagine you want to create a type that generates event handlers for all string properties of an object:

type EventHandlers<T> = {
  [K in keyof T as T[K] extends string ? `on${Capitalize<string & K>}Change` : never]: (value: T[K]) => void
}

interface FormData {
  username: string;
  email: string;
  age: number; // This won't generate a handler since it's not a string
  bio: string;
}

type FormHandlers = EventHandlers<FormData>;
// Result: {
//   onUsernameChange: (value: string) => void;
//   onEmailChange: (value: string) => void;
//   onBioChange: (value: string) => void;
// }

This pattern automatically generates type-safe event handlers for all string properties, ignoring non-string fields. When you add or remove string properties from FormData, the handlers update automatically.

Another advanced technique involves creating recursive mapped types for nested transformations. Here's a utility that can make all properties optional at any depth:

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
}

interface AppConfig {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  api: {
    version: string;
    timeout: number;
  };
}

type PartialConfig = DeepPartial<AppConfig>;
// All properties at every level become optional

This recursive pattern lets you transform deeply nested structures, something the built-in utility types can't handle.

Solving Real Problems and Best Practices

Now comes the fun part. Let's use everything we've learned to solve actual problems you might encounter in your projects. These aren't just academic exercises but real utility types that can make your code more robust and maintainable.

Let's start with a practical problem: finding all properties of a specific type. Maybe you want all the string properties, or all the boolean ones. This is incredibly useful for creating focused update types or validation schemas:

type KeysOfType<T, TargetType> = {
  [PropertyName in keyof T]: T[PropertyName] extends TargetType ? PropertyName : never;
}[keyof T];

interface User {
  id: string;
  name: string;
  age: number;
  isActive: boolean;
  lastLogin: Date;
}

type StringKeys = KeysOfType<User, string>;
// Result: "id" | "name"

type BooleanKeys = KeysOfType<User, boolean>;
// Result: "isActive"

// Now you can create focused update types
type StringOnlyUpdate = Pick<User, StringKeys>;
// Result: { id: string; name: string; }

This pattern is incredibly useful for creating type-safe APIs where you want to group properties by their types.

Here's another real-world scenario: creating a type that makes specific properties required while keeping others optional. This is perfect for form validation where some fields are always required:

type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;

interface UserProfile {
  name?: string;
  email?: string;
  bio?: string;
  avatar?: string;
}

// Make name and email required, others remain optional
type ProfileFormData = RequireFields<UserProfile, 'name' | 'email'>;
// Result: { name: string; email: string; bio?: string; avatar?: string; }

function validateProfile(data: ProfileFormData) {
  // TypeScript knows name and email are always present
  console.log(`Validating profile for ${data.name} (${data.email})`);
  
  // bio and avatar might be undefined
  if (data.bio) {
    console.log(`Bio: ${data.bio}`);
  }
}

This utility type combines intersection types (&) with the built-in Required and Pick utilities to create exactly the behavior we want.

Best Practices and When to Use Custom Types

As you start creating your own utility types, keep these guidelines in mind to ensure your code stays maintainable and performant.

First, start simple and build up complexity gradually. It's tempting to create elaborate types that handle every edge case, but often a simpler solution is more readable and maintainable:

// Good: Simple and clear
type MakeOptional<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P];
} & {
  [P in K]?: T[P];
};

Second, always test your utility types with real examples. Create a few test cases to make sure they behave as expected:

interface TestUser {
  id: string;
  name: string;
  email?: string;
}

// Test your custom types
type TestResult = MakeOptional<TestUser, 'name'>;
// Should result in: { id: string; email?: string; name?: string; }

const testUser: TestResult = {
  id: "123",
  // name is now optional thanks to our utility type
  email: "[email protected]"
};

Custom utility types are powerful tools, but they're not always the right solution. Here's when you should consider creating them:

Build custom types when:

  • You find yourself repeating the same type transformation pattern multiple times
  • The built-in utility types don't quite solve your specific problem
  • You want to enforce consistent patterns across your entire codebase
  • You're building a library and need flexible, reusable type APIs

Stick with simpler solutions when:

  • A built-in utility type already solves your problem
  • You only need the transformation in one place
  • The custom type would be more complex than the problem it solves
  • Your team isn't comfortable with advanced TypeScript features yet

The goal is to make your code more maintainable and type-safe, not to show off advanced TypeScript knowledge. Custom utility types should solve real problems and make your development experience better.

You now have the fundamental building blocks to create your own utility types: mapped types for property transformations, template literal types for string patterns, and conditional types for smart type logic. These tools give you the power to encode your business logic directly into your type system, catching errors at compile time instead of runtime.

Start with the basics we've covered here, and as you encounter specific problems in your projects, you'll find natural opportunities to apply these patterns. The TypeScript community has created many utility type libraries, but understanding how to build your own gives you the flexibility to solve problems that are unique to your application.


Support ExplainThis

If you found this content helpful, please consider supporting our work with a one-time donation of whatever amount feels right to you through this Buy Me a Coffee page, or share the article with your friends to help us reach more readers.

Creating in-depth technical content takes significant time. Your support helps us continue producing high-quality educational content accessible to everyone.

☕️ Support Us
Your support will help us to continue to provide quality content.👉 Buy Me a Coffee