-
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
236: Mapped Type Modifiers: +/- readonly and +/- optional
I ran into a situation last week where a junior dev was trying to build a configuration loader. They had a set of default settings that were marked as readonly and optional, because the user might only override a few of them. The problem started when they tried to create a "finalized" config object—something that had been validated, filled in, and was ready to be mutated by the application's internal state manager.
interface AppConfig {
readonly apiEndpoint?: string;
readonly timeout?: number;
readonly retryAttempts?: number;
}
// They wanted a type where everything is required AND mutable
type FinalConfig = {
[K in keyof AppConfig]: AppConfig[K];
};
const config: FinalConfig = {
apiEndpoint: "https://api.example.com",
timeout: 5000,
retryAttempts: 3,
};
// Error: Cannot assign to 'apiEndpoint' because it is a read-only property.
config.apiEndpoint = "https://staging.api.example.com";
The persistence of modifiers
You'll notice that even though we tried to "re-map" the type into FinalConfig, the readonly and optional (?) modifiers didn't just vanish. In TypeScript, when you map over a type, the modifiers from the source type are carried over by default. It's as if TypeScript is saying, "I see you're making a new type based on AppConfig, so I'll keep those safety constraints intact."
This is frustrating when you're intentionally trying to move from a "loose" type (like an API response or a set of options) to a "strict" type (like a validated internal state). You can't just ignore the modifiers; you have to explicitly tell TypeScript to strip them away.
Stripping modifiers with -readonly and -?
To fix this, we use the mapping modifiers + and -. While + is the default (it adds or keeps the modifier), - explicitly removes it. To turn our AppConfig into a fully mutable, fully required FinalConfig, we need to prepend the minus sign to both the readonly keyword and the optionality marker.
interface AppConfig {
readonly apiEndpoint?: string;
readonly timeout?: number;
readonly retryAttempts?: number;
}
type FinalConfig = {
// -readonly removes the readonly constraint
// -? removes the optionality (makes it required)
[K in keyof AppConfig]-readonly -?: AppConfig[K];
};
const config: FinalConfig = {
apiEndpoint: "https://api.example.com",
timeout: 5000,
retryAttempts: 3,
};
// This now works perfectly!
config.apiEndpoint = "https://staging.api.example.com";
I like to think of these as "filter" operations on the type's metadata. By using -?, you're essentially telling the compiler: "I don't care if this was optional before; in this new type, it is mandatory."
You can also use these independently. If you want a type that is still optional but no longer read-only, you'd just use -readonly. If you want it to stay read-only but become required, you'd just use -?. It gives you granular control over how your types evolve as data moves through different stages of your application logic.
📋 Practical Task
Transforming a Loose User Profile into a Strict Form State
You are building a user profile editor. The UserApiResponse type coming from the server is read-only and has several optional fields because some users haven't filled out their profiles yet. However, your ProfileFormState type needs to be completely mutable (so the form can update values) and every field must be present (even if it's an empty string) to avoid undefined errors in the UI components.
Implement the ProfileFormState mapped type to remove both the readonly and optional modifiers from the UserApiResponse.
interface UserApiResponse {
readonly firstName?: string;
readonly lastName?: string;
readonly bio?: string;
readonly location?: string;
}
// TODO: Implement this mapped type to strip both 'readonly' and '?'
type ProfileFormState = {
// Your code here
};
const apiData: UserApiResponse = {
firstName: "Jane",
lastName: "Doe",
// bio and location are missing
};
const formState: ProfileFormState = {
firstName: apiData.firstName || "",
lastName: apiData.lastName || "",
bio: apiData.bio || "",
location: apiData.location || "",
};
// This should not throw a compiler error
formState.firstName = "Janet";
There are no comments for now.