Skip to main content

Mastering TypeScript isn't about knowing every feature. It's about knowing which feature NOT to use.

You've learned 22 chapters of TypeScript. Primitives, unions, generics, conditional types, mapped types, template literal types, type-level programming, pattern matching, decorators, const type parameters. You have more tools than you'll ever need.

The question now is: when do you use which tool? When do you reach for a discriminated union vs. a simple if/else? When do you write a generic type vs. copying a type definition? When do you use as any and move on with your life?

This chapter is about judgment. The TypeScript mindset. The principles that guide your decisions when the type system offers you infinite ways to solve a problem.

Principle 1: Types Are for Readers, Not Just the Compiler​

The primary audience for your types is not the TypeScript compiler. It's the next developer who reads your code. (Who is probably you, six months from now.)

// CLEVER — but what does it mean?
type DeepWritable<T> = { -readonly [K in keyof T]: T[K] extends object ? DeepWritable<T[K]> : T[K] };

// CLEAR — anyone can understand this
// Makes all properties of T and its nested objects mutable (removes readonly)
type DeepWritable<T> = { -readonly [K in keyof T]: T[K] extends object ? DeepWritable<T[K]> : T[K] };

The difference is a comment. But the principle is deeper: if a type is clever enough to need explanation, it might be too clever. The best types are the ones that make the code OBVIOUS, not the ones that demonstrate your mastery of the type system.

The Readability Test​

After writing a type, ask:

  1. Would a new team member understand this without asking me?
  2. Would I understand this six months from now?
  3. Is there a simpler way to express the same constraint?

If the answer to any of these is "no," simplify.

Principle 2: Start Simple, Add Complexity Only When Needed​

The best TypeScript codebases I've seen use surprisingly simple types. interface, type, union, generic — that's 80% of what you need.

// START HERE — simple, clear, works
interface User {
name: string;
email: string;
}

// ADD COMPLEXITY ONLY WHEN YOU NEED IT
type User =
| { status: "active"; name: string; email: string; lastLogin: Date }
| { status: "inactive"; name: string; email: string; deactivatedAt: Date };

Don't reach for conditional types until a union won't work. Don't reach for template literal types until string manipulation is actually needed. Don't reach for type-level programming until you've exhausted simpler options.

The Complexity Escalator​

ProblemStart WithEscalate To
"This value can be one of several things"Union typeDiscriminated union
"This function should work with any type"Generic functionGeneric constraint
"I need to transform an object type"Mapped typeConditional mapped type
"I need to extract a type from another type"inferRecursive infer
"I need string manipulation at the type level"Template literal typeRecursive template literal

Principle 3: any and as Are Escape Hatches, Not Solutions​

Every as assertion is a hole in your type safety. Every any is a promise you're making to yourself: "I'll come back and type this properly later."

Sometimes that's the right call. Shipping is a feature. A working app with a few as any assertions is better than a perfectly typed app that never ships.

But treat escape hatches as technical debt:

// Mark escape hatches with a standard comment
const data = response.json() as any; // TODO(amit): Type this properly — ticket TS-1234

// Better: use 'unknown' and narrow
const raw: unknown = response.json();
if (typeof raw === "object" && raw !== null && "data" in raw) {
// Now we know it has a 'data' property
}

The Escape Hatch Hierarchy​

ApproachSafetyWhen to Use
Proper types✅ SafeAlways — the default
unknown + narrowing✅ SafeUnknown data from external sources
as with validation🟡 ModerateWhen you've validated at runtime
as without validation🔴 UnsafeOnly when you're absolutely certain
any🔴 Very unsafeTemporary migration aid only

Principle 4: Let the Compiler Work for You​

The compiler is your collaborator, not your adversary. When it flags an error, your first instinct shouldn't be "how do I suppress this?" It should be "what is the compiler telling me about my design?"

// Compiler: "Object is possibly 'undefined'"
// BAD: Suppress it
const name = user!.name;

// GOOD: Handle the case
if (!user) {
throw new Error("User is required");
}
const name = user.name;

// BETTER: Design so it can't be undefined
function greet(user: User): string {
return `Hello, ${user.name}`;
}

Every type error is a design question. The compiler is asking: "Are you SURE about this? Have you handled the edge case?" Answer the question, don't silence the questioner.

Principle 5: Types Should Match Reality​

Your types should describe what your code ACTUALLY does, not what you WISH it did. If your API sometimes returns { data: User } and sometimes returns { error: string }, your type should reflect that — even if it makes the calling code more complex.

// WISHFUL THINKING — doesn't match reality
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json(); // What if the response is an error?
}

// REALITY — matches what actually happens
async function fetchUser(id: number): Promise<User | null> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) return null;
return response.json();
} catch {
return null;
}
}

Types that lie are worse than no types at all. They give you false confidence. You write code assuming the type is correct, and when reality diverges, you get runtime errors that the compiler said couldn't happen.

The TypeScript Code Review Checklist​

When reviewing TypeScript code, ask these questions:

Type Safety​

  • Are there any as any or as Type assertions? Can they be replaced with proper types?
  • Are there any any types? Can they be replaced with unknown or a proper type?
  • Are optional properties handled correctly? (Check for missing ? or missing null checks)
  • Are API responses validated at runtime? (Zod, type guards, or assertion functions)

Type Design​

  • Do the types make invalid states impossible?
  • Are discriminated unions used where a value can be one of several variants?
  • Are the types named clearly? (Not T, U, V — use descriptive names for public APIs)
  • Is there a single source of truth for each type? (Not duplicated across files)

Complexity​

  • Is there a simpler way to express the same constraint?
  • Are conditional types, mapped types, or template literal types used only where necessary?
  • Would a new team member understand these types?
  • Are complex types documented with comments?

Performance​

  • Are there any recursive types that might hit the recursion limit?
  • Are there any unions with thousands of members?
  • Does type checking complete in a reasonable time?

When NOT to Use Advanced TypeScript​

The most important skill in TypeScript is knowing when to stop. Here are the signs you've gone too far:

Sign 1: The Type Is Harder to Read Than the Code​

// If your type looks like this...
type DeepConditionalMappedInferred<T> =
T extends infer U
? U extends Record<infer K extends string, infer V>
? { [P in K as `on${Capitalize<P>}Change`]: (value: V) => void }
: never
: never;

// ...you've probably gone too far.

Sign 2: You're Fighting the Compiler​

If you're spending more time making the types happy than making the code work, the types are too complex. Simplify.

Sign 3: The Type Error Is Longer Than the Function​

If a one-line function produces a 50-line type error, the types are too clever. Break them down.

Sign 4: You're the Only One Who Understands It​

If you have to explain your types to every team member who encounters them, they're too complex. Types should be self-documenting.

The TypeScript Ecosystem​

You don't need to build everything from scratch. The TypeScript ecosystem has tools for common problems:

ToolUse For
ZodRuntime validation, API schemas
ts-resetFixing TypeScript's standard library types
type-festUtility types beyond the standard library
ts-patternPattern matching (if not on TS7 yet)
zod-to-tsGenerating TypeScript types from Zod schemas
tsxRunning TypeScript files directly (no compile step)
dts-bundle-generatorBundling .d.ts files for npm packages

Your Path Forward​

You've finished the book. You know TypeScript from primitives to type-level programming. Here's what to do next:

This Week​

  1. Enable strict: true on your current project (if it's not already)
  2. Find one any in your codebase and replace it with a proper type
  3. Write one discriminated union for a state that's currently modeled with booleans

This Month​

  1. Add Zod for runtime validation at your API boundaries
  2. Refactor one switch statement to use pattern matching (if on TS7)
  3. Write one generic utility type that eliminates duplication in your codebase

This Year​

  1. Lead a TypeScript migration for a JavaScript codebase
  2. Publish a TypeScript library with proper type definitions
  3. Teach TypeScript to a colleague — teaching is the best way to master

The Return​

You started this book knowing JavaScript and wondering if TypeScript was worth the effort. You're finishing it with the ability to:

  • Model any domain with precise types
  • Catch entire categories of bugs at compile time
  • Design APIs that are self-documenting through their types
  • Use the most advanced features of TypeScript 7
  • Know when to use those features — and when not to

But more importantly, you've developed the TypeScript mindset. You see types not as extra work but as a design tool. You treat the compiler as a collaborator. You write types for the next developer, not just for the compiler.

The bug that cost a billion dollars — the one from Chapter 1 — that bug doesn't happen in TypeScript. Not because TypeScript developers are smarter. Because the compiler catches it before it leaves the editor.

That's the gift of TypeScript. Not that you'll never write bugs. But that the obvious ones, the preventable ones, the "how did this ever work" ones — those get caught. By a tireless, thorough, infinitely patient collaborator that never forgets a type and never lets a mismatch slide.

You are now that collaborator's partner. Use it well.


End of TypeScript 7: Zero to Mastery.

Now go build something.