diff --git a/docs/README.skills.md b/docs/README.skills.md index c5d3fdc11..5064196c4 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -218,6 +218,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [terraform-azurerm-set-diff-analyzer](../skills/terraform-azurerm-set-diff-analyzer/SKILL.md) | Analyze Terraform plan JSON output for AzureRM Provider to distinguish between false-positive diffs (order-only changes in Set-type attributes) and actual resource changes. Use when reviewing terraform plan output for Azure resources like Application Gateway, Load Balancer, Firewall, Front Door, NSG, and other resources with Set-type attributes that cause spurious diffs due to internal ordering changes. | `references/azurerm_set_attributes.json`
`references/azurerm_set_attributes.md`
`scripts/.gitignore`
`scripts/README.md`
`scripts/analyze_plan.py` | | [tldr-prompt](../skills/tldr-prompt/SKILL.md) | Create tldr summaries for GitHub Copilot files (prompts, agents, instructions, collections), MCP servers, or documentation from URLs and queries. | None | | [transloadit-media-processing](../skills/transloadit-media-processing/SKILL.md) | Process media files (video, audio, images, documents) using Transloadit. Use when asked to encode video to HLS/MP4, generate thumbnails, resize or watermark images, extract audio, concatenate clips, add subtitles, OCR documents, or run any media processing pipeline. Covers 86+ processing robots for file transformation at scale. | None | +| [typescript-coder](../skills/typescript-coder/SKILL.md) | Expert 10x engineer with extensive knowledge of TypeScript fundamentals, migration strategies, and best practices. Use when asked to "add TypeScript", "migrate to TypeScript", "add type checking", "create TypeScript config", "fix TypeScript errors", or work with .ts/.tsx files. Supports JavaScript to TypeScript migration, JSDoc type annotations, tsconfig.json configuration, and type-safe code patterns. | `references/basics.md`
`references/cheatsheet.md`
`references/classes.md`
`references/elements.md`
`references/essentials.md`
`references/keywords.md`
`references/miscellaneous.md`
`references/types.md` | | [typescript-mcp-server-generator](../skills/typescript-mcp-server-generator/SKILL.md) | Generate a complete MCP server project in TypeScript with tools, resources, and proper configuration | None | | [typespec-api-operations](../skills/typespec-api-operations/SKILL.md) | Add GET, POST, PATCH, and DELETE operations to a TypeSpec API plugin with proper routing, parameters, and adaptive cards | None | | [typespec-create-agent](../skills/typespec-create-agent/SKILL.md) | Generate a complete TypeSpec declarative agent with instructions, capabilities, and conversation starters for Microsoft 365 Copilot | None | diff --git a/skills/typescript-coder/SKILL.md b/skills/typescript-coder/SKILL.md new file mode 100644 index 000000000..e71c2be6d --- /dev/null +++ b/skills/typescript-coder/SKILL.md @@ -0,0 +1,798 @@ +--- +name: typescript-coder +description: 'Expert 10x engineer with extensive knowledge of TypeScript fundamentals, migration strategies, and best practices. Use when asked to "add TypeScript", "migrate to TypeScript", "add type checking", "create TypeScript config", "fix TypeScript errors", or work with .ts/.tsx files. Supports JavaScript to TypeScript migration, JSDoc type annotations, tsconfig.json configuration, and type-safe code patterns.' +--- + +# TypeScript Coder Skill + +Master TypeScript development with expert-level knowledge of type systems, migration strategies, and modern JavaScript/TypeScript patterns. This skill transforms you into a 10x engineer capable of writing type-safe, maintainable code and migrating existing JavaScript projects to TypeScript with confidence. + +## When to Use This Skill + +- User asks to "add TypeScript", "migrate to TypeScript", or "convert to TypeScript" +- Need to "add type checking" or "fix TypeScript errors" in a project +- Creating or configuring `tsconfig.json` for a project +- Working with `.ts`, `.tsx`, `.mts`, or `.d.ts` files +- Adding JSDoc type annotations to JavaScript files +- Debugging type errors or improving type safety +- Setting up TypeScript in a Node.js or JavaScript project +- Creating type definitions or ambient declarations +- Implementing advanced TypeScript patterns (generics, conditional types, mapped types) + +## Prerequisites + +- Basic understanding of JavaScript (ES6+) +- Node.js and npm/yarn installed (for TypeScript compilation) +- Familiarity with the project structure and build tools +- Access to the `typescript` package (can be installed if needed) + +## Shorthand Keywords + +Keywords to trigger this skill as if using a command-line tool: + +```javascript +openPrompt = ["typescript-coder", "ts-coder"] +``` + +Use these shorthand commands to quickly invoke TypeScript expertise without lengthy explanations. For example: + +- `typescript-coder --check "this code"` +- `typescript-coder check this type guard` +- `ts-coder migrate this file` +- `ts-coder --migrate project-to-typescript` + +## Role and Expertise + +As a TypeScript expert, you operate with: + +- **Deep Type System Knowledge**: Understanding of TypeScript's structural type system, generics, and advanced types +- **Migration Expertise**: Proven strategies for incremental JavaScript to TypeScript migration +- **Best Practices**: Knowledge of TypeScript patterns, conventions, and anti-patterns +- **Tooling Mastery**: Configuration of TypeScript compiler, build tools, and IDE integration +- **Problem Solving**: Ability to resolve complex type errors and design type-safe architectures + +## Core TypeScript Concepts + +### The TypeScript Type System + +TypeScript uses **structural typing** (duck typing) rather than nominal typing: + +```typescript +interface Point { + x: number; + y: number; +} + +// This works because the object has the right structure +const point: Point = { x: 10, y: 20 }; + +// This also works - structural compatibility +const point3D = { x: 1, y: 2, z: 3 }; +const point2D: Point = point3D; // OK - has x and y +``` + +### Type Inference + +TypeScript infers types when possible, reducing boilerplate: + +```typescript +// Type inferred as string +const message = "Hello, TypeScript!"; + +// Type inferred as number +const count = 42; + +// Type inferred as string[] +const names = ["Alice", "Bob", "Charlie"]; + +// Return type inferred as number +function add(a: number, b: number) { + return a + b; // Returns number +} +``` + +### Key TypeScript Features + +| Feature | Purpose | When to Use | +|---------|---------|-------------| +| **Interfaces** | Define object shapes | Defining data structures, API contracts | +| **Type Aliases** | Create custom types | Union types, complex types, type utilities | +| **Generics** | Type-safe reusable components | Functions/classes that work with multiple types | +| **Enums** | Named constants | Fixed set of related values | +| **Type Guards** | Runtime type checking | Narrowing union types safely | +| **Utility Types** | Transform types | `Partial`, `Pick`, `Omit`, etc. | + +## Step-by-Step Workflows + +### Task 1: Install and Configure TypeScript + +For a new or existing JavaScript project: + +1. **Install TypeScript as a dev dependency**: + ```bash + npm install --save-dev typescript + ``` + +2. **Install type definitions for Node.js** (if using Node.js): + ```bash + npm install --save-dev @types/node + ``` + +3. **Initialize TypeScript configuration**: + ```bash + npx tsc --init + ``` + +4. **Configure `tsconfig.json`** for your project: + ```json + { + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] + } + ``` + +5. **Add build script to `package.json`**: + ```json + { + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "check": "tsc --noEmit" + } + } + ``` + +### Task 2: Migrate JavaScript to TypeScript (Incremental Approach) + +Safe, incremental migration strategy: + +1. **Enable TypeScript to process JavaScript files**: + ```json + { + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "noEmit": true + } + } + ``` + +2. **Add JSDoc type annotations to JavaScript files** (optional intermediate step): + ```javascript + // @ts-check + + /** + * Calculates the sum of two numbers + * @param {number} a - First number + * @param {number} b - Second number + * @returns {number} The sum + */ + function add(a, b) { + return a + b; + } + + /** @type {string[]} */ + const names = ["Alice", "Bob"]; + + /** @typedef {{ id: number, name: string, email?: string }} User */ + + /** @type {User} */ + const user = { + id: 1, + name: "Alice" + }; + ``` + +3. **Rename files incrementally** from `.js` to `.ts`: + ```bash + # Start with utility files and leaf modules + mv src/utils/helpers.js src/utils/helpers.ts + ``` + +4. **Fix TypeScript errors in converted files**: + - Add explicit type annotations where inference fails + - Define interfaces for complex objects + - Handle `any` types appropriately + - Add type guards for runtime checks + +5. **Gradually convert remaining files**: + - Start with utilities and shared modules + - Move to leaf components (no dependencies) + - Finally convert orchestration/entry files + +6. **Enable strict mode progressively**: + ```json + { + "compilerOptions": { + "strict": false, + "noImplicitAny": true, + "strictNullChecks": true + // Enable other strict flags one at a time + } + } + ``` + +### Task 3: Define Types and Interfaces + +Creating robust type definitions: + +1. **Define interfaces for data structures**: + ```typescript + // User data model + interface User { + id: number; + name: string; + email: string; + age?: number; // Optional property + readonly createdAt: Date; // Read-only property + } + + // API response structure + interface ApiResponse { + success: boolean; + data?: T; + error?: { + code: string; + message: string; + }; + } + ``` + +2. **Use type aliases for complex types**: + ```typescript + // Union type + type Status = 'pending' | 'active' | 'completed' | 'failed'; + + // Intersection type + type Employee = User & { + employeeId: string; + department: string; + salary: number; + }; + + // Function type + type TransformFn = (input: T) => U; + + // Conditional type + type NonNullable = T extends null | undefined ? never : T; + ``` + +3. **Create type definitions in `.d.ts` files** for external modules: + ```typescript + // types/custom-module.d.ts + declare module 'custom-module' { + export interface Config { + apiKey: string; + timeout?: number; + } + + export function initialize(config: Config): Promise; + export function fetchData(endpoint: string): Promise; + } + ``` + +### Task 4: Work with Generics + +Type-safe reusable components: + +1. **Generic functions**: + ```typescript + // Basic generic function + function identity(value: T): T { + return value; + } + + const num = identity(42); // Type: number + const str = identity("hello"); // Type: string + + // Generic with constraints + function getProperty(obj: T, key: K): T[K] { + return obj[key]; + } + + const user = { name: "Alice", age: 30 }; + const name = getProperty(user, "name"); // Type: string + const age = getProperty(user, "age"); // Type: number + ``` + +2. **Generic classes**: + ```typescript + class DataStore { + private items: T[] = []; + + add(item: T): void { + this.items.push(item); + } + + get(index: number): T | undefined { + return this.items[index]; + } + + filter(predicate: (item: T) => boolean): T[] { + return this.items.filter(predicate); + } + } + + const numberStore = new DataStore(); + numberStore.add(42); + + const userStore = new DataStore(); + userStore.add({ id: 1, name: "Alice", email: "alice@example.com" }); + ``` + +3. **Generic interfaces**: + ```typescript + interface Repository { + findById(id: string): Promise; + findAll(): Promise; + create(item: Omit): Promise; + update(id: string, item: Partial): Promise; + delete(id: string): Promise; + } + + class UserRepository implements Repository { + async findById(id: string): Promise { + // Implementation + return null; + } + // ... other methods + } + ``` + +### Task 5: Handle Type Errors + +Common type errors and solutions: + +1. **"Property does not exist" errors**: + ```typescript + // ❌ Error: Property 'name' does not exist on type '{}' + const user = {}; + user.name = "Alice"; + + // ✅ Solution 1: Define interface + interface User { + name: string; + } + const user: User = { name: "Alice" }; + + // ✅ Solution 2: Type assertion (use cautiously) + const user = {} as User; + user.name = "Alice"; + + // ✅ Solution 3: Index signature + interface DynamicObject { + [key: string]: any; + } + const user: DynamicObject = {}; + user.name = "Alice"; + ``` + +2. **"Cannot find name" errors**: + ```typescript + // ❌ Error: Cannot find name 'process' + const env = process.env.NODE_ENV; + + // ✅ Solution: Install type definitions + // npm install --save-dev @types/node + const env = process.env.NODE_ENV; // Now works + ``` + +3. **`any` type issues**: + ```typescript + // ❌ Implicit any (with noImplicitAny: true) + function process(data) { + return data.value; + } + + // ✅ Solution: Add explicit types + function process(data: { value: number }): number { + return data.value; + } + + // ✅ Or use generic + function process(data: T): T { + return data; + } + ``` + +4. **Union type narrowing**: + ```typescript + function processValue(value: string | number) { + // ❌ Error: Property 'toUpperCase' does not exist on type 'string | number' + return value.toUpperCase(); + + // ✅ Solution: Type guard + if (typeof value === "string") { + return value.toUpperCase(); // TypeScript knows it's string here + } + return value.toString(); + } + ``` + +### Task 6: Configure for Specific Environments + +Environment-specific configurations: + +#### Node.js Project + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "node", + "esModuleInterop": true + } +} +``` + +#### Browser/DOM Project + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "esnext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", + "resolveJsonModule": true, + "noEmit": true + } +} +``` + +#### Library/Package + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "esnext", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + } +} +``` + +## TypeScript Best Practices + +### Do's + +- ✅ **Enable strict mode** (`"strict": true`) for maximum type safety +- ✅ **Use type inference** - let TypeScript infer types when it's obvious +- ✅ **Prefer interfaces over type aliases** for object shapes (better error messages) +- ✅ **Use `unknown` instead of `any`** - forces type checking before use +- ✅ **Create utility types** for common transformations +- ✅ **Use const assertions** (`as const`) for literal types +- ✅ **Leverage type guards** for runtime type checking +- ✅ **Document complex types** with JSDoc comments +- ✅ **Use discriminated unions** for type-safe state management +- ✅ **Keep types DRY** - extract and reuse type definitions + +### Don'ts + +- ❌ **Don't use `any` everywhere** - defeats the purpose of TypeScript +- ❌ **Don't ignore TypeScript errors** with `@ts-ignore` without good reason +- ❌ **Don't over-complicate types** - balance safety with readability +- ❌ **Don't use type assertions excessively** - indicates design issues +- ❌ **Don't duplicate type definitions** - use shared types +- ❌ **Don't forget null/undefined checks** - enable `strictNullChecks` +- ❌ **Don't use enums for everything** - consider union types instead +- ❌ **Don't skip type definitions for external libraries** - install `@types/*` +- ❌ **Don't disable strict flags without justification** +- ❌ **Don't mix JavaScript and TypeScript in production** - complete the migration + +## Common Patterns + +### Pattern 1: Discriminated Unions + +Type-safe state management: + +```typescript +type LoadingState = { status: 'loading' }; +type SuccessState = { status: 'success'; data: T }; +type ErrorState = { status: 'error'; error: Error }; + +type AsyncState = LoadingState | SuccessState | ErrorState; + +function handleState(state: AsyncState) { + switch (state.status) { + case 'loading': + console.log('Loading...'); + break; + case 'success': + console.log('Data:', state.data); // TypeScript knows state.data exists + break; + case 'error': + console.log('Error:', state.error.message); // TypeScript knows state.error exists + break; + } +} +``` + +### Pattern 2: Builder Pattern + +Type-safe fluent API: + +```typescript +class QueryBuilder { + private filters: Array<(item: T) => boolean> = []; + private sortFn?: (a: T, b: T) => number; + private limitCount?: number; + + where(predicate: (item: T) => boolean): this { + this.filters.push(predicate); + return this; + } + + sortBy(compareFn: (a: T, b: T) => number): this { + this.sortFn = compareFn; + return this; + } + + limit(count: number): this { + this.limitCount = count; + return this; + } + + execute(data: T[]): T[] { + let result = data.filter(item => + this.filters.every(filter => filter(item)) + ); + + if (this.sortFn) { + result = result.sort(this.sortFn); + } + + if (this.limitCount) { + result = result.slice(0, this.limitCount); + } + + return result; + } +} + +// Usage +const users = [/* ... */]; +const result = new QueryBuilder() + .where(u => u.age > 18) + .where(u => u.email.includes('@example.com')) + .sortBy((a, b) => a.name.localeCompare(b.name)) + .limit(10) + .execute(users); +``` + +### Pattern 3: Type-Safe Event Emitter + +```typescript +type EventMap = { + 'user:created': { id: string; name: string }; + 'user:updated': { id: string; changes: Partial }; + 'user:deleted': { id: string }; +}; + +class TypedEventEmitter> { + private listeners: { [K in keyof T]?: Array<(data: T[K]) => void> } = {}; + + on(event: K, listener: (data: T[K]) => void): void { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event]!.push(listener); + } + + emit(event: K, data: T[K]): void { + const eventListeners = this.listeners[event]; + if (eventListeners) { + eventListeners.forEach(listener => listener(data)); + } + } +} + +// Usage with type safety +const emitter = new TypedEventEmitter(); + +emitter.on('user:created', (data) => { + console.log(data.id, data.name); // TypeScript knows the shape +}); + +emitter.emit('user:created', { id: '123', name: 'Alice' }); // Type-checked +``` + +## Troubleshooting + +| Issue | Cause | Solution | +|-------|-------|----------| +| **Module not found** | Missing type definitions | Install `@types/[package-name]` or add `declare module` | +| **Implicit any errors** | `noImplicitAny` enabled | Add explicit type annotations | +| **Cannot find global types** | Missing lib in `compilerOptions` | Add to `lib`: `["ES2020", "DOM"]` | +| **Type errors in node_modules** | Third-party library types | Add `skipLibCheck: true` to `tsconfig.json` | +| **Import errors with .ts extension** | Import resolving issues | Use imports without extensions | +| **Build takes too long** | Compiling too many files | Use `incremental: true` and `tsBuildInfoFile` | +| **Type inference not working** | Complex inferred types | Add explicit type annotations | +| **Circular dependency errors** | Import cycles | Refactor to break cycles, use interfaces | + +## Advanced TypeScript Features + +### Mapped Types + +Transform existing types: + +```typescript +type Readonly = { + readonly [P in keyof T]: T[P]; +}; + +type Partial = { + [P in keyof T]?: T[P]; +}; + +type Pick = { + [P in K]: T[P]; +}; + +// Usage +interface User { + id: number; + name: string; + email: string; +} + +type ReadonlyUser = Readonly; // All properties readonly +type PartialUser = Partial; // All properties optional +type UserNameEmail = Pick; // Only name and email +``` + +### Conditional Types + +Types that depend on conditions: + +```typescript +type IsString = T extends string ? true : false; + +type A = IsString; // true +type B = IsString; // false + +// Practical example: Extract function return type +type ReturnType = T extends (...args: any[]) => infer R ? R : never; + +function getUser() { + return { id: 1, name: "Alice" }; +} + +type User = ReturnType; // { id: number; name: string } +``` + +### Template Literal Types + +String manipulation at type level: + +```typescript +type Greeting = `Hello, ${T}!`; + +type WelcomeMessage = Greeting<"World">; // "Hello, World!" + +// Practical: Create event names +type EventName = `on${Capitalize}`; + +type ClickEvent = EventName<"click">; // "onClick" +type HoverEvent = EventName<"hover">; // "onHover" +``` + +## TypeScript Configuration Reference + +### Key `tsconfig.json` Options + +| Option | Purpose | Recommended | +|--------|---------|-------------| +| `strict` | Enable all strict type checking | `true` | +| `target` | ECMAScript target version | `ES2020` or higher | +| `module` | Module system | `commonjs` (Node) or `esnext` (bundlers) | +| `lib` | Include type definitions | `["ES2020"]` + `DOM` if browser | +| `outDir` | Output directory | `./dist` | +| `rootDir` | Root source directory | `./src` | +| `sourceMap` | Generate source maps | `true` for debugging | +| `declaration` | Generate .d.ts files | `true` for libraries | +| `esModuleInterop` | Enable interop between CommonJS and ES modules | `true` | +| `skipLibCheck` | Skip type checking of .d.ts files | `true` for performance | +| `forceConsistentCasingInFileNames` | Enforce consistent file casing | `true` | +| `resolveJsonModule` | Allow importing JSON files | `true` if needed | +| `allowJs` | Allow JavaScript files | `true` during migration | +| `checkJs` | Type check JavaScript files | `false` during migration | +| `noEmit` | Don't emit files (use external bundler) | `true` with bundlers | +| `incremental` | Enable incremental compilation | `true` for faster builds | + +## Migration Checklist + +When migrating a JavaScript project to TypeScript: + +### Phase 1: Setup + +- [ ] Install TypeScript and @types packages +- [ ] Create `tsconfig.json` with permissive settings +- [ ] Configure build scripts +- [ ] Set up IDE/editor TypeScript support + +### Phase 2: Incremental Migration + +- [ ] Enable `allowJs: true` and `checkJs: false` +- [ ] Rename utility files to `.ts` +- [ ] Add type annotations to function signatures +- [ ] Create interfaces for data structures +- [ ] Fix TypeScript errors in converted files + +### Phase 3: Strengthen Types + +- [ ] Enable `noImplicitAny: true` +- [ ] Enable `strictNullChecks: true` +- [ ] Remove `any` types where possible +- [ ] Add type guards for union types +- [ ] Create type definitions for external modules + +### Phase 4: Full Strict Mode + +- [ ] Enable `strict: true` +- [ ] Fix all remaining type errors +- [ ] Remove JSDoc annotations (now redundant) +- [ ] Optimize type definitions +- [ ] Document complex types + +### Phase 5: Maintenance + +- [ ] Set up pre-commit type checking +- [ ] Configure CI/CD type checking +- [ ] Establish code review standards for types +- [ ] Keep TypeScript and @types packages updated + +## References + +This skill includes bundled reference documentation for TypeScript essentials. + +### Reference Documentation (`references/`) + +#### Core Concepts & Fundamentals + +- **[basics.md](references/basics.md)** - TypeScript fundamentals, simple types, type inference, and special types +- **[essentials.md](references/essentials.md)** - Core TypeScript concepts every developer should know +- **[cheatsheet.md](references/cheatsheet.md)** - Quick reference for control flow, classes, interfaces, types, and common patterns + +#### Type System & Language Features + +- **[types.md](references/types.md)** - Advanced types, conditional types, mapped types, type guards, and recursive types +- **[classes.md](references/classes.md)** - Class syntax, inheritance, generics, and utility types +- **[elements.md](references/elements.md)** - Arrays, tuples, objects, enums, functions, and casting +- **[keywords.md](references/keywords.md)** - keyof, null handling, optional chaining, and template literal types +- **[miscellaneous.md](references/miscellaneous.md)** - Async programming, promises, decorators, and JSDoc integration + +### External Resources + +- [TypeScript Official Documentation](https://www.typescriptlang.org/docs/) +- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) +- [TypeScript tsconfig Reference](https://www.typescriptlang.org/tsconfig/) +- [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/) +- [TypeScript Playground](https://www.typescriptlang.org/play) - Test TypeScript code online + +## Summary + +The TypeScript Coder skill empowers you to write type-safe, maintainable code with expert-level TypeScript knowledge. Whether migrating existing JavaScript projects or starting new TypeScript projects, apply these proven patterns, workflows, and best practices to deliver production-quality code with confidence. + +**Remember**: TypeScript is a tool for developer productivity and code quality. Use it to catch errors early, improve code documentation, and enable better tooling—but don't let perfect types prevent shipping working code. diff --git a/skills/typescript-coder/references/basics.md b/skills/typescript-coder/references/basics.md new file mode 100644 index 000000000..90138f8f0 --- /dev/null +++ b/skills/typescript-coder/references/basics.md @@ -0,0 +1,362 @@ +# TypeScript Basics + +## TypeScript Tutorial + +- Reference material for [Tutorial](https://www.w3schools.com/typescript/index.php) +- See [The TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) for additional information + +## Getting Started + +### Most Basic Syntax + +```ts +console.log('Hello World!'); +``` + +- Reference material for [Getting Started](https://www.w3schools.com/typescript/typescript_getstarted.php) +- See [TS for the New Programmer](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html) for additional information +- See [TypeScript for JS Programmers](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) for additional information +- See [TS for Java/C# Programmers](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-oop.html) for additional information +- See [TS for Functional Programmers](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html) for additional information +- See [TypeScript Tooling in 5 minutes](https://www.typescriptlang.org/docs/handbook/typescript-tooling-in-5-minutes.html) for additional information + +### Installing Compiler + +```bash +npm install typescript --save-dev +``` + +```bash +added 1 package, and audited 2 packages in 2s +found 0 vulnerabilities +``` + +```bash +npx tsc +``` + +```bash +Version 4.5.5 +tsc: The TypeScript Compiler - Version 4.5.5 +``` + +### Configuring compiler + +```bash +npx tsc --init +``` + +```ts +Created a new tsconfig.json with: + target: es2016 + module: commonjs + strict: true + esModuleInterop: true + skipLibCheck: true + forceConsistentCasingInFileNames: true +``` + +### Configuration example + +```json +{ + "include": ["src"], + "compilerOptions": { + "outDir": "./build" + } +} +``` + +### Your First Program + +```ts +function greet(name: string): string { + return `Hello, ${name}!`; +} + +const message: string = greet("World"); +console.log(message); +``` + +### Compile and run + +```bash +npx tsc hello.ts +``` + +### Compiled JavaScript output + +```js +function greet(name) { + return "Hello, ".concat(name, "!"); +} + +const message = greet("World"); +console.log(message); +``` + +```bash +node hello.js +``` + +``` +Hello, World! +``` + +## TypeScript Simple Types + +- Reference material for [Simple Types](https://www.w3schools.com/typescript/typescript_simple_types.php) +- See [The Basics](https://www.typescriptlang.org/docs/handbook/2/basic-types.html) for additional information +- See [Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) for additional information + +### Boolean + +```ts +let isActive: boolean = true; +let hasPermission = false; // TypeScript infers 'boolean' type +``` + +### Number + +```ts +let decimal: number = 6; +let hex: number = 0xf00d; // Hexadecimal +let binary: number = 0b1010; // Binary +let octal: number = 0o744; // Octal +let float: number = 3.14; // Floating point +``` + +### String + +```ts +let color: string = "blue"; +let fullName: string = 'John Doe'; +let age: number = 30; +let sentence: string = `Hello, my name is ${fullName} and I'll be ${age + 1} next year.`; +``` + +### BigInt (ES2020+) + +```ts +const bigNumber: bigint = 9007199254740991n; +const hugeNumber = BigInt(9007199254740991); // Alternative syntax +``` + +### Symbol + +```ts +const uniqueKey: symbol = Symbol('description'); +const obj = { + [uniqueKey]: 'This is a unique property' +}; +console.log(obj[uniqueKey]); // "This is a unique property" +``` + +## TypeScript Explicit Types and Inference + +- Reference material for [Explicit Types and Inference](https://www.w3schools.com/typescript/typescript_explicit_inference.php) +- See [Type Inference](https://www.typescriptlang.org/docs/handbook/type-inference.html) for additional information +- See [Variable Declaration](https://www.typescriptlang.org/docs/handbook/variable-declarations.html) for additional information + +### Explicit Type Annotations + +```ts +// String +let greeting: string = "Hello, TypeScript!"; + +// Number +let userCount: number = 42; + +// Boolean +let isLoading: boolean = true; + +// Array of numbers +let scores: number[] = [100, 95, 98]; +``` + +```ts +// Function with explicit parameter and return types +function greet(name: string): string { + return `Hello, ${name}!`; +} + +// TypeScript will ensure you pass the correct argument type +greet("Alice"); // OK +greet(42); +// Error: Argument of type '42' is not assignable to parameter of type 'string' +``` + +### Type Inference + +```ts +// TypeScript infers 'string' +let username = "alice"; + +// TypeScript infers 'number' +let score = 100; + +// TypeScript infers 'boolean[]' +let flags = [true, false, true]; + +// TypeScript infers return type as 'number' +function add(a: number, b: number) { + return a + b; +} +``` + +```ts +// TypeScript infers the shape of the object +const user = { +name: "Alice", +age: 30, +isAdmin: true +}; + +// TypeScript knows these properties exist +console.log(user.name); // OK +console.log(user.email); + // Error: Property 'email' does not exist +``` + +### Type Safety in Action + +```ts +let username: string = "alice"; +username = 42; +// Error: Type 'number' is not assignable to type 'string' +``` + +```ts +let score = 100; // TypeScript infers 'number' +score = "high"; +// Error: Type 'string' is not assignable to type 'number' +``` + +```ts +// This is valid JavaScript but can lead to bugs +function add(a, b) { +return a + b; +} + +console.log(add("5", 3)); // Returns "53" (string concatenation) +``` + +```ts +function add(a: number, b: number): number { +return a + b; +} + +console.log(add("5", 3)); +// Error: Argument of type 'string' is not assignable to parameter of type 'number' +``` + +```ts +// 1. JSON.parse returns 'any' because the structure isn't known at compile time +const data = JSON.parse('{ "name": "Alice", "age": 30 }'); + +// 2. Variables declared without initialization +let something; // Type is 'any' +something = 'hello'; +something = 42; // No error +``` + +## TypeScript Special Types + +- Reference material for [Special Types](https://www.w3schools.com/typescript/typescript_special_types.php) +- See [Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) for additional information +- See [Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) for additional information + +### Type: any + +```ts +let u = true; +u = "string"; +// Error: Type 'string' is not assignable to type 'boolean'. +Math.round(u); +// Error: Argument of type 'boolean' is not assignable to parameter of type 'number'. +``` + +```ts +let v: any = true; +v = "string"; // no error as it can be "any" type +Math.round(v); // no error as it can be "any" type +``` + +### Type: unknown + +```ts +let w: unknown = 1; +w = "string"; // no error +w = { + runANonExistentMethod: () => { + console.log("I think therefore I am"); + } +} as { runANonExistentMethod: () => void} +// How can we avoid the error for the code commented out below when +// we don't know the type? +// w.runANonExistentMethod(); // Error: Object is of type 'unknown'. +if(typeof w === 'object' && w !== null) { + (w as { runANonExistentMethod: Function }).runANonExistentMethod(); +} +// Although we have to cast multiple times we can do a check in the +// if to secure our type and have a safer casting +``` + +### Type: never + +```ts +function throwError(message: string): never { + throw new Error(message); +} +``` + +```ts +type Shape = Circle | Square | Triangle; + +function getArea(shape: Shape): number { + switch (shape.kind) { + case 'circle': + return Math.PI * shape.radius ** 2; + case 'square': + return shape.sideLength ** 2; + default: + // TypeScript knows this should never happen + const _exhaustiveCheck: never = shape; + return _exhaustiveCheck; + } +} +``` + +```ts +let x: never = true; +// Error: Type 'boolean' is not assignable to type 'never'. +``` + +### Type: undefined & null + +```ts +let y: undefined = undefined; +let z: null = null; +``` + +```ts +// Optional parameter (implicitly `string | undefined`) +function greet(name?: string) { + return `Hello, ${name || 'stranger'}`; +} + +// Optional property in an interface +interface User { + name: string; + age?: number; // Same as `number | undefined` +} +``` + +```ts +// Nullish coalescing (??) - only uses default +// if value is null or undefined +const value = input ?? 'default'; + +// Optional chaining (?.) - safely access nested properties +const street = user?.address?.street; +``` diff --git a/skills/typescript-coder/references/cheatsheet.md b/skills/typescript-coder/references/cheatsheet.md new file mode 100644 index 000000000..35da60575 --- /dev/null +++ b/skills/typescript-coder/references/cheatsheet.md @@ -0,0 +1,531 @@ +# TypeScript Cheat Sheets + +Quick reference guides for TypeScript features. These cheat sheets are based on the official TypeScript documentation. + +## Control Flow Analysis + +TypeScript's control flow analysis helps narrow types based on code structure. + +### Type Guards + +```typescript +// typeof type guards +function process(value: string | number) { + if (typeof value === "string") { + return value.toUpperCase(); // value is string + } + return value.toFixed(2); // value is number +} + +// instanceof type guards +class Animal { name: string; } +class Dog extends Animal { bark(): void {} } + +function handleAnimal(animal: Animal) { + if (animal instanceof Dog) { + animal.bark(); // animal is Dog + } +} + +// in operator +type Fish = { swim: () => void }; +type Bird = { fly: () => void }; + +function move(animal: Fish | Bird) { + if ("swim" in animal) { + animal.swim(); // animal is Fish + } else { + animal.fly(); // animal is Bird + } +} +``` + +### Truthiness Narrowing + +```typescript +function printLength(str: string | null) { + if (str) { + console.log(str.length); // str is string + } +} + +// Falsy values: false, 0, -0, 0n, "", null, undefined, NaN +``` + +### Equality Narrowing + +```typescript +function example(x: string | number, y: string | boolean) { + if (x === y) { + // x and y are both string + x.toUpperCase(); + y.toUpperCase(); + } +} +``` + +### Discriminated Unions + +```typescript +type Shape = + | { kind: "circle"; radius: number } + | { kind: "square"; sideLength: number } + | { kind: "triangle"; base: number; height: number }; + +function getArea(shape: Shape): number { + switch (shape.kind) { + case "circle": + return Math.PI * shape.radius ** 2; + case "square": + return shape.sideLength ** 2; + case "triangle": + return (shape.base * shape.height) / 2; + } +} +``` + +### Assertion Functions + +```typescript +function assert(condition: any, msg?: string): asserts condition { + if (!condition) { + throw new AssertionError(msg); + } +} + +function yell(str: string | undefined) { + assert(str !== undefined, "str should be defined"); + // str is now string + return str.toUpperCase(); +} +``` + +## Classes + +TypeScript class syntax and features. + +### Basic Class + +```typescript +class Point { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + distance(): number { + return Math.sqrt(this.x ** 2 + this.y ** 2); + } +} + +const p = new Point(3, 4); +console.log(p.distance()); // 5 +``` + +### Parameter Properties + +```typescript +class Point { + // Shorthand for declaring and initializing + constructor( + public x: number, + public y: number + ) {} +} +``` + +### Visibility Modifiers + +```typescript +class BankAccount { + private balance: number = 0; + protected accountNumber: string; + public owner: string; + + constructor(owner: string, accountNumber: string) { + this.owner = owner; + this.accountNumber = accountNumber; + } + + public deposit(amount: number): void { + this.balance += amount; + } + + public getBalance(): number { + return this.balance; + } +} +``` + +### Readonly Properties + +```typescript +class Person { + readonly birthDate: Date; + + constructor(birthDate: Date) { + this.birthDate = birthDate; + } +} + +const person = new Person(new Date(1990, 0, 1)); +// person.birthDate = new Date(); // Error: readonly +``` + +### Inheritance + +```typescript +class Animal { + constructor(public name: string) {} + + move(distance: number): void { + console.log(`${this.name} moved ${distance}m`); + } +} + +class Dog extends Animal { + constructor(name: string, public breed: string) { + super(name); + } + + bark(): void { + console.log("Woof!"); + } + + override move(distance: number): void { + console.log("Running..."); + super.move(distance); + } +} +``` + +### Abstract Classes + +```typescript +abstract class Shape { + abstract getArea(): number; + + describe(): string { + return `Area: ${this.getArea()}`; + } +} + +class Circle extends Shape { + constructor(public radius: number) { + super(); + } + + getArea(): number { + return Math.PI * this.radius ** 2; + } +} +``` + +### Static Members + +```typescript +class MathUtils { + static PI: number = 3.14159; + + static circleArea(radius: number): number { + return this.PI * radius ** 2; + } +} + +console.log(MathUtils.PI); +console.log(MathUtils.circleArea(5)); +``` + +## Interfaces + +Defining contracts for object shapes. + +### Basic Interface + +```typescript +interface User { + id: number; + name: string; + email: string; + age?: number; // optional + readonly createdAt: Date; // readonly +} + +const user: User = { + id: 1, + name: "Alice", + email: "alice@example.com", + createdAt: new Date() +}; +``` + +### Function Types + +```typescript +interface SearchFunc { + (source: string, substring: string): boolean; +} + +const search: SearchFunc = (src, sub) => { + return src.includes(sub); +}; +``` + +### Indexable Types + +```typescript +interface StringArray { + [index: number]: string; +} + +const myArray: StringArray = ["Alice", "Bob"]; + +interface StringMap { + [key: string]: number; +} + +const ages: StringMap = { + alice: 30, + bob: 25 +}; +``` + +### Extending Interfaces + +```typescript +interface Shape { + color: string; +} + +interface Square extends Shape { + sideLength: number; +} + +const square: Square = { + color: "blue", + sideLength: 10 +}; + +// Multiple inheritance +interface Timestamped { + createdAt: Date; + updatedAt: Date; +} + +interface Document extends Shape, Timestamped { + title: string; +} +``` + +### Implementing Interfaces + +```typescript +interface ClockInterface { + currentTime: Date; + setTime(d: Date): void; +} + +class Clock implements ClockInterface { + currentTime: Date = new Date(); + + setTime(d: Date): void { + this.currentTime = d; + } +} +``` + +### Hybrid Types + +```typescript +interface Counter { + (start: number): string; + interval: number; + reset(): void; +} + +function getCounter(): Counter { + const counter = function(start: number) { + return `Count: ${start}`; + } as Counter; + + counter.interval = 123; + counter.reset = function() {}; + + return counter; +} +``` + +## Types + +TypeScript's type system features. + +### Primitive Types + +```typescript +let isDone: boolean = false; +let decimal: number = 6; +let color: string = "blue"; +let big: bigint = 100n; +let sym: symbol = Symbol("key"); +let notDefined: undefined = undefined; +let empty: null = null; +``` + +### Array Types + +```typescript +let list: number[] = [1, 2, 3]; +let list2: Array = [1, 2, 3]; +let readonly: readonly number[] = [1, 2, 3]; +``` + +### Tuple Types + +```typescript +let tuple: [string, number] = ["hello", 10]; +let labeled: [name: string, age: number] = ["Alice", 30]; +let rest: [string, ...number[]] = ["items", 1, 2, 3]; +``` + +### Union Types + +```typescript +let value: string | number; +value = "hello"; +value = 42; + +type Status = "success" | "error" | "pending"; +``` + +### Intersection Types + +```typescript +type Person = { name: string }; +type Employee = { employeeId: number }; + +type Staff = Person & Employee; + +const staff: Staff = { + name: "Alice", + employeeId: 123 +}; +``` + +### Type Aliases + +```typescript +type ID = string | number; +type Point = { x: number; y: number }; +type Callback = (result: string) => void; +``` + +### Literal Types + +```typescript +let direction: "north" | "south" | "east" | "west"; +direction = "north"; // OK +// direction = "up"; // Error + +type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"; +``` + +### Generic Types + +```typescript +function identity(arg: T): T { + return arg; +} + +interface Box { + value: T; +} + +type Pair = [T, U]; + +class DataStore { + private data: T[] = []; + + add(item: T): void { + this.data.push(item); + } +} +``` + +### Utility Types + +```typescript +// Partial - make all properties optional +type PartialUser = Partial; + +// Required - make all properties required +type RequiredUser = Required; + +// Readonly - make all properties readonly +type ReadonlyUser = Readonly; + +// Pick - select specific properties +type UserPreview = Pick; + +// Omit - exclude specific properties +type UserWithoutEmail = Omit; + +// Record - create object type with specific keys +type Roles = Record; + +// ReturnType - extract function return type +type Result = ReturnType; + +// Parameters - extract function parameters +type Params = Parameters; +``` + +### Mapped Types + +```typescript +type Readonly = { + readonly [P in keyof T]: T[P]; +}; + +type Optional = { + [P in keyof T]?: T[P]; +}; + +type Nullable = { + [P in keyof T]: T[P] | null; +}; +``` + +### Conditional Types + +```typescript +type IsString = T extends string ? true : false; + +type ExtractArray = T extends (infer U)[] ? U : never; + +type NonNullable = T extends null | undefined ? never : T; +``` + +### Template Literal Types + +```typescript +type Greeting = `Hello, ${string}`; +type EventName = `on${Capitalize}`; + +type Color = "red" | "blue" | "green"; +type Size = "small" | "large"; +type Style = `${Color}-${Size}`; +// "red-small" | "red-large" | "blue-small" | ... +``` + +--- + +**Reference Links:** + +- [Control Flow Analysis](https://www.typescriptlang.org/static/TypeScript%20Control%20Flow%20Analysis-8a549253ad8470850b77c4c5c351d457.png) +- [Classes](https://www.typescriptlang.org/static/TypeScript%20Classes-83cc6f8e42ba2002d5e2c04221fa78f9.png) +- [Interfaces](https://www.typescriptlang.org/static/TypeScript%20Interfaces-34f1ad12132fb463bd1dfe5b85c5b2e6.png) +- [Types](https://www.typescriptlang.org/static/TypeScript%20Types-ae199d69aeecf7d4a2704a528d0fd3f9.png) +- [Download PDFs and PNGs](https://www.typescriptlang.org/assets/typescript-cheat-sheets.zip) diff --git a/skills/typescript-coder/references/classes.md b/skills/typescript-coder/references/classes.md new file mode 100644 index 000000000..c541008a2 --- /dev/null +++ b/skills/typescript-coder/references/classes.md @@ -0,0 +1,380 @@ +# TypeScript Classes + +## TypeScript Classes + +- Reference material for [Classes](https://www.w3schools.com/typescript/typescript_classes.php) +- See [Classes](https://www.typescriptlang.org/docs/handbook/2/classes.html) for additional information +- See [Mixins](https://www.typescriptlang.org/docs/handbook/mixins.html) for additional information + +### Members: Types + +```ts +class Person { + name: string; +} + +const person = new Person(); +person.name = "Jane"; +``` + +### Members: Visibility + +```ts +class Person { + private name: string; + + public constructor(name: string) { + this.name = name; + } + + public getName(): string { + return this.name; + } +} + +const person = new Person("Jane"); +console.log(person.getName()); +// person.name isn't accessible from outside the class since it's private +``` + +### Parameter Properties + +```ts +class Person { + // name is a private member variable + public constructor(private name: string) {} + + public getName(): string { + return this.name; + } +} + +const person = new Person("Jane"); +console.log(person.getName()); +``` + +### Readonly + +```ts +class Person { + private readonly name: string; + + public constructor(name: string) { + // name cannot be changed after this initial definition, which has to be + // either at its declaration or in the constructor. + this.name = name; + } + + public getName(): string { + return this.name; + } +} + +const person = new Person("Jane"); +console.log(person.getName()); +``` + +### Inheritance: Implements + +```ts +interface Shape { + getArea: () => number; +} + +class Rectangle implements Shape { + public constructor( + protected readonly width: number, + protected readonly height: number + ) {} + + public getArea(): number { + return this.width * this.height; + } +} +``` + +### Inheritance: Extends + +```ts +interface Shape { + getArea: () => number; +} + +class Rectangle implements Shape { + public constructor(protected readonly width: number, protected readonly height: number) {} + + public getArea(): number { + return this.width * this.height; + } +} + +class Square extends Rectangle { + public constructor(width: number) { + super(width, width); + } + + // getArea gets inherited from Rectangle +} +``` + +### Override + +```ts +interface Shape { + getArea: () => number; +} + +class Rectangle implements Shape { + // using protected for these members allows access from classes + // that extend from this class, such as Square + public constructor( + protected readonly width: number, + protected readonly height: number + ) {} + + public getArea(): number { + return this.width * this.height; + } + + public toString(): string { + return `Rectangle[width=${this.width}, height=${this.height}]`; + } +} + +class Square extends Rectangle { + public constructor(width: number) { + super(width, width); + } + + // this toString replaces the toString from Rectangle + public override toString(): string { + return `Square[width=${this.width}]`; + } +} +``` + +### Abstract Classes + +```ts +abstract class Polygon { + public abstract getArea(): number; + + public toString(): string { + return `Polygon[area=${this.getArea()}]`; + } +} + +class Rectangle extends Polygon { + public constructor( + protected readonly width: number, + protected readonly height: number + ) { + super(); + } + + public getArea(): number { + return this.width * this.height; + } +} +``` + +## TypeScript Basic Generics + +- Reference material for [Basic Generics](https://www.w3schools.com/typescript/typescript_basic_generics.php) +- See [Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) for additional information +- See [Creating Types from Types](https://www.typescriptlang.org/docs/handbook/2/types-from-types.html) for additional information + +### Functions + +```ts +function createPair(v1: S, v2: T): [S, T] { + return [v1, v2]; +} +console.log(createPair('hello', 42)); // ['hello', 42] +``` + +### Classes + +```ts +class NamedValue { + private _value: T | undefined; + + constructor(private name: string) {} + + public setValue(value: T) { + this._value = value; + } + + public getValue(): T | undefined { + return this._value; + } + + public toString(): string { + return `${this.name}: ${this._value}`; + } +} + +let value = new NamedValue('myNumber'); +value.setValue(10); +console.log(value.toString()); // myNumber: 10 +``` + +### Type Aliases + +```ts +type Wrapped = { value: T }; + +const wrappedValue: Wrapped = { value: 10 }; +``` + +### Default Value + +```ts +class NamedValue { + private _value: T | undefined; + + constructor(private name: string) {} + + public setValue(value: T) { + this._value = value; + } + + public getValue(): T | undefined { + return this._value; + } + + public toString(): string { + return `${this.name}: ${this._value}`; + } +} + +let value = new NamedValue('myNumber'); +value.setValue('myValue'); +console.log(value.toString()); // myNumber: myValue +``` + +### Extends Constraint + +```ts +function createLoggedPair(v1: S, v2: T): [S, T] { + console.log(`creating pair: v1='${v1}', v2='${v2}'`); + return [v1, v2]; +} +``` + +## TypeScript Utility Types + +- Reference material for [Utility Types](https://www.w3schools.com/typescript/typescript_utility_types.php) +- See [Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html) for additional information + +### Partial + +```ts +interface Point { + x: number; + y: number; +} + +let pointPart: Partial = {}; // `Partial` allows x and y to be optional +pointPart.x = 10; +``` + +### Required + +```ts +interface Car { + make: string; + model: string; + mileage?: number; +} + +let myCar: Required = { + make: 'Ford', + model: 'Focus', + mileage: 12000 // `Required` forces mileage to be defined +}; +``` + +### Record + +```ts +const nameAgeMap: Record = { + 'Alice': 21, + 'Bob': 25 +}; +``` + +### Omit + +```ts +interface Person { + name: string; + age: number; + location?: string; +} + +const bob: Omit = { + name: 'Bob' + // `Omit` has removed age and location from the type and they can't be defined here +}; +``` + +### Pick + +```ts +interface Person { + name: string; + age: number; + location?: string; +} + +const bob: Pick = { + name: 'Bob' + // `Pick` has only kept name, so age and location were removed + // from the type and they can't be defined here +}; +``` + +### Exclude + +```ts +type Primitive = string | number | boolean +const value: Exclude = true; +// a string cannot be used here since Exclude removed it from the type. +``` + +### ReturnType + +```ts +type PointGenerator = () => { x: number; y: number; }; +const point: ReturnType = { + x: 10, + y: 20 +}; +``` + +### Parameters + +```ts +type PointPrinter = (p: { x: number; y: number; }) => void; +const point: Parameters[0] = { + x: 10, + y: 20 +}; +``` + +### Readonly + +```ts +interface Person { + name: string; + age: number; +} +const person: Readonly = { + name: "Dylan", + age: 35, +}; +person.name = 'Israel'; +// prog.ts(11,8): error TS2540: Cannot assign to 'name' because it is a +// read-only property. +``` diff --git a/skills/typescript-coder/references/elements.md b/skills/typescript-coder/references/elements.md new file mode 100644 index 000000000..5653298bf --- /dev/null +++ b/skills/typescript-coder/references/elements.md @@ -0,0 +1,420 @@ +# TypeScript Elements + +## TypeScript Arrays + +- Reference material for [Arrays](https://www.w3schools.com/typescript/typescript_arrays.php) +- See [Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) for additional information + +### Elements Syntax + +```ts +const names: string[] = []; +names.push("Dylan"); // no error +// names.push(3); +// Error: Argument of type 'number' is not assignable to parameter of type 'string'. +``` + +### Readonly + +```ts +const names: readonly string[] = ["Dylan"]; +names.push("Jack"); +// Error: Property 'push' does not exist on type 'readonly string[]'. +// try removing the readonly modifier and see if it works? +``` + +```ts +const numbers = [1, 2, 3]; // inferred to type number[] +numbers.push(4); // no error +// comment line below out to see the successful assignment +numbers.push("2"); +// Error: Argument of type 'string' is not assignable to parameter of type 'number'. +let head: number = numbers[0]; // no error +``` + +## TypeScript Tuples + +- Reference material for [Tuples](https://www.w3schools.com/typescript/typescript_tuples.php) +- See [Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) for additional information + +### Typed Arrays + +```ts +// define our tuple +let ourTuple: [number, boolean, string]; + +// initialize correctly +ourTuple = [5, false, 'Coding God was here']; +``` + +```ts +// define our tuple +let ourTuple: [number, boolean, string]; + +// initialized incorrectly which throws an error +ourTuple = [false, 'Coding God was mistaken', 5]; +``` + +### Readonly Tuple + +```ts +// define our tuple +let ourTuple: [number, boolean, string]; +// initialize correctly +ourTuple = [5, false, 'Coding God was here']; +// We have no type safety in our tuple for indexes 3+ +ourTuple.push('Something new and wrong'); +console.log(ourTuple); +``` + +```ts +// define our readonly tuple +const ourReadonlyTuple: readonly [number, boolean, string] = + [5, true, 'The Real Coding God']; +// throws error as it is readonly. +ourReadonlyTuple.push('Coding God took a day off'); +``` + +### Named Tuples + +```ts +const graph: [x: number, y: number] = [55.2, 41.3]; +``` + +```ts +const graph: [number, number] = [55.2, 41.3]; +const [x, y] = graph; +``` + +## TypeScript Object Types + +- Reference material for [Object Types](https://www.w3schools.com/typescript/typescript_object_types.php) +- See [Object Types](https://www.typescriptlang.org/docs/handbook/2/objects.html) for additional information +- See [Indexed Access Types](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html) for additional information + +### Elements Syntax + +```ts +const car: { type: string, model: string, year: number } = { + type: "Toyota", + model: "Corolla", + year: 2009 +}; +``` + +### Type Inference + +```ts +const car = { + type: "Toyota", +}; +car.type = "Ford"; // no error +car.type = 2; +// Error: Type 'number' is not assignable to type 'string'. +``` + +### Optional Properties + +```ts +const car: { type: string, mileage: number } = { + // Error: Property 'mileage' is missing in type '{ type: string;}' + // but required in type '{ type: string; mileage: number; }'. + type: "Toyota", +}; +car.mileage = 2000; +``` + +```ts +const car: { type: string, mileage?: number } = { // no error + type: "Toyota" +}; +car.mileage = 2000; +``` + +### Index Signatures + +```ts +const nameAgeMap: { [index: string]: number } = {}; +nameAgeMap.Jack = 25; // no error +nameAgeMap.Mark = "Fifty"; +// Error: Type 'string' is not assignable to type 'number'. +``` + +## TypeScript Enums + +- Reference material for [Enums](https://www.w3schools.com/typescript/typescript_enums.php) +- See [Enums](https://www.typescriptlang.org/docs/handbook/enums.html) for additional information + +### Numeric Enums - Default + +```ts +enum CardinalDirections { + North, + East, + South, + West +} +let currentDirection = CardinalDirections.North; +// logs 0 +console.log(currentDirection); +// throws error as 'North' is not a valid enum +currentDirection = 'North'; +// Error: "North" is not assignable to type 'CardinalDirections'. +``` + +### Numeric Enums - Initialized + +```ts +enum CardinalDirections { + North = 1, + East, + South, + West +} +// logs 1 +console.log(CardinalDirections.North); +// logs 4 +console.log(CardinalDirections.West); +``` + +### Numeric Enums - Fully Initialized + +```ts +enum StatusCodes { + NotFound = 404, + Success = 200, + Accepted = 202, + BadRequest = 400 +} +// logs 404 +console.log(StatusCodes.NotFound); +// logs 200 +console.log(StatusCodes.Success); +``` + +### String Enums + +```ts +enum CardinalDirections { + North = 'North', + East = "East", + South = "South", + West = "West" +}; +// logs "North" +console.log(CardinalDirections.North); +// logs "West" +console.log(CardinalDirections.West); +``` + +## TypeScript Type Aliases and Interfaces + +- Reference material for [Type Aliases and Interfaces](https://www.w3schools.com/typescript/typescript_aliases_and_interfaces.php) +- See [Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) for additional information +- See [Object Types](https://www.typescriptlang.org/docs/handbook/2/objects.html) for additional information + +### Type Aliases + +```ts +type CarYear = number +type CarType = string +type CarModel = string +type Car = { + year: CarYear, + type: CarType, + model: CarModel +} + +const carYear: CarYear = 2001 +const carType: CarType = "Toyota" +const carModel: CarModel = "Corolla" +const car: Car = { + year: carYear, + type: carType, + model: carModel +}; +``` + +```ts +type Animal = { name: string }; +type Bear = Animal & { honey: boolean }; +const bear: Bear = { name: "Winnie", honey: true }; + +type Status = "success" | "error"; +let response: Status = "success"; +``` + +### Interfaces + +```ts +interface Rectangle { + height: number, + width: number +} + +const rectangle: Rectangle = { + height: 20, + width: 10 +}; +``` + +```ts +interface Animal { + name: string; +} +interface Animal { + age: number; +} +const dog: Animal = { + name: "Fido", + age: 5 +}; +``` + +### Extending Interfaces + +```ts +interface Rectangle { + height: number, + width: number +} + +interface ColoredRectangle extends Rectangle { + color: string +} + +const coloredRectangle: ColoredRectangle = { + height: 20, + width: 10, + color: "red" +}; +``` + +## TypeScript Union Types + +- Reference material for [Union Types](https://www.w3schools.com/typescript/typescript_union_types.php) +- See [Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) for additional information +- See [Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) for additional information + +### Union | (OR) + +```ts +function printStatusCode(code: string | number) { + console.log(`My status code is ${code}.`) +} +printStatusCode(404); +printStatusCode('404'); +``` + +### Type Guards + +```ts +function printStatusCode(code: string | number) { + console.log(`My status code is ${code.toUpperCase()}.`) + // error: Property 'toUpperCase' does not exist on type 'string | number'. + // Property 'toUpperCase' does not exist on type 'number' +} +``` + +> [!NOTE] +> In our example we are having an issue invoking `toUpperCase()` as it's a string method and number doesn't have access to it. + +## TypeScript Functions + +- Reference material for [Functions](https://www.w3schools.com/typescript/typescript_functions.php) +- See [More on Functions](https://www.typescriptlang.org/docs/handbook/2/functions.html) for additional information + +### Return Type + +```ts +// the `: number` here specifies that this function returns a number +function getTime(): number { + return new Date().getTime(); +} +``` + +### Void Return Type + +```ts +function printHello(): void { + console.log('Hello!'); +} +``` + +### Parameters + +```ts +function multiply(a: number, b: number) { + return a * b; +} +``` + +### Optional Parameters + +```ts +// the `?` operator here marks parameter `c` as optional +function add(a: number, b: number, c?: number) { + return a + b + (c || 0); +} +``` + +### Default Parameters + +```ts +function pow(value: number, exponent: number = 10) { + return value ** exponent; +} +``` + +### Named Parameters + +```ts +function divide({ dividend, divisor }: { dividend: number, divisor: number }) { + return dividend / divisor; +} +``` + +### Rest Parameters + +```ts +function add(a: number, b: number, ...rest: number[]) { + return a + b + rest.reduce((p, c) => p + c, 0); +} +``` + +### Type Alias + +```ts +type Negate = (value: number) => number; + +// in this function, the parameter `value` automatically gets assigned +// the type `number` from the type `Negate` +const negateFunction: Negate = (value) => value * -1; +``` + +## TypeScript Casting + +- Reference material for [Casting](https://www.w3schools.com/typescript/typescript_casting.php) +- See [Type Compatibility](https://www.typescriptlang.org/docs/handbook/type-compatibility.html) for additional information + +### Casting with as + +```ts +let x: unknown = 'hello'; +console.log((x as string).length); +``` + +### Casting with <> + +```ts +let x: unknown = 'hello'; +console.log((x).length); +``` + +### Force Casting + +```ts +let x = 'hello'; +console.log(((x as unknown) as number).length); +// x is not actually a number so this will return undefined +``` diff --git a/skills/typescript-coder/references/essentials.md b/skills/typescript-coder/references/essentials.md new file mode 100644 index 000000000..f00b4bf70 --- /dev/null +++ b/skills/typescript-coder/references/essentials.md @@ -0,0 +1,882 @@ +# TypeScript Essentials + +TypeScript reference material covering essential language features and concepts from the official TypeScript documentation. + +## Utility Types + +- Reference material for [Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html) + +Built-in generic types that transform existing types into new ones. + +```ts +// Partial — makes all properties optional +interface User { name: string; age: number; } +const update: Partial = { name: "Alice" }; + +// Required — makes all properties required +interface Config { timeout?: number; retries?: number; } +const config: Required = { timeout: 3000, retries: 3 }; + +// Readonly — makes all properties read-only +const p: Readonly<{ x: number; y: number }> = { x: 1, y: 2 }; +// p.x = 5; // Error + +// Record — key-value map type +type Roles = "admin" | "user" | "guest"; +const permissions: Record = { admin: true, user: false, guest: false }; + +// Pick — select a subset of keys +type UserPreview = Pick; + +// Omit — remove a subset of keys +type PublicUser = Omit<{ id: number; name: string; password: string }, "password">; + +// Exclude — remove union members assignable to U +type ActiveStatus = Exclude<"active" | "inactive" | "pending", "inactive" | "pending">; +// "active" + +// Extract — keep only union members assignable to U +type StringOrNumber = Extract; + +// NonNullable — remove null and undefined +type DefiniteString = NonNullable; // string + +// ReturnType — extract function return type +function getUser() { return { id: 1, name: "Alice" }; } +type UserType = ReturnType; + +// Parameters — extract function parameter types as a tuple +type Params = Parameters<(name: string, age: number) => void>; +// [name: string, age: number] + +// InstanceType — extract class instance type +class MyClass { x = 10; } +type Instance = InstanceType; + +// ConstructorParameters — extract constructor parameters +class Point { constructor(public x: number, public y: number) {} } +type PointArgs = ConstructorParameters; // [number, number] + +// Awaited — recursively unwrap Promise +type A = Awaited>>; // number + +// String manipulation +type Upper = Uppercase<"hello">; // "HELLO" +type Lower = Lowercase<"WORLD">; // "world" +type Cap = Capitalize<"typescript">; // "Typescript" +type Uncap = Uncapitalize<"TypeScript">; // "typeScript" +``` + +Key utility types reference: + +| Utility Type | Purpose | +|---|---| +| `Partial` | All props optional | +| `Required` | All props required | +| `Readonly` | All props read-only | +| `Record` | Key-value map type | +| `Pick` | Select subset of keys | +| `Omit` | Remove subset of keys | +| `Exclude` | Remove union members | +| `Extract` | Keep union members | +| `NonNullable` | Remove null/undefined | +| `ReturnType` | Function return type | +| `Parameters` | Function param types | +| `InstanceType` | Class instance type | +| `Awaited` | Unwrap Promise type | + +## Cheat Sheets + +- Reference material for [Cheat Sheets](https://www.typescriptlang.org/cheatsheets/) + +Four downloadable cheat sheets covering core TypeScript topics: + +**Control Flow Analysis** — narrowing, type guards, `typeof`, `instanceof`, `in`, assignments, exhaustiveness checks. + +**Types** — primitive types, unions, intersections, generics, utility types, mapped types, conditional types. + +**Interfaces** — object shapes, optional/readonly properties, index signatures, extending, declaration merging. + +**Classes** — constructors, fields, access modifiers (`public`, `private`, `protected`), `abstract`, generics, decorators. + +```ts +// Control flow narrowing example +function padLeft(value: string, padding: string | number) { + if (typeof padding === "number") { + return " ".repeat(padding) + value; // padding: number + } + return padding + value; // padding: string +} + +// Mapped type (from Types sheet) +type Optional = { [K in keyof T]?: T[K] }; + +// Interface extending (from Interfaces sheet) +interface Animal { name: string; } +interface Dog extends Animal { breed: string; } + +// Abstract class (from Classes sheet) +abstract class Shape { + abstract getArea(): number; + toString() { return `Area: ${this.getArea()}`; } +} +``` + +## Decorators + +- Reference material for [Decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) + +Decorators are special declarations that attach to classes, methods, accessors, properties, or parameters. Require `"experimentalDecorators": true` in `tsconfig.json` for legacy decorators. TypeScript 5.0+ supports Stage 3 decorators without a flag. + +```ts +// tsconfig.json (legacy decorators) +// { "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } } + +// Decorator factory — customizes the decorator +function color(value: string) { + return function (target: any) { + target.prototype.color = value; + }; +} + +// Class decorator +function sealed(constructor: Function) { + Object.seal(constructor); + Object.seal(constructor.prototype); +} + +@sealed +@color("blue") +class BugReport { + type = "report"; + title: string; + constructor(t: string) { this.title = t; } +} + +// Method decorator +function enumerable(value: boolean) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + descriptor.enumerable = value; + }; +} + +class Greeter { + @enumerable(false) + greet() { return "Hello!"; } +} + +// Property decorator (using reflect-metadata) +// import "reflect-metadata"; +// const formatMetadataKey = Symbol("format"); +// function format(formatString: string) { +// return Reflect.metadata(formatMetadataKey, formatString); +// } + +// Parameter decorator +function required(target: Object, propertyKey: string | symbol, parameterIndex: number) { + // mark parameter as required via metadata +} + +// TypeScript 5.0 Stage 3 decorator (no flag needed) +function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) { + const methodName = String(context.name); + return function (this: any, ...args: any[]) { + console.log(`Entering '${methodName}'`); + const result = originalMethod.call(this, ...args); + console.log(`Exiting '${methodName}'`); + return result; + }; +} + +class Person { + name: string; + constructor(name: string) { this.name = name; } + + @loggedMethod + greet() { console.log(`Hello, my name is ${this.name}.`); } +} +``` + +Decorator evaluation order: multiple decorators on one declaration are evaluated top-down but applied bottom-up. Across a class: instance members first (parameter → method/accessor/property), then static members, then constructor parameters, then class decorators. + +## Declaration Merging + +- Reference material for [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) + +TypeScript merges two or more separate declarations with the same name into a single definition. + +```ts +// Interface merging — merged into one interface +interface Box { height: number; width: number; } +interface Box { scale: number; } +const box: Box = { height: 5, width: 6, scale: 10 }; + +// Function member overloads — later interface members have higher priority +interface Cloner { clone(animal: Animal): Animal; } +interface Cloner { clone(animal: Sheep): Sheep; } +// Merged: Sheep overload first, then Animal + +// Namespace merging +namespace Animals { export class Zebra {} } +namespace Animals { export class Dog {} } +// Both Zebra and Dog are available on Animals + +// Namespace + class +class Album { label: Album.AlbumLabel; } +namespace Album { export class AlbumLabel {} } + +// Namespace + function (adds properties to a function) +function buildLabel(name: string): string { + return buildLabel.prefix + name + buildLabel.suffix; +} +namespace buildLabel { + export let suffix = ""; + export let prefix = "Hello, "; +} + +// Namespace + enum (adds methods to an enum) +enum Color { red = 1, green = 2, blue = 4 } +namespace Color { + export function mixColor(colorName: string) { + if (colorName === "yellow") return Color.red + Color.green; + } +} + +// Module augmentation — add to an existing module +// In map.ts: +// import { Observable } from "./observable"; +// declare module "./observable" { +// interface Observable { +// map(f: (x: T) => U): Observable; +// } +// } + +// Global augmentation from a module file +declare global { + interface Array { + toObservable(): Observable; + } +} +``` + +Key limitations: cannot create new top-level declarations in module augmentation; classes cannot merge with other classes (use mixins instead). + +## Enums + +- Reference material for [Enums](https://www.typescriptlang.org/docs/handbook/enums.html) + +Enums define a set of named constants. TypeScript supports numeric, string, and heterogeneous enums. + +```ts +// Numeric enums — auto-incremented from 0 (or a custom start) +enum Direction { Up, Down, Left, Right } // 0, 1, 2, 3 +enum Direction { Up = 1, Down, Left, Right } // 1, 2, 3, 4 + +// Numeric enums support reverse mapping +enum Color { Red = 1 } +Color[1]; // "Red" +Color.Red; // 1 + +// String enums — no reverse mapping, more debuggable +enum Direction { + Up = "UP", + Down = "DOWN", + Left = "LEFT", + Right = "RIGHT", +} + +// Heterogeneous enums (not recommended) +enum BooleanLike { No = 0, Yes = "YES" } + +// Computed and constant members +enum FileAccess { + None, + Read = 1 << 1, + Write = 1 << 2, + ReadWrite = Read | Write, + G = "123".length, // computed +} + +// const enums — fully inlined at compile time, no runtime object +const enum Direction2 { Up, Down, Left, Right } +let dir = Direction2.Up; // compiles to: let dir = 0; + +// Ambient enums — describe existing enum shapes +declare enum Enum { A = 1, B, C = 2 } + +// Alternative: as const objects (recommended for new code) +const Dir = { Up: "UP", Down: "DOWN" } as const; +type Dir = typeof Dir[keyof typeof Dir]; // "UP" | "DOWN" +``` + +| Feature | Numeric | String | `const` | Ambient | +|---|---|---|---|---| +| Reverse mapping | Yes | No | No | No | +| Runtime object | Yes | Yes | No | No | +| Computed members | Yes | No | No | Yes | +| Inlined at compile | No | No | Yes | N/A | + +## Iterators and Generators + +- Reference material for [Iterators and Generators](https://www.typescriptlang.org/docs/handbook/iterators-and-generators.html) + +```ts +// Core interfaces +interface Iterator { + next(...args: [] | [TNext]): IteratorResult; + return?(value?: TReturn): IteratorResult; + throw?(e?: any): IteratorResult; +} + +interface Iterable { + [Symbol.iterator](): Iterator; +} + +// IterableIterator combines both — most useful in practice +interface IterableIterator extends Iterator { + [Symbol.iterator](): IterableIterator; +} + +// for...of vs for...in +let list = [4, 5, 6]; +for (let i in list) { console.log(i); } // "0", "1", "2" (keys) +for (let i of list) { console.log(i); } // 4, 5, 6 (values) + +// Generator function — yield pauses execution +function* infiniteSequence(): IterableIterator { + let i = 0; + while (true) { yield i++; } +} + +function* range(start: number, end: number): IterableIterator { + for (let i = start; i < end; i++) { yield i; } +} + +for (const num of range(1, 5)) { console.log(num); } // 1, 2, 3, 4 + +// Custom iterable class +class Range implements Iterable { + constructor(private start: number, private end: number) {} + + [Symbol.iterator](): Iterator { + let current = this.start; + const end = this.end; + return { + next(): IteratorResult { + if (current <= end) return { value: current++, done: false }; + return { value: undefined as any, done: true }; + } + }; + } +} +``` + +For older compile targets, enable full iterator support: + +```ts +// tsconfig.json +// { "compilerOptions": { "target": "ES5", "downlevelIteration": true } } +``` + +## JSX + +- Reference material for [JSX](https://www.typescriptlang.org/docs/handbook/jsx.html) + +TypeScript supports JSX syntax for projects that use JSX-based libraries. To use JSX: name files `.tsx` and set the `jsx` compiler option. + +```ts +// tsconfig.json +// { "compilerOptions": { "jsx": "preserve" } } // or "react-jsx", etc. +``` + +JSX emission modes: + +| Mode | Input | Output | File Extension | +|------|-------|--------|----------------| +| `preserve` | `
` | `
` | `.jsx` | +| `react` | `
` | `React.createElement("div")` | `.js` | +| `react-jsx` | `
` | `_jsx("div", {})` | `.js` | + +```tsx +// Intrinsic elements — lowercase, mapped via JSX.IntrinsicElements +const element =
; + +// Value-based elements — capitalized components +function MyButton(props: { label: string }) { + return ; +} +const btn = ; + +// Props type checking +interface ButtonProps { + label: string; + onClick: () => void; + disabled?: boolean; +} +function Button({ label, onClick, disabled }: ButtonProps) { + return ; +} + +// Generic components +function Identity(props: { value: T; render: (val: T) => JSX.Element }) { + return props.render(props.value); +} +``` + +Key compiler options: `jsx`, `jsxFactory`, `jsxFragmentFactory`, `jsxImportSource`. + +## Mixins + +- Reference material for [Mixins](https://www.typescriptlang.org/docs/handbook/mixins.html) + +Mixins compose classes from reusable parts since TypeScript does not support multiple inheritance directly. A mixin is a function that takes a base class and returns a new extended class. + +```ts +// Base constructor type +type Constructor = new (...args: any[]) => T; + +// Timestamped mixin +function Timestamped(Base: TBase) { + return class extends Base { + timestamp = Date.now(); + }; +} + +// Activatable mixin +function Activatable(Base: TBase) { + return class extends Base { + isActive = false; + activate() { this.isActive = true; } + deactivate() { this.isActive = false; } + }; +} + +class User { name = ""; } + +// Compose multiple mixins +const TimestampedUser = Timestamped(User); +const TimestampedActivatableUser = Timestamped(Activatable(User)); + +const u = new TimestampedActivatableUser(); +u.activate(); +console.log(u.isActive, u.timestamp); + +// Constrained mixin — requires base to have specific shape +type Spritable = Constructor<{ speed: number }>; + +function Jumpable(Base: TBase) { + return class extends Base { + jump() { console.log(`Jumping at speed ${this.speed}`); } + }; +} +``` + +| Rule | Detail | +|------|--------| +| Mixin is a function | Takes a class, returns a new class | +| Type constraint | Use `Constructor` to require specific base shape | +| Composition order | Applied right-to-left: `A(B(C))` | +| Constructors | Must call `super(...args)` in mixin constructors | + +## Namespaces + +- Reference material for [Namespaces](https://www.typescriptlang.org/docs/handbook/namespaces.html) + +Namespaces (formerly "internal modules") organize code into named scopes to prevent global naming collisions. Only items marked `export` are accessible outside the namespace. + +```ts +// Basic namespace +namespace Validation { + export interface StringValidator { + isAcceptable(s: string): boolean; + } + + const lettersRegexp = /^[A-Za-z]+$/; // private + + export class LettersOnlyValidator implements StringValidator { + isAcceptable(s: string) { return lettersRegexp.test(s); } + } +} + +const validator = new Validation.LettersOnlyValidator(); + +// Multi-file namespaces — use reference tags +// /// +// namespace Validation { ... } + +// Compile with: tsc --outFile sample.js Validation.ts LettersValidator.ts + +// Namespace alias — shorthand for deep nesting +namespace Shapes { + export namespace Polygons { + export class Triangle {} + export class Square {} + } +} +import polygons = Shapes.Polygons; +let sq = new polygons.Square(); + +// Ambient namespace — for external JS libraries without types +declare namespace D3 { + export interface Selectors { + select(selector: string): Selection; + } + export interface Selection { + attr(name: string, value: string): Selection; + } +} +``` + +Note: for new projects, prefer ES modules (`import`/`export`) over namespaces. + +## Namespaces and Modules + +- Reference material for [Namespaces and Modules](https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html) + +Modules (ES modules) are the modern, standard approach for organizing TypeScript code. Namespaces are a legacy mechanism primarily useful for ambient declarations and declaration files. + +```ts +// Modules — file-based, isolated scope +// math.ts +export function add(a: number, b: number): number { return a + b; } + +// app.ts +import { add } from "./math"; +console.log(add(1, 2)); + +// Namespace — global scope by default +namespace Validation { + export class LettersOnlyValidator { /* ... */ } +} + +// Anti-pattern: wrapping module exports in a namespace +export namespace Utils { + export function helper() {} // callers need Utils.helper() — unnecessary nesting +} + +// Better: export directly +export function helper() {} +``` + +Pitfalls of using namespaces in module files: + +| Pitfall | Description | +|---|---| +| Namespace over modules | Redundant and confusing in a module system | +| Global pollution | Namespaces live on the global scope by default | +| No tree-shaking | Bundlers cannot eliminate unused namespace code | +| `/// ` complexity | Multi-file namespaces require manual dependency ordering | +| Deep nesting | `App.Utils.Strings.trim` becomes unwieldy | + +When to use modules vs namespaces: + +- **Modules**: all new applications and libraries, any code using a bundler or Node.js +- **Namespaces**: `.d.ts` declaration files for global scripts, legacy non-module scripts, UMD global library typings + +## Symbols + +- Reference material for [Symbols](https://www.typescriptlang.org/docs/handbook/symbols.html) + +`symbol` is a primitive type (ES2015+). Each `Symbol()` produces a unique, immutable value. + +```ts +// Symbol creation — always unique +const sym1 = Symbol(); +const sym2 = Symbol("description"); +const sym3 = Symbol("description"); +sym2 === sym3; // false + +// unique symbol — compile-time unique identity, must be const +const KEY: unique symbol = Symbol("key"); + +// Symbols as object keys — not enumerable in for...in or Object.keys() +const id = Symbol("id"); +const user = { name: "Alice", [id]: 42 }; +console.log(user[id]); // 42 + +// Global symbol registry — shared across realms +const s1 = Symbol.for("shared"); +const s2 = Symbol.for("shared"); +s1 === s2; // true +Symbol.keyFor(s1); // "shared" + +// Well-known symbols customize language behavior +class Range { + constructor(public start: number, public end: number) {} + + [Symbol.iterator]() { + let current = this.start; + const end = this.end; + return { + next(): IteratorResult { + return current <= end + ? { value: current++, done: false } + : { value: undefined as any, done: true }; + } + }; + } +} + +// Symbol.toPrimitive — custom type coercion +class Temperature { + constructor(private celsius: number) {} + [Symbol.toPrimitive](hint: string) { + if (hint === "number") return this.celsius; + if (hint === "string") return `${this.celsius}°C`; + return this.celsius; + } +} + +// Symbol.hasInstance — custom instanceof behavior +class EvenNumber { + static [Symbol.hasInstance](value: unknown): boolean { + return typeof value === "number" && value % 2 === 0; + } +} +4 instanceof EvenNumber; // true +5 instanceof EvenNumber; // false +``` + +Well-known symbols: + +| Symbol | Purpose | +|---|---| +| `Symbol.iterator` | Custom iteration (`for...of`) | +| `Symbol.asyncIterator` | Async iteration | +| `Symbol.hasInstance` | `instanceof` behavior | +| `Symbol.toPrimitive` | Type coercion | +| `Symbol.toStringTag` | `Object.prototype.toString` tag | +| `Symbol.species` | Constructor for derived objects | + +## Triple-Slash Directives + +- Reference material for [Triple-Slash Directives](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) + +Triple-slash directives are single-line XML-tag comments that must appear at the top of a file before any code. They serve as compiler instructions. Directives are ignored inside module files (files that contain `import`/`export`). + +```ts +// /// — declares dependency on another file +/// +/// + +// /// — declares dependency on a @types package +/// +/// + +// /// — includes a specific built-in lib +/// +/// +/// + +function toArray(iter: Iterable): T[] { return [...iter]; } + +// /// — marks file as a library, +// prevents compiler from including default lib.d.ts files +// Found in TypeScript's own built-in lib files; rarely used in user code. + +// /// — assigns a name to an AMD module +/// +export class C {} +// Compiled AMD output: define("NamedModule", ["require", "exports"], ...) + +// /// — deprecated; use import instead +``` + +Key rules: + +| Rule | Detail | +|------|--------| +| Placement | Must appear before any code, imports, or non-directive comments | +| Modules | Directives are ignored inside ES module files | +| `noResolve` | With `--noResolve` flag, `reference path` directives are ignored | + +Common use in published `.d.ts` files: + +```ts +// mylib.d.ts +/// +export declare function readConfig(path: string): NodeJS.ProcessEnv; +``` + +## Type Compatibility + +- Reference material for [Type Compatibility](https://www.typescriptlang.org/docs/handbook/type-compatibility.html) + +TypeScript uses structural typing (duck typing) — types are compatible if their shapes match, regardless of how they were declared. + +```ts +// Structural typing — shape matters, not declaration +interface Named { name: string; } +class Person { name: string = ""; } + +let p: Named = new Person(); // OK — Person has the required shape + +// Object compatibility — target needs at least the source's properties +interface Pet { name: string; } +interface Dog { name: string; breed: string; } +let pet: Pet; +let dog: Dog = { name: "Rex", breed: "Lab" }; +pet = dog; // OK — Dog has everything Pet needs +// dog = pet; // Error — Pet lacks `breed` + +// Function compatibility — fewer parameters is OK +let handler: (a: number, b: number) => void; +handler = (a: number) => {}; // OK — extra params are ignored + +// Return types — source return type must be a subtype of target +class Animal { feet: number = 4; } +class DogAnimal extends Animal { name: string = ""; } +let getAnimal: () => Animal; +let getDog: () => DogAnimal = () => new DogAnimal(); +getAnimal = getDog; // OK — DogAnimal is a subtype of Animal + +// Generics — compared after substitution +interface Empty {} +let x: Empty; +let y: Empty; +x = y; // OK — identical structure regardless of T + +interface NotEmpty { data: T; } +let a: NotEmpty; +let b: NotEmpty; +// a = b; // Error — number !== string + +// Enum compatibility — different enums are incompatible with each other +enum Status { Active, Inactive } +enum Color { Red, Green } +// let s: Status = Color.Red; // Error + +let n: number = Status.Active; // OK — numeric enums are compatible with number + +// Class compatibility — only instance members compared (ignore static) +class A { feet = 4; } +class B { feet = 4; name = ""; } +let ca: A = new B(); // OK + +// Private members must come from the same declaration +class X { private secret = 1; } +class Z { private secret = 1; } +// let cx: X = new Z(); // Error — different private declarations +``` + +Assignment compatibility extends subtype compatibility: `any` is assignable to/from anything; `void` is interchangeable with `undefined` in assignments. + +## Type Inference + +- Reference material for [Type Inference](https://www.typescriptlang.org/docs/handbook/type-inference.html) + +TypeScript infers types automatically when no explicit annotation is given. + +```ts +// Basic inference from initializers +let x = 3; // number +let name = "Alice"; // string +let flag = true; // boolean + +// Best common type — union of candidate types +let arr = [0, 1, null]; +// inferred as: (number | null)[] + +class Animal {} +class Dog extends Animal {} +class Cat extends Animal {} + +let animals = [new Dog(), new Cat()]; +// inferred as: (Dog | Cat)[] +// NOT Animal[] — Animal must be explicitly included: +let animals2: Animal[] = [new Dog(), new Cat()]; + +// Contextual typing — type flows in from context +window.onmousedown = function(mouseEvent) { + console.log(mouseEvent.button); // OK — inferred as MouseEvent + // console.log(mouseEvent.kangaroo); // Error +}; + +const names = ["Alice", "Bob"]; +names.forEach(function(name) { + console.log(name.toUpperCase()); // name inferred as string +}); + +// Return type inference +function add(x: number, y: number) { + return x + y; // return type: number +} + +function getValue(flag: boolean) { + if (flag) return 42; + return "default"; + // return type: number | string +} +``` + +| Concept | Behavior | +|---|---| +| Basic inference | Type flows from initializer/value | +| Best common type | Union of candidate types | +| Contextual typing | Type flows from usage context | +| Return type inference | Derived from all `return` paths | + +## Variable Declaration + +- Reference material for [Variable Declaration](https://www.typescriptlang.org/docs/handbook/variable-declarations.html) + +```ts +// var — function-scoped, hoisted, allows re-declaration (avoid) +var x = 10; +for (var i = 0; i < 10; i++) { + setTimeout(() => console.log(i), 100); // prints 10, ten times (closure over same i) +} + +// let — block-scoped, temporal dead zone, no re-declaration +let y = 10; +if (true) { + let y = 20; // different y — block-scoped +} +for (let i = 0; i < 10; i++) { + setTimeout(() => console.log(i), 100); // prints 0–9 correctly +} + +// const — block-scoped, immutable binding (object content still mutable) +const PI = 3.14; +// PI = 3; // Error +const obj = { name: "Alice" }; +obj.name = "Bob"; // OK — content is mutable +// obj = {}; // Error — binding is immutable + +// Array destructuring +let [first, second] = [1, 2]; +let [, , third] = [1, 2, 3]; // skip elements +let [head, ...tail] = [1, 2, 3, 4]; // rest: tail = [2, 3, 4] +let [a = 0, b = 1] = [5]; // defaults: a=5, b=1 +[first, second] = [second, first]; // swap + +// Object destructuring +let { name, age } = { name: "Alice", age: 30 }; +let { name: userName } = { name: "Alice" }; // rename +let { p = 5, q = 10 } = { p: 3 }; // defaults +let { x, ...rest } = { x: 1, y: 2, z: 3 }; // rest properties + +// Function parameter destructuring +function greet({ name, age = 0 }: { name: string; age?: number }) { + console.log(`${name} is ${age}`); +} + +// Spread operator +let arr1 = [1, 2]; +let arr2 = [3, 4]; +let combined = [...arr1, ...arr2]; // [1, 2, 3, 4] + +let obj1 = { a: 1, b: 2 }; +let obj2 = { b: 3, c: 4 }; +let merged = { ...obj1, ...obj2 }; // { a: 1, b: 3, c: 4 } — later props overwrite +``` + +Scoping rules: + +| Feature | Scope | Hoisted | Re-declarable | Re-assignable | +|---|---|---|---|---| +| `var` | Function/Global | Yes | Yes | Yes | +| `let` | Block | No (TDZ) | No | Yes | +| `const` | Block | No (TDZ) | No | No | + +Best practice: prefer `const` by default, use `let` when reassignment is needed, avoid `var`. diff --git a/skills/typescript-coder/references/keywords.md b/skills/typescript-coder/references/keywords.md new file mode 100644 index 000000000..618b96cf1 --- /dev/null +++ b/skills/typescript-coder/references/keywords.md @@ -0,0 +1,135 @@ +# TypeScript Keywords + +## TypeScript Keyof + +- Reference material for [Keyof](https://www.w3schools.com/typescript/typescript_keyof.php) +- See [Keyof Type Operator](https://www.typescriptlang.org/docs/handbook/2/keyof-types.html) for additional information +- See [Typeof Type Operator](https://www.typescriptlang.org/docs/handbook/2/typeof-types.html) for additional information + +### Parameters + +```ts +interface Person { + name: string; + age: number; +} +// `keyof Person` here creates a union type of "name" and "age", +// other strings will not be allowed +function printPersonProperty(person: Person, property: keyof Person) { + console.log(`Printing person property ${property}: "${person[property]}"`); +} +let person = { + name: "Max", + age: 27 +}; +printPersonProperty(person, "name"); // Printing person property name: "Max" +``` + +```ts +type StringMap = { [key: string]: unknown }; +// `keyof StringMap` resolves to `string` here +function createStringPair(property: keyof StringMap, value: string): StringMap { + return { [property]: value }; +} +``` + +## TypeScript Null & Undefined + +- Reference material for [Null & Undefined](https://www.w3schools.com/typescript/typescript_null.php) +- See [Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) for additional information +- See [Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) for additional information + +### Types + +```ts +let value: string | undefined | null = null; +value = 'hello'; +value = undefined; +``` + +### Optional Chaining + +```ts +interface House { + sqft: number; + yard?: { + sqft: number; + }; +} +function printYardSize(house: House) { + const yardSize = house.yard?.sqft; + if (yardSize === undefined) { + console.log('No yard'); + } else { + console.log(`Yard is ${yardSize} sqft`); + } +} + +let home: House = { + sqft: 500 +}; + +printYardSize(home); // Prints 'No yard' +``` + +### Nullish Coalescing + +```ts +function printMileage(mileage: number | null | undefined) { + console.log(`Mileage: ${mileage ?? 'Not Available'}`); +} + +printMileage(null); // Prints 'Mileage: Not Available' +printMileage(0); // Prints 'Mileage: 0' +``` + +### Null Assertion + +```ts +function getValue(): string | undefined { + return 'hello'; +} +let value = getValue(); +console.log('value length: ' + value!.length); +``` + +```ts +let array: number[] = [1, 2, 3]; +let value = array[0]; +// with `noUncheckedIndexedAccess` this has the type `number | undefined` +``` + +## TypeScript Definitely Typed + +- Reference material for [Definitely Typed](https://www.w3schools.com/typescript/typescript_definitely_typed.php) +- See [Declaration Files Introduction](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) for additional information + +### Installation + +```bash +npm install --save-dev @types/jquery +``` + +## TypeScript 5.x Update + +- Reference material for [5.x Update](https://www.w3schools.com/typescript/typescript_5_updates.php) +- See [Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) for additional information + +### Template Literal Types + +```ts +type Color = "red" | "green" | "blue"; +type HexColor = `#${string}`; + +// Usage: +let myColor: HexColor<"blue"> = "#0000FF"; +``` + +### Index Signature Labels + +```ts +type DynamicObject = { [key: `dynamic_${string}`]: string }; + +// Usage: +let obj: DynamicObject = { dynamic_key: "value" }; +``` diff --git a/skills/typescript-coder/references/miscellaneous.md b/skills/typescript-coder/references/miscellaneous.md new file mode 100644 index 000000000..cd1317b8b --- /dev/null +++ b/skills/typescript-coder/references/miscellaneous.md @@ -0,0 +1,2361 @@ +# TypeScript Miscellaneous + +## TypeScript Async Programming + +- Reference material for [Async Programming](https://www.w3schools.com/typescript/typescript_async.php) +- See [Iterators and Generators](https://www.typescriptlang.org/docs/handbook/iterators-and-generators.html) for additional information +- See [Symbols](https://www.typescriptlang.org/docs/handbook/symbols.html) for additional information + +### Promises in TypeScript + +```ts +// Create a typed Promise that resolves to a string +const fetchGreeting = (): Promise => { + return new Promise((resolve, reject) => { + setTimeout(() => { + const success = Math.random() > 0.5; + if (success) { + resolve("Hello, TypeScript!"); + } else { + reject(new Error("Failed to fetch greeting")); + } + }, 1000); + }); +}; + +// Using the Promise with proper type inference +fetchGreeting() + .then((greeting) => { + // TypeScript knows 'greeting' is a string + console.log(greeting.toUpperCase()); + }) + .catch((error: Error) => { + console.error("Error:", error.message); + }); +``` + +### Async/Await with TypeScript + +```ts +// Define types for our API response +interface User { + id: number; + name: string; + email: string; + role: 'admin' | 'user' | 'guest'; +} + +// Function that returns a Promise of User array +async function fetchUsers(): Promise { + console.log('Fetching users...'); + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1000)); + return [ + { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' }, + { id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' } + ]; +} + +// Async function to process users +async function processUsers() { + try { + // TypeScript knows users is User[] + const users = await fetchUsers(); + console.log(`Fetched ${users.length} users`); + + // Type-safe property access + const adminEmails = users + .filter(user => user.role === 'admin') + .map(user => user.email); + + console.log('Admin emails:', adminEmails); + return users; + } catch (error) { + if (error instanceof Error) { + console.error('Failed to process users:', error.message); + } else { + console.error('An unknown error occurred'); + } + throw error; // Re-throw to let caller handle + } +} + +// Execute the async function +processUsers() + .then(users => console.log('Processing complete')) + .catch(err => console.error('Processing failed:', err)); +``` + +### Run multiple async operations in parallel + +```ts +interface Product { + id: number; + name: string; + price: number; +} + +async function fetchProduct(id: number): Promise { + console.log(`Fetching product ${id}...`); + await new Promise(resolve => setTimeout(resolve, Math.random() * 1000)); + return { id, name: `Product ${id}`, price: Math.floor(Math.random() * 100) }; +} + +async function fetchMultipleProducts() { + try { + // Start all fetches in parallel + const [product1, product2, product3] = await Promise.all([ + fetchProduct(1), + fetchProduct(2), + fetchProduct(3) + ]); + + const total = [product1, product2, product3] + .reduce((sum, product) => sum + product.price, 0); + console.log(`Total price: $${total.toFixed(2)}`); + } catch (error) { + console.error('Error fetching products:', error); + } +} + +fetchMultipleProducts(); +``` + +### Typing Callbacks for Async Operations + +```ts +// Define a type for the callback +type FetchCallback = (error: Error | null, data?: string) => void; + +// Function that takes a typed callback +function fetchDataWithCallback(url: string, callback: FetchCallback): void { + // Simulate async operation + setTimeout(() => { + try { + // Simulate successful response + callback(null, "Response data"); + } catch (error) { + callback(error instanceof Error ? error : new Error('Unknown error')); + } + }, 1000); +} + +// Using the callback function +fetchDataWithCallback('https://api.example.com', (error, data) => { + if (error) { + console.error('Error:', error.message); + return; + } + + // TypeScript knows data is a string (or undefined) + if (data) { + console.log(data.toUpperCase()); + } +}); +``` + +### Promise.all - Run multiple promises in parallel + +```ts +// Different types of promises +const fetchUser = (id: number): Promise<{ id: number; name: string }> => + Promise.resolve({ id, name: `User ${id}` }); + +const fetchPosts = (userId: number): Promise> => + Promise.resolve([ + { id: 1, title: 'Post 1' }, + { id: 2, title: 'Post 2' } + ]); + +const fetchStats = (userId: number): Promise<{ views: number; likes: number }> => + Promise.resolve({ views: 100, likes: 25 }); + +// Run all in parallel +async function loadUserDashboard(userId: number) { + try { + const [user, posts, stats] = await Promise.all([ + fetchUser(userId), + fetchPosts(userId), + fetchStats(userId) + ]); + + // TypeScript knows the types of user, posts, and stats + console.log(`User: ${user.name}`); + console.log(`Posts: ${posts.length}`); + console.log(`Likes: ${stats.likes}`); + + return { user, posts, stats }; + } catch (error) { + console.error('Failed to load dashboard:', error); + throw error; + } +} + +// Execute with a user ID +loadUserDashboard(1); +``` + +### Promise.race - Useful for timeouts + +```ts +// Helper function for timeout +const timeout = (ms: number): Promise => + new Promise((_, reject) => + setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms) + ); + +// Simulate API call with timeout +async function fetchWithTimeout( + promise: Promise, + timeoutMs: number = 5000 +): Promise { + return Promise.race([ + promise, + timeout(timeoutMs).then(() => { + throw new Error(`Request timed out after ${timeoutMs}ms`); + }), + ]); +} + +// Usage example +async function fetchUserData() { + try { + const response = await fetchWithTimeout( + fetch('https://api.example.com/user/1'), + 3000 // 3 second timeout + ); + const data = await response.json(); + return data; + } catch (error) { + console.error('Error:', (error as Error).message); + throw error; + } +} +``` + +### Promise.allSettled - Wait for all promises regardless of outcome + +```ts +// Simulate multiple API calls with different outcomes +const fetchData = async (id: number) => { + // Randomly fail some requests + if (Math.random() > 0.7) { + throw new Error(`Failed to fetch data for ID ${id}`); + } + return { id, data: `Data for ${id}` }; +}; + +// Process multiple items with individual error handling +async function processBatch(ids: number[]) { + const promises = ids.map(id => + fetchData(id) + .then(value => ({ status: 'fulfilled' as const, value })) + .catch(reason => ({ status: 'rejected' as const, reason })) + ); + + // Wait for all to complete + const results = await Promise.allSettled(promises); + + // Process results + const successful = results + .filter((result): result is PromiseFulfilledResult<{ + status: 'fulfilled', value: any }> => + result.status === 'fulfilled' && + result.value.status === 'fulfilled' + ) + .map(r => r.value.value); + + const failed = results + .filter((result): result is PromiseRejectedResult | + PromiseFulfilledResult<{ status: 'rejected', reason: any }> => { + if (result.status === 'rejected') return true; + return result.value.status === 'rejected'; + }); + + console.log(`Successfully processed: ${successful.length}`); + console.log(`Failed: ${failed.length}`); + + return { successful, failed }; +} + +// Process a batch of IDs +processBatch([1, 2, 3, 4, 5]); +``` + +### Custom Error Classes for Async Operations + +```ts +// Base error class for our application +class AppError extends Error { + constructor( + message: string, + public readonly code: string, + public readonly details?: unknown + ) { + super(message); + this.name = this.constructor.name; + Error.captureStackTrace?.(this, this.constructor); + } +} + +// Specific error types +class NetworkError extends AppError { + constructor(message: string, details?: unknown) { + super(message, 'NETWORK_ERROR', details); + } +} + +class ValidationError extends AppError { + constructor( + public readonly field: string, + message: string + ) { + super(message, 'VALIDATION_ERROR', { field }); + } +} + +class NotFoundError extends AppError { + constructor(resource: string, id: string | number) { + super( + `${resource} with ID ${id} not found`, + 'NOT_FOUND', + { resource, id } + ); + } +} + +// Usage example +async function fetchUserData(userId: string): +Promise<{ id: string; name: string }> { + try { + // Simulate API call + const response = await fetch(`/api/users/${userId}`); + + if (!response.ok) { + if (response.status === 404) { + throw new NotFoundError('User', userId); + } else if (response.status >= 500) { + throw new NetworkError('Server error', { status: response.status }); + } else { + throw new Error(`HTTP error! status: ${response.status}`); + } + } + + const data = await response.json(); + + // Validate response data + if (!data.name) { + throw new ValidationError('name', 'Name is required'); + } + + return data; + } catch (error) { + if (error instanceof AppError) { + // Already one of our custom errors + throw error; + } + // Wrap unexpected errors + throw new AppError( + 'Failed to fetch user data', + 'UNEXPECTED_ERROR', + { cause: error } + ); + } +} + +// Error handling in the application +async function displayUserProfile(userId: string) { + try { + const user = await fetchUserData(userId); + console.log('User profile:', user); + } catch (error) { + if (error instanceof NetworkError) { + console.error('Network issue:', error.message); + // Show retry UI + } else if (error instanceof ValidationError) { + console.error('Validation failed:', error.message); + // Highlight the invalid field + } else if (error instanceof NotFoundError) { + console.error('Not found:', error.message); + // Show 404 page + } else { + console.error('Unexpected error:', error); + // Show generic error message + } + } +} + +// Execute with example data +displayUserProfile('123'); +``` + +### Async Generators + +```ts +// Async generator function +async function* generateNumbers(): AsyncGenerator { + let i = 0; + while (i < 5) { + // Simulate async operation + await new Promise(resolve => setTimeout(resolve, 1000)); + yield i++; + } +} + +// Using the async generator +async function consumeNumbers() { + for await (const num of generateNumbers()) { + // TypeScript knows num is a number + console.log(num * 2); + } +} +``` + +## TypeScript Decorators + +- Reference material for [Decorators](https://www.w3schools.com/typescript/typescript_decorators.php) +- See [Decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) for additional information +- See [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) for additional information + +### Enabling Decorators + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "strictPropertyInitialization": false + }, + "include": ["src/**/*.ts"] +} +``` + +### Class Decorators + +```ts +// A simple class decorator that logs class definition +function logClass(constructor: Function) { + console.log(`Class ${constructor.name} was defined at ${new Date().toISOString()}`); +} + +// Applying the decorator +@logClass +class UserService { + getUsers() { + return ['Alice', 'Bob', 'Charlie']; + } +} + +// Output when the file is loaded: "Class UserService was defined at [timestamp]" +``` + +### Class Decorators - Adding Properties and Methods + +```ts +// A decorator that adds a version property and logs instantiation +function versioned(version: string) { + return function (constructor: Function) { + // Add a static property + constructor.prototype.version = version; + + // Store the original constructor + const original = constructor; + // Create a new constructor that wraps the original + const newConstructor: any = function (...args: any[]) { + console.log(`Creating instance of ${original.name} v${version}`); + return new original(...args); + }; + + // Copy prototype so instanceof works + newConstructor.prototype = original.prototype; + return newConstructor; + }; +} + +// Applying the decorator with a version +@versioned('1.0.0') +class ApiClient { + fetchData() { + console.log('Fetching data...'); + } +} + +const client = new ApiClient(); +console.log((client as any).version); // Outputs: 1.0.0 +client.fetchData(); +``` + +### Class Decorators - Sealed Classes + +```ts +function sealed(constructor: Function) { + console.log(`Sealing ${constructor.name}...`); + Object.seal(constructor); + Object.seal(constructor.prototype); +} + +@sealed +class Greeter { + greeting: string; + constructor(message: string) { + this.greeting = message; + } + greet() { + return `Hello, ${this.greeting}`; + } +} + +// This will throw an error in strict mode +// Greeter.prototype.newMethod = function() {}; +// Error: Cannot add property newMethod +``` + +### Method Decorators - Measure Execution Time + +```ts +// Method decorator to measure execution time +function measureTime( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor +) { + const originalMethod = descriptor.value; + descriptor.value = function (...args: any[]) { + const start = performance.now(); + const result = originalMethod.apply(this, args); + const end = performance.now(); + console.log(`${propertyKey} executed in ${(end - start).toFixed(2)}ms`); + return result; + }; + return descriptor; +} + +// Using the decorator +class DataProcessor { + @measureTime + processData(data: number[]): number[] { + // Simulate processing time + for (let i = 0; i < 100000000; i++) { + /* processing */ + } + return data.map(x => x * 2); + } +} + +// When called, it will log the execution time +const processor = new DataProcessor(); +processor.processData([1, 2, 3, 4, 5]); +``` + +### Method Decorators - Role-Based Access Control + +```ts +// User roles +type UserRole = 'admin' | 'editor' | 'viewer'; + +// Current user context (simplified) +const currentUser = { + id: 1, + name: 'John Doe', + roles: ['viewer'] as UserRole[] +}; + +// Decorator factory for role-based access control +function AllowedRoles(...allowedRoles: UserRole[]) { + return function ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor + ) { + const originalMethod = descriptor.value; + descriptor.value = function (...args: any[]) { + const hasPermission = allowedRoles.some(role => + currentUser.roles.includes(role) + ); + if (!hasPermission) { + throw new Error( + `User ${currentUser.name} is not authorized to call ${propertyKey}` + ); + } + return originalMethod.apply(this, args); + }; + return descriptor; + }; +} + +// Using the decorator +class DocumentService { + @AllowedRoles('admin', 'editor') + deleteDocument(id: string) { + console.log(`Document ${id} deleted`); + } + + @AllowedRoles('admin', 'editor', 'viewer') + viewDocument(id: string) { + console.log(`Viewing document ${id}`); + } +} + +// Usage +const docService = new DocumentService(); +try { + docService.viewDocument('doc123'); // Works - viewer role is allowed + docService.deleteDocument('doc123'); // Throws error - viewer cannot delete +} catch (error) { + console.error(error.message); +} + +// Change user role to admin +currentUser.roles = ['admin']; +docService.deleteDocument('doc123'); // Now works - admin can delete +``` + +### Method Decorators - Deprecation Warning + +```ts +function deprecated(message: string) { + return function ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor + ) { + const originalMethod = descriptor.value; + descriptor.value = function (...args: any[]) { + console.warn(`Warning: ${propertyKey} is deprecated. ${message}`); + return originalMethod.apply(this, args); + }; + return descriptor; + }; +} + +class PaymentService { + @deprecated('Use processPaymentV2 instead') + processPayment(amount: number, currency: string) { + console.log(`Processing payment of ${amount} ${currency}`); + } + + processPaymentV2(amount: number, currency: string) { + console.log(`Processing payment v2 of ${amount} ${currency}`); + } +} + +const payment = new PaymentService(); +payment.processPayment(100, 'USD'); // Shows deprecation warning +payment.processPaymentV2(100, 'USD'); // No warning +``` + +### Property Decorators - Format Properties + +```ts +// Property decorator to format a string property +function format(formatString: string) { + return function (target: any, propertyKey: string) { + let value: string; + const getter = () => value; + const setter = (newVal: string) => { + value = formatString.replace('{}', newVal); + }; + Object.defineProperty(target, propertyKey, { + get: getter, + set: setter, + enumerable: true, + configurable: true + }); + }; +} + +class Greeter { + @format('Hello, {}!') + greeting: string; +} + +const greeter = new Greeter(); +greeter.greeting = 'World'; +console.log(greeter.greeting); // Outputs: Hello, World! +``` + +### Property Decorators - Log Property Access + +```ts +function logProperty(target: any, propertyKey: string) { + let value: any; + const getter = function() { + console.log(`Getting ${propertyKey}: ${value}`); + return value; + }; + + const setter = function(newVal: any) { + console.log(`Setting ${propertyKey} from ${value} to ${newVal}`); + value = newVal; + }; + + Object.defineProperty(target, propertyKey, { + get: getter, + set: setter, + enumerable: true, + configurable: true + }); +} + +class Product { + @logProperty + name: string; + + @logProperty + price: number; + + constructor(name: string, price: number) { + this.name = name; + this.price = price; + } +} + +const product = new Product('Laptop', 999.99); +product.price = 899.99; // Logs: Setting price from 999.99 to 899.99 +console.log(product.name); // Logs: Getting name: Laptop +``` + +### Property Decorators - Required Properties + +```ts +function required(target: any, propertyKey: string) { + let value: any; + + const getter = function() { + if (value === undefined) { + throw new Error(`Property ${propertyKey} is required`); + } + return value; + }; + + const setter = function(newVal: any) { + value = newVal; + }; + + Object.defineProperty(target, propertyKey, { + get: getter, + set: setter, + enumerable: true, + configurable: true + }); +} + +class User { + @required + username: string; + + @required + email: string; + + age?: number; + + constructor(username: string, email: string) { + this.username = username; + this.email = email; + } +} + +const user1 = new User('johndoe', 'john@example.com'); // Works +// const user2 = new User(undefined, 'test@example.com'); +// Throws error: Property username is required +``` + +### Parameter Decorators - Validation + +```ts +function validateParam(type: 'string' | 'number' | 'boolean') { + return function ( + target: any, + propertyKey: string | symbol, + parameterIndex: number + ) { + const existingValidations: any[] = + Reflect.getOwnMetadata('validations', target, propertyKey) || []; + + existingValidations.push({ index: parameterIndex, type }); + Reflect.defineMetadata( + 'validations', existingValidations, target, propertyKey + ); + }; +} + +function validate( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor +) { + const originalMethod = descriptor.value; + descriptor.value = function (...args: any[]) { + const validations: Array<{index: number, type: string}> = + Reflect.getOwnMetadata('validations', target, propertyKey) || []; + + for (const validation of validations) { + const { index, type } = validation; + const param = args[index]; + let isValid = false; + + switch (type) { + case 'string': + isValid = typeof param === 'string' && param.length > 0; + break; + case 'number': + isValid = typeof param === 'number' && !isNaN(param); + break; + case 'boolean': + isValid = typeof param === 'boolean'; + } + + if (!isValid) { + throw new Error(`Parameter at index ${index} failed ${type} validation`); + } + } + + return originalMethod.apply(this, args); + }; + return descriptor; +} + +class UserService { + @validate + createUser( + @validateParam('string') name: string, + @validateParam('number') age: number, + @validateParam('boolean') isActive: boolean + ) { + console.log(`Creating user: ${name}, ${age}, ${isActive}`); + } +} + +const service = new UserService(); +service.createUser('John', 30, true); // Works +// service.createUser('', 30, true); +// Throws error: Parameter at index 0 failed string validation +``` + +### Decorator Factories - Configurable Logging + +```ts +// Decorator factory that accepts configuration +function logWithConfig(config: { + level: 'log' | 'warn' | 'error', + message?: string +}) { + return function ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor + ) { + const originalMethod = descriptor.value; + descriptor.value = function (...args: any[]) { + const { level = 'log', message = 'Executing method' } = config; + + console[level](`${message}: ${propertyKey}`, { arguments: args }); + const result = originalMethod.apply(this, args); + console[level](`${propertyKey} completed`); + return result; + }; + return descriptor; + }; +} + +class PaymentService { + @logWithConfig({ level: 'log', message: 'Processing payment' }) + processPayment(amount: number) { + console.log(`Processing payment of $${amount}`); + } +} +``` + +### Decorator Evaluation Order + +```ts +function first() { + console.log('first(): factory evaluated'); + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + console.log('first(): called'); + }; +} + +function second() { + console.log('second(): factory evaluated'); + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + console.log('second(): called'); + }; +} + +class ExampleClass { + @first() + @second() + method() {} +} + +// Output: +// second(): factory evaluated +// first(): factory evaluated +// first(): called +// second(): called +``` + +### Real-World Example - API Controller + +```ts +// Simple decorator implementations (simplified for example) +const ROUTES: any[] = []; + +function Controller(prefix: string = '') { + return function (constructor: Function) { + constructor.prototype.prefix = prefix; + }; +} + +function Get(path: string = '') { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + ROUTES.push({ + method: 'get', + path, + handler: descriptor.value, + target: target.constructor + }); + }; +} + +// Using the decorators +@Controller('/users') +class UserController { + @Get('/') + getAllUsers() { + return { users: [{ id: 1, name: 'John' }] }; + } + + @Get('/:id') + getUserById(id: string) { + return { id, name: 'John' }; + } +} + +// Simulate route registration +function registerRoutes() { + ROUTES.forEach(route => { + const prefix = route.target.prototype.prefix || ''; + console.log(`Registered ${route.method.toUpperCase()} ${prefix}${route.path}`); + }); +} + +registerRoutes(); +// Output: +// Registered GET /users +// Registered GET /users/:id +``` + +### Common Pitfalls + +```ts +function readonly(target: any, propertyKey: string) { + Object.defineProperty(target, propertyKey, { + writable: false + }); +} + +class Person { + @readonly + name = "John"; +} +``` + +```ts +function logParameter(target: any, propertyKey: string, parameterIndex: number) { + console.log(`Parameter in ${propertyKey} at index ${parameterIndex}`); +} + +class Demo { + greet(@logParameter message: string) { + return message; + } +} +``` + +```json +{ + "compilerOptions": { + "experimentalDecorators": true + } +} +``` + +## TypeScript in JavaScript Projects (*JSDoc*) + +- Reference material for [JavaScript Projects (*JSDoc*)](https://www.w3schools.com/typescript/typescript_jsdoc.php) +- See [JS Projects Utilizing TypeScript](https://www.typescriptlang.org/docs/handbook/intro-to-js-ts.html) for additional information +- See [JSDoc Reference](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html) for additional information + +### Getting Started + +```ts +// @ts-check + +/** + * Adds two numbers. + * @param {number} a + * @param {number} b + * @returns {number} + */ +function add(a, b) { + return a + b; +} +``` + +### Objects and Interfaces + +```ts +// @ts-check + +/** + * @param {{ firstName: string, lastName: string, age?: number }} person + */ +function greet(person) { + return `Hello, ${person.firstName} ${person.lastName}`; +} + +greet({ firstName: 'John', lastName: 'Doe' }); // OK +greet({ firstName: 'Jane' }); + // Error: Property 'lastName' is missing +``` + +### Type Definitions with @typedef + +```ts +// @ts-check + +/** + * @typedef {Object} User + * @property {number} id - The user ID + * @property {string} username - The username + * @property {string} [email] - Optional email address + * @property {('admin'|'user'|'guest')} role - User role + * @property {() => string} getFullName - Method that returns full name + */ + +/** @type {User} */ +const currentUser = { + id: 1, + username: 'johndoe', + role: 'admin', + getFullName() { + return 'John Doe'; + } +}; + +// TypeScript will provide autocomplete for User properties +console.log(currentUser.role); +``` + +### Intersection Types + +```ts +// @ts-check + +/** @typedef {{ x: number, y: number }} Point */ + +/** + * @typedef {Point & { z: number }} Point3D + */ + +/** @type {Point3D} */ +const point3d = { x: 1, y: 2, z: 3 }; + +// @ts-expect-error - missing z property +const point2d = { x: 1, y: 2 }; +``` + +### Function Types - Basic + +```ts +// @ts-check + +/** + * Calculates the area of a rectangle + * @param {number} width - The width of the rectangle + * @param {number} height - The height of the rectangle + * @returns {number} The calculated area + */ +function calculateArea(width, height) { + return width * height; +} + +// TypeScript knows the parameter and return types +const area = calculateArea(10, 20); +``` + +### Function Types - Callbacks + +```ts +// @ts-check + +/** + * @callback StringProcessor + * @param {string} input + * @returns {string} + */ + +/** + * @type {StringProcessor} + */ +const toUpperCase = (str) => str.toUpperCase(); + +/** + * @param {string[]} strings + * @param {StringProcessor} processor + * @returns {string[]} + */ +function processStrings(strings, processor) { + return strings.map(processor); +} + +const result = processStrings(['hello', 'world'], toUpperCase); +// result will be ['HELLO', 'WORLD'] +``` + +### Function Overloads + +```ts +// @ts-check + +/** + * @overload + * @param {string} a + * @param {string} b + * @returns {string} + */ +/** + * @overload + * @param {number} a + * @param {number} b + * @returns {number} + */ +/** + * @param {string | number} a + * @param {string | number} b + * @returns {string | number} + */ +function add(a, b) { + if (typeof a === 'string' || typeof b === 'string') { + return String(a) + String(b); + } + return a + b; +} + +const strResult = add('Hello, ', 'World!'); // string +const numResult = add(10, 20); // number +``` + +### Advanced Types - Union and Intersection + +```ts +// @ts-check + +/** @typedef {{ name: string, age: number }} Person */ +/** @typedef {Person & { employeeId: string }} Employee */ +/** @typedef {Person | { guestId: string, visitDate: Date }} Visitor */ + +/** @type {Employee} */ +const employee = { + name: 'Alice', + age: 30, + employeeId: 'E123' +}; + +/** @type {Visitor} */ +const guest = { + guestId: 'G456', + visitDate: new Date() +}; + +/** + * @param {Visitor} visitor + * @returns {string} + */ +function getVisitorId(visitor) { + if ('guestId' in visitor) { + return visitor.guestId; // TypeScript knows this is a guest + } + return visitor.name; // TypeScript knows this is a Person +} +``` + +### Advanced Types - Mapped Types + +```ts +// @ts-check + +/** + * @template T + * @typedef {[K in keyof T]: T[K] extends Function ? K : never}[keyof T] MethodNames + */ + +/** + * @template T + * @typedef {{[K in keyof T as `get${Capitalize}`]: () => T[K]}} Getters + */ + +/** @type {Getters<{ name: string, age: number }>} */ +const userGetters = { + getName: () => 'John', + getAge: () => 30 +}; + +// TypeScript enforces the return types +const name = userGetters.getName(); // string +const age = userGetters.getAge(); // number +``` + +### Type Imports + +```ts +// @ts-check + +// Importing types from TypeScript files +/** @typedef {import('./types').User} User */ + +// Importing types from node_modules +/** @typedef {import('http').IncomingMessage} HttpRequest */ + +// Importing with renaming +/** @typedef {import('./api').default as ApiClient} ApiClient */ +``` + +### Create a types.d.ts file + +```ts +// types.d.ts +declare module 'my-module' { + export interface Config { + apiKey: string; + timeout?: number; + retries?: number; + } + + export function initialize(config: Config): void; + export function fetchData(url: string): Promise; +} +``` + +### Using type imports in JavaScript + +```ts +// @ts-check + +/** @type {import('my-module').Config} */ +const config = { + apiKey: '12345', + timeout: 5000 +}; + +// TypeScript will provide autocomplete and type checking +import { initialize } from 'my-module'; +initialize(config); +``` + +### Create a types.d.ts file in your project + +```ts +// types.d.ts +declare module 'my-module' { + export interface Config { + apiKey: string; + timeout?: number; + retries?: number; + } + + export function initialize(config: Config): void; + export function fetchData(url: string): Promise; +} +``` + +#### Then use it in your JavaScript files + +```ts +// @ts-check + +/** @type {import('my-module').Config} */ +const config = { + apiKey: '12345', + timeout: 5000 +}; + +// TypeScript will provide autocomplete and type checking +import { initialize } from 'my-module'; +initialize(config); +``` + +## TypeScript Migration + +- Reference material for [Migration](https://www.w3schools.com/typescript/typescript_migration.php) +- See [Migrating from JavaScript](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html) for additional information + +### Create a new branch for the migration + +```ts +git checkout -b typescript-migration + +# Commit your current state +git add . +git commit -m "Pre-TypeScript migration state" +``` + +### Configuration + +```ts +# Install TypeScript as a dev dependency +npm install --save-dev typescript @types/node +``` + +### Create a basic tsconfig.json to start with:Configuration + +```ts +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +``` + +> [!Note] +> Adjust the target based on your minimum supported environments. + +### Create a basic tsconfig.json with these recommended settings:Step-by-Step Migration + +```ts +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "allowJs": true, + "checkJs": true, + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +### Add // @ts-check to the top of your JavaScript files to enable type checking:Step-by-Step Migration + +```ts +// @ts-check + +/** @type {string} */ +const name = 'John'; + +// TypeScript will catch this error +name = 42; +// Error: Type '42' is not assignable to type 'string' +``` + +> [!Note] +> You can disable type checking for specific lines using // @ts-ignore. + +### Start with non-critical files and rename them from .js to .ts:Step-by-Step Migration + +```ts +# Rename a single file +mv src/utils/helpers.js src/utils/helpers.ts + +# Or rename all files in a directory (use with caution) +find src/utils -name "*.js" -exec sh -c 'mv "$0" "${0%.js}.ts"' {} \; +``` + +### Gradually add type annotations to your code:Step-by-Step Migration + +```ts +// Before +function add(a, b) { + return a + b; +} + +// After +function add(a: number, b: number): number { + return a + b; +} + +// With interface +interface User { + id: number; + name: string; + email?: string; +} + +function getUser(id: number): User { + return { id, name: 'John Doe' }; +} +``` + +### Modify your package.json to include TypeScript compilation:Step-by-Step Migration + +```ts + { + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "test": "jest" + } + } +``` + +> [!Note] +> Make sure to update your test configuration to work with TypeScript files. + +### Best Practices for Migration + +```ts + // Use type inference where possible + const name = 'John'; // TypeScript infers 'string' + const age = 30; // TypeScript infers 'number' + + // Use union types for flexibility + type Status = 'active' | 'inactive' | 'pending'; + + // Use type guards for runtime checks + function isString(value: any): value is string { + return typeof value === 'string'; + } +``` + +### Common Challenges and Solutions + +```ts + // Before + const user = {}; + user.name = 'John'; + // Error: Property 'name' does not exist +``` + +### Common Challenges and Solutions + +```ts + // Option 1: Index signature + interface User { + [key: string]: any; + } + const user: User = {}; + user.name = 'John'; // OK + + // Option 2: Type assertion + const user = {} as { name: string }; + user.name = 'John'; // OK +``` + +### Common Challenges and Solutions + +```ts + class Counter { + count = 0; + increment() { + setTimeout(function() { + this.count++; + // Error: 'this' is not defined + }, 1000); + } + } +``` + +### Common Challenges and Solutions + +```ts + // Solution 1: Arrow function + setTimeout(() => { + this.count++; // 'this' is lexically scoped + }, 1000); + + // Solution 2: Bind 'this' + setTimeout(function(this: Counter) { + this.count++; + }.bind(this), 1000); +``` + +## TypeScript Error Handling + +- Reference material for [Error Handling](https://www.w3schools.com/typescript/typescript_error_handling.php) +- See [The Basics](https://www.typescriptlang.org/docs/handbook/2/basic-types.html) for additional information +- See [Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) for additional information + +### Basic Error Handling + +```ts +function divide(a: number, b: number): number { + if (b === 0) { + throw new Error('Division by zero'); + } + return a / b; +} + +try { + const result = divide(10, 0); + console.log(result); +} catch (error) { + console.error('An error occurred:', error.message); +} +``` + +### Custom Error Classes + +```ts +class ValidationError extends Error { + constructor(message: string, public field?: string) { + super(message); + this.name = 'ValidationError'; + // Restore prototype chain + Object.setPrototypeOf(this, ValidationError.prototype); + } +} + +class DatabaseError extends Error { + constructor(message: string, public code: number) { + super(message); + this.name = 'DatabaseError'; + Object.setPrototypeOf(this, DatabaseError.prototype); + } +} + +// Usage +function validateUser(user: any) { + if (!user.name) { + throw new ValidationError('Name is required', 'name'); + } + if (!user.email.includes('@')) { + throw new ValidationError('Invalid email format', 'email'); + } +} +``` + +### Type Guards for Errors + +```ts +// Type guards +function isErrorWithMessage(error: unknown): error is { message: string } { + return ( + typeof error === 'object' && + error !== null && + 'message' in error && + typeof (error as Record).message === 'string' + ); +} + +function isValidationError(error: unknown): error is ValidationError { + return error instanceof ValidationError; +} + +// Usage in catch block +try { + validateUser({}); +} catch (error: unknown) { + if (isValidationError(error)) { + console.error(`Validation error in ${error.field}: ${error.message}`); + } else if (isErrorWithMessage(error)) { + console.error('An error occurred:', error.message); + } else { + console.error('An unknown error occurred'); + } +} +``` + +### Type Assertion Functions + +```ts +function assertIsError(error: unknown): asserts error is Error { + if (!(error instanceof Error)) { + throw new Error('Caught value is not an Error instance'); + } +} + +try { + // ... +} catch (error) { + assertIsError(error); + console.error(error.message); // TypeScript now knows error is Error +} +``` + +### Async Error Handling + +```ts +interface User { + id: number; + name: string; + email: string; +} + +// Using async/await with try/catch +async function fetchUser(userId: number): Promise { + try { + const response = await fetch(`/api/users/${userId}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json() as User; + } catch (error) { + if (error instanceof Error) { + console.error('Failed to fetch user:', error.message); + } + throw error; // Re-throw to allow caller to handle + } +} + +// Using Promise.catch() for error handling +function fetchUserPosts(userId: number): Promise { + return fetch(`/api/users/${userId}/posts`) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .catch(error => { + console.error('Failed to fetch posts:', error); + return []; // Return empty array as fallback + }); +} +``` + +### Always Handle Promise Rejections + +```ts +// Bad: Unhandled promise rejection +fetchData().then(data => console.log(data)); + +// Good: Handle both success and error cases +fetchData() + .then(data => console.log('Success:', data)) + .catch(error => console.error('Error:', error)); + +// Or use void for intentionally ignored errors +void fetchData().catch(console.error); +``` + +### Best Practices - Don't Swallow Errors + +```ts +// Bad: Silent failure +try { /* ... */ } catch { /* empty */ } + +// Good: At least log the error +try { /* ... */ } catch (error) { + console.error('Operation failed:', error); +} +``` + +### Best Practices - Use Custom Error Types + +```ts +class NetworkError extends Error { + constructor(public status: number, message: string) { + super(message); + this.name = 'NetworkError'; + } +} + +class ValidationError extends Error { + constructor(public field: string, message: string) { + super(message); + this.name = 'ValidationError'; + } +} +``` + +### Best Practices - Handle Errors at Appropriate Layers + +```ts +// In a data access layer +async function getUser(id: string): Promise { + const response = await fetch(`/api/users/${id}`); + if (!response.ok) { + throw new NetworkError(response.status, 'Failed to fetch user'); + } + return response.json(); +} + +// In a UI component +async function loadUser() { + try { + const user = await getUser('123'); + setUser(user); + } catch (error) { + if (error instanceof NetworkError) { + if (error.status === 404) { + showError('User not found'); + } else { + showError('Network error. Please try again later.'); + } + } else { + showError('An unexpected error occurred'); + } + } +} +``` + +## TypeScript Best Practices + +- Reference material for [Best Practices](https://www.w3schools.com/typescript/typescript_best_practices.php) +- See [The TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) for additional information +- See [Do's and Don'ts](https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html) for additional information + +### Project Configuration - Enable Strict Mode + +```json +// tsconfig.json +{ + "compilerOptions": { + /* Enable all strict type-checking options */ + "strict": true, + /* Additional recommended settings */ + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + } +} +``` + +### Project Configuration - Additional Strict Checks + +```json +{ + "compilerOptions": { + /* Additional strict checks */ + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true + } +} +``` + +### Type System - Let TypeScript Infer + +```ts +// Bad: Redundant type annotation +const name: string = 'John'; + +// Good: Let TypeScript infer the type +const name = 'John'; + +// Bad: Redundant return type +function add(a: number, b: number): number { + return a + b; +} + +// Good: Let TypeScript infer return type +function add(a: number, b: number) { + return a + b; +} +``` + +### Type System - Be Explicit with Public APIs + +```ts +// Bad: No type information +function processUser(user) { + return user.name.toUpperCase(); +} + +// Good: Explicit parameter and return types +interface User { + id: number; + name: string; + email?: string; // Optional property +} + +function processUser(user: User): string { + return user.name.toUpperCase(); +} +``` + +### Type System - Interface vs Type + +```ts +// Use interface for object shapes that can be extended/implemented +interface User { + id: number; + name: string; +} + +// Extending an interface +interface AdminUser extends User { + permissions: string[]; +} + +// Use type for unions, tuples, or mapped types +type UserRole = 'admin' | 'editor' | 'viewer'; + +// Union types +type UserId = number | string; + +// Mapped types +type ReadonlyUser = Readonly; + +// Tuple types +type Point = [number, number]; +``` + +### Type System - Prefer Specific Types Over 'any' + +```ts +// Bad: Loses type safety +function logValue(value: any) { + console.log(value.toUpperCase()); // No error until runtime +} + +// Better: Use generic type parameter +function logValue(value: T) { + console.log(String(value)); // Safer, but still not ideal +} + +// Best: Be specific about expected types +function logString(value: string) { + console.log(value.toUpperCase()); // Type-safe +} + +// When you need to accept any value but still be type-safe +function logUnknown(value: unknown) { + if (typeof value === 'string') { + console.log(value.toUpperCase()); + } else { + console.log(String(value)); + } +} +``` + +### Code Organization - Logical Modules + +```ts +// user/user.model.ts +export interface User { + id: string; + name: string; + email: string; +} + +// user/user.service.ts +import { User } from './user.model'; + +export class UserService { + private users: User[] = []; + + addUser(user: User) { + this.users.push(user); + } + + getUser(id: string): User | undefined { + return this.users.find(user => user.id === id); + } +} + +// user/index.ts (barrel file) +export * from './user.model'; +export * from './user.service'; +``` + +### Code Organization - File Naming Patterns + +```ts +// Good +user.service.ts // Service classes +user.model.ts // Type definitions +user.controller.ts // Controllers +user.component.ts // Components +user.utils.ts // Utility functions +user.test.ts // Test files + +// Bad +UserService.ts // Avoid PascalCase for file names +user_service.ts // Avoid snake_case +userService.ts // Avoid camelCase for file names +``` + +### Functions and Methods - Type-Safe Functions + +```ts +// Bad: No type information +function process(user, notify) { + notify(user.name); +} + +// Good: Explicit parameter and return types +function processUser( + user: User, + notify: (message: string) => void +): void { + notify(`Processing user: ${user.name}`); +} + +// Use default parameters instead of conditionals +function createUser( + name: string, + role: UserRole = 'viewer', + isActive: boolean = true +): User { + return { name, role, isActive }; +} + +// Use rest parameters for variable arguments +function sum(...numbers: number[]): number { + return numbers.reduce((total, num) => total + num, 0); +} +``` + +### Functions and Methods - Single Responsibility + +```ts +// Bad: Too many responsibilities +function processUserData(userData: any) { + // Validation + if (!userData || !userData.name) throw new Error('Invalid user data'); + + // Data transformation + const processedData = { + ...userData, + name: userData.name.trim(), + createdAt: new Date() + }; + + // Side effect + saveToDatabase(processedData); + + // Notification + sendNotification(processedData.email, 'Profile updated'); + + return processedData; +} + +// Better: Split into smaller, focused functions +function validateUserData(data: unknown): UserData { + if (!data || typeof data !== 'object') { + throw new Error('Invalid user data'); + } + return data as UserData; +} + +function processUserData(userData: UserData): ProcessedUserData { + return { + ...userData, + name: userData.name.trim(), + createdAt: new Date() + }; +} +``` + +### Async/Await Patterns - Proper Error Handling + +```ts +// Bad: Not handling errors +async function fetchData() { + const response = await fetch('/api/data'); + return response.json(); +} + +// Good: Proper error handling +async function fetchData(url: string): Promise { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json() as T; + } catch (error) { + console.error('Failed to fetch data:', error); + throw error; // Re-throw to allow caller to handle + } +} + +// Better: Use Promise.all for parallel operations +async function fetchMultipleData(urls: string[]): Promise { + try { + const promises = urls.map(url => fetchData(url)); + return await Promise.all(promises); + } catch (error) { + console.error('One or more requests failed:', error); + throw error; + } +} + +// Example usage +interface User { + id: string; + name: string; + email: string; +} + +// Fetch user data with proper typing +async function getUserData(userId: string): Promise { + return fetchData(`/api/users/${userId}`); +} +``` + +### Async/Await Patterns - Flatten Code + +```ts +// Bad: Nested async/await (callback hell) +async function processUser(userId: string) { + const user = await getUser(userId); + if (user) { + const orders = await getOrders(user.id); + if (orders.length > 0) { + const latestOrder = orders[0]; + const items = await getOrderItems(latestOrder.id); + return { user, latestOrder, items }; + } + } + return null; +} + +// Better: Flatten the async/await chain +async function processUser(userId: string) { + const user = await getUser(userId); + if (!user) return null; + + const orders = await getOrders(user.id); + if (orders.length === 0) return { user, latestOrder: null, items: [] }; + + const latestOrder = orders[0]; + const items = await getOrderItems(latestOrder.id); + + return { user, latestOrder, items }; +} + +// Best: Use Promise.all for independent async operations +async function processUser(userId: string) { + const [user, orders] = await Promise.all([ + getUser(userId), + getOrders(userId) + ]); + + if (!user) return null; + if (orders.length === 0) return { user, latestOrder: null, items: [] }; + + const latestOrder = orders[0]; + const items = await getOrderItems(latestOrder.id); + + return { user, latestOrder, items }; +} +``` + +### Testing and Quality - Dependency Injection + +```ts +// Bad: Hard to test due to direct dependencies +class PaymentProcessor { + async processPayment(amount: number) { + const paymentGateway = new PaymentGateway(); + return paymentGateway.charge(amount); + } +} + +// Better: Use dependency injection +interface PaymentGateway { + charge(amount: number): Promise; +} + +class PaymentProcessor { + constructor(private paymentGateway: PaymentGateway) {} + + async processPayment(amount: number): Promise { + if (amount <= 0) { + throw new Error('Amount must be greater than zero'); + } + return this.paymentGateway.charge(amount); + } +} + +// Test example with Jest +describe('PaymentProcessor', () => { + let processor: PaymentProcessor; + let mockGateway: jest.Mocked; + + beforeEach(() => { + mockGateway = { + charge: jest.fn() + }; + processor = new PaymentProcessor(mockGateway); + }); + + it('should process a valid payment', async () => { + mockGateway.charge.mockResolvedValue(true); + const result = await processor.processPayment(100); + expect(result).toBe(true); + expect(mockGateway.charge).toHaveBeenCalledWith(100); + }); + + it('should throw for invalid amount', async () => { + await expect(processor.processPayment(-50)) + .rejects.toThrow('Amount must be greater than zero'); + }); +}); +``` + +### Testing and Quality - Type Testing + +```ts +// Using @ts-expect-error to test for type errors +// @ts-expect-error - Should not allow negative values +const invalidUser: User = { id: -1, name: 'Test' }; + +// Using type assertions in tests +function assertIsString(value: unknown): asserts value is string { + if (typeof value !== 'string') { + throw new Error('Not a string'); + } +} + +// Using utility types for testing +type IsString = T extends string ? true : false; +type Test1 = IsString; // true +type Test2 = IsString; // false + +// Using tsd for type testing (install with: npm install --save-dev tsd) +/* +import { expectType } from 'tsd'; + +const user = { id: 1, name: 'John' }; +expectType<{ id: number; name: string }>(user); +expectType(user.name); +*/ +``` + +### Performance - Type-Only Imports + +```ts +// Bad: Imports both type and value +import { User, fetchUser } from './api'; + +// Good: Separate type and value imports +import type { User } from './api'; +import { fetchUser } from './api'; + +// Even better: Use type-only imports when possible +import type { User, UserSettings } from './types'; + +// Type-only export +export type { User }; + +// Runtime export +export { fetchUser }; + +// In tsconfig.json, enable "isolatedModules": true +// to ensure type-only imports are properly handled +``` + +### Performance - Avoid Complex Types + +```ts +// Bad: Deeply nested mapped types can be slow +type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; + +// Better: Use built-in utility types when possible +type User = { + id: string; + profile: { + name: string; + email: string; + }; + preferences?: { + notifications: boolean; + }; +}; + +// Instead of DeepPartial, use Partial with type assertions +const updateUser = (updates: Partial) => { + // Implementation +}; + +// For complex types, consider using interfaces +interface UserProfile { + name: string; + email: string; +} + +interface UserPreferences { + notifications: boolean; +} + +interface User { + id: string; + profile: UserProfile; + preferences?: UserPreferences; +} +``` + +### Performance - Const Assertions + +```ts +// Without const assertion (wider type) +const colors = ['red', 'green', 'blue']; +// Type: string[] + +// With const assertion (narrower, more precise type) +const colors = ['red', 'green', 'blue'] as const; +// Type: readonly ["red", "green", "blue"] + +// Extract union type from const array +type Color = typeof colors[number]; // "red" | "green" | "blue" + +// Objects with const assertions +const config = { + apiUrl: 'https://api.example.com', + timeout: 5000, + features: ['auth', 'notifications'], +} as const; + +// Type is: +// { +// readonly apiUrl: "https://api.example.com"; +// readonly timeout: 5000; +// readonly features: readonly ["auth", "notifications"]; +// } +``` + +### Common Mistakes - Avoid 'any' + +```ts +// Bad: Loses all type safety +function process(data: any) { + return data.map(item => item.name); +} + +// Better: Use generics for type safety +function process(items: T[]) { + return items.map(item => item.name); +} + +// Best: Use specific types when possible +interface User { + name: string; + age: number; +} + +function processUsers(users: User[]) { + return users.map(user => user.name); +} +``` + +### Common Mistakes - Enable Strict Mode + +```json +// tsconfig.json +{ + "compilerOptions": { + "strict": true, + /* Additional strictness flags */ + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true + } +} +``` + +### Common Mistakes - Let TypeScript Infer + +```ts +// Redundant type annotation +const name: string = 'John'; + +// Let TypeScript infer the type +const name = 'John'; // TypeScript knows it's a string + +// Redundant return type +function add(a: number, b: number): number { + return a + b; +} + +// Let TypeScript infer the return type +function add(a: number, b: number) { + return a + b; // TypeScript infers number +} +``` + +### Common Mistakes - Use Type Guards + +```ts +// Without type guard +function process(input: string | number) { + return input.toUpperCase(); // Error: toUpperCase doesn't exist on number +} + +// With type guard +function isString(value: unknown): value is string { + return typeof value === 'string'; +} + +function process(input: string | number) { + if (isString(input)) { + return input.toUpperCase(); // TypeScript knows input is string here + } else { + return input.toFixed(2); // TypeScript knows input is number here + } +} + +// Built-in type guards +if (typeof value === 'string') { /* value is string */ } +if (value instanceof Date) { /* value is Date */ } +if ('id' in user) { /* user has id property */ } +``` + +### Common Mistakes - Handle Null/Undefined + +```ts +// Bad: Potential runtime error +function getLength(str: string | null) { + return str.length; // Error: Object is possibly 'null' +} + +// Good: Null check +function getLength(str: string | null) { + if (str === null) return 0; + return str.length; +} + +// Better: Use optional chaining and nullish coalescing +function getLength(str: string | null) { + return str?.length ?? 0; +} + +// For arrays +const names: string[] | undefined = []; +const count = names?.length ?? 0; // Safely handle undefined + +// For object properties +interface User { + profile?: { + name?: string; + }; +} + +const user: User = {}; +const name = user.profile?.name ?? 'Anonymous'; +``` diff --git a/skills/typescript-coder/references/types.md b/skills/typescript-coder/references/types.md new file mode 100644 index 000000000..59405da3b --- /dev/null +++ b/skills/typescript-coder/references/types.md @@ -0,0 +1,1897 @@ +# TypeScript Types + +## TypeScript Advanced Types + +- Reference material for [Advanced Types](https://www.w3schools.com/typescript/typescript_advanced_types.php) +- See [Creating Types from Types](https://www.typescriptlang.org/docs/handbook/2/types-from-types.html) for additional information +- See [Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) for additional information + +### Mapped Types + +```ts +// Convert all properties to boolean +type Flags = { + [K in keyof T]: boolean; +}; + +interface User { + id: number; + name: string; + email: string; +} + +type UserFlags = Flags; +// Equivalent to: +// { +// id: boolean; +// name: boolean; +// email: boolean; +// } +``` + +```ts +// Make all properties optional +interface Todo { + title: string; + description: string; + completed: boolean; +} + +type OptionalTodo = { + [K in keyof Todo]?: Todo[K]; +}; + +// Remove 'readonly' and '?' modifiers +type Concrete = { + -readonly [K in keyof T]-?: T[K]; +}; + +// Add 'readonly' and 'required' to all properties +type ReadonlyRequired = { + +readonly [K in keyof T]-?: T[K]; +}; +``` + +```ts +// Add prefix to all property names +type Getters = { + [K in keyof T as `get${Capitalize}`]: () => T[K]; +}; + +type UserGetters = Getters; +// { +// getId: () => number; +// getName: () => string; +// getEmail: () => string; +// } + +// Filter out properties +type MethodsOnly = { + [K in keyof T as T[K] extends Function ? K : never]: T[K]; +}; +``` + +### Conditional Types + +```ts +type IsString = T extends string ? true : false; + +type A = IsString; // true +type B = IsString; // false +type C = IsString<'hello'>; // true +type D = IsString; // boolean + +// Extract array element type +type ArrayElement = T extends (infer U)[] ? U : never; +type Numbers = ArrayElement; // number +``` + +```ts +// Get return type of a function +type ReturnType = T extends (...args: any[]) => infer R ? R : any; + +// Get parameter types as a tuple +type Parameters = T extends (...args: infer P) => any ? P : never; + +// Get constructor parameter types +type ConstructorParameters any> = + T extends new (...args: infer P) => any ? P : never; + +// Get instance type from a constructor +type InstanceType any> = + T extends new (...args: any) => infer R ? R : any; +``` + +```ts +// Without distribution +type ToArrayNonDist = T extends any ? T[] : never; +type StrOrNumArr = ToArrayNonDist; // (string | number)[] + +// With distribution +type ToArray = [T] extends [any] ? T[] : never; +type StrOrNumArr2 = ToArray; // string[] | number[] + +// Filter out non-string types +type FilterStrings = T extends string ? T : never; +type Letters = FilterStrings<'a' | 'b' | 1 | 2 | 'c'>; // 'a' | 'b' | 'c' +``` + +### Template Literal Types + +```ts +type Greeting = `Hello, ${string}`; + +const validGreeting: Greeting = 'Hello, World!'; +const invalidGreeting: Greeting = 'Hi there!'; // Error + +// With unions +type Color = 'red' | 'green' | 'blue'; +type Size = 'small' | 'medium' | 'large'; + +type Style = `${Color}-${Size}`; +// 'red-small' | 'red-medium' | 'red-large' | +// 'green-small' | 'green-medium' | 'green-large' | +// 'blue-small' | 'blue-medium' | 'blue-large' +``` + +```ts +// Built-in string manipulation types +type T1 = Uppercase<'hello'>; // 'HELLO' +type T2 = Lowercase<'WORLD'>; // 'world' +type T3 = Capitalize<'typescript'>; // 'Typescript' +type T4 = Uncapitalize<'TypeScript'>; // 'typeScript' + +// Create an event handler type +type EventType = 'click' | 'change' | 'keydown'; +type EventHandler = `on${Capitalize}`; +// 'onClick' | 'onChange' | 'onKeydown' +``` + +```ts +// Extract route parameters +type ExtractRouteParams = + T extends `${string}:${infer Param}/${infer Rest}` + ? { [K in Param | keyof ExtractRouteParams<`${Rest}`>]: string } + : T extends `${string}:${infer Param}` + ? { [K in Param]: string } + : {}; + +type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>; +// { userId: string; postId: string; } + +// Create a type-safe event emitter +type EventMap = { + click: { x: number; y: number }; + change: string; + keydown: { key: string; code: number }; +}; + +type EventHandlers = { + [K in keyof EventMap as `on${Capitalize}`]: (event: EventMap[K]) => void; +}; +``` + +### Utility Types + +```ts +// Basic types +interface User { + id: number; + name: string; + email: string; + createdAt: Date; +} + +// Make all properties optional +type PartialUser = Partial; + +// make all properties required +type RequiredUser = Required; + +// make all properties read-only +type ReadonlyUser = Readonly; + +// pick specific properties +type UserPreview = Pick; + +// omit specific properties +type UserWithoutEmail = Omit; + +// extract property types +type UserId = User['id']; // number +type UserKeys = keyof User; // 'id' | 'name' | 'email' | 'createdAt' +``` + +```ts +// Create a type that excludes null and undefined +type NonNullable = T extends null | undefined ? never : T; + +// Exclude types from a union +type Numbers = 1 | 2 | 3 | 'a' | 'b'; +type JustNumbers = Exclude; // 1 | 2 | 3 + +// Extract types from a union +type JustStrings = Extract; // 'a' | 'b' + +// Get the type that is not in the second type +type A = { a: string; b: number; c: boolean }; +type B = { a: string; b: number }; +type C = Omit; // { c: boolean } + +// Create a type with all properties as mutable +type Mutable = { + -readonly [K in keyof T]: T[K]; +}; +``` + +### Recursive Types + +```ts +// Simple binary tree +type BinaryTree = { + value: T; + left?: BinaryTree; + right?: BinaryTree; +}; + +// JSON-like data structure +type JSONValue = + | string + | number + | boolean + | null + | JSONValue[] + | { [key: string]: JSONValue }; + +// Nested comments +type Comment = { + id: number; + content: string; + replies: Comment[]; + createdAt: Date; +}; +``` + +```ts +// Type for a linked list +type LinkedList = { + value: T; + next: LinkedList | null; +}; + +// Type for a directory structure +type File = { + type: 'file'; + name: string; + size: number; +}; + +type Directory = { + type: 'directory'; + name: string; + children: (File | Directory)[]; +}; + +// Type for a state machine +type State = { + value: string; + transitions: { + [event: string]: State; + }; +}; + +// Type for a recursive function +type RecursiveFunction = (x: T | RecursiveFunction) => void; +``` + +## TypeScript Type Guards + +- Reference material for [Type Guards](https://www.w3schools.com/typescript/typescript_type_guards.php) +- See [Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) for additional information +- See [Typeof Type Operator](https://www.typescriptlang.org/docs/handbook/2/typeof-types.html) for additional information + +### typeof Type Guards + +```ts +// Simple type guard with typeof +function formatValue(value: string | number): string { + if (typeof value === 'string') { + // TypeScript knows value is string here + return value.trim().toUpperCase(); + } else { + // TypeScript knows value is number here + return value.toFixed(2); + } +} + +// Example usage +const result1 = formatValue(' hello '); // "HELLO" +const result2 = formatValue(42.1234); // "42.12" +``` + +### instanceof Type Guards + +```ts +class Bird { + fly() { + console.log("Flying..."); + } +} + +class Fish { + swim() { + console.log("Swimming..."); + } +} + +function move(animal: Bird | Fish) { + if (animal instanceof Bird) { + // TypeScript knows animal is Bird here + animal.fly(); + } else { + // TypeScript knows animal is Fish here + animal.swim(); + } +} +``` + +### User-Defined Type Guards + +```ts +interface Car { + make: string; + model: string; + year: number; +} + +interface Motorcycle { + make: string; + model: string; + year: number; + type: "sport" | "cruiser"; +} + +// Type predicate function +function isCar(vehicle: Car | Motorcycle): vehicle is Car { + return (vehicle as Motorcycle).type === undefined; +} + +function displayVehicleInfo(vehicle: Car | Motorcycle) { + console.log(`Make: ${vehicle.make}, Model: ${vehicle.model}, Year: ${vehicle.year}`); + + if (isCar(vehicle)) { + // TypeScript knows vehicle is Car here + console.log("This is a car"); + } else { + // TypeScript knows vehicle is Motorcycle here + console.log(`This is a ${vehicle.type} motorcycle`); + } +} +``` + +### Discriminated Unions + +```ts +interface Circle { + kind: "circle"; + radius: number; +} + +interface Square { + kind: "square"; + sideLength: number; +} + +type Shape = Circle | Square; + +function calculateArea(shape: Shape) { + switch (shape.kind) { + case "circle": + // TypeScript knows shape is Circle here + return Math.PI * shape.radius ** 2; + case "square": + // TypeScript knows shape is Square here + return shape.sideLength ** 2; + } +} +``` + +### 'in' Operator Type Guards + +```ts +interface Dog { + bark(): void; +} + +interface Cat { + meow(): void; +} + +function makeSound(animal: Dog | Cat) { + if ("bark" in animal) { + // TypeScript knows animal is Dog here + animal.bark(); + } else { + // TypeScript knows animal is Cat here + animal.meow(); + } +} +``` + +### Assertion Functions + +```ts +// Type assertion function +function assertIsString(value: unknown): asserts value is string { + if (typeof value !== 'string') { + throw new Error('Value is not a string'); + } +} + +// Type assertion function with custom error +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} + +// Usage +function processInput(input: unknown) { + assertIsString(input); + // input is now typed as string + console.log(input.toUpperCase()); +} + +// With custom error +function processNumber(value: unknown): number { + assert(typeof value === 'number', 'Value must be a number'); + // value is now typed as number + return value * 2; +} +``` + +## Advanced Conditional Types + +- Reference material for [Conditional Types](https://www.w3schools.com/typescript/typescript_conditional_types.php) +- See [Conditional Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) for additional information + +### Basic Conditional Type Syntax + +```ts +type IsString = T extends string ? true : false; + +// Usage examples +type Result1 = IsString; // true +type Result2 = IsString; // false +type Result3 = IsString<"hello">; // true (literal types extend their base types) + +// We can use this with variables too +let a: IsString; // a has type 'true' +let b: IsString; // b has type 'false' +``` + +### Conditional Types with Unions + +```ts +type ToArray = T extends any ? T[] : never; + +// When used with a union type, it applies to each member of the union +type StringOrNumberArray = ToArray; +// This becomes ToArray | ToArray +// Which becomes string[] | number[] + +// We can also extract specific types from a union +type ExtractString = T extends string ? T : never; +type StringsOnly = ExtractString; +// Result: string | "hello" +``` + +### Infer keyword with conditional types + +```ts +// Extract the return type of a function type +type ReturnType = T extends (...args: any[]) => infer R ? R : never; + +// Examples +function greet() { return "Hello, world!"; } +function getNumber() { return 42; } + +type GreetReturnType = ReturnType; // string +type NumberReturnType = ReturnType; // number + +// Extract element type from array +type ElementType = T extends (infer U)[] ? U : never; +type NumberArrayElement = ElementType; // number +type StringArrayElement = ElementType; // string +``` + +### Built-in Conditional Types + +```ts +// Extract - Extracts types from T that are assignable to U +type OnlyStrings = Extract; // string + +// Exclude - Excludes types from T that are assignable to U +type NoStrings = Exclude; // number | boolean + +// NonNullable - Removes null and undefined from T +type NotNull = NonNullable; // string + +// Parameters - Extracts parameter types from a function type +type Params = Parameters<(a: string, b: number) => void>; // [string, number] + +// ReturnType - Extracts the return type from a function type +type Return = ReturnType<() => string>; // string +``` + +### Advanced Patterns and Techniques + +```ts +// Deeply unwrap Promise types +type UnwrapPromise = T extends Promise ? UnwrapPromise : T; + +// Examples +type A = UnwrapPromise>; // string +type B = UnwrapPromise>>; // number +type C = UnwrapPromise; // boolean +``` + +### Type name mapping + +```ts +type TypeName = + T extends string ? "string" : + T extends number ? "number" : + T extends boolean ? "boolean" : + T extends undefined ? "undefined" : + T extends Function ? "function" : + "object"; + +// Usage +type T0 = TypeName; // "string" +type T1 = TypeName<42>; // "number" +type T2 = TypeName; // "boolean" +type T3 = TypeName<() => void>; // "function" +type T4 = TypeName; // "object" +``` + +### Conditional return types + +```ts +// A function that returns different types based on input type +function processValue(value: T): T extends string + ? string + : T extends number + ? number + : T extends boolean + ? boolean + : never { + + if (typeof value === "string") { + return value.toUpperCase() as any; // Type assertion needed due to limitations + } else if (typeof value === "number") { + return (value * 2) as any; + } else if (typeof value === "boolean") { + return (!value) as any; + } else { + throw new Error("Unsupported type"); + } +} + +// Usage +const stringResult = processValue("hello"); // Returns "HELLO" (type is string) +const numberResult = processValue(10); // Returns 20 (type is number) +const boolResult = processValue(true); // Returns false (type is boolean) +``` + +## Advanced Mapped Types + +- Reference material for [Mapped Types](https://www.w3schools.com/typescript/typescript_mapped_types.php) +- See [Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) for additional information +- See [Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) for additional information + +### Type Syntax Example + +```ts +// Small example +type Person = { name: string; age: number }; +type PartialPerson = { [P in keyof Person]?: Person[P] }; +type ReadonlyPerson = { readonly [P in keyof Person]: Person[P] }; +``` + +### Basic Mapped Type Syntax + +```ts +// Define an object type +interface Person { + name: string; + age: number; + email: string; +} + +// Create a mapped type that makes all properties optional +type PartialPerson = { + [P in keyof Person]?: Person[P]; +}; + +// Usage +const partialPerson: PartialPerson = { + name: "John" + // age and email are optional +}; + +// Create a mapped type that makes all properties readonly +type ReadonlyPerson = { + readonly [P in keyof Person]: Person[P]; +}; + +// Usage +const readonlyPerson: ReadonlyPerson = { + name: "Alice", + age: 30, + email: "alice@example.com" +}; + +// readonlyPerson.age = 31; +// Error: Cannot assign to 'age' because it is a read-only property +``` + +### Built-in Mapped Types + +```ts +interface User { + id: number; + name: string; + email: string; + isAdmin: boolean; +} + +// Partial - Makes all properties optional +type PartialUser = Partial; +// Equivalent to: { id?: number; name?: string; email?: string; isAdmin?: boolean; } + +// Required - Makes all properties required +type RequiredUser = Required>; +// Equivalent to: { id: number; name: string; email: string; isAdmin: boolean; } + +// Readonly - Makes all properties readonly +type ReadonlyUser = Readonly; +// Equivalent to: { readonly id: number; readonly name: string; ... } + +// Pick - Creates a type with a subset of properties from T +type UserCredentials = Pick; +// Equivalent to: { email: string; id: number; } + +// Omit - Creates a type by removing specified properties from T +type PublicUser = Omit; +// Equivalent to: { name: string; email: string; } + +// Record - Creates a type with specified keys and value types +type UserRoles = Record<"admin" | "user" | "guest", string>; +// Equivalent to: { admin: string; user: string; guest: string; } +``` + +### Creating Custom Mapped Types + +```ts +// Base interface +interface Product { + id: number; + name: string; + price: number; + inStock: boolean; +} + +// Create a mapped type to convert all properties to string type +type StringifyProperties = { + [P in keyof T]: string; +}; + +// Usage +type StringProduct = StringifyProperties; +// Equivalent to: { id: string; name: string; price: string; inStock: string; } + +// Create a mapped type that adds validation functions for each property +type Validator = { + [P in keyof T]: (value: T[P]) => boolean; +}; + +// Usage +const productValidator: Validator = { + id: (id) => id > 0, + name: (name) => name.length > 0, + price: (price) => price >= 0, + inStock: (inStock) => typeof inStock === "boolean" +}; +``` + +### Modifying Property Modifiers + +```ts +// Base interface with some readonly and optional properties +interface Configuration { + readonly apiKey: string; + readonly apiUrl: string; + timeout?: number; + retries?: number; +} + +// Remove readonly modifier from all properties +type Mutable = { + -readonly [P in keyof T]: T[P]; +}; + +// Usage +type MutableConfig = Mutable; +// Equivalent to: +/* { + apiKey: string; + apiUrl: string; + timeout?: number; + retries?: number; + } + */ + +// Make all optional properties required +type RequiredProps = { + [P in keyof T]-?: T[P]; +}; + +// Usage +type RequiredConfig = RequiredProps; +// Equivalent to: + /* { + readonly apiKey: string; + readonly apiUrl: string; + timeout: number; + retries: number; + } +*/ +``` + +### Conditional Mapped Types + +```ts +// Base interface +interface ApiResponse { + data: unknown; + status: number; + message: string; + timestamp: number; +} + +// Conditional mapped type: Convert each numeric property to a formatted string +type FormattedResponse = { + [P in keyof T]: T[P] extends number ? string : T[P]; +}; + +// Usage +type FormattedApiResponse = FormattedResponse; +// Equivalent to: +/* { + data: unknown; + status: string; + message: string; + timestamp: string; + } +*/ + +// Another example: Filter for only string properties +type StringPropsOnly = { + [P in keyof T as T[P] extends string ? P : never]: T[P]; +}; + +// Usage +type ApiResponseStringProps = StringPropsOnly; +// Equivalent to: { message: string; } +``` + +## TypeScript Type Inference + +- Reference material for [Type Inference](https://www.w3schools.com/typescript/typescript_type_inference.php) +- See [Type Inference](https://www.typescriptlang.org/docs/handbook/type-inference.html) for additional information + +### Understanding Type Inference in TypeScript + +```ts +// TypeScript infers these variable types +let name = "Alice"; // inferred as string +let age = 30; // inferred as number +let isActive = true; // inferred as boolean +let numbers = [1, 2, 3]; // inferred as number[] +let mixed = [1, "two", true]; // inferred as (string | number | boolean)[] + +// Using the inferred types +name.toUpperCase(); // Works because name is inferred as string +age.toFixed(2); // Works because age is inferred as number +// name.toFixed(2); + // Error: Property 'toFixed' does not exist on type 'string' +``` + +### Function Return Type Inference + +```ts +// Return type is inferred as string +function greet(name: string) { + return `Hello, ${name}!`; +} + +// Return type is inferred as number +function add(a: number, b: number) { + return a + b; +} + +// Return type is inferred as string | number +function getValue(key: string) { + if (key === "name") { + return "Alice"; + } else { + return 42; + } +} +// Using the inferred return types +let greeting = greet("Bob"); // inferred as string +let sum = add(5, 3); // inferred as number +let value = getValue("age"); // inferred as string | number +``` + +### Contextual Typing + +```ts +// The type of the callback parameter is inferred from the array method context +const names = ["Alice", "Bob", "Charlie"]; + +// Parameter 'name' is inferred as string +names.forEach(name => { + console.log(name.toUpperCase()); +}); + +// Parameter 'name' is inferred as string, and the return type is inferred as number +const nameLengths = names.map(name => { + return name.length; +}); + +// nameLengths is inferred as number[] + +// Parameter types in event handlers are also inferred +document.addEventListener("click", event => { + // 'event' is inferred as MouseEvent + console.log(event.clientX, event.clientY); +}); +``` + +### Type Inference in Object Literals + +```ts +// TypeScript infers the type of this object +const user = { + id: 1, + name: "Alice", + email: "alice@example.com", + active: true, + details: { + age: 30, + address: { + city: "New York", + country: "USA" + } + } +}; + +// Accessing inferred properties +console.log(user.name.toUpperCase()); +console.log(user.details.age.toFixed(0)); +console.log(user.details.address.city.toLowerCase()); + +// Type errors would be caught +// console.log(user.age); + // Error: Property 'age' does not exist on type '...' +// console.log(user.details.name); + // Error: Property 'name' does not exist on type '...' +// console.log(user.details.address.zip); + // Error: Property 'zip' does not exist on type '...' +``` + +### Advanced Patterns - Const Assertions + +```ts +// Regular type inference (widens to string) +let name = "Alice"; // type: string + +// Const assertion (narrows to literal type) +const nameConst = "Alice" as const; // type: "Alice" + +// With objects +const user = { + id: 1, + name: "Alice", + roles: ["admin", "user"] as const // readonly tuple +} as const; + +// user.name = "Bob"; // Error: Cannot assign to 'name' because it is a read-only property +``` + +### Advanced Patterns - Type Narrowing + +```ts +function processValue(value: string | number) { + // Type is narrowed to string in this block + if (typeof value === "string") { + console.log(value.toUpperCase()); + } + // Type is narrowed to number here + else { + console.log(value.toFixed(2)); + } +} + +// Discriminated unions +interface Circle { kind: "circle"; radius: number; } +interface Square { kind: "square"; size: number; } +type Shape = Circle | Square; + +function area(shape: Shape) { + // Type is narrowed based on 'kind' property + switch (shape.kind) { + case "circle": + return Math.PI * shape.radius ** 2; + case "square": + return shape.size ** 2; + } +} +``` + +### Best Practices + +```ts +// 1. Let TypeScript infer simple types +let message = "Hello"; // Good: no need for explicit type here + +// 2. Provide explicit types for function parameters +function formatName(firstName: string, lastName: string) { + return `${firstName} ${lastName}`; +} + +// 3. Consider adding return type annotations for complex functions +function processData(input: string[]): { count: number; items: string[] } { + return { + count: input.length, + items: input.map(item => item.trim()) + }; +} + +// 4. Use explicit type annotations for empty arrays or objects +const emptyArray: string[] = []; // Without annotation, inferred as any[] +const configOptions: Record = {}; // Without annotation, inferred as {} + +// 5. Use type assertions when TypeScript cannot infer correctly +const canvas = document.getElementById("main-canvas") as HTMLCanvasElement; +``` + +```ts +// Good: Explicit type for complex return values +function processData(input: string[]): { results: string[]; count: number } { + return { + results: input.map(processItem), + count: input.length + }; +} + +// Good: Explicit type for empty arrays +const items: Array<{ id: number; name: string }> = []; + +// Good: Explicit type for configuration objects +const config: { + apiUrl: string; + retries: number; + timeout: number; +} = { + apiUrl: "https://api.example.com", + retries: 3, + timeout: 5000 +}; +``` + +## Advanced Literal Types + +- Reference material for [Literal Types](https://www.w3schools.com/typescript/typescript_literal_types.php) +- See [Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) for additional information +- See [Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) for additional information + +### String Literal Types + +```ts +// A variable with a string literal type +let direction: "north" | "south" | "east" | "west"; + +// Valid assignments +direction = "north"; +direction = "south"; + +// Invalid assignments would cause errors +// direction = "northeast"; +// Error: Type '"northeast"' is not assignable to + // type '"north" | "south" | "east" | "west"' +// direction = "up"; +// Error: Type '"up"' is not assignable to + // type '"north" | "south" | "east" | "west"' + +// Using string literal types in functions +function move(direction: "north" | "south" | "east" | "west") { + console.log(`Moving ${direction}`); +} + +move("east"); // Valid +// move("up"); +// Error: Argument of type '"up"' is not assignable to parameter of type... +``` + +### Numeric Literal Types + +```ts +// A variable with a numeric literal type +let diceRoll: 1 | 2 | 3 | 4 | 5 | 6; + +// Valid assignments +diceRoll = 1; +diceRoll = 6; + +// Invalid assignments would cause errors +// diceRoll = 0; +// Error: Type '0' is not assignable to type '1 | 2 | 3 | 4 | 5 | 6' +// diceRoll = 7; +// Error: Type '7' is not assignable to type '1 | 2 | 3 | 4 | 5 | 6' +// diceRoll = 2.5; +// Error: Type '2.5' is not assignable to type '1 | 2 | 3 | 4 | 5 | 6' + +// Using numeric literal types in functions +function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 { + return Math.floor(Math.random() * 6) + 1 as 1 | 2 | 3 | 4 | 5 | 6; +} + +const result = rollDice(); +console.log(`You rolled a ${result}`); +``` + +### Boolean Literal Types + +```ts +// A type that can only be the literal value 'true' +type YesOnly = true; + +// A function that must return true +function alwaysSucceed(): true { + // Always returns the literal value 'true' + return true; +} + +// Boolean literal combined with other types +type SuccessFlag = true | "success" | 1; +type FailureFlag = false | "failure" | 0; + +function processResult(result: SuccessFlag | FailureFlag) { + if (result === true || result === "success" || result === 1) { + console.log("Operation succeeded"); + } else { + console.log("Operation failed"); + } +} + +processResult(true); // "Operation succeeded" +processResult("success"); // "Operation succeeded" +processResult(1); // "Operation succeeded" +processResult(false); // "Operation failed" +``` + +### Literal Types with Objects + +```ts +// Object with literal property values +type HTTPSuccess = { + status: 200 | 201 | 204; + statusText: "OK" | "Created" | "No Content"; + data: any; +}; + +type HTTPError = { + status: 400 | 401 | 403 | 404 | 500; + statusText: "Bad Request" | + "Unauthorized" | + "Forbidden" | + "Not Found" | + "Internal Server Error"; + error: string; +}; + +type HTTPResponse = HTTPSuccess | HTTPError; + +function handleResponse(response: HTTPResponse) { + if (response.status >= 200 && response.status < 300) { + console.log(`Success: ${response.statusText}`); + console.log(response.data); + } else { + console.log(`Error ${response.status}: ${response.statusText}`); + console.log(`Message: ${response.error}`); + } +} + +// Example usage +const successResponse: HTTPSuccess = { + status: 200, + statusText: "OK", + data: { username: "john_doe", email: "john@example.com" } +}; + +const errorResponse: HTTPError = { + status: 404, + statusText: "Not Found", + error: "User not found in database" +}; + +handleResponse(successResponse); +handleResponse(errorResponse); +``` + +### Template Literal Types + +```ts +// Basic template literals +type Direction = "north" | "south" | "east" | "west"; +type Distance = "1km" | "5km" | "10km"; + +// Using template literals to combine them +type DirectionAndDistance = `${Direction}-${Distance}`; +// "north-1km" | "north-5km" | "north-10km" | "south-1km" | ... + +let route: DirectionAndDistance; +route = "north-5km"; // Valid +route = "west-10km"; // Valid +// route = "north-2km"; // Error +// route = "5km-north"; // Error + +// Advanced string manipulation +type EventType = "click" | "hover" | "scroll"; +type EventTarget = "button" | "link" | "div"; +type EventName = `on${Capitalize}${Capitalize}`; +// "onClickButton" | "onClickLink" | "onClickDiv" | ... + +// Dynamic property access +type User = { + id: number; + name: string; + email: string; + createdAt: Date; +}; + +type GetterName = `get${Capitalize}`; +type UserGetters = { + [K in keyof User as GetterName]: () => User[K]; +}; +// { getId: () => number; getName: () => string; ... } + +// String pattern matching +type ExtractRouteParams = + T extends `${string}:${infer Param}/${infer Rest}` + ? Param | ExtractRouteParams + : T extends `${string}:${infer Param}` + ? Param + : never; + +type Params = ExtractRouteParams<"/users/:userId/posts/:postId">; +// "userId" | "postId" + +// CSS units and values +type CssUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw'; +type CssValue = `${number}${CssUnit}`; + +let width: CssValue = '100px'; // Valid +let height: CssValue = '50%'; // Valid +// let margin: CssValue = '10'; // Error +// let padding: CssValue = '2ex'; // Error + +// API versioning +type ApiVersion = 'v1' | 'v2' | 'v3'; +type Endpoint = 'users' | 'products' | 'orders'; +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; + +type ApiUrl = `https://api.example.com/${ApiVersion}/${Endpoint}`; + +// Complex example: Dynamic SQL query builder +type Table = 'users' | 'products' | 'orders'; +type Column = + T extends 'users' ? 'id' | 'name' | 'email' | 'created_at' : + T extends 'products' ? 'id' | 'name' | 'price' | 'in_stock' : + T extends 'orders' ? 'id' | 'user_id' | 'total' | 'status' : never; + +type WhereCondition = { + [K in Column]?: { + equals?: any; + notEquals?: any; + in?: any[]; + }; +}; + +function query( + table: T, + where?: WhereCondition +): `SELECT * FROM ${T}${string}` { + // Implementation would build the query + return `SELECT * FROM ${table}` as const; +} + +// Usage +const userQuery = query('users', { + name: { equals: 'John' }, + created_at: { in: ['2023-01-01', '2023-12-31'] } +}); +// Type: "SELECT * FROM users WHERE ..." +``` + +## TypeScript Namespaces + +- Reference material for [Namespaces](https://www.w3schools.com/typescript/typescript_namespaces.php) +- See [Namespaces](https://www.typescriptlang.org/docs/handbook/namespaces.html) for additional information +- See [Namespaces and Modules](https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html) for additional information + +### Basic Namespace Syntax + +```ts +namespace Validation { + // Everything inside this block belongs to the Validation namespace + + // Export things you want to make available outside the namespace + export interface StringValidator { + isValid(s: string): boolean; + } + + // This is private to the namespace (not exported) + const lettersRegexp = /^[A-Za-z]+$/; + + // Exported class - available outside the namespace + export class LettersValidator implements StringValidator { + isValid(s: string): boolean { + return lettersRegexp.test(s); + } + } + + // Another exported class + export class ZipCodeValidator implements StringValidator { + isValid(s: string): boolean { + return /^[0-9]+$/.test(s) && s.length === 5; + } + } +} + +// Using the namespace members +let letterValidator = new Validation.LettersValidator(); +let zipCodeValidator = new Validation.ZipCodeValidator(); + +console.log(letterValidator.isValid("Hello")); // true +console.log(letterValidator.isValid("Hello123")); // false + +console.log(zipCodeValidator.isValid("12345")); // true +console.log(zipCodeValidator.isValid("1234")); // false - wrong length +``` + +### Nested Namespaces + +```ts +namespace App { + export namespace Utils { + export function log(msg: string): void { + console.log(`[LOG]: ${msg}`); + } + + export function error(msg: string): void { + console.error(`[ERROR]: ${msg}`); + } + } + + export namespace Models { + export interface User { + id: number; + name: string; + email: string; + } + + export class UserService { + getUser(id: number): User { + return { id, name: "John Doe", email: "john@example.com" }; + } + } + } +} + +// Using nested namespaces +App.Utils.log("Application starting"); + +const userService = new App.Models.UserService(); +const user = userService.getUser(1); + +App.Utils.log(`User loaded: ${user.name}`); + +// This would be a type error in TypeScript +// App.log("directly accessing log"); // Error - log is not a direct member of App +``` + +### Namespace Aliases + +```ts +namespace VeryLongNamespace { + export namespace DeeplyNested { + export namespace Components { + export class Button { + display(): void { + console.log("Button displayed"); + } + } + export class TextField { + display(): void { + console.log("TextField displayed"); + } + } + } + } +} + +// Without alias - very verbose +const button1 = new VeryLongNamespace.DeeplyNested.Components.Button(); +button1.display(); + +// With namespace alias +import Components = VeryLongNamespace.DeeplyNested.Components; +const button2 = new Components.Button(); +button2.display(); + +// With specific member alias +import Button = VeryLongNamespace.DeeplyNested.Components.Button; +const button3 = new Button(); +button3.display(); +``` + +### Multi-file Namespaces + +**validators.ts:** + +```ts +namespace Validation { + export interface StringValidator { + isValid(s: string): boolean; + } +} +``` + +**letters-validator.ts:** + +```ts +/// +namespace Validation { + const lettersRegexp = /^[A-Za-z]+$/; + + export class LettersValidator implements StringValidator { + isValid(s: string): boolean { + return lettersRegexp.test(s); + } + } +} +``` + +**zipcode-validator.ts:** + +```ts +/// +namespace Validation { + const zipCodeRegexp = /^[0-9]+$/; + + export class ZipCodeValidator implements StringValidator { + isValid(s: string): boolean { + return zipCodeRegexp.test(s) && s.length === 5; + } + } +} +``` + +**main.ts:** + +```ts +/// +/// +/// + +// Now you can use the validators from multiple files +let validators: { [s: string]: Validation.StringValidator } = {}; +validators["letters"] = new Validation.LettersValidator(); +validators["zipcode"] = new Validation.ZipCodeValidator(); + +// Some samples to validate +let strings = ["Hello", "98052", "101"]; + +// Validate each +strings.forEach(s => { + for (let name in validators) { + console.log(` + "${s}" - ${validators[name].isValid(s) ? + "matches" : "does not match"} ${name}`); + } +}); +``` + +**Compile:** + +```bash +tsc --outFile sample.js main.ts +``` + +### Namespace Augmentation + +```ts +// Original namespace in a library +declare namespace App { + interface Request { + id: string; + timestamp: number; + } + interface Response { + send(data: any): void; + } +} + +// Later in your application (e.g., in a .d.ts file) +declare namespace App { + // Augment the Request interface + interface Request { + // Add custom properties + userId?: number; + // Add methods + log(message: string): void; + } + + // Add new types + interface Session { + token: string; + expires: Date; + } +} + +// Usage in your application +function handleRequest(req: App.Request, res: App.Response) { + // Augmented properties and methods are available + req.userId = 123; + req.log('Request received'); + res.send({ success: true }); +} +``` + +### Generic Namespaces + +```ts +// Generic namespace example +namespace DataStorage { + export interface Repository { + getAll(): T[]; + getById(id: number): T | undefined; + add(item: T): void; + update(id: number, item: T): boolean; + delete(id: number): boolean; + } + + // Concrete implementation + export class InMemoryRepository implements Repository { + private items: T[] = []; + + getAll(): T[] { + return [...this.items]; + } + + getById(id: number): T | undefined { + return this.items[id]; + } + + add(item: T): void { + this.items.push(item); + } + + update(id: number, item: T): boolean { + if (id >= 0 && id < this.items.length) { + this.items[id] = item; + return true; + } + return false; + } + + delete(id: number): boolean { + if (id >= 0 && id < this.items.length) { + this.items.splice(id, 1); + return true; + } + return false; + } + } +} + +// Usage +interface User { + id: number; + name: string; + email: string; +} + +const userRepo = new DataStorage.InMemoryRepository(); +userRepo.add({ id: 1, name: 'John Doe', email: 'john@example.com' }); +const allUsers = userRepo.getAll(); +``` + +### Namespaces vs Modules + +```ts +// Before: Using namespaces +namespace MyApp { + export namespace Services { + export class UserService { + getUser(id: number) { /* ... */ } + } + } +} + +// After: Using ES modules +// services/UserService.ts +export class UserService { + getUser(id: number) { /* ... */ } +} + +// app.ts +import { UserService } from './services/UserService'; +const userService = new UserService(); +``` + +## TypeScript Index Signatures + +- Reference material for [Index Signatures](https://www.w3schools.com/typescript/typescript_index_signatures.php) +- See [Object Types](https://www.typescriptlang.org/docs/handbook/2/objects.html) for additional information +- See [Indexed Access Types](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html) for additional information + +### Basic String Index Signatures + +```ts +// This interface represents an object with string keys and string values +interface StringDictionary { + [key: string]: string; +} + +// Creating a compliant object +const names: StringDictionary = { + firstName: "Alice", + lastName: "Smith", + "100": "One Hundred" +}; + +// Accessing properties +console.log(names["firstName"]); // "Alice" +console.log(names["lastName"]); // "Smith" +console.log(names["100"]); // "One Hundred" + +// Adding new properties dynamically +names["age"] = "30"; + +// This would cause an error +// names["age"] = 30; // Error: Type 'number' is not assignable to type 'string' +``` + +### Basic Number Index Signatures + +```ts +// Object with number indexes +interface NumberDictionary { + [index: number]: any; +} + +const scores: NumberDictionary = { + 0: "Zero", + 1: 100, + 2: true +}; + +console.log(scores[0]); // "Zero" +console.log(scores[1]); // 100 +console.log(scores[2]); // true + +// Adding a complex object +scores[3] = { passed: true }; +``` + +### Combining Index Signatures with Named Properties + +```ts +interface UserInfo { + name: string; // Required property with specific name + age: number; // Required property with specific name + [key: string]: string | number; // All other properties must be string or number +} + +const user: UserInfo = { + name: "Alice", // Required + age: 30, // Required + address: "123 Main St", // Optional + zipCode: 12345 // Optional +}; + +// This would cause an error +// const invalidUser: UserInfo = { +// name: "Bob", +// age: "thirty", // Error: Type 'string' is not assignable to type 'number' +// isAdmin: true // Error: Type 'boolean' is not assignable to type 'string | number' +// }; +``` + +### Readonly Index Signatures + +```ts +interface ReadOnlyStringArray { + readonly [index: number]: string; +} + +const names: ReadOnlyStringArray = ["Alice", "Bob", "Charlie"]; + +console.log(names[0]); // "Alice" + +// This would cause an error +// names[0] = "Andrew"; +// Error: Index signature in type 'ReadOnlyStringArray' only permits reading +``` + +### Real-World API Response Example + +```ts +// Type for API responses with dynamic keys +interface ApiResponse { + data: { + [resourceType: string]: T[]; // e.g., { "users": User[], "posts": Post[] } + }; + meta: { + page: number; + total: number; + [key: string]: any; // Allow additional metadata + }; +} + +// Example usage with a users API +interface User { + id: number; + name: string; + email: string; +} + +// Mock API response +const apiResponse: ApiResponse = { + data: { + users: [ + { id: 1, name: "Alice", email: "alice@example.com" }, + { id: 2, name: "Bob", email: "bob@example.com" } + ] + }, + meta: { + page: 1, + total: 2, + timestamp: "2023-01-01T00:00:00Z" + } +}; + +// Accessing the data +const users = apiResponse.data.users; +console.log(users[0].name); // "Alice" +``` + +### Index Signature Type Compatibility + +```ts +interface ConflictingTypes { + [key: string]: number; + name: string; // Error: not assignable to string index type 'number' +} + +interface FixedTypes { + [key: string]: number | string; + name: string; // OK + age: number; // OK +} +``` + +## TypeScript Declaration Merging + +- Reference material for [Declaration Merging](https://www.w3schools.com/typescript/typescript_declaration_merging.php) +- See [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) for additional information + +### Interface Merging + +```ts +// First declaration +interface Person { + name: string; + age: number; +} + +// Second declaration with the same name +interface Person { + address: string; + email: string; +} + +// TypeScript merges them into: +// interface Person { +// name: string; +// age: number; +// address: string; +// email: string; +// } + +const person: Person = { + name: "John", + age: 30, + address: "123 Main St", + email: "john@example.com" +}; + +console.log(person); +``` + +### Function Overloads + +```ts +// Function overloads +function processValue(value: string): string; +function processValue(value: number): number; +function processValue(value: boolean): boolean; + +// Implementation that handles all overloads +function processValue(value: string | number | boolean): string | number | boolean { + if (typeof value === "string") { + return value.toUpperCase(); + } else if (typeof value === "number") { + return value * 2; + } else { + return !value; + } +} + +// Using the function with different types +console.log(processValue("hello")); // "HELLO" +console.log(processValue(10)); // 20 +console.log(processValue(true)); // false +``` + +### Namespace Merging + +```ts +namespace Validation { + export interface StringValidator { + isValid(s: string): boolean; + } +} + +namespace Validation { + export interface NumberValidator { + isValid(n: number): boolean; + } + + export class ZipCodeValidator implements StringValidator { + isValid(s: string): boolean { + return s.length === 5 && /^\d+$/.test(s); + } + } +} + +// After merging: +// namespace Validation { +// export interface StringValidator { isValid(s: string): boolean; } +// export interface NumberValidator { isValid(n: number): boolean; } +// export class ZipCodeValidator implements StringValidator { ... } +// } + +// Using the merged namespace +const zipValidator = new Validation.ZipCodeValidator(); + +console.log(zipValidator.isValid("12345")); // true +console.log(zipValidator.isValid("1234")); // false +console.log(zipValidator.isValid("abcde")); // false +``` + +### Class and Interface Merging + +```ts +// Interface declaration +interface Cart { + calculateTotal(): number; +} + +// Class declaration with same name +class Cart { + items: { name: string; price: number }[] = []; + + addItem(name: string, price: number): void { + this.items.push({ name, price }); + } + + // Must implement the interface method + calculateTotal(): number { + return this.items.reduce((sum, item) => sum + item.price, 0); + } +} + +// Using the merged class and interface +const cart = new Cart(); +cart.addItem("Book", 15.99); +cart.addItem("Coffee Mug", 8.99); + +console.log(`Total: $${cart.calculateTotal().toFixed(2)}`); +``` + +### Enum Merging + +```ts +// First part of the enum +enum Direction { + North, + South +} + +// Second part of the enum +enum Direction { + East = 2, + West = 3 +} + +// After merging: +// enum Direction { +// North = 0, +// South = 1, +// East = 2, +// West = 3 +// } + +console.log(Direction.North); // 0 +console.log(Direction.South); // 1 +console.log(Direction.East); // 2 +console.log(Direction.West); // 3 + +// Can also access by value +console.log(Direction[0]); // "North" +console.log(Direction[2]); // "East" +``` + +### Module Augmentation + +```ts +// Original library definition +// Imagine this comes from a third-party library +declare namespace LibraryModule { + export interface User { + id: number; + name: string; + } + export function getUser(id: number): User; +} + +// Augmenting with additional functionality (your code) +declare namespace LibraryModule { + // Add new interface + export interface UserPreferences { + theme: string; + notifications: boolean; + } + + // Add new property to existing interface + export interface User { + preferences?: UserPreferences; + } + + // Add new function + export function getUserPreferences(userId: number): UserPreferences; +} + +// Using the augmented module +const user = LibraryModule.getUser(123); +console.log(user.preferences?.theme); + +const prefs = LibraryModule.getUserPreferences(123); +console.log(prefs.notifications); +```