TypeScript
Completed
-
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
110: Higher-Order Type Functions
Up until now, you've used types to describe the shape of your data. But in complex systems, you often find yourself wishing you could "calculate" a type based on another type. This is where higher-order type functions come in. In TypeScript, we don't have functions that run at runtime to create types, but we have conditional types and mapped types that act as logic gates at compile-time.
Let's build something practical. Imagine we're building a form system where some fields are optional by default, but depending on the "mode" of the form (like 'Edit' vs 'Create'), we need to force certain fields to be required. We want a type-level function that takes a base type and a list of keys, and returns a new type where only those specific keys are required.
The goal: Selective Strictness
I want to start with a basic User interface. Most of the time, when updating a user, I only want to send a few fields. But for a "Security Update," I must ensure the email and password are always provided.
interface User {
id: string;
username: string;
email: string;
password?: string;
bio?: string;
}
// I want a type that makes 'email' and 'password' required,
// but leaves everything else as it is.
Where I tripped up
My first instinct was to try a simple mapped type. I thought I could just loop through the keys and check if they were in my "required" list. I wrote something like this:
type MakeRequired<T, K extends keyof T> = {
[P in keyof T]: P extends K ? T[P] : T[P];
};
I hit a wall immediately. While this looks like it's doing something, TypeScript's type system is smarter (and sometimes more stubborn) than we are. Because the mapping happens across the whole object, the resulting type didn't actually "strip" the optionality of the properties in the way I expected. It just returned the original type because T[P] is still T[P], whether it's in K or not. I was essentially saying "if it's a required key, keep it as it is; otherwise, keep it as it is."
The Conditional Logic
To actually force a property to be required, I need to use the Required<T> utility or the -? mapping modifier. The -? is a powerful little tool that explicitly removes the "optional" flag from a property.
Here is how I actually solved it. I used a mapped type combined with a conditional check to see if the current key P exists within the union of keys K that we want to make required.
type SelectivelyRequired<T, K extends keyof T> = {
[P in keyof T]: P extends K
? T[P] // This still feels wrong... wait.
: T[P];
};
Actually, looking at that again, I'm making the same mistake. The trick isn't in the value T[P], but in the mapping. I need to apply the -? modifier specifically to the keys in K. But you can't put a conditional inside the modifier itself. Instead, I have to intersect the original type with a version where only the selected keys are mapped and forced to be required.
type SelectivelyRequired<T, K extends keyof T> = T & {
[P in K]-?: T[P];
};
Now we're talking. By using the intersection operator (&), we take the original User and merge it with a new object type that contains only the keys in K, and the -? tells TypeScript: "Regardless of whether this was optional in the original type, it is now strictly required."
Putting it into practice
Let's see this in action with our User object. If I try to create a security update without a password, the compiler should now scream at me.
type SecurityUpdate = SelectivelyRequired<User, 'email' | 'password'>;
const update: SecurityUpdate = {
id: '123',
username: 'dev_mentor',
email: 'mentor@example.com',
// password is missing!
};
// Error: Property 'password' is missing in type...
This is the essence of a higher-order type function: we've created a reusable utility (SelectivelyRequired) that takes types as arguments and computes a new, specialized type based on the logic we defined. It's not just a label; it's a transformation.
📋 Practical Task
Implementing a Selective-Readonly Wrapper
You are building a state management system where certain parts of the state should be immutable (readonly) based on the user's permissions, while other parts remain editable.
Your task is to create a higher-order type called SelectivelyReadonly<T, K extends keyof T>. This type should take an object type T and a union of keys K, returning a new type where only the keys specified in K are marked as readonly, while all other keys remain mutable.
Requirements:
- Use the
readonly` modifier in a mapped type. - Use an intersection (
&) or a mapped conditional to ensure the other properties ofTare preserved. - Test your type with a
Productinterface (containingid,name, andprice) to ensure that makingidreadonly prevents it from being reassigned.
There are no comments for now.