Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
TypeScript Fundamentals
TypeScript adds a static type system on top of JavaScript. Understanding types is the foundation of everything else in TypeScript. If you skip this chapter, nothing else will make sense — types are to TypeScript what notes are to music theory.How TypeScript Works: Transpilation
TypeScript is not directly executed. Browsers and Node.js only understand JavaScript. TypeScript must be transpiled (compiled) to JavaScript before it can run. Think of TypeScript as writing in a markup language (like Markdown) — the final consumer (the browser) never sees the markup, only the rendered output (JavaScript). The types are scaffolding that helps you build correctly, then gets removed from the finished building.The TypeScript Compilation Pipeline
| Stage | What Happens | Can You See It? |
|---|---|---|
| Lexing | Source code is broken into tokens | Internal |
| Parsing | Tokens become an Abstract Syntax Tree (AST) | ts.createSourceFile() |
| Type Checking | Types are validated, errors reported | Your IDE red squiggles! |
| Emitting | AST is transformed to JavaScript | The .js files |
Type Erasure
Here is the key insight that many beginners miss: Types exist only at compile time. They are completely erased in the output JavaScript. This means TypeScript has zero runtime cost — no performance penalty, no extra bytes shipped to users. But it also means you cannot use TypeScript types for runtime decisions (more on that below).The TypeScript Compiler (tsc)
Thetsc command is the TypeScript compiler:
Compile Time vs Runtime Errors
| Error Type | When Caught | Example |
|---|---|---|
| Compile-time | Before code runs | Type mismatch, missing properties |
| Runtime | While code runs | Network failures, user input |
1. Type Annotations
Type annotations explicitly declare the type of a variable, parameter, or return value.Why Annotate?
2. Type Inference
TypeScript is smart. It can infer types from the value you assign — meaning you often do not need to write types at all. The compiler looks at the right-hand side of the assignment and figures out the type automatically. This is what makes TypeScript feel lightweight instead of verbose like Java.When to Annotate vs Infer
3. Primitive Types
TypeScript has the same primitive types as JavaScript, plus a few extras.Basic Primitives
Special Types
4. Arrays
Arrays can be typed in two ways:Array Methods with Types
5. Tuples
Tuples are fixed-length arrays with specific types at each position. While a regular array says “this is a list of strings,” a tuple says “this is exactly a string, then a number, then a boolean, in that order.” They are useful for representing structured data without creating a full interface — like function return values that pack multiple pieces of information together.Labeled Tuples (TS 4.0+)
Optional Tuple Elements
6. Enums
Enums define a set of named constants.Numeric Enums
String Enums
const Enums (Inlined at compile time)
7. Object Types
Define the shape of objects with inline types or type aliases.Inline Object Types
Type Aliases
Create reusable type definitions.8. Union Types
A value can be one of several types.Narrowing Union Types
Literal Types
9. Type Assertions
Tell TypeScript you know better about the type. Assertions do not change the runtime value — they only override the compiler’s type analysis. Think of it as telling the compiler “trust me, I know what this is” rather than actually converting anything.Non-null Assertion
10. Type Narrowing
Type narrowing is how you go from a broad type to a specific one inside a code block. TypeScript’s control flow analysis tracks which branches you have taken and automatically narrows the type — this is one of the most powerful and practical features of the type system.typeof Guard
Truthiness Narrowing
instanceof Guard
in Operator
Summary
| Concept | Example |
|---|---|
| Type Annotation | let name: string = 'Alice' |
| Type Inference | let age = 25 (inferred as number) |
| Arrays | let nums: number[] = [1, 2, 3] |
| Tuples | let pair: [string, number] = ['a', 1] |
| Union Types | let id: string | number |
| Literal Types | type Status = 'on' | 'off' |
| Type Aliases | type User = { name: string } |
| Type Assertions | value as string |
| Type Guards | typeof, instanceof, in |
Interview Deep-Dive
Q: Explain the difference between 'any', 'unknown', and 'never' in TypeScript. When would you use each, and what are the dangers of each?
Q: Explain the difference between 'any', 'unknown', and 'never' in TypeScript. When would you use each, and what are the dangers of each?
Strong Answer:These three types represent the extremes of TypeScript’s type system, and confusing them is a common interview red flag.
any: The escape hatch. It disables all type checking for that value — you can call any method, access any property, assign it to anything. Think of it as telling the compiler “stop looking at this.” It is dangerous because errors that TypeScript would normally catch at compile time become runtime exceptions. The only legitimate uses are: migrating a large JavaScript codebase incrementally (temporarily marking untyped code asany), and interacting with truly dynamic third-party code where writing types is impractical. In production TypeScript, havinganyin your codebase is technical debt.unknown: The type-safe alternative toany. It says “this value could be anything, but you must prove what it is before you use it.” You cannot access properties, call methods, or assignunknownto a typed variable without narrowing first (viatypeof,instanceof, or a custom type guard). This is the correct type for external data: API responses,JSON.parse()output, user input,catchblock errors. You accept the data asunknown, validate its shape, and only then work with it as a typed value.never: Represents values that can never occur. A function that always throws returnsnever. A variable in thedefaultbranch of an exhaustive switch isnever(if you handled all cases, no value can reach here). The most powerful use ofneveris exhaustiveness checking:const _exhaustive: never = shape;in a switch default forces a compile error if you add a new union variant without handling it.
any is “I do not care about types,” unknown is “I do not know the type yet but I will find out,” and never is “this should be impossible.”Follow-up: Show me how ‘never’ is used for exhaustiveness checking in a discriminated union.Consider type Action = { type: 'add' } | { type: 'delete' } | { type: 'update' } and a switch on action.type. If you handle all three cases, the default branch’s action is narrowed to never — no value can reach it. Assigning const _: never = action compiles fine. Now if someone adds { type: 'archive' } to the union but forgets to add a case, the default branch sees action as { type: 'archive' }, which is not assignable to never, and the compiler errors immediately. This turns a potential runtime bug into a compile-time error, which is exactly the value proposition of TypeScript.Q: What is type erasure and why does it mean you cannot use TypeScript interfaces for runtime type checking?
Q: What is type erasure and why does it mean you cannot use TypeScript interfaces for runtime type checking?
Strong Answer:Type erasure is the fundamental architectural decision of TypeScript: all type annotations, interfaces, type aliases, and generic type parameters are removed during compilation. The output JavaScript has zero traces of the type system.
- What this means:
interface User { name: string; email: string }does not exist at runtime. There is noUserobject, noUserclass, noUsersymbol in the compiled JavaScript. If you writeif (x instanceof User), it is a compile error becauseUseris not a value — it is a type that was erased. - Why this matters: You cannot validate incoming data against a TypeScript interface at runtime.
JSON.parse(apiResponse)returnsany(orunknownif you are careful), and no amount of TypeScript types can verify that the parsed object actually matches yourUserinterface. The JSON could contain{ name: 42, email: null }and TypeScript would not catch it because the type system is gone at runtime. - The solution: Runtime validation libraries (Zod, io-ts, Yup) or custom type guard functions. Zod, for example, lets you define a schema that serves as both a runtime validator and a TypeScript type:
const UserSchema = z.object({ name: z.string(), email: z.string().email() }); type User = z.infer<typeof UserSchema>;. NowUserSchema.parse(data)validates at runtime, andUserprovides compile-time types — single source of truth. - Classes are the exception: TypeScript classes compile to JavaScript classes, so
instanceofworks on classes. This is why some teams prefer classes over interfaces for domain entities that need runtime validation.
function identity<T>(value: T): T compiles to function identity(value) { return value; } — there is no way to know at runtime what T was. You cannot write if (T === string) inside the function because T does not exist at runtime. If you need different behavior based on the type, you must pass a runtime discriminator (an explicit string tag, a class constructor, or a type guard function) alongside the value.Q: When should you rely on type inference versus writing explicit type annotations? What is the professional rule of thumb?
Q: When should you rely on type inference versus writing explicit type annotations? What is the professional rule of thumb?
Strong Answer:The rule I follow is: let TypeScript infer when the type is obvious from the assignment, annotate when it is not obvious or when you are defining a contract boundary.
- Infer for local variables:
const name = 'Alice'— the typestringis obvious. Adding: stringis visual noise that makes the code harder to scan. Same forconst users = [{ id: 1, name: 'Alice' }]— TypeScript infers the full array type correctly. - Annotate function parameters always:
function greet(name)givesnamethe typeany(with strict mode, it is an error). Function parameters are contract boundaries — the caller needs to know what to pass.function greet(name: string)is a contract. - Annotate function return types for public APIs: For exported functions, library functions, or anything that forms a module boundary, explicit return types serve as documentation and prevent accidental type changes. If you refactor the function body and accidentally change the return type, an explicit annotation catches it. For internal helper functions, return type inference is usually fine.
- Annotate when inference gets it wrong:
const status = 'active'infers the literal type'active', notstring. If you wantstring, you annotate. Conversely,let status = 'active'infersstring(becauseletcan be reassigned). If you want the literal type, useas constor annotate. - Annotate empty initializations:
const items = []infersany[]. You must annotate:const items: string[] = [].
const declarations and object literals. const config = { timeout: 3000 } infers { timeout: number }, which is fine. But const method = 'GET' infers the literal type 'GET', not string. If you pass it to a function expecting string, it works (literals are subtypes of string). But if a function returns a string and you compare it to method, TypeScript may warn about comparing string to 'GET'. The tools are: as const to make types narrower (literal), explicit annotation to make types wider (general), and satisfies (TypeScript 4.9+) to validate a type without widening it.Q: Explain how TypeScript's control flow narrowing works. What happens when you write an 'if (typeof x === string)' check?
Q: Explain how TypeScript's control flow narrowing works. What happens when you write an 'if (typeof x === string)' check?
Strong Answer:Control flow narrowing is one of TypeScript’s most sophisticated features, and it is what makes union types practical rather than painful.
- The mechanism: TypeScript’s type checker tracks the type of every variable through every branch of your code. When you write
if (typeof x === 'string'), TypeScript narrows the type ofxtostringinside theifblock and to “whatever was left” in theelseblock. Ifxwasstring | number, it becomesstringin theifandnumberin theelse. - What triggers narrowing:
typeofchecks,instanceofchecks,inoperator ('swim' in animal), equality checks (x === null), truthiness checks (if (x)), and custom type guards (function isFish(x): x is Fish). TypeScript also narrows on assignment: if you assign a string to astring | numbervariable, the type narrows tostringafter the assignment. - Control flow analysis: TypeScript follows the actual control flow, not just the immediate block. If you check
if (x === null) return;, TypeScript knows that after the return,xis not null for the rest of the function. This works withthrow,return,break, andcontinue— any statement that makes a code path unreachable narrows the types for the remaining paths. - The limitation: Narrowing is per-reference, not per-value. If you narrow
obj.valuetostringinside anifblock, TypeScript narrows it for that block. But if you call a function in between that could mutateobj, TypeScript may widen the type back because it cannot guarantee the function did not changeobj.value. This is why storing narrowed values in local constants (const val = obj.value; if (typeof val === 'string') ...) is a common pattern — local constants cannot be mutated by external calls.
function isUser(data: unknown): data is User. When this function returns true, TypeScript narrows the argument to User in the calling scope. The advantage over inline checks is reusability and encapsulation. Validating that an API response is a User requires checking multiple properties (typeof data.name === 'string' && typeof data.id === 'number' && ...). Inlining that check everywhere is verbose and error-prone. Encapsulating it in isUser() means you write the validation once, test it once, and use it everywhere. TypeScript propagates the narrowing wherever you call it.