From 42d6c6e653091c703a1c16396740d7afd00f1778 Mon Sep 17 00:00:00 2001 From: Krishna Ayyalasomayajula Date: Mon, 1 Jun 2026 20:56:06 -0500 Subject: [PATCH] content: expand hello-world post with full body --- content/posts/hello-world.mdx | 523 ++++++++++++++++++++++++++++++++-- 1 file changed, 492 insertions(+), 31 deletions(-) diff --git a/content/posts/hello-world.mdx b/content/posts/hello-world.mdx index bc2d55f..f4c9fc5 100644 --- a/content/posts/hello-world.mdx +++ b/content/posts/hello-world.mdx @@ -1,58 +1,519 @@ --- -title: "Hello, World! Building Your First TypeScript App" +title: "Mastering TypeScript: From Basics to Advanced Patterns" date: "2025-01-15" -excerpt: "A beginner-friendly introduction to TypeScript with practical examples and best practices." +excerpt: "A comprehensive TypeScript tutorial covering type system fundamentals, utility types, generics, decorators, and real-world architectural patterns for building robust applications." --- -Welcome to the blog! In this post, we'll explore TypeScript from the ground up. +Welcome to this comprehensive TypeScript tutorial! Whether you are migrating a JavaScript project or starting fresh, this guide will walk you through TypeScript's type system from fundamentals to advanced patterns used in production codebases. -## Why TypeScript? +## 1. Understanding the Type System -TypeScript adds static typing to JavaScript, catching errors at compile time rather than at runtime. Here's a simple example: +TypeScript's type system is a layer of **static analysis** that sits atop JavaScript. The compiler (`tsc`) performs type checking at build time and emits plain JavaScript that runs in any engine. ```ts -function greet(name: string): string { - return `Hello, ${name}!` -} +// Primitive types — the building blocks +let username: string = "alice"; +let age: number = 28; +let isActive: boolean = true; +let tags: string[] = ["typescript", "frontend"]; +let nullable: string | null = null; -console.log(greet("World")) +// Type inference — TypeScript can often guess types for you +const port = 3000; // inferred as `number` +const config = { host: "localhost", port: 8080 }; // inferred as `{ host: string; port: number }` + +// The `unknown` vs `any` distinction — critical for type safety +let value: unknown = 42; +// value.toPrecision(3); // ❌ Error: Property 'toPrecision' does not exist on 'unknown' + +if (typeof value === "number") { + console.log(value.toPrecision(3)); // ✅ Safe after narrowing +} ``` -## Type Safety in Practice +> **Key principle:** Prefer `unknown` over `any`. `any` disables all type checking, while `unknown` forces you to perform a runtime check before using the value. -Let's look at a more complex example with interfaces: +## 2. Interfaces, Types, and Object Shapes + +TypeScript offers two primary ways to define object structures: `interface` and `type`. Understanding when to use each is essential for clean codebases. ```ts -interface User { - id: number - name: string - email: string - role: "admin" | "user" +// --- Interface: best for object contracts (open extension via declaration merging) --- +interface BaseConfig { + debug: boolean; + logLevel: "info" | "warn" | "error"; } -function createUser(data: Partial): User { +// Declaration merging: multiple declarations are combined +interface BaseConfig { + retryCount: number; +} + +// --- Type alias: better for unions, intersections, and mapped types --- +type Status = "idle" | "loading" | "success" | "error"; + +// Intersection types — composing types +type AdminConfig = BaseConfig & { + permissions: string[]; +}; + +// --- Practical example: a fully-typed service layer --- +interface DatabaseClient { + connect(): Promise; + disconnect(): Promise; + query(sql: string, params?: unknown[]): Promise; +} + +type GetUserById = (id: number) => Promise<{ id: number; name: string } | null>; + +async function createUserClient(dsn: string): Promise { + const connection = await fetch(`https://${dsn}/connect`); + if (!connection.ok) throw new Error(`Connection failed: ${connection.status}`); + return { - id: Math.random(), - name: data.name ?? "Anonymous", - email: data.email ?? "", - role: data.role ?? "user", + async connect() { + await connection.json(); + }, + async disconnect() { + await connection.json(); + }, + async query(sql: string, params: unknown[] = []) { + const response = await connection.clone().json(); + return response.rows as T[]; + }, + }; +} +``` + +## 3. Generics — Writing Reusable, Type-Safe Code + +Generics let you write functions and classes that work with multiple types while preserving type information. They are the backbone of reusable TypeScript libraries. + +```ts +// --- Generic function: identity with preserved type --- +function identity(value: T): T { + return value; +} + +const num = identity(42); // num has type `number` +const str = identity("hi"); // str has type `string` + +// --- Constrained generics: limiting what types are accepted --- +interface HasId { + id: number; +} + +function findFirst(items: T[], predicate: (item: T) => boolean): T | undefined { + return items.find(predicate); +} + +// --- Generic type with default — useful for API client patterns --- +type ApiResponse = + | { status: "success"; data: TData } + | { status: "error"; error: TError }; + +type UserApiResponse = ApiResponse<{ id: number; name: string }>; + +// --- Real-world: a typed event emitter --- +type EventMap = { + click: { x: number; y: number }; + keydown: { key: string; code: string }; + load: { url: string }; +}; + +class TypedEmitter> { + private handlers = new Map void>>(); + + on(event: K, handler: (payload: Map[K]) => void): void { + const key = event as string; + if (!this.handlers.has(key)) { + this.handlers.set(key, []); + } + this.handlers.get(key)!.push(handler as (payload: unknown) => void); + } + + emit(event: K, payload: Map[K]): void { + this.handlers.get(event as string)?.forEach((h) => h(payload)); } } -const user = createUser({ name: "Alice", email: "alice@example.com" }) +// Usage — type safety for every event +const emitter = new TypedEmitter(); + +emitter.on("click", (payload) => { + // payload is { x: number; y: number } — auto-completion works! + console.log(`Clicked at ${payload.x}, ${payload.y}`); +}); + +emitter.on("keydown", (payload) => { + // payload is { key: string; code: string } + if (payload.key === "Escape") { + console.log("User pressed Escape"); + } +}); ``` -## Key Concepts +## 4. Utility Types — Building Blocks for Complex Schemas -- **Interfaces**: Define object shapes -- **Generics**: Create reusable components -- **Union types**: Handle multiple possible values -- **Type guards**: Narrow types at runtime +TypeScript ships with a rich set of built-in utility types that transform existing types. Mastering these is essential for writing maintainable type definitions. -> "TypeScript is JavaScript with syntax for types." — TypeScript Handbook +```ts +interface Article { + id: number; + title: string; + body: string; + tags: string[]; + publishedAt: Date; + authorId: number; +} + +// Partial — make all properties optional +type ArticleInput = Partial
; + +const draft: ArticleInput = { title: "Hello World" }; // id, body, etc. are all optional + +// Required — make all properties required +function upsertArticle(input: Required
): void { + console.log(`Upserting article: ${input.title}`); +} + +// Pick — select a subset of properties +type ArticleSummary = Pick; + +const summary: ArticleSummary = { + id: 1, + title: "TypeScript Basics", + publishedAt: new Date(), +}; + +// Omit — exclude certain properties +type ArticlePreview = Omit; +// Useful: body is too large to send in a list response + +// Record — map-like type +const statusMessages: Record = { + idle: "No request in progress", + loading: "Fetching data...", + success: "Data loaded", + error: "Request failed", +}; + +// Readonly — make all properties immutable +interface Config { + host: string; + port: number; +} + +const config: Readonly = { host: "localhost", port: 3000 }; +// config.port = 8080; // ❌ Error: Cannot assign to 'port' because it is a read-only property. + +// --- Advanced: extracting types from values --- +const API_ENDPOINTS = { + users: "/api/users", + articles: "/api/articles", + comments: "/api/comments", +} as const; + +// Type is `"users" | "articles" | "comments"` +type Endpoint = keyof typeof API_ENDPOINTS; + +// Type is `"/api/users" | "/api/articles" | "/api/comments"` +type EndpointUrl = (typeof API_ENDPOINTS)[keyof typeof API_ENDPOINTS]; + +// --- Extract and Exclude type utilities --- +type NumericFields = Extract; +// NumericFields is "id" | "authorId" — only string keys that are numbers? No, extracted by assignment. + +// Using Exclude to remove unwanted keys +type NonNullableFields = Exclude; +``` + +## 5. Type Guards and Runtime Narrowing + +TypeScript narrows types at compile time based on runtime checks. This bridges the gap between static types and dynamic runtime behavior. + +```ts +// --- typeof type guards --- +function formatValue(value: string | number): string { + if (typeof value === "number") { + return value.toFixed(2); + } + return value.toUpperCase(); +} + +// --- in type guard --- +interface Cat { + meow(): void; + furColor: string; +} + +interface Dog { + bark(): void; + breed: string; +} + +function describePet(pet: Cat | Dog): string { + if ("meow" in pet) { + // TypeScript knows pet is Cat here + return `A ${pet.furColor} cat says meow!`; + } + // Here pet is Dog + return `A ${pet.breed} dog says woof!`; +} + +// --- Custom type guard — reusable predicate --- +function isUser(obj: unknown): obj is { id: number; name: string; email: string } { + return ( + typeof obj === "object" && + obj !== null && + "id" in obj && + "name" in obj && + "email" in obj && + typeof (obj as any).id === "number" && + typeof (obj as any).name === "string" && + typeof (obj as any).email === "string" + ); +} + +// --- Discriminated unions — the most powerful narrowing pattern --- +type NetworkResult = + | { status: "pending"; timestamp: number } + | { status: "success"; data: T; timestamp: number } + | { status: "error"; error: { code: number; message: string }; timestamp: number }; + +function renderResult(result: NetworkResult): string { + switch (result.status) { + case "pending": + return `Loading since ${result.timestamp}`; + case "success": + return `Got ${result.data as unknown as string}`; + case "error": + return `Error ${result.error.code}: ${result.error.message}`; + } +} + +// Exhaustiveness checking — catch unhandled cases at compile time +function assertNever(value: never): never { + throw new Error(`Unexpected value: ${JSON.stringify(value)}`); +} + +function exhaustiveCheck(result: NetworkResult): never { + return assertNever(result); // ❌ Compile error if a case is missing! +} +``` + +## 6. Advanced Patterns: Decorators and Conditional Types + +These advanced patterns appear frequently in enterprise TypeScript codebases. Let's explore practical uses for both. + +```ts +// --- Decorators (experimental syntax, widely used in NestJS, Angular) --- +function LogExecution(target: Object, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + + descriptor.value = async function (...args: unknown[]) { + const start = performance.now(); + console.log(`[LOG] ${propertyKey} called with args:`, args); + + try { + const result = await originalMethod.apply(this, args); + const elapsed = performance.now() - start; + console.log(`[LOG] ${propertyKey} completed in ${elapsed.toFixed(2)}ms`); + return result; + } catch (error) { + console.error(`[LOG] ${propertyKey} threw:`, error); + throw error; + } + }; + + return descriptor; +} + +class DataService { + @LogExecution + async fetchData(url: string): Promise> { + const response = await fetch(url); + return response.json(); + } +} + +// --- Conditional types — types that depend on other types --- +type Flattened = T extends Array ? U : T; + +type A = Flattened; // number +type B = Flattened; // string +type C = Flattened; // boolean + +// --- Conditional with `infer` in multiple positions --- +type UnwrapPromise = T extends Promise ? U : T; + +type MaybePromise = T extends Promise | PromiseLike + ? Promise> + : Promise; + +// --- Brand types for runtime type discrimination --- +interface UserId { + _tag: "UserId"; + value: number; +} + +interface ArticleId { + _tag: "ArticleId"; + value: number; +} + +// These are NOT interchangeable despite both being `{ value: number }` +const userId: UserId = { _tag: "UserId", value: 42 }; +// const articleId: ArticleId = userId; // ❌ Error! + +// Factory functions with branded return types +function createUserId(id: number): UserId { + if (id <= 0) throw new Error("Invalid user ID"); + return { _tag: "UserId", value: id }; +} + +function createArticleId(id: number): ArticleId { + if (id <= 0) throw new Error("Invalid article ID"); + return { _tag: "ArticleId", value: id }; +} +``` + +## 7. Error Handling Best Practices + +A robust error handling strategy improves debugging and makes failures explicit in the type system. + +```ts +// --- Result type pattern — explicit error handling without exceptions --- +type Result = + | { ok: true; value: T } + | { ok: false; error: E }; + +function safeParseJson(input: string): Result { + try { + return { ok: true, value: JSON.parse(input) }; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error : new Error(String(error)), + }; + } +} + +function chainResults( + result: Result, + fn: (value: T) => Result +): Result { + if (!result.ok) return { ok: false, error: result.error }; + return fn(result.value); +} + +// Usage — no try/catch needed at call sites +const jsonResult = safeParseJson('{"name":"Alice"}'); +const parsed = chainResults(jsonResult, (value) => ({ + ok: true, + value: (value as Record).name as string, +})); + +// --- Custom error classes with named constructors --- +class AppError extends Error { + constructor( + public readonly code: string, + message: string, + public readonly metadata?: Record + ) { + super(message); + this.name = "AppError"; + } +} + +class ValidationError extends AppError { + constructor( + field: string, + message: string, + metadata?: Record + ) { + super("VALIDATION_ERROR", `${field}: ${message}`, { field, ...metadata }); + this.name = "ValidationError"; + } +} + +class NotFoundError extends AppError { + constructor(resource: string, id: string) { + super("NOT_FOUND", `${resource} with id "${id}" not found`, { resource, id }); + this.name = "NotFoundError"; + } +} + +// --- Centralized error handler --- +function handleError(error: unknown): { status: number; message: string } { + if (error instanceof AppError) { + switch (error.code) { + case "VALIDATION_ERROR": + return { status: 400, message: error.message }; + case "NOT_FOUND": + return { status: 404, message: error.message }; + default: + return { status: 500, message: "An unexpected error occurred" }; + } + } + + if (error instanceof Error) { + return { status: 500, message: error.message }; + } + + return { status: 500, message: "Unknown error" }; +} +``` + +## 8. Configuration and Build Setup + +A proper TypeScript project needs thoughtful configuration. Here is a production-ready `tsconfig.json` pattern: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} +``` + +Key flags explained: +1. `strict: true` — enables all strict type checking options at once +2. `noImplicitAny: true` — disallows implicit `any` types +3. `strictNullChecks: true` — `null` and `undefined` are distinct types +4. `noUnusedLocals` / `noUnusedParameters` — catch dead code at compile time +5. `declaration: true` — emits `.d.ts` files for library consumers ## Next Steps -Check out our other posts for deep dives into: -- Mathematics with KaTeX -- Hybrid content with code and equations +You now have a solid foundation in TypeScript's type system. To continue your journey, here are some recommended directions: + +- **Deep dive into generics** — explore higher-kinded types and type-level programming +- **Learn about module resolution** — understand `--moduleResolution` strategies (`NodeNext` vs `Node16` vs `Classic`) +- **Explore testing with TypeScript** — type-safe assertions with libraries like `vitest` and `tsd` +- **Study architectural patterns** — dependency injection, CQRS, and hexagonal architecture in TypeScript +- **Contribute to type definitions** — help maintain `@types/*` packages on DefinitelyTyped + +TypeScript is a journey, not a destination. The type system rewards patience and pays dividends in developer confidence and reduced bug rates. Start small, enforce `strict` mode early, and let the compiler be your guide. + +Happy coding! 🚀