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
244: Typing a Multi-Step Wizard Form State
I've seen this pattern in almost every mid-sized React or Vue project I've joined. You're building a multi-step onboarding wizard, and the instinct is to create one big "God Object" to hold all the form data. It feels intuitive at first, but it usually leads to a codebase littered with non-null assertions (the ! operator) and defensive if checks that shouldn't need to exist.
// The "I'll just make everything optional" approach
interface OnboardingState {
step: 1 | 2 | 3;
// Step 1 data
username?: string;
email?: string;
// Step 2 data
plan?: 'free' | 'premium';
billingCycle?: 'monthly' | 'yearly';
// Step 3 data
cardNumber?: string;
cvv?: string;
}
const state: OnboardingState = { step: 3 };
// Even though we are on Step 3, TypeScript has no idea
// that Step 1 and 2 are already complete.
function submitForm(state: OnboardingState) {
// Error: Object is possibly 'undefined'
console.log(`Sending ${state.username} to the database...`);
// This "fix" is a ticking time bomb
console.log(`Plan selected: ${state.plan!.toUpperCase()}`);
}
The Optional Property Trap
The problem here is that our type definition is too loose. By marking everything as optional, we're telling TypeScript, "At any given moment, any of these fields might be missing." While that's technically true for the entire lifecycle of the wizard, it's not true for the specific state of the wizard at a specific step.
When the user is on Step 3, you know that the username and plan have already been collected. But because we used a single interface with optional properties, TypeScript can't track the progression of data. You end up fighting the compiler, and eventually, you start using ! just to make the red lines go away. That's how bugs slip into production—you're telling the compiler to shut up instead of telling it how the data actually flows.
Modeling State as a Discriminated Union
The fix is to stop thinking of the state as one object and start thinking of it as a set of possible states. We can use a Discriminated Union to tie the step number directly to the data that must exist at that step.
type Step1State = {
step: 1;
username: string;
email: string;
};
type Step2State = {
step: 2;
username: string;
email: string;
plan: 'free' | 'premium';
billingCycle: 'monthly' | 'yearly';
};
type Step3State = {
step: 3;
username: string;
email: string;
plan: 'free' | 'premium';
billingCycle: 'monthly' | 'yearly';
cardNumber: string;
cvv: string;
};
type WizardState = Step1State | Step2State | Step3State;
function submitForm(state: WizardState) {
// Now we narrow the type using the 'step' discriminator
if (state.step === 3) {
// Inside this block, TypeScript knows exactly which fields are present.
// No more optional chaining or non-null assertions!
console.log(`Sending ${state.username} with plan ${state.plan} to the API.`);
}
}
I'll admit, this feels like more boilerplate because you're repeating fields (like username and email) across the types. However, this is a trade-off I'll take every single time. You've moved the "truth" of your application from a developer's mental note ("I think we collected the email in step 1") into the type system itself.
If you really hate the repetition, you can use intersection types to build these incrementally, but the core concept remains: your state should be a union of specific steps, not a single object of optional values.
📋 Practical Task
Implementing a Flight Booking State Machine
You are building a flight booking wizard. The process has three distinct stages:
- Step 1 (Search): Requires
originanddestination(both strings). - Step 2 (Seat Selection): Requires everything from Step 1, plus a
seatNumber(string) andclass('economy' | 'business' | 'first'). - Step 3 (Payment): Requires everything from Step 2, plus a
paymentMethod('credit_card' | 'paypal').
Your Task:
- Create a Discriminated Union called
BookingStatethat represents these three steps. - Write a function called
finalizeBookingthat acceptsBookingStateas an argument. - Inside
finalizeBooking, use a type guard (like aniforswitchstatement) to ensure the code only attempts to access thepaymentMethodif the state is specifically at step 3. - Ensure that you do not use any optional properties (
?) or non-null assertions (!) in your solution.
There are no comments for now.