-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
34: Utility Types: Required, Readonly, Exclude, Extract
Think of these utility types as a set of "filter" or "modifier" lenses you can place over your existing types. Imagine you have a master blueprint for a house. Most of the time, that blueprint is great. But sometimes, you need a version of that blueprint specifically for the electrician, where everything except the wiring is faded out. Other times, you need a version for the homeowner where certain "optional" additions—like a deck or a finished basement—are now mandatory requirements for the final walkthrough. You aren't drawing a whole new blueprint from scratch; you're just applying a rule to the existing one to change how it's used in a specific context.
In TypeScript, we do this with Utility Types. Instead of declaring five different interfaces that look almost identical, we take one "source of truth" and wrap it in a utility to change its properties on the fly.
Locking Down and Filling In Your Objects
Let's start with Required and Readonly. These are essentially the opposite of Partial (which you've already seen) and the opposite of mutable properties.
I often run into a situation where I have a UserConfig object. Most fields are optional because the user might only want to change a few settings. But when that config is finally sent to the database, my backend API demands that every field be present. I don't want to manually redefine the interface. I just use Required.
interface UserConfig {
theme?: 'light' | 'dark';
notifications?: boolean;
language?: string;
}
// This is fine for a "settings" form
const userPrefs: Partial<UserConfig> = { theme: 'dark' };
// But for the database save function, everything must be there
function saveToDb(config: Required<UserConfig>) {
// TypeScript now ensures theme, notifications, and language are all present
console.log(`Saving ${config.language} settings...`);
}
Now, Readonly is my go-to for preventing "accidental mutations." If you've ever spent three hours debugging a bug where a function halfway down the call stack changed a property on an object it wasn't supposed to touch, you'll love this. It makes all properties immutable.
interface AppState {
apiKey: string;
endpoint: string;
}
const globalConfig: Readonly<AppState> = {
apiKey: 'secret_123',
endpoint: 'https://api.example.com'
};
// This will trigger a compiler error immediately
globalConfig.apiKey = 'new_key';
Sifting Through Union Types
While the first two deal with objects, Exclude and Extract are all about Unions. I like to think of these as "subtract" and "intersect" operations for your types.
Suppose you have a union of all possible status codes for a ticket system. You might want a type that represents "all statuses except the closed ones" so you can pass it to a function that updates active tickets.
type TicketStatus = 'Open' | 'In Progress' | 'On Hold' | 'Resolved' | 'Closed';
// "Give me everything in TicketStatus EXCEPT 'Resolved' and 'Closed'"
type ActiveStatus = Exclude<TicketStatus, 'Resolved' | 'Closed'>;
// ActiveStatus is now: 'Open' | 'In Progress' | 'On Hold'
Extract does the exact opposite. It's like a magnet. It looks at a union and pulls out only the members that match a specific criteria. This is incredibly useful when you have a large union and you want to create a smaller, specialized subset of it based on a pattern.
type EventPayload =
| { type: 'MOUSE_MOVE'; x: number; y: number }
| { type: 'MOUSE_CLICK'; button: number }
| { type: 'KEY_PRESS'; key: string }
| { type: 'WINDOW_RESIZE'; width: number; height: number };
// I only care about mouse events right now
type MouseEvent = Extract<EventPayload, { type: 'MOUSE_MOVE' } | { type: 'MOUSE_CLICK' }>;
function handleMouse(event: MouseEvent) {
// TypeScript knows this is either a MOVE or a CLICK
}
Between these four, you can stop duplicating your type definitions. If the base interface changes, these utility types update automatically. It keeps your code DRY and, more importantly, keeps the compiler working for you rather than against you.
📋 Practical Task
Hardening the Project Task Management Schema
You are building a task manager. You have a base Task interface where most fields are optional because tasks are created in stages. However, you need to implement a "Finalize Task" feature where certain rules apply.
Requirements:
- Create a
Taskinterface with:id(string),title(string),description(string, optional), andpriority(a union of 'Low' | 'Medium' | 'High' | 'Urgent'). - Create a type called
FinalizedTaskthat makes all properties ofTaskrequired. - Create a type called
TaskMetadatathat makes theTaskproperties readonly. - Create a type called
CriticalPrioritythat usesExtractto pull only 'High' and 'Urgent' from the priority union. - Create a type called
NonCriticalPrioritythat usesExcludeto remove 'High' and 'Urgent' from the priority union.
Verification: Try to assign a Task (with missing fields) to a FinalizedTask variable and verify that TypeScript throws an error. Try to change the id of a TaskMetadata object and verify it is blocked.
There are no comments for now.