content: expand hello-world post with full body
This commit is contained in:
@@ -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>): 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<void>;
|
||||
disconnect(): Promise<void>;
|
||||
query<T>(sql: string, params?: unknown[]): Promise<T[]>;
|
||||
}
|
||||
|
||||
type GetUserById = (id: number) => Promise<{ id: number; name: string } | null>;
|
||||
|
||||
async function createUserClient(dsn: string): Promise<DatabaseClient> {
|
||||
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<T>(sql: string, params: unknown[] = []) {
|
||||
const response = await connection.clone().json();
|
||||
return response.rows as T[];
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const user = createUser({ name: "Alice", email: "alice@example.com" })
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
## 3. Generics — Writing Reusable, Type-Safe Code
|
||||
|
||||
- **Interfaces**: Define object shapes
|
||||
- **Generics**: Create reusable components
|
||||
- **Union types**: Handle multiple possible values
|
||||
- **Type guards**: Narrow types at runtime
|
||||
Generics let you write functions and classes that work with multiple types while preserving type information. They are the backbone of reusable TypeScript libraries.
|
||||
|
||||
> "TypeScript is JavaScript with syntax for types." — TypeScript Handbook
|
||||
```ts
|
||||
// --- Generic function: identity with preserved type ---
|
||||
function identity<T>(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<T extends HasId>(items: T[], predicate: (item: T) => boolean): T | undefined {
|
||||
return items.find(predicate);
|
||||
}
|
||||
|
||||
// --- Generic type with default — useful for API client patterns ---
|
||||
type ApiResponse<TData, TError = string> =
|
||||
| { 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<Map extends Record<string, unknown>> {
|
||||
private handlers = new Map<string, Array<(payload: unknown) => void>>();
|
||||
|
||||
on<K extends keyof Map>(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<K extends keyof Map>(event: K, payload: Map[K]): void {
|
||||
this.handlers.get(event as string)?.forEach((h) => h(payload));
|
||||
}
|
||||
}
|
||||
|
||||
// Usage — type safety for every event
|
||||
const emitter = new TypedEmitter<EventMap>();
|
||||
|
||||
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");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 4. Utility Types — Building Blocks for Complex Schemas
|
||||
|
||||
TypeScript ships with a rich set of built-in utility types that transform existing types. Mastering these is essential for writing maintainable type definitions.
|
||||
|
||||
```ts
|
||||
interface Article {
|
||||
id: number;
|
||||
title: string;
|
||||
body: string;
|
||||
tags: string[];
|
||||
publishedAt: Date;
|
||||
authorId: number;
|
||||
}
|
||||
|
||||
// Partial<T> — make all properties optional
|
||||
type ArticleInput = Partial<Article>;
|
||||
|
||||
const draft: ArticleInput = { title: "Hello World" }; // id, body, etc. are all optional
|
||||
|
||||
// Required<T> — make all properties required
|
||||
function upsertArticle(input: Required<Article>): void {
|
||||
console.log(`Upserting article: ${input.title}`);
|
||||
}
|
||||
|
||||
// Pick<T, K> — select a subset of properties
|
||||
type ArticleSummary = Pick<Article, "id" | "title" | "publishedAt">;
|
||||
|
||||
const summary: ArticleSummary = {
|
||||
id: 1,
|
||||
title: "TypeScript Basics",
|
||||
publishedAt: new Date(),
|
||||
};
|
||||
|
||||
// Omit<T, K> — exclude certain properties
|
||||
type ArticlePreview = Omit<Article, "body">;
|
||||
// Useful: body is too large to send in a list response
|
||||
|
||||
// Record<K, T> — map-like type
|
||||
const statusMessages: Record<Status, string> = {
|
||||
idle: "No request in progress",
|
||||
loading: "Fetching data...",
|
||||
success: "Data loaded",
|
||||
error: "Request failed",
|
||||
};
|
||||
|
||||
// Readonly<T> — make all properties immutable
|
||||
interface Config {
|
||||
host: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
const config: Readonly<Config> = { 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<keyof Article, "id" | "authorId" | "publishedAt">;
|
||||
// NumericFields is "id" | "authorId" — only string keys that are numbers? No, extracted by assignment.
|
||||
|
||||
// Using Exclude to remove unwanted keys
|
||||
type NonNullableFields = Exclude<keyof Article, "body">;
|
||||
```
|
||||
|
||||
## 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<T> =
|
||||
| { status: "pending"; timestamp: number }
|
||||
| { status: "success"; data: T; timestamp: number }
|
||||
| { status: "error"; error: { code: number; message: string }; timestamp: number };
|
||||
|
||||
function renderResult<T>(result: NetworkResult<T>): 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<T>): 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<Record<string, unknown>> {
|
||||
const response = await fetch(url);
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Conditional types — types that depend on other types ---
|
||||
type Flattened<T> = T extends Array<infer U> ? U : T;
|
||||
|
||||
type A = Flattened<number[]>; // number
|
||||
type B = Flattened<string>; // string
|
||||
type C = Flattened<boolean[]>; // boolean
|
||||
|
||||
// --- Conditional with `infer` in multiple positions ---
|
||||
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
|
||||
|
||||
type MaybePromise<T> = T extends Promise<any> | PromiseLike<any>
|
||||
? Promise<Awaited<T>>
|
||||
: Promise<T>;
|
||||
|
||||
// --- 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<T, E = Error> =
|
||||
| { ok: true; value: T }
|
||||
| { ok: false; error: E };
|
||||
|
||||
function safeParseJson(input: string): Result<unknown> {
|
||||
try {
|
||||
return { ok: true, value: JSON.parse(input) };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function chainResults<T, U>(
|
||||
result: Result<T>,
|
||||
fn: (value: T) => Result<U>
|
||||
): Result<U> {
|
||||
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<string, unknown>).name as string,
|
||||
}));
|
||||
|
||||
// --- Custom error classes with named constructors ---
|
||||
class AppError extends Error {
|
||||
constructor(
|
||||
public readonly code: string,
|
||||
message: string,
|
||||
public readonly metadata?: Record<string, unknown>
|
||||
) {
|
||||
super(message);
|
||||
this.name = "AppError";
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends AppError {
|
||||
constructor(
|
||||
field: string,
|
||||
message: string,
|
||||
metadata?: Record<string, unknown>
|
||||
) {
|
||||
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! 🚀
|
||||
|
||||
Reference in New Issue
Block a user