
TypeScript Cookbook
You need to be signed in to add a collection
"Less is more in TypeScript" - Stefan Baumgartner shares practical wisdom on TypeScript configuration, strategic type annotations, and alternatives to enums in this insightful chat with Peter Kröner about his new book "The TypeScript Cookbook."
Transcript
From Introduction to Cookbook: The Evolution of TypeScript Books
Peter Kröner: Hello and welcome to the GOTO Book Club. I'm Peter Kröner, I'm a freelance low-level frontend web tech person and with me, I have my friend and podcasting buddy, Stefan Baumgartner.
Stefan Baumgartner: Morning, Peter Kröner. Hi.
Peter Kröner: Hi, welcome to the show. So, I guess you did it again, right? You wrote another book on TypeScript.
Stefan Baumgartner: After I wrote "TypeScript in 50 Lessons" in 2020, I wrote another one, the TypeScript... Oh cool, you have it. Fantastic.
Peter Kröner: Of course.
Stefan Baumgartner: I have here the follow-up, "TypeScript Cookbook." Exactly, which is sort of a sequel companion to the first book. I wouldn't have thought that I would write another one, but here we are.
Peter Kröner: This was of course going to be my first question. Why another one? I mean, you said that it is kind of like a sequel, as in built on top of the first one.
Stefan Baumgartner:You can read both books independently. The second book is a sequel only in the sense that it reflects where I was in my TypeScript journey. TypeScript in 50 Lessons was meant as a timeless introduction to the type system. After reading it, you should be able to write TypeScript in real-world projects and navigate the basics confidently.
But once you start using TypeScript extensively, more complex questions arise. The type system generally follows rules, but there are edge cases where compromises are made for real-world compatibility. That’s where the TypeScript Cookbook comes in. It focuses on practical scenarios and provides solutions to common problems that appear in actual development.
While the first book covers ideal use cases, the second tackles real-world challenges. Interestingly, while the first had 50 lessons, the second ended up with around 100—same style, but reflecting the complexity of real-world TypeScript programming.
Peter Kröner: The opposite of a best-case scenario is a real-world scenario. That's also telling.
Stefan Baumgartner: That's saying something, isn't it? Yeah, so the funny thing is, I really wasn't going to write another TypeScript book.
Peter Kröner: What's wrong with writing a book?
Stefan Baumgartner: I love writing books, but I thought I was done with TypeScript. Then O'Reilly approached me—they liked the first book and wanted to do a cookbook. The title was already decided, and they asked if I was interested and had enough content. At first, I was hesitant. I thought, “I've already written the TypeScript book—do I really have more to say?”
During the proposal phase, you're expected to draft a rough table of contents to outline the book’s direction. I spent just three hours on it and came up with around 100 items. That’s when I realized I did have enough material for a second, valuable book that could stand on its own.
About 95% of the content is new. There's some necessary overlap to cover the basics, especially since it's with a different publisher and should be self-contained. But the vast majority of the 100 lessons weren’t in the first book at all.
Peter Kröner: The only real similarity is the general table of contents—core topics like generics, conditional types, and other essentials you need to cover.
Stefan Baumgartner: The broad strokes are similar—topics like the type system, generics, conditional types, template literal types, and variadic tuple types. But while the first book had about 7 chapters, the second has 11 or 12—I’d have to check.
This time, I went further. I included a chapter on TypeScript with React, since it's widely used and full of nuances. React wasn’t originally written in TypeScript, so the community had to build type definitions around it, which leads to many real-world challenges. I also covered classes and object-oriented features, which were completely left out of the first book.
Peter Kröner: I noticed that you wrote an entire chapter about classes. We are going to get to this.
Stefan Baumgartner: I also cover type development strategies—how to create types from scratch or model a domain in TypeScript. It’s different from other languages because TypeScript is just a thin layer over JavaScript. There's this unique duality: you're writing JavaScript, but modeling it with TypeScript. I don’t know another language where that kind of split exists—it operates on two distinct levels.
The book explores that relationship. We define types, test how they align with the actual code, and in one chapter, revisit advanced conditional types. We push their limits, then ask: is it worth having perfect type safety if it makes the code hard to read, maintain, or forces awkward workarounds in JavaScript?
At some point, it might be better to accept a type that’s "good enough" for 99% of use cases, so it doesn’t get in the way of real-world development. That’s where the book becomes a bit philosophical—but that’s just my style.
Peter Kröner: You can expect this from any good cookbook if you ask me.
Stefan Baumgartner: It's often one of the two.
Peter Kröner: True, true. Specifically not software. Another thing I noticed in comparison to the first book is that you spent an entire chapter on project setup and talking about different stacks. I mean, React got its own chapter. There's also the general approach with introducing types to an existing project and this entire basically set-up procedure that you can go through, that you can take to different extremes. I mean, if you write a book, if you have something that actually gets printed on paper and you talk about essentially dependencies in a JavaScript project, I mean, you run the risk that before the ink has dried, that all the dependencies have turned over and nothing that you wrote matches anymore. All the screenshots are outdated. How do you deal with this if you write a book for the modern age of development?
Stefan Baumgartner: This is an excellent question, honestly, because I struggle a lot with the chapter to even convince myself that we need it. The reality, unfortunately, is that even if you know the TypeScript type system very, very well, you might struggle in wiring everything up into a project set-up that actually does what you intended to do. And I was very fortunate that after the first book that people invited me into the company, so I consulted them on TypeScript in a sense, and I figured out that everything that you can learn about the TypeScript system is basically done really, really quick. But getting there is the hard part. And this is what the first chapter deals with. So, yeah, we are talking about dependencies. We're talking about different JavaScript front ends, wiring up in Node projects, which is quite interesting because Node just released TypeScript support within Node and not everything from TypeScript, but a great deal from it. So, my main focus with that chapter was not only to wire up all the dependencies that could exist at the point of time where I've written it, but rather learn or teach the reader to learn about the TS config items, TS config compiler flags that make the entire thing work. So, yeah, I guess I have an item that tells you how to deal with ECMAScript imports where you load dependency via URL.
But what you actually learn is path mapping. How can you have this string identifier and map it to some types somewhere? And those config flags, they exist for a long, long time. They existed even before ECMAScript modules were supported in TypeScript and can now be used in this context. So, the goal of the book in this particular chapter is, learn as much as you can about TS configs so you understand how those properties work together, how do type re-routes work together, how does the file include mechanism work or exclude mechanism work, and how do you add a particular set of types to a particular scope of your project, especially if you have a front-end, back-end project where you write Node on one side and React on the other side, you don't want to have Node types in your React code and vice versa. So, how do you deal with that? And this is basically just configuration of the TS config JSON. And this is what you get in this chapter. So, yeah, it was a fine line that I had to walk there. But actually, it's one of my most favorite chapters because it opens up the idea of this cookbook really, really well. You need a recipe to solve this one issue or to cook this one dish if you will. And after you use it, you not only learn just how exactly to solve this one particular problem, but also get more skills that work for a multitude of projects really.
Peter Kröner: You can take your TS config JSON and just copy it to the next project once you have it working for one.
Stefan Baumgartner: Exactly. Or you understand which parts to exclude or extract from it for the next project. I guess this is even more important. It's so funny because there are some properties within TS config JSON that conflict with each other. And this is the stuff that you realize the hard way when you think everything that you've done should work, but it doesn't. But then you need to go into some base TS config JSON where you inherit all the properties that you want to spread across your entire project and need to delete this one particular line to make it work. This is where things get nasty. But those are the things that are being tackled in this book.
Project Setup: Keeping It Simple
Peter Kröner: If you were to start a Greenfield project with TypeScript today, no strings attached, no dependencies, no stack chosen for you. What would you pick in terms of build system, compiler and a few TS config flags that are simply must-haves in your opinion?
Stefan Baumgartner: Okay, no dependency at all. So, I'm not forced to use...
Peter Kröner: You can choose and rely. You can write a Node project React, what have you.
Stefan Baumgartner: Okay, this is a tough question. So, I mean, for the stuff that I write, I write very bare bones, JavaScript and TypeScript. They work mostly on the backend. And it's a joy working on the backend because if you write Deno or Node, you don't need to do anything with Deno. If you want to have a couple of overwrites, then all you need is a TS config JSON. And that's it. You can even write JSX in Deno and produce HTML output with it through a couple of libraries. The fresh UI library is excellent at that. The latest Node developments are heading in a similar direction. I've been writing Node for over a decade now, and setting up an Express server to handle a simple backend API is still a joy. Now, you get type support without needing an extra transpile step. All you need is to install the necessary type definitions from DefinitelyTyped. A minimal tsconfig.json file just tells the compiler to use those types—and that's it. That covers everything you need for the backend.
If I were to choose a build system for front-end code, it would be Weed. Weed is a battery-included build system that's highly flexible regarding runtime and framework choices. It's very ECMAScript-forward, meaning that while there's a build system involved, the primary experience is a development server that transpiles whatever meta-language you're using—TypeScript, .vue, .jsx, etc.—into JavaScript, while keeping the module system intact. There's no hidden module logic.
They’re heading in the right direction with a lot of their ideas. What’s especially important to me is the ability to switch execution runtimes—for tests, backend code, or anything else—while using consistent APIs. I’d also go with their tsconfig recommendations. With Deno, you barely need one. With Node, there's a five-line config available in the official docs. The Weed team also provides a setup out of the box, and you can override only what you need. It’s minimal, low on complexity, and very open and extensible. You don’t need to download a gigabyte of NPM dependencies just to run "Hello World." It’s about 20 MB, does what it needs to, and keeps things simple. That’s what I appreciate.
Peter Kröner: That's interesting. The man who wrote an entire book about TypeScript wants to have the most basic setup of all.
Stefan Baumgartner: Sure. Less is more in a lot of things, especially in TypeScript.
Peter Kröner: ...this stuff takes nicely into the first point you make in your chapter about basic types, which is essentially don't write any types if you can avoid it at any point.
Stefan Baumgartner: Infer types when you can—it’s great. TypeScript plays an interesting role in the JavaScript ecosystem because it has to model everything that JavaScript can do. And since JavaScript is incredibly flexible—and developers use it in all sorts of unconventional ways—the TypeScript team is constantly chasing edge cases, trying to formalize them and apply static typing. It’s a real challenge.
I really applaud the TypeScript team for managing to cover so much of the JavaScript landscape, which can be difficult. The best JavaScript code I’ve written is still worse than the code I write with TypeScript as my copilot.
TypeScript brings the full expressiveness of JavaScript, and that means it also understands a lot about how JavaScript works. You can take any JavaScript file, add a @ts-check comment at the top, and TypeScript will highlight issues right away. That alone gets you about 70% of the way—catching most of the bugs you’d otherwise miss. The other 30% comes from actively working with TypeScript, even at a basic level.
You don’t need every feature to get started. Unless you're in some very specific situations—like dealing with dependencies that aren't typed or have unusual APIs—you can go far with just a few type annotations, a solid understanding of the basics, and a focus on type checking and erasure.
Peter Kröner: As usual, types in 20% of the code handle 80% of the work.
Stefan Baumgartner: The Pareto principle, isn't it?
Recommended talk: Deno 2 • Ryan Dahl • GOTO 2024
Use Type Annotations Strategically
Peter Kröner: One question I have is—when you're consulting and step into a company that's starting to adopt TypeScript or considering it, I often notice that people value explicitness. They want to write types for every variable, essentially doing the opposite of what you initially suggested.
How do you reconcile that? People coming from JavaScript, which can be quite chaotic, are often looking for structure—something to ground their code. And you're essentially saying, “JavaScript is fine—just add a few guardrails, but don’t overdo it.”
How do you present that in a way that resonates?
Stefan Baumgartner:That’s a fantastic question. The answer is actually quite simple—both approaches can become dogmatic.
On one side, you have people who want to annotate everything because they come from backgrounds like Java, where you'd write something like int i = new Integer(3), repeating the type multiple times. They’re used to that level of explicitness and find comfort in it.
On the other side, you have JavaScript developers who take a more loose, dynamic approach—"let's just figure it out later." As with most things, the real answer lies in the nuance.
What I tell people when consulting—and also mentioned in the book—is that when you add a type annotation, you need to understand what it's doing. It's a type check. It's the moment in your code where you ask the compiler to ensure the value on one side is compatible with the expected type on the other.
If you're just assigning a string or number to a variable, ask yourself: “Do I really need a type check here? Do I care about the type, or just the value?” But once you're passing it into a function, method, or interface—then it becomes important to ensure that compatibility.
And there are different ways to do that—through function signatures, return types, and so on. A while ago, there was a big debate online about whether to annotate return types. It was a silly argument at its core. The real question is: do you want to check the return type or not?
Sometimes, you might be prototyping and still figuring out what the return type should be. In that case, it's fine to leave it out and let the code evolve. But always remember: adding a type to a variable or constant is a type check. If you assign a complex object to a typed variable, TypeScript treats it as that type from then on.
This works great in many scenarios, but in others—like when you're working with a large config object used in multiple contexts—you might not want to annotate the type upfront. You could lose useful type information. That’s all the colon-type annotation does—it sets a type, and from that point forward, TypeScript treats the value as only that type.
It also has performance implications: early type checks mean TypeScript can just compare labels during checking. If you assign a variable as :Person, it will always be treated as a Person, which is efficient. If you skip the annotation, TypeScript has to infer and re-check more often. But this is usually a micro-optimization—it’s not about performance, it’s about expression.
So ask yourself: “Do I want this to be a Person right now, or am I okay with it being a more flexible object that gets refined later?” Once you understand how the compiler interprets your types, the dogma fades away. You begin to make more thoughtful decisions, and your TypeScript code becomes much better.
Peter Kröner: You're moving checkpoints around, basically.
Stefan Baumgartner: Yes.
Peter Kröner: Okay, so that's something to note. First things, don't write annotations if you don't need to. Another thing that I picked up is don't use enums if you don't have to. I mean, if I talk to real developers who do backend code, Java, C Sharp, what have you, they always like their types, their classes, and their enums.
Stefan Baumgartner: Beautiful.
Recommended talk: Why I Was Wrong About TypeScript • TJ VanToll • GOTO 2018
Avoiding enums in Favor of Simpler Alternatives
Peter Kröner: We've talked about not writing too many type annotations and teased that you aren't the greatest fan of classes. Now you're telling me that we can't even use enums.
Stefan Baumgartner: I'm not telling somebody to not use enums. I just want people to be aware of what enums do, especially string enums and number enums. This depends on which TypeScript version you're using.
The problem with enums is that they work on a different level than the rest of the type system. TypeScript has a structural type system, which makes sense given its JavaScript roots. You don't want a nominal type system where you need the exact name for a type check when you're writing object literals without types. You want structural checks to compare objects with some formalization.
TypeScript mostly follows a structural type system, except with enums. String and number enums work in a nominal space, but not entirely. This is where things get nasty.
With a number enum like enum Direction { Up, Down, Left, Right }, each item gets assigned a number (0, 1, 2, 3). When you specify this enum in a function signature, you can use Direction.Up or Direction.Down. This gives you named identifiers for arbitrary numbers.
However, in some TypeScript versions, any number is allowed. You could use 1000 or pi - number enums don't type-check except on numbers. This is because you might use bitmask operations with enums, producing numbers not defined in the enum. TypeScript only ensures it's some enum, not a specific value.
With string enums (like enum Role { Administrator = "administrator", Moderator = "moderator" }), you can't use any other string, not even from other enums. This restriction can prevent you from accessing the actual value that ends up in the output code.
Enums can also bloat your output code tremendously. I consulted on a project with an auto-generated string enum of about 2000 lines. That code can't be removed and creates a huge closure with a complex object.
All they needed was a string union type. You get the same type safety, the same IntelliSense, the same auto-complete features, but with something that can be erased afterward and is compatible with the rest of your type system. You don't need to box it into an enum or type annotations - you can just use the string directly.
If you like the enum development style, you can recreate it with a plain JavaScript object and a simple TypeScript type. You get the same developer experience, but it's better for bundlers and interfaces to other parts of your project. You don't create unexpected code, and you can work with values from outside your application using your internal type system.
Peter Kröner: So this is the part in the book where you have the recipe that turns an object into a plain type.
Stefan Baumgartner: Yes. We have the real object, like a roles object with properties and string values. With “as const” you lock it down to those literal values. Then you have a type that extracts the values from it.
Since TypeScript and JavaScript work on different levels - type level and value level - you can define the same name in both without clashing. If I have a const role in JavaScript, I can have a type role in TypeScript. You get the same type and behavior, but it's clear which part stays in the type system and which in JavaScript code. For JavaScript code, you get much better compatibility with everything else.
Peter Kröner: In this specific case, you're taking a runtime object and getting a type from it, rather than the other way around, which people are probably more used to.
Stefan Baumgartner: If you think about TypeScript being a thin layer around JavaScript, it makes sense. You can pull stuff from JavaScript into TypeScript, but not the other way around.
Peter Kröner: Is it just a layer or another programming language itself? We have derived types from runtime objects, conditionals, generic types that look like functions with freaky syntax. Is this just a layer and type system or something else?
Stefan Baumgartner: Someone figured out that TypeScript's type system is Turing complete. Where does meta programming stop and real programming start? It gets philosophical.
With conditional types and generics, you have a functional programming-like experience, working on sets of possible values and narrowing them through conditions to generate new types. It's immensely powerful, making TypeScript's type system its own programming language. But you don't always need that complexity - it's fine to have just types, which is what TypeScript was made for.
Some people have created incredible things - types that can read SQL statements and create objects with the right properties based on the SQL, string types that implement dictionaries for spell-checking. It's amazing that you can do this, but do you need it in everyday JavaScript projects?
Peter Kröner: I guess it depends on the project. If you're writing something that handles SQL or wants to be an ORM, it's not inconceivable you might want to use these features.
Stefan Baumgartner: It depends on how deep you want to go. In the book, I show examples like deconstructing Express routes to find placeholders and create objects for accessing them in callback functions. It's just five lines of TypeScript code, but the type safety from this small investment is amazing - no more typos or guessing variable names. TypeScript can be really helpful in these cases.
Recommended talk: TypeScript vs KotlinJS • Eamonn Boyle & Garth Gilmour • GOTO 2022
Classes: When to Use Them and When to Avoid Them
Peter Kröner: Speaking of things you enjoy, in Chapter 11 you discuss classes for an entire chapter. From our podcasting sessions, I know classes in TypeScript, especially in your backend work, aren't your favorite language feature.
Stefan Baumgartner: It's tough. I've seen people try to use classes as an excuse for implementing every object-oriented design pattern they can find. TypeScript and JavaScript are expressive enough to allow this, but it's not always the right choice.
For example, I've seen classes that work with environment variables, providing access methods and modifiers. It's basically a JavaScript module with a class containing static methods. You can do that, but it's the wrong choice for what you want to achieve. If you want encapsulation for features connected to a particular domain, an ECMAScript module with functions is all you need. You have encapsulation and everything for that domain in one file.
If you add a class layer with static methods, you end up with similar output but a file incompatible with tree shakers. You carry the entire code whether you use it or not. I've seen projects with 100 classes, each with 10 functions, where they only use a few but carry the whole library because bundlers can't optimize it.
There are valid reasons for classes. If you need many instances of the same thing with operations connected to the data, then use classes. But often, a module with functions is better.
I also discuss which flavor of classes to use. Classes have been a key TypeScript feature from the beginning, with features that differ from ECMAScript classes. For example, TypeScript has multiple visibility modifiers while JavaScript has just one, and they work differently. Use whatever you like, but understand that some features get rendered in JavaScript while others disappear after transpilation.
I cover which class flavor to use, whether classes are appropriate for specific scenarios, and how to work with decorators - comparing TypeScript's original decorators to the new ECMAScript standard. With so many programming styles and variations of classes in JavaScript and TypeScript, you need to understand what's available, the implications, and whether you should use classes at all.
Peter Kröner: That's what the cookbook is for. The final recipe is entitled "Knowing When to Stop," which we can apply to this discussion, but I have one last question. Every time I explain anything, I learn something new. Did you learn anything about TypeScript that you didn't know when you wrote the initial proposal?
Stefan Baumgartner: The features of the TypeScript playground are phenomenal. You can create entire projects and courses within it, load NPM dependencies, define external types. There's a huge development environment hidden in what's basically a simple type checker. That was my biggest surprise.
Peter Kröner: Great. I look forward to your third TypeScript book, which is certainly coming, right?
Stefan Baumgartner: I don't want to spoil anything, but yeah. Thank you, Peter. It was very interesting and fun talking to you.
Peter Kröner: Thank you. Let's stay tuned for this.
About the speakers

Peter Kröner ( interviewer )

Stefan Baumgartner ( author )