In TypeScript, every variable, every function parameter, every return value carries a type. When those types are wrong, you get bugs that surface at the worst possible moment. Runtime crashes, silent data corruption, and hours spent debugging issues that the compiler could have caught before you ever hit save.
The problem is that you haven't built the mental model for how its type system actually works. You annotate a few variables, sprinkle in some any when things get frustrating, and move on, never realizing you've turned off the very safety net that makes TypeScript worth using.
The Type Safety Workshop takes you deep into TypeScript's type system. You'll learn how to model your data precisely with type aliases, union types, and generics, then use that precision to catch entire categories of bugs at compile time instead of in production.
In this workshop, you'll work through real patterns like discriminated unions for API responses, type guards for runtime narrowing, and as const for deriving types from your source code, the same techniques used in production TypeScript codebases every day.
Type Aliases
Every time you duplicate a type annotation across multiple variables or functions, you're creating a maintenance problem. Change the shape in one place, forget the other, and you've got a type mismatch that might not surface for weeks. Type aliases let you define a type once and reference it everywhere.
In this section, you'll create reusable types and learn how to compose them from smaller building blocks:
- Creating type aliases for objects with
type User = { id: string; name: string } - Using
typeofto derive types from existing runtime values - Composing types from primitive aliases like
ID,Timestamp, andEmailto communicate intent - Using
Recordto type objects with dynamic string keys - Choosing between explicit type definitions and inference with
typeof
Union Types
Real application data is rarely just one thing. An API response might be a success object or an error. A user ID might be a string or a number. Without union types, you're forced to use any or write separate functions for every possible shape-- neither of which scales.
This section covers one of TypeScript's most powerful features and the patterns that make it practical:
- Defining types that accept multiple shapes with the
|operator - Narrowing unions with
typeofandinstanceofchecks - Using the
inoperator to distinguish between object shapes - Writing custom type guard functions with the
value is Typesyntax - Building discriminated unions with a shared status or type property
- Exhaustive checking with
switchstatements and thenevertype
Literal Types
TypeScript infers const x = "hello" as the literal type "hello", but let y = "hello" becomes just string. That distinction matters more than you'd think-- especially when you want your types to reflect the exact values in your code rather than broad categories.
Here you'll learn how to preserve and leverage literal types in your programs:
- Using
as constto preserve literal types on objects and arrays - Deriving union types from object keys with
keyof typeof - Extracting value types from constant objects for precise unions
- Building a single source of truth where types flow from runtime values
- Practical patterns like typed route maps and discriminated return types
Intersection Types
Union types let a value be one of several shapes. Intersection types go the other direction-- combining multiple types into one that has all their properties. It's the type-level equivalent of spreading objects together.
This section teaches you how to compose types from reusable pieces:
- Combining types with the
&operator to build composite shapes - Creating reusable building blocks like
WithID,WithTimestamps, andWithAuthor - Assembling entity types (
User,Post,Comment) from shared base types - Understanding how intersection differs from union in practice
Any vs Unknown
When TypeScript gets in your way, it's tempting to reach for any and move on. The problem is that any doesn't just silence one error-- it turns off type checking for everything that value touches. It's an escape hatch that defeats the purpose of using TypeScript in the first place.
This section shows you the safer alternative and when each escape hatch is appropriate:
- Understanding why
anydisables all type checking on a value - Using
unknownas a type-safe alternative that requires narrowing before use - Narrowing
unknownvalues withtypeofchecks and error throwing - Processing values of different types through runtime checks
- When you'll encounter
anyin library code and why it exists for backward compatibility
Generics
Without generics, you'd need a separate function for every type you want to support-- firstNumber, firstString, firstUser-- or you'd give up and use any. Generics let you write a single function that works with any type while preserving full type safety through the entire call chain.
This section takes you from basic generic syntax through real-world patterns with constraints:
- Writing generic functions with type parameters (
<Value>) - Letting TypeScript infer generic types from function arguments
- Creating generic types for reusable patterns like loading states
- Constraining generics with
extendsto require specific shapes - Building a type-safe
getPropertyfunction usingkeyofconstraints - Merging objects with fully typed intersection return values
Interfaces
Interfaces look almost identical to type aliases-- and for most purposes, they behave the same way. So why do they exist? Historical reasons, mostly. But interfaces have one unique capability that types can't replicate: declaration merging.
Here you'll learn when interfaces matter and when to stick with types:
- Defining object shapes with
interfacesyntax - Extending interfaces to build up from base types
- Understanding declaration merging and how interfaces combine across modules
- Augmenting global interfaces for plugin systems and library configuration
- Why types are preferred for day-to-day code and when interfaces earn their place
Enums
Enums give you a way to define a set of named constants-- OrderStatus.Pending, OrderStatus.Active, OrderStatus.Completed. They're one of the few TypeScript features that generate actual runtime code, which makes them unique in the type system and controversial in the community.
In this section, you'll learn how enums work and why union types are often the better choice:
- Creating string enums for order statuses and log levels
- Using enums in switch statements and type-safe functions
- Understanding the runtime code that enums generate after compilation
- Migrating from enums to union types for simpler, cleaner code
- Comparing the developer experience of enums vs. unions for autocomplete and type safety

