TypeScript Utility Types: A Beginner's Guide to Writing Less Code
TypeScript Utility Types: A Beginner's Guide to Writing Less Code
You know that feeling when you copy-paste a Type, delete two lines, add a ? to another line, and rename it UserButOptional?
And then 10 minutes later, you create UserButReadOnly?
And then UserButOnlyNameAndEmail?
Congratulations, you have entered Type Fatigue. It’s real, it’s painful, and it makes your codebase look like a graveyard of duplicate interfaces.
But I have a secret weapon for you: TypeScript Utility Types.
What Are TypeScript Utility Types?
Utility types are basically TypeScript saying: “Relax. You don’t have to rewrite that type again.”
They are built-in tools that take an existing type and transform it into a new one. Think of them as filters for your types. You pass a type in, apply a transformation (like "make everything optional" or "remove 'password' field"), and get a shiny new type out.
Using them makes your code:
- Cleaner: Less duplicate code.
- Safer: If you update the original type, all the utility types update automatically.
- smarter: You look like a TypeScript wizard.
Let's dive into the ones you will actually use.
Object Utility Types
These are the bread and butter of your daily work.
1. Pick (The Selector)
The Problem: You have a massive User object, but your profile card only needs the name and image.
The Solution: Pick allows you to choose only the specific keys you want.
- Analogy: Pick is like ordering only fries from a combo meal.
interface User {
id: number;
name: string;
email: string;
passwordHash: string; // strict private
isAdmin: boolean;
}
// We only want to show these publicly
type UserPreview = Pick<User, "name" | "email">;
const currentUser: UserPreview = {
name: "Sushek",
email: "sushek@example.com",
// id: 1 <- Error! 'id' is not allowed here.
};
2. Omit (The Excluder)
The Problem: You want the whole User object, except the sensitive stuff.
The Solution: Omit creates a new type with everything except the keys you specify.
- Analogy: Omit is like saying "I'll have the burger, no pickles."
// Everything EXCEPT the password
type SafeUser = Omit<User, "passwordHash">;
const user: SafeUser = {
id: 1,
name: "Dave",
email: "dave@example.com",
isAdmin: false,
// passwordHash <- Error! We removed this.
};
3. Partial (The Optionalizer)
The Problem: You are writing an "Update User" function. The user might want to update just their name, or just their email, or nothing.
The Solution: Partial makes every single property optional.
- Analogy: Partial switches your type from "Required Fields" mode to "Whatever You Got" mode.
function updateUser(id: number, changes: Partial<User>) {
// Call API...
}
// Valid!
updateUser(1, { name: "New Name" });
// Also Valid!
updateUser(1, { email: "new@email.com", isAdmin: true });
4. Required (The Enforcer)
The Problem: You have a type with lots of optional fields (?), but in one specific function (like "Submit to Database"), you need to guarantee they are all present.
The Solution: Required removes all those ? marks.
- Analogy: Required is the bouncer at the club checking IDs. No optional entry allowed.
interface Props {
title?: string;
description?: string;
}
const submitPost = (data: Required<Props>) => {
// TypeScript knows 'title' and 'description' are DEFINITELY strings here.
// No need to check for undefined!
console.log(data.title.toUpperCase());
};
5. Readonly (The Protector)
The Problem: You want to make sure no one accidentally mutates an object after it's created.
The Solution: Readonly makes all properties... well, read-only.
- Analogy: Readonly laminates your document. You can look, but you can't touch.
const config: Readonly<User> = {
id: 1,
name: "Admin",
email: "admin@admin.com",
passwordHash: "123",
isAdmin: true
};
// config.name = "Hacker" <- Error! Cannot assign to 'name' because it is read-only.
6. Record (The Dictionary)
The Problem: You need an object where the keys are IDs (strings) and the values are Users, but you don't know the keys ahead of time.
The Solution: Record<Key, Value> lets you define the shape of a dictionary map.
- Analogy: Record is a template for a filing cabinet. "Labels must be Strings, contents must be Users."
// Key is string (ID), Value is User object
const userMap: Record<string, User> = {
"u-123": { id: 1, name: "Alice" } as User,
"u-456": { id: 2, name: "Bob" } as User,
};
Function Utility Types (Beginner Safe Zone)
For when you need to talk about functions without invoking them.
7. Parameters
Extracts the parameter types from a function type as a tuple (an array).
- Use Case: Someone else wrote a function inside a library, didn't export the types, and you need to match their arguments.
function saveUser(id: number, data: User, isDraft: boolean) {}
// extracting the types
type SaveUserArgs = Parameters<typeof saveUser>;
// Result: [number, User, boolean]
8. ReturnType
Extracts what a function returns.
- Use Case: You want to shape a variable to match the output of a helper function.
function createSession() {
return { token: "abc", expires: 999, userId: 5 };
}
// Instead of manually typing interface Session { ... }
type Session = ReturnType<typeof createSession>;
// Result: { token: string, expires: number, userId: number }
9. ConstructorParameters
Just like Parameters, but for class constructors. Honestly? You won't use this much unless you are writing mixins or high-level frameworks. But it's good to know it exists!
Union & Safety Utility Types
10. Extract & Exclude
These work on Unions ("a" | "b" | "c").
- Exclude: "Give me everything EXCEPT these."
- Extract: "Give me ONLY these."
type Status = "success" | "warning" | "error" | "info";
// Remove 'info'
type CriticalStatus = Exclude<Status, "info">;
// Result: "success" | "warning" | "error"
// Only keep the happy path
type HappyStatus = Extract<Status, "success">;
// Result: "success"
11. NonNullable
Removes null and undefined from a type.
- Analogy: A vacuum cleaner that sucks up all the uncertainty.
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// Result: string
Async & String Utility Types (Fun Section)
12. Awaited
Unwraps a Promise. If you have Promise<User>, Awaited gives you just User.
async function fetchUser(): Promise<User> {
return {} as User;
}
// We want the type of the DATA, not the PRomise
type UserData = Awaited<ReturnType<typeof fetchUser>>;
// Result: User
13. String Manipulation
Did you know TypeScript can format strings? It's wild.
Uppercase<T>Lowercase<T>Capitalize<T>Uncapitalize<T>
type Role = "admin" | "user";
type ShoutyRole = Uppercase<Role>;
// Result: "ADMIN" | "USER"
// Practical usage: Generating Event names automatically
type EventName = `on${Capitalize<Role>}Created`;
// Result: "onAdminCreated" | "onUserCreated"
Class-Related Utility Type
14. InstanceType
Gets the type of an instance of a class. Useful when you are dealing with class factories.
class Car {
drive() {}
}
type CarInstance = InstanceType<typeof Car>;
// Result: Car
One Big Example: Form Data Types
Let's put it all together. Imagine a simple form editor.
interface Product {
id: number;
name: string;
price: number;
description: string;
tags: string[];
}
// 1. API Response (Readonly)
type ProductAPIResponse = Readonly<Product>;
// 2. Form State (Omit ID because it's new, and make tags Optional)
type CreateProductForm = Omit<Product, "id" | "tags"> & { tags?: string[] };
// 3. Update Form (Everything is optional, except ID which is required to find it)
type UpdateProductPayload = Required<Pick<Product, "id">> & Partial<Omit<Product, "id">>;
const update: UpdateProductPayload = {
id: 55, // Strict!
price: 19.99 // Optional!
}
See how we generated three distinct use-cases from one single interface? That is the power of Utility Types.
Common Beginner Mistakes
- Over-Golfing: Don't try to be too clever.
Pick<Partial<Omit<User, 'id'>>, 'name'>is readable to computers, not humans. If it gets too complex, just write a newinterface. - Using
any: Utility types exist so you DON'T have to useany. Use them! - Forgetting They Exist: The biggest mistake is manually rewriting types. Print out the official docs list and tape it to your monitor.
Conclusion: Utility Types Are Your Best Teammate
TypeScript Utility Types aren't just trivia for interviews. They are practical tools to help you write code that is arguably lazier.
And as developers, isn't being efficiently lazy the ultimate goal?
You write the standard Type once, and let TypeScript do the grunt work of reshaping it for every other file.
Go forth and Pick, Omit, and Partial your way to a cleaner codebase!
