đź’ˇ Utility Types That Supercharge Developer Experience in TypeScript
If you’ve spent any time working with TypeScript, you know the language is a gift that keeps on giving — especially when it comes to types that make your life easier.
But even with the rich standard library, you’ll often find yourself rewriting certain utility types again and again — making something nullable, prettifying an inferred type, or toggling optional keys.
Today, I’ll share a small but powerful set of TypeScript helper types that can drastically boost your developer experience (DX) and simplify your type logic.
These utilities are small, elegant, and built to solve real problems you hit every day in complex projects.
⚙️ The Utility Pack
Here’s the full collection of the helper types we’ll explore today:
/** Union of all JavaScript primitive types (excluding `symbol` and `function`) */
type Primitive = string | number | bigint | boolean | null | undefined;
/** Wraps a type in `null` */
type Nullable<T> = T | null;
/** Deeply nullable (all nested props can be null) */
type NullableDeep<T> = {
[K in keyof T]: T[K] extends object ? NullableDeep<T[K]> | null : T[K] | null;
};
/** String literal representation of a primitive */
type Enum<T extends Primitive> = `${T}`;
/** Flattens and normalizes a type’s shape for readability */
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
/** Makes only specific keys optional */
type Partialize<T, K extends keyof T> = Prettify<
Omit<T, K> & Partial<Pick<T, K>>
>;
/** Makes only specific keys required */
type RequireBy<T, K extends keyof T> = Prettify<
Omit<T, K> & Required<Pick<T, K>>
>;
Get it from my github gist
Let’s break them down one by one 👇
đź§© 1. Primitive
— The Building Blocks
type Primitive = string | number | bigint | boolean | null | undefined;
This type simply represents all serializable primitive types in JavaScript (excluding symbol
and function
).
Why is this helpful?
Because when you define utility types or constraints, you often want to limit the scope to only serializable or basic values.
For example:
function logPrimitive<T extends Primitive>(value: T) {
console.log(value);
}
This ensures you never accidentally pass a complex object or a function — great for cleaner, safer APIs.
đź§µ 2. Nullable<T>
— Making a Type Nullable
type Nullable<T> = T | null;
A simple yet surprisingly powerful helper.
Whenever you want to express that a value might be null, instead of repeatedly writing string | null
or User | null
, you can just wrap it in Nullable<T>
.
Example:
type User = { name: string; email: string };
type NullableUser = Nullable<User>;
// Equivalent to: User | null
It’s small, elegant, and improves code readability — especially in domain models or API responses.
🌊 3. NullableDeep<T>
— Make Everything Nullable
type NullableDeep<T> = {
[K in keyof T]: T[K] extends object ? NullableDeep<T[K]> | null : T[K] | null;
};
Ever had to deal with deeply nested types where every property can be null
(like in API responses)?
Instead of making every property manually nullable, use this.
Example:
type User = {
name: string;
address: {
city: string;
country: string;
};
};
type NullableUser = NullableDeep<User>;
// Result:
type NullableUser = {
name: string | null;
address: {
city: string | null;
country: string | null;
} | null;
};
🎯 Use case:
When working with APIs that may return incomplete or partial data — NullableDeep
keeps your types aligned without manually updating every nested key.
⚡ 4. Enum<T>
— Boosting DX for Enums
type Enum<T extends Primitive> = `${T}`;
This one’s a DX (developer experience) lifesaver.
Here’s the deal — in TypeScript, when you define an enum
, hovering over its property doesn’t always show the string values you actually use in your code.
Example:
enum Gender {
MALE = 'male',
FEMALE = 'female'
}
type Person = {
gender: Gender;
};
If you hover over gender
, you’ll see enum Gender
— not 'male' | 'female'
.
That’s fine for strictness, but not so friendly when you want hover hints or intellisense clarity.
Enter our hero:
type Enum<T extends Primitive> = `${T}`;
Usage:
type Person = {
gender: Enum<Gender>;
};
Now, hover over gender
, and you’ll see:
gender: "male" | "female"
✨ Why it matters:
- Improves hover readability
- Plays well with JSON APIs and UI forms
- Great for autocompletion and validation types
đź§± 5. Prettify<T>
— Making Complex Types Readable
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
When you use TypeScript’s utility types like Omit
, Pick
, or intersections, you often end up with ugly nested inferred types like:
type Example = Omit<User, 'id'> & { id?: string };
Hovering over Example
might give you something like:
Omit<User, "id"> & { id?: string; }
Not helpful, right?
By wrapping it in Prettify<T>
, TypeScript flattens it for readability:
type PrettyExample = Prettify<Example>;
Now it shows up neatly as:
{ name: string; id?: string; }
🪄 Perfect for hover docs, DX improvements, and cleaner autocomplete.
đź§© 6. Partialize<T, K>
— Make Specific Keys Optional
type Partialize<T, K extends keyof T> = Prettify<
Omit<T, K> & Partial<Pick<T, K>>
>;
Sometimes you don’t want to make all properties optional — just a few.
Example:
type User = { name: string; email: string; age: number };
type OptionalEmail = Partialize<User, 'email'>;
// Result:
type OptionalEmail = {
name: string;
age: number;
email?: string;
};
This is especially useful in form states or update payloads where only certain fields are optional.
đź”’ 7. RequireBy<T, K>
— Make Specific Keys Required
type RequireBy<T, K extends keyof T> = Prettify<
Omit<T, K> & Required<Pick<T, K>>
>;
The opposite of Partialize
.
Perfect when you have a type with optional fields, but want to enforce some of them as mandatory in a specific context.
Example:
type User = { name: string; email?: string; age?: number };
type RequireEmail = RequireBy<User, 'email'>;
// Result:
type RequireEmail = {
name: string;
email: string;
age?: number;
};
Ideal for validation layers, admin panels, or API mutations.
đź’¬ Wrapping Up
These types might look simple at first glance, but they can massively improve your TypeScript experience.
They’re reusable, intuitive, and composition-friendly — perfect for any project where type safety and developer ergonomics matter.
Here’s what they bring to the table:
Utility | Purpose | DX Boost |
---|---|---|
Primitive |
Define base JS types | Clean constraints |
Nullable<T> |
Make type nullable | Simplicity |
NullableDeep<T> |
Deeply nullable object | Real-world API resilience |
Enum<T> |
Enum hover clarity | Intellisense-friendly |
Prettify<T> |
Flatten nested types | Cleaner hovers |
Partialize<T, K> |
Make specific keys optional | Form handling |
RequireBy<T, K> |
Make specific keys required | Input validation |
đź§ Pro Tip:
Keep these in a types/utils.ts
file and reuse them across your codebase.
Over time, you’ll notice your type definitions become cleaner, more expressive, and far easier to maintain.
What about you?
Have you built similar DX helpers in your projects?
Drop your favorites — I’d love to discover more community-built utility types! 🚀
Top comments (0)