From 81ca93c844f3072d6c4d09adb55284475f6510db Mon Sep 17 00:00:00 2001 From: jhauga Date: Thu, 5 Mar 2026 20:07:19 -0500 Subject: [PATCH 01/11] new skill typescript-coder --- docs/README.skills.md | 1 + skills/typescript-coder/SKILL.md | 793 ++++++ .../references/typescript-basics.md | 350 +++ .../references/typescript-cheatsheet.md | 530 ++++ .../references/typescript-classes.md | 373 +++ .../references/typescript-elements.md | 405 +++ .../references/typescript-handbook.md | 488 ++++ .../references/typescript-keywords.md | 127 + .../references/typescript-miscellaneous.md | 2420 +++++++++++++++++ .../references/typescript-projects.md | 803 ++++++ .../references/typescript-types.md | 1881 +++++++++++++ 11 files changed, 8171 insertions(+) create mode 100644 skills/typescript-coder/SKILL.md create mode 100644 skills/typescript-coder/references/typescript-basics.md create mode 100644 skills/typescript-coder/references/typescript-cheatsheet.md create mode 100644 skills/typescript-coder/references/typescript-classes.md create mode 100644 skills/typescript-coder/references/typescript-elements.md create mode 100644 skills/typescript-coder/references/typescript-handbook.md create mode 100644 skills/typescript-coder/references/typescript-keywords.md create mode 100644 skills/typescript-coder/references/typescript-miscellaneous.md create mode 100644 skills/typescript-coder/references/typescript-projects.md create mode 100644 skills/typescript-coder/references/typescript-types.md diff --git a/docs/README.skills.md b/docs/README.skills.md index ca41e425a..e70294a83 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -213,6 +213,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/typescript-basics.md`
`references/typescript-cheatsheet.md`
`references/typescript-classes.md`
`references/typescript-elements.md`
`references/typescript-handbook.md`
`references/typescript-keywords.md`
`references/typescript-miscellaneous.md`
`references/typescript-projects.md`
`references/typescript-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..7eba14c17 --- /dev/null +++ b/skills/typescript-coder/SKILL.md @@ -0,0 +1,793 @@ +--- +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, React, or other 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-javascript` + +## 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 + } +} +``` + +#### React Project + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "esnext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": 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 in the `references/` directory: + +- **[typescript-basics.md](references/typescript-basics.md)** - TypeScript fundamentals, simple types, type inference, and special types +- **[typescript-cheatsheet.md](references/typescript-cheatsheet.md)** - Quick reference for control flow, classes, interfaces, types, and common patterns +- **[typescript-classes.md](references/typescript-classes.md)** - Class syntax, inheritance, generics, and utility types +- **[typescript-elements.md](references/typescript-elements.md)** - Arrays, tuples, objects, enums, functions, and casting +- **[typescript-handbook.md](references/typescript-handbook.md)** - Comprehensive handbook covering core concepts from official TypeScript documentation +- **[typescript-keywords.md](references/typescript-keywords.md)** - keyof, null handling, optional chaining, and template literal types +- **[typescript-miscellaneous.md](references/typescript-miscellaneous.md)** - Async programming, promises, decorators, and JSDoc integration +- **[typescript-projects.md](references/typescript-projects.md)** - Project configuration, Node.js setup, Express integration, React TypeScript +- **[typescript-types.md](references/typescript-types.md)** - Advanced types, conditional types, mapped types, type guards, and recursive types + +### External Resources + +- [TypeScript Official Documentation](https://www.typescriptlang.org/docs/) +- [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/typescript-basics.md b/skills/typescript-coder/references/typescript-basics.md new file mode 100644 index 000000000..18757f9a9 --- /dev/null +++ b/skills/typescript-coder/references/typescript-basics.md @@ -0,0 +1,350 @@ +# TypeScript Basics + +## TypeScript Tutorial + +- Reference [Tutorial](https://www.w3schools.com/typescript/index.php) + +## Getting Started + +### Most Basic Syntax + +```ts +console.log('Hello World!'); +``` + +- Reference [Getting Started](https://www.w3schools.com/typescript/typescript_getstarted.php) + +### 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 [Simple Types](https://www.w3schools.com/typescript/typescript_simple_types.php) + +### 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 [Explicit Types and Inference](https://www.w3schools.com/typescript/typescript_explicit_inference.php) + +### Explicit Type Annotations: + +```ts +// String +greeting: string = "Hello, TypeScript!"; + +// Number +userCount: number = 42; + +// Boolean +isLoading: boolean = true; + +// Array of numbers +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 [Special Types](https://www.w3schools.com/typescript/typescript_special_types.php) + +### 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/typescript-cheatsheet.md b/skills/typescript-coder/references/typescript-cheatsheet.md new file mode 100644 index 000000000..9dbe2c01a --- /dev/null +++ b/skills/typescript-coder/references/typescript-cheatsheet.md @@ -0,0 +1,530 @@ +# 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) diff --git a/skills/typescript-coder/references/typescript-classes.md b/skills/typescript-coder/references/typescript-classes.md new file mode 100644 index 000000000..8a1fa7c35 --- /dev/null +++ b/skills/typescript-coder/references/typescript-classes.md @@ -0,0 +1,373 @@ +# TypeScript Classes + +## TypeScript Classes + +- Reference [Classes](https://www.w3schools.com/typescript/typescript_classes.php) + +### 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 [Basic Generics](https://www.w3schools.com/typescript/typescript_basic_generics.php) + +### 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 [Utility Types](https://www.w3schools.com/typescript/typescript_utility_types.php) + +### 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/typescript-elements.md b/skills/typescript-coder/references/typescript-elements.md new file mode 100644 index 000000000..f59164f3c --- /dev/null +++ b/skills/typescript-coder/references/typescript-elements.md @@ -0,0 +1,405 @@ +# TypeScript Elements + +## TypeScript Arrays + +- Reference [Arrays](https://www.w3schools.com/typescript/typescript_arrays.php) + +### 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 [Tuples](https://www.w3schools.com/typescript/typescript_tuples.php) + +### 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 [Object Types](https://www.w3schools.com/typescript/typescript_object_types.php) + +### 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 [Enums](https://www.w3schools.com/typescript/typescript_enums.php) + +### 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 [Type Aliases and Interfaces](https://www.w3schools.com/typescript/typescript_aliases_and_interfaces.php) + +### 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 [Union Types](https://www.w3schools.com/typescript/typescript_union_types.php) + +### 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 [Functions](https://www.w3schools.com/typescript/typescript_functions.php) + +### 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 [Casting](https://www.w3schools.com/typescript/typescript_functions.php) + +### 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/typescript-handbook.md b/skills/typescript-coder/references/typescript-handbook.md new file mode 100644 index 000000000..453ba3495 --- /dev/null +++ b/skills/typescript-coder/references/typescript-handbook.md @@ -0,0 +1,488 @@ +# TypeScript Handbook + +Comprehensive TypeScript reference based on the official TypeScript Handbook. This document covers core concepts and patterns. + +## Getting Started + +TypeScript is a strongly typed programming language that builds on JavaScript. It adds optional static typing to JavaScript, enabling better tooling, error detection, and code quality. + +### Key Benefits + +- **Type Safety**: Catch errors at compile time instead of runtime +- **Better IDE Support**: Enhanced autocomplete, refactoring, and navigation +- **Code Documentation**: Types serve as inline documentation +- **Modern JavaScript Features**: Use latest ECMAScript features with backward compatibility +- **Gradual Adoption**: Add TypeScript incrementally to existing projects + +## The Basics + +### Static Type Checking + +TypeScript analyzes your code to find errors before execution: + +```typescript +// TypeScript catches this error at compile time +const message = "hello"; +message(); // Error: This expression is not callable +``` + +### Non-Exception Failures + +TypeScript catches common mistakes: + +```typescript +const user = { name: "Alice", age: 30 }; + +// Typos +user.location; // Error: Property 'location' does not exist + +// Uncalled functions +if (user.age.toFixed) // Error: Did you mean to call this? + +// Logical errors +const value = Math.random() < 0.5 ? "a" : "b"; +if (value !== "a") { + // ... +} else if (value === "b") { // Error: This comparison is always false +``` + +## Everyday Types + +### Primitives + +```typescript +let name: string = "Alice"; +let age: number = 30; +let isActive: boolean = true; +``` + +### Arrays + +```typescript +let numbers: number[] = [1, 2, 3]; +let strings: Array = ["a", "b", "c"]; +``` + +### Functions + +```typescript +// Parameter type annotations +function greet(name: string): string { + return `Hello, ${name}!`; +} + +// Optional parameters +function buildName(first: string, last?: string): string { + return last ? `${first} ${last}` : first; +} + +// Default parameters +function multiply(a: number, b: number = 1): number { + return a * b; +} + +// Rest parameters +function sum(...numbers: number[]): number { + return numbers.reduce((acc, n) => acc + n, 0); +} +``` + +### Object Types + +```typescript +// Anonymous object type +function printCoord(pt: { x: number; y: number }) { + console.log(pt.x, pt.y); +} + +// Optional properties +function printName(obj: { first: string; last?: string }) { + // ... +} + +// Readonly properties +interface ReadonlyPerson { + readonly name: string; + readonly age: number; +} +``` + +### Union Types + +```typescript +function printId(id: number | string) { + if (typeof id === "string") { + console.log(id.toUpperCase()); + } else { + console.log(id); + } +} +``` + +### Type Aliases + +```typescript +type Point = { + x: number; + y: number; +}; + +type ID = number | string; +``` + +### Interfaces + +```typescript +interface Point { + x: number; + y: number; +} + +// Extending interfaces +interface ColoredPoint extends Point { + color: string; +} +``` + +## Narrowing + +### typeof Guards + +```typescript +function padLeft(padding: number | string, input: string) { + if (typeof padding === "number") { + return " ".repeat(padding) + input; + } + return padding + input; +} +``` + +### Truthiness Narrowing + +```typescript +function printAll(strs: string | string[] | null) { + if (strs && typeof strs === "object") { + for (const s of strs) { + console.log(s); + } + } else if (typeof strs === "string") { + console.log(strs); + } +} +``` + +### Equality Narrowing + +```typescript +function example(x: string | number, y: string | boolean) { + if (x === y) { + x.toUpperCase(); // x is string + y.toUpperCase(); // y is string + } +} +``` + +### in Operator Narrowing + +```typescript +type Fish = { swim: () => void }; +type Bird = { fly: () => void }; + +function move(animal: Fish | Bird) { + if ("swim" in animal) { + return animal.swim(); + } + return animal.fly(); +} +``` + +### instanceof Narrowing + +```typescript +function logValue(x: Date | string) { + if (x instanceof Date) { + console.log(x.toUTCString()); + } else { + console.log(x.toUpperCase()); + } +} +``` + +### Discriminated Unions + +```typescript +interface Circle { + kind: "circle"; + radius: number; +} + +interface Square { + kind: "square"; + sideLength: number; +} + +type Shape = Circle | Square; + +function getArea(shape: Shape) { + switch (shape.kind) { + case "circle": + return Math.PI * shape.radius ** 2; + case "square": + return shape.sideLength ** 2; + } +} +``` + +## Functions + +### Function Type Expressions + +```typescript +type GreetFunction = (name: string) => void; + +function greeter(fn: GreetFunction) { + fn("World"); +} +``` + +### Call Signatures + +```typescript +type DescribableFunction = { + description: string; + (someArg: number): boolean; +}; +``` + +### Generic Functions + +```typescript +function firstElement(arr: T[]): T | undefined { + return arr[0]; +} + +// Inference +const s = firstElement(["a", "b", "c"]); // string +const n = firstElement([1, 2, 3]); // number +``` + +### Constraints + +```typescript +function longest(a: T, b: T) { + if (a.length >= b.length) { + return a; + } + return b; +} +``` + +### Function Overloads + +```typescript +function makeDate(timestamp: number): Date; +function makeDate(m: number, d: number, y: number): Date; +function makeDate(mOrTimestamp: number, d?: number, y?: number): Date { + if (d !== undefined && y !== undefined) { + return new Date(y, mOrTimestamp, d); + } + return new Date(mOrTimestamp); +} +``` + +## Object Types + +### Index Signatures + +```typescript +interface StringArray { + [index: number]: string; +} + +interface NumberDictionary { + [key: string]: number; + length: number; +} +``` + +### Extending Types + +```typescript +interface BasicAddress { + name?: string; + street: string; + city: string; +} + +interface AddressWithUnit extends BasicAddress { + unit: string; +} +``` + +### Intersection Types + +```typescript +interface Colorful { + color: string; +} + +interface Circle { + radius: number; +} + +type ColorfulCircle = Colorful & Circle; +``` + +## Generics + +### Generic Types + +```typescript +function identity(arg: T): T { + return arg; +} + +let myIdentity: (arg: T) => T = identity; +``` + +### Generic Classes + +```typescript +class GenericNumber { + zeroValue: T; + add: (x: T, y: T) => T; +} +``` + +### Generic Constraints + +```typescript +interface Lengthwise { + length: number; +} + +function loggingIdentity(arg: T): T { + console.log(arg.length); + return arg; +} +``` + +## Manipulation Types + +### Mapped Types + +```typescript +type OptionsFlags = { + [Property in keyof T]: boolean; +}; +``` + +### Conditional Types + +```typescript +type NameOrId = T extends number + ? IdLabel + : NameLabel; +``` + +### Template Literal Types + +```typescript +type World = "world"; +type Greeting = `hello ${World}`; +``` + +## Classes + +### Class Members + +```typescript +class Point { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + scale(n: number): void { + this.x *= n; + this.y *= n; + } +} +``` + +### Inheritance + +```typescript +class Animal { + move() { + console.log("Moving along!"); + } +} + +class Dog extends Animal { + bark() { + console.log("Woof!"); + } +} +``` + +### Member Visibility + +```typescript +class Base { + public x = 0; + protected y = 0; + private z = 0; +} +``` + +### Abstract Classes + +```typescript +abstract class Base { + abstract getName(): string; + + printName() { + console.log("Hello, " + this.getName()); + } +} +``` + +## Modules + +### Exporting + +```typescript +// Named exports +export function add(x: number, y: number): number { + return x + y; +} + +export interface Point { + x: number; + y: number; +} + +// Default export +export default class Calculator { + add(x: number, y: number): number { + return x + y; + } +} +``` + +### Importing + +```typescript +import { add, Point } from "./math"; +import Calculator from "./Calculator"; +import * as math from "./math"; +``` + +--- + +> [!NOTE] +> This handbook covers the core concepts from the official TypeScript documentation. For the most up-to-date information, visit [typescriptlang.org/docs/handbook](https://www.typescriptlang.org/docs/handbook/intro.html) diff --git a/skills/typescript-coder/references/typescript-keywords.md b/skills/typescript-coder/references/typescript-keywords.md new file mode 100644 index 000000000..a4619a07b --- /dev/null +++ b/skills/typescript-coder/references/typescript-keywords.md @@ -0,0 +1,127 @@ +# TypeScript Keywords + +## TypeScript Keyof + +- Reference [Keyof](https://www.w3schools.com/typescript/typescript_keyof.php) + +### 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 [Null & Undefined](https://www.w3schools.com/typescript/typescript_null.php) + +### 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 [Definitely Typed](https://www.w3schools.com/typescript/typescript_definitely_typed.php) + +### Installation: + +```bash +npm install --save-dev @types/jquery +``` + +## TypeScript 5.x Update + +- Reference [5.x Update](https://www.w3schools.com/typescript/typescript_5_updates.php) + +### 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/typescript-miscellaneous.md b/skills/typescript-coder/references/typescript-miscellaneous.md new file mode 100644 index 000000000..a4630dcec --- /dev/null +++ b/skills/typescript-coder/references/typescript-miscellaneous.md @@ -0,0 +1,2420 @@ +# TypeScript Miscellaneous + +## TypeScript Async Programming + +- Reference [Async Programming](https://www.w3schools.com/typescript/typescript_async.php) + +### 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 [Decorators](https://www.w3schools.com/typescript/typescript_decorators.php) + +### 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 [JavaScript Projects (*JSDoc*)](https://www.w3schools.com/typescript/typescript_jsdoc.php) + +### 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('express').Request} ExpressRequest */ + +// 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); +``` +### Type Imports: + +```ts +// @ts-check + +// Importing types from TypeScript files +/** @typedef {import('./types').User} User */ + +// Importing types from node_modules +/** @typedef {import('express').Request} ExpressRequest */ + +// Importing with renaming +/** @typedef {import('./api').default as ApiClient} ApiClient */ +``` + +### Create a types.d.ts file in your project:Type Imports: + +```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:Type Imports: + +```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 [Migration](https://www.w3schools.com/typescript/typescript_migration.php) + +### 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 Hanlding + +- Reference [Error Handling](https://www.w3schools.com/typescript/typescript_error_handling.php) + +### 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); +``` + +### Error Boundaries in React: + +```tsx +import React, { Component, ErrorInfo, ReactNode } from 'react'; + +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error?: Error; +} + +class ErrorBoundary extends Component { + public state: ErrorBoundaryState = { + hasError: false + }; + + public static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Uncaught error:', error, errorInfo); + // Log to error reporting service + } + + public render() { + if (this.state.hasError) { + return this.props.fallback || ( +
+

Something went wrong

+

{this.state.error?.message}

+ +
+ ); + } + + return this.props.children; + } +} + +// Usage +function App() { + return ( + Oops! Something broke.}> + + + ); +} +``` + +### 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 [Best Practices](https://www.w3schools.com/typescript/typescript_best_practices.php) + +### 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/typescript-projects.md b/skills/typescript-coder/references/typescript-projects.md new file mode 100644 index 000000000..2c8700a90 --- /dev/null +++ b/skills/typescript-coder/references/typescript-projects.md @@ -0,0 +1,803 @@ +# TypeScript Projects + +## TypeScript Configuration + +- Reference [Configuration](https://www.w3schools.com/typescript/typescript_config.php) + +### Basic Configuration: + +```json +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs" + }, + "include": ["src/**/*"] +} +``` + +### Advanced Configuration: + +```json +{ + "compilerOptions": { + "target": "es2020", + "module": "esnext", + "strict": true, + "baseUrl": ".", + "paths": { + "@app/*": ["src/app/*"] + }, + "outDir": "dist", + "esModuleInterop": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} +``` + +### Initialize Configuration: + +```bash +tsc --init +``` + +## TypeScript Node.js + +- Reference [Node.js](https://www.w3schools.com/typescript/typescript_nodejs.php) + +### Setting Up Node.js Project: + +```bash +mkdir my-ts-node-app +cd my-ts-node-app +npm init -y +npm install typescript @types/node --save-dev +npx tsc --init +``` + +### Create Project Structure: + +```bash +mkdir src +# later add files like: src/server.ts, src/middleware/auth.ts +``` + +### TypeScript Configuration: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} +``` + +### Install Dependencies: + +```bash +npm install express body-parser +npm install --save-dev ts-node nodemon @types/express +``` + +### Project Structure: + +``` +my-ts-node-app/ + src/ + server.ts + middleware/ + auth.ts + entity/ + User.ts + config/ + database.ts + dist/ + node_modules/ + package.json + tsconfig.json +``` + +### Basic Express Server Example: + +```ts +import express, { Request, Response, NextFunction } from 'express'; +import { json } from 'body-parser'; + +interface User { + id: number; + username: string; + email: string; +} + +// Initialize Express app +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(json()); + +// In-memory database +const users: User[] = [ + { id: 1, username: 'user1', email: 'user1@example.com' }, + { id: 2, username: 'user2', email: 'user2@example.com' } +]; + +// Routes +app.get('/api/users', (req: Request, res: Response) => { + res.json(users); +}); + +app.get('/api/users/:id', (req: Request, res: Response) => { + const user = users.find(u => u.id === parseInt(req.params.id)); + if (!user) return res.status(404).json({ message: 'User not found' }); + res.json(user); +}); + +app.post('/api/users', (req: Request, res: Response) => { + const { username, email } = req.body; + + if (!username || !email) { + return res.status(400).json({ message: 'Username and email are required' }); + } + + const newUser: User = { + id: users.length + 1, + username, + email + }; + + users.push(newUser); + res.status(201).json(newUser); +}); + +// Error handling middleware +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + console.error(err.stack); + res.status(500).json({ message: 'Something went wrong!' }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`Server is running on http://localhost:${PORT}`); +}); +``` + +### Express Middleware with Authentication: + +```ts +import { Request, Response, NextFunction } from 'express'; + +// Extend the Express Request type to include custom properties +declare global { + namespace Express { + interface Request { + user?: { id: number; role: string }; + } + } +} + +export const authenticate = (req: Request, res: Response, next: NextFunction) => { + const token = req.header('Authorization')?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ message: 'No token provided' }); + } + + try { + // In a real app, verify the JWT token here + const decoded = { id: 1, role: 'admin' }; // Mock decoded token + req.user = decoded; + next(); + } catch (error) { + res.status(401).json({ message: 'Invalid token' }); + } +}; + +export const authorize = (roles: string[]) => { + return (req: Request, res: Response, next: NextFunction) => { + if (!req.user) { + return res.status(401).json({ message: 'Not authenticated' }); + } + + if (!roles.includes(req.user.role)) { + return res.status(403).json({ message: 'Not authorized' }); + } + + next(); + }; +}; +``` + +### Using Middleware in Routes: + +```ts +// src/server.ts +import { authenticate, authorize } from './middleware/auth'; + +app.get('/api/admin', authenticate, authorize(['admin']), (req, res) => { + res.json({ message: `Hello admin ${req.user?.id}` }); +}); +``` + +### Database Integration with TypeORM - Entity: + +```ts +import { Entity, PrimaryGeneratedColumn, Column, + CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('users') +export class User { + @PrimaryGeneratedColumn() + id: number; + + @Column({ unique: true }) + username: string; + + @Column({ unique: true }) + email: string; + + @Column({ select: false }) + password: string; + + @Column({ default: 'user' }) + role: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} +``` + +### Database Configuration: + +```ts +import 'reflect-metadata'; +import { DataSource } from 'typeorm'; +import { User } from '../entity/User'; + +export const AppDataSource = new DataSource({ + type: 'postgres', + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '5432'), + username: process.env.DB_USERNAME || 'postgres', + password: process.env.DB_PASSWORD || 'postgres', + database: process.env.DB_NAME || 'mydb', + synchronize: process.env.NODE_ENV !== 'production', + logging: false, + entities: [User], + migrations: [], + subscribers: [], +}); +``` + +### Initialize Database: + +```ts +// src/server.ts +import { AppDataSource } from './config/database'; + +AppDataSource.initialize() + .then(() => { + app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); + }) + .catch((err) => { + console.error('DB init error', err); + process.exit(1); + }); +``` + +### Package Scripts: + +```json +{ + "scripts": { + "build": "tsc", + "start": "node dist/server.js", + "dev": "nodemon --exec ts-node src/server.ts", + "watch": "tsc -w", + "test": "jest --config jest.config.js" + } +} +``` + +### Development Mode: + +```bash +npm run dev +``` + +### Production Build: + +```bash +npm run build +npm start +``` + +### Run with Source Maps: + +```bash +node --enable-source-maps dist/server.js +``` +## TypeScript React + +- Reference [React](https://www.w3schools.com/typescript/typescript_react.php) + +### Getting Started: + +```bash +npm create vite@latest my-app -- --template react-ts +cd my-app +npm install +npm run dev +``` + +### TypeScript Configuration for React: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Node", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} +``` + +### Component Typing: + +```tsx +// Greeting.tsx +type GreetingProps = { + name: string; + age?: number; +}; + +export function Greeting({ name, age }: GreetingProps) { + return ( +
+

Hello, {name}!

+ {age !== undefined &&

You are {age} years old

} +
+ ); +} +``` + +### Event Handlers: + +```tsx +// Input change +function NameInput() { + function handleChange(e: React.ChangeEvent) { + console.log(e.target.value); + } + return ; +} + +// Button click +function SaveButton() { + function handleClick(e: React.MouseEvent) { + e.preventDefault(); + } + return ; +} +``` + +### useState Hook: + +```tsx +const [count, setCount] = React.useState(0); +const [status, setStatus] = React.useState<'idle' | 'loading' | 'error'>('idle'); + +type User = { id: string; name: string }; +const [user, setUser] = React.useState(null); +``` + +### useRef Hook: + +```tsx +function FocusInput() { + const inputRef = React.useRef(null); + return inputRef.current?.select()} />; +} +``` + +### Children Props: + +```tsx +type CardProps = { title: string; children?: React.ReactNode }; +function Card({ title, children }: CardProps) { + return ( +
+

{title}

+ {children} +
+ ); +} +``` + +### Generic Fetch Function: + +```tsx +async function fetchJson(url: string): Promise { + const res = await fetch(url); + if (!res.ok) throw new Error('Network error'); + return res.json() as Promise; +} + +// Usage inside an async function/component effect +async function loadPosts() { + type Post = { id: number; title: string }; + const posts = await fetchJson("/api/posts"); + console.log(posts); +} +``` + +### Context API with TypeScript: + +```tsx +type Theme = 'light' | 'dark'; +const ThemeContext = + React.createContext<{ theme: Theme; toggle(): void } | null>(null); + +function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = React.useState('light'); + const value = + { theme, toggle: () => setTheme(t => (t === 'light' ? 'dark' : 'light')) }; + return {children}; +} + +function useTheme() { + const ctx = React.useContext(ThemeContext); + if (!ctx) throw new Error('useTheme must be used within ThemeProvider'); + return ctx; +} +``` + +### Vite Environment Types: + +```ts +// src/vite-env.d.ts +/// +``` + +### TypeScript Config for Vite Types: + +```json +{ + "compilerOptions": { + "types": ["vite/client"] + } +} +``` + +### Path Aliases: + +```json +// tsconfig.json +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@components/*": ["src/components/*"], + "@utils/*": ["src/utils/*"] + } + } +} +``` + +```ts +// Usage in your code +import { Button } from '@/components/Button'; +import { formatDate } from '@utils/date'; +``` + +## TypeScript Tooling + +- Reference [Tooling](https://www.w3schools.com/typescript/typescript_tooling.php) + +### Install ESLint: + +```bash +# Install ESLint with TypeScript support +npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin +``` + +### ESLint Configuration: + +```json +// .eslintrc.json +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" + ], + "parserOptions": { + "project": "./tsconfig.json", + "ecmaVersion": 2020, + "sourceType": "module" + }, + "rules": { + "@typescript-eslint/explicit-function-return-type": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] + } +} +``` + +### ESLint Scripts: + +```json +// package.json +{ + "scripts": { + "lint": "eslint . --ext .ts,.tsx", + "lint:fix": "eslint . --ext .ts,.tsx --fix", + "type-check": "tsc --noEmit" + } +} +``` + +### Install Prettier: + +```bash +# Install Prettier and related packages +npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier +``` + +### Prettier Configuration: + +```json +// .prettierrc +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "printWidth": 100, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "avoid" +} +``` + +### Prettier Ignore File: + +``` +// .prettierignore +node_modules +build +dist +.next +.vscode +``` + +### Integrate Prettier with ESLint: + +```json +// .eslintrc.json +{ + "extends": [ + // ... other configs + "plugin:prettier/recommended" // Must be last in the array + ] +} +``` + +### Setup with Vite: + +```bash +# Create a new project with React + TypeScript +npm create vite@latest my-app -- --template react-ts + +# Navigate to project directory +cd my-app + +# Install dependencies +npm install + +# Start development server +npm run dev +``` + +### Webpack Configuration: + +```js +// webpack.config.js +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = { + entry: './src/index.tsx', + module: { + rules: [ + { + test: /\.(ts|tsx)$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, + plugins: [ + new HtmlWebpackPlugin({ + template: './public/index.html', + }), + ], + devServer: { + static: path.join(__dirname, 'dist'), + compress: true, + port: 3000, + hot: true, + }, +}; +``` + +### TypeScript Configuration for Build Tools: + +```json +// tsconfig.json +{ + "compilerOptions": { + "target": "es2020", + "module": "esnext", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules"] +} +``` + +### VS Code Settings: + +```json +// .vscode/settings.json +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true, + "source.organizeImports": true + }, + "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.preferences.importModuleSpecifier": "non-relative" +} +``` + +### VS Code Launch Configuration: + +```json +// .vscode/launch.json +{ + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///./~/*": "${workspaceFolder}/node_modules/*", + "webpack:///./*": "${workspaceFolder}/src/*" + } + }, + { + "type": "node", + "request": "launch", + "name": "Debug Tests", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/jest", + "args": ["--runInBand", "--watchAll=false"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "sourceMaps": true + } + ] +} +``` + +### Install Testing Dependencies: + +```bash +# Install testing dependencies +npm install --save-dev jest @types/jest ts-jest @testing-library/react @testing-library/jest-dom @testing-library/user-event +``` + +### Jest Configuration: + +```js +// jest.config.js +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + setupFilesAfterEnv: ['@testing-library/jest-dom'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + '\\\.(css|less|scss|sass)$': 'identity-obj-proxy', + }, + transform: { + '^.+\\\\.tsx?$': 'ts-jest', + }, + testMatch: ['**/__tests__/**/*.test.(ts|tsx)'], +}; +``` + +### Example Test File: + +```tsx +// src/__tests__/Button.test.tsx +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Button from '../components/Button'; + +describe('Button', () => { + it('renders button with correct text', () => { + render(); + expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument(); + }); + + it('calls onClick when clicked', () => { + const handleClick = jest.fn(); + render(); + + fireEvent.click(screen.getByRole('button')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); +}); +``` diff --git a/skills/typescript-coder/references/typescript-types.md b/skills/typescript-coder/references/typescript-types.md new file mode 100644 index 000000000..8a3258ba3 --- /dev/null +++ b/skills/typescript-coder/references/typescript-types.md @@ -0,0 +1,1881 @@ +# TypeScript Types + +## TypeScript Advanced Types + +- Reference [Advanced Types](https://www.w3schools.com/typescript/typescript_advanced_types.php) + +### 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]; +}; +``` + +## TypeScript Conditional Types + +### 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 [Type Guards](https://www.w3schools.com/typescript/typescript_type_guards.php) + +### 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; +} +``` + +## TypeScript Conditional Types + +- Reference [Conditional Types](https://www.w3schools.com/typescript/typescript_conditional_types.php) + +### 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) +``` + +## TypeScript Mapped Types + +- Reference [Mapped Types](https://www.w3schools.com/typescript/typescript_mapped_types.php) + +### 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 [Type Inference](https://www.w3schools.com/typescript/typescript_type_inference.php) + +### 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 +}; +``` + +## TypeScript Literal Types + +- Reference [Literal Types](https://www.w3schools.com/typescript/typescript_literal_types.php) + +### 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 [Namespaces](https://www.w3schools.com/typescript/typescript_namespaces.php) + +### 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 +declare namespace Express { + interface Request { + user?: { id: number; name: string }; + } + interface Response { + json(data: any): void; + } +} + +// Later in your application (e.g., in a .d.ts file) +declare namespace Express { + // Augment the Request interface + interface Request { + // Add custom properties + requestTime?: number; + // Add methods + log(message: string): void; + } + + // Add new types + interface UserSession { + userId: number; + expires: Date; + } +} + +// Usage in your application +const app = express(); + +app.use((req: Express.Request, res: Express.Response, next) => { + // Augmented properties and methods are available + req.requestTime = Date.now(); + req.log('Request started'); + next(); +}); +``` + +### 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 [Index Signatures](https://www.w3schools.com/typescript/typescript_index_signatures.php) + +### 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 [Declaration Merging](https://www.w3schools.com/typescript/typescript_declaration_merging.php) + +### 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); +``` + From 16eab1b4da698888eecf6d08b47941ad21740b5a Mon Sep 17 00:00:00 2001 From: jhauga Date: Thu, 5 Mar 2026 21:04:01 -0500 Subject: [PATCH 02/11] codespell: resolve mispelling --- .../references/typescript-miscellaneous.md | 169 +++++++++--------- 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/skills/typescript-coder/references/typescript-miscellaneous.md b/skills/typescript-coder/references/typescript-miscellaneous.md index a4630dcec..256cedbe4 100644 --- a/skills/typescript-coder/references/typescript-miscellaneous.md +++ b/skills/typescript-coder/references/typescript-miscellaneous.md @@ -4,7 +4,7 @@ - Reference [Async Programming](https://www.w3schools.com/typescript/typescript_async.php) -### Promises in TypeScript: +### Promises in TypeScript ```ts // Create a typed Promise that resolves to a string @@ -32,7 +32,7 @@ fetchGreeting() }); ``` -### Async/Await with TypeScript: +### Async/Await with TypeScript ```ts // Define types for our API response @@ -84,7 +84,7 @@ processUsers() .catch(err => console.error('Processing failed:', err)); ``` -### Run multiple async operations in parallel: +### Run multiple async operations in parallel ```ts interface Product { @@ -119,7 +119,7 @@ async function fetchMultipleProducts() { fetchMultipleProducts(); ``` -### Typing Callbacks for Async Operations: +### Typing Callbacks for Async Operations ```ts // Define a type for the callback @@ -152,7 +152,7 @@ fetchDataWithCallback('https://api.example.com', (error, data) => { }); ``` -### Promise.all - Run multiple promises in parallel: +### Promise.all - Run multiple promises in parallel ```ts // Different types of promises @@ -193,7 +193,7 @@ async function loadUserDashboard(userId: number) { loadUserDashboard(1); ``` -### Promise.race - Useful for timeouts: +### Promise.race - Useful for timeouts ```ts // Helper function for timeout @@ -231,7 +231,7 @@ async function fetchUserData() { } ``` -### Promise.allSettled - Wait for all promises regardless of outcome: +### Promise.allSettled - Wait for all promises regardless of outcome ```ts // Simulate multiple API calls with different outcomes @@ -280,7 +280,7 @@ async function processBatch(ids: number[]) { processBatch([1, 2, 3, 4, 5]); ``` -### Custom Error Classes for Async Operations: +### Custom Error Classes for Async Operations ```ts // Base error class for our application @@ -387,7 +387,7 @@ async function displayUserProfile(userId: string) { displayUserProfile('123'); ``` -### Async Generators: +### Async Generators ```ts // Async generator function @@ -413,7 +413,7 @@ async function consumeNumbers() { - Reference [Decorators](https://www.w3schools.com/typescript/typescript_decorators.php) -### Enabling Decorators: +### Enabling Decorators ```json { @@ -428,7 +428,7 @@ async function consumeNumbers() { } ``` -### Class Decorators: +### Class Decorators ```ts // A simple class decorator that logs class definition @@ -447,7 +447,7 @@ class UserService { // Output when the file is loaded: "Class UserService was defined at [timestamp]" ``` -### Class Decorators - Adding Properties and Methods: +### Class Decorators - Adding Properties and Methods ```ts // A decorator that adds a version property and logs instantiation @@ -483,7 +483,7 @@ console.log((client as any).version); // Outputs: 1.0.0 client.fetchData(); ``` -### Class Decorators - Sealed Classes: +### Class Decorators - Sealed Classes ```ts function sealed(constructor: Function) { @@ -508,7 +508,7 @@ class Greeter { // Error: Cannot add property newMethod ``` -### Method Decorators - Measure Execution Time: +### Method Decorators - Measure Execution Time ```ts // Method decorator to measure execution time @@ -545,7 +545,7 @@ const processor = new DataProcessor(); processor.processData([1, 2, 3, 4, 5]); ``` -### Method Decorators - Role-Based Access Control: +### Method Decorators - Role-Based Access Control ```ts // User roles @@ -608,7 +608,7 @@ currentUser.roles = ['admin']; docService.deleteDocument('doc123'); // Now works - admin can delete ``` -### Method Decorators - Deprecation Warning: +### Method Decorators - Deprecation Warning ```ts function deprecated(message: string) { @@ -642,7 +642,7 @@ payment.processPayment(100, 'USD'); // Shows deprecation warning payment.processPaymentV2(100, 'USD'); // No warning ``` -### Property Decorators - Format Properties: +### Property Decorators - Format Properties ```ts // Property decorator to format a string property @@ -672,7 +672,7 @@ greeter.greeting = 'World'; console.log(greeter.greeting); // Outputs: Hello, World! ``` -### Property Decorators - Log Property Access: +### Property Decorators - Log Property Access ```ts function logProperty(target: any, propertyKey: string) { @@ -713,7 +713,7 @@ 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: +### Property Decorators - Required Properties ```ts function required(target: any, propertyKey: string) { @@ -758,7 +758,7 @@ const user1 = new User('johndoe', 'john@example.com'); // Works // Throws error: Property username is required ``` -### Parameter Decorators - Validation: +### Parameter Decorators - Validation ```ts function validateParam(type: 'string' | 'number' | 'boolean') { @@ -830,7 +830,7 @@ service.createUser('John', 30, true); // Works // Throws error: Parameter at index 0 failed string validation ``` -### Decorator Factories - Configurable Logging: +### Decorator Factories - Configurable Logging ```ts // Decorator factory that accepts configuration @@ -864,7 +864,7 @@ class PaymentService { } ``` -### Decorator Evaluation Order: +### Decorator Evaluation Order ```ts function first() { @@ -894,7 +894,7 @@ class ExampleClass { // second(): called ``` -### Real-World Example - API Controller: +### Real-World Example - API Controller ```ts // Simple decorator implementations (simplified for example) @@ -945,7 +945,7 @@ registerRoutes(); // Registered GET /users/:id ``` -### Common Pitfalls: +### Common Pitfalls ```ts function readonly(target: any, propertyKey: string) { @@ -984,7 +984,7 @@ class Demo { - Reference [JavaScript Projects (*JSDoc*)](https://www.w3schools.com/typescript/typescript_jsdoc.php) -### Getting Started: +### Getting Started ```ts // @ts-check @@ -1000,7 +1000,7 @@ function add(a, b) { } ``` -### Objects and Interfaces: +### Objects and Interfaces ```ts // @ts-check @@ -1017,7 +1017,7 @@ greet({ firstName: 'Jane' }); // Error: Property 'lastName' is missing ``` -### Type Definitions with @typedef: +### Type Definitions with @typedef ```ts // @ts-check @@ -1045,7 +1045,7 @@ const currentUser = { console.log(currentUser.role); ``` -### Intersection Types: +### Intersection Types ```ts // @ts-check @@ -1063,7 +1063,7 @@ const point3d = { x: 1, y: 2, z: 3 }; const point2d = { x: 1, y: 2 }; ``` -### Function Types - Basic: +### Function Types - Basic ```ts // @ts-check @@ -1082,7 +1082,7 @@ function calculateArea(width, height) { const area = calculateArea(10, 20); ``` -### Function Types - Callbacks: +### Function Types - Callbacks ```ts // @ts-check @@ -1111,7 +1111,7 @@ const result = processStrings(['hello', 'world'], toUpperCase); // result will be ['HELLO', 'WORLD'] ``` -### Function Overloads: +### Function Overloads ```ts // @ts-check @@ -1144,7 +1144,7 @@ const strResult = add('Hello, ', 'World!'); // string const numResult = add(10, 20); // number ``` -### Advanced Types - Union and Intersection: +### Advanced Types - Union and Intersection ```ts // @ts-check @@ -1178,7 +1178,7 @@ function getVisitorId(visitor) { } ``` -### Advanced Types - Mapped Types: +### Advanced Types - Mapped Types ```ts // @ts-check @@ -1204,7 +1204,7 @@ const name = userGetters.getName(); // string const age = userGetters.getAge(); // number ``` -### Type Imports: +### Type Imports ```ts // @ts-check @@ -1219,7 +1219,7 @@ const age = userGetters.getAge(); // number /** @typedef {import('./api').default as ApiClient} ApiClient */ ``` -### Create a types.d.ts file: +### Create a types.d.ts file ```ts // types.d.ts @@ -1235,7 +1235,7 @@ declare module 'my-module' { } ``` -### Using type imports in JavaScript: +### Using type imports in JavaScript ```ts // @ts-check @@ -1250,7 +1250,8 @@ const config = { import { initialize } from 'my-module'; initialize(config); ``` -### Type Imports: + +### Type Imports ```ts // @ts-check @@ -1265,7 +1266,7 @@ initialize(config); /** @typedef {import('./api').default as ApiClient} ApiClient */ ``` -### Create a types.d.ts file in your project:Type Imports: +### Create a types.d.ts file in your project:Type Imports ```ts // types.d.ts @@ -1281,7 +1282,7 @@ declare module 'my-module' { } ``` -#### Then use it in your JavaScript files:Type Imports: +#### Then use it in your JavaScript files:Type Imports ```ts // @ts-check @@ -1311,14 +1312,14 @@ git add . git commit -m "Pre-TypeScript migration state" ``` -### Configuration: +### Configuration ```ts # Install TypeScript as a dev dependency npm install --save-dev typescript @types/node ``` -### Create a basic tsconfig.json to start with:Configuration: +### Create a basic tsconfig.json to start with:Configuration ```ts { @@ -1340,7 +1341,7 @@ npm install --save-dev typescript @types/node > [!Note] > Adjust the target based on your minimum supported environments. -### Create a basic tsconfig.json with these recommended settings:Step-by-Step Migration: +### Create a basic tsconfig.json with these recommended settings:Step-by-Step Migration ```ts { @@ -1362,7 +1363,7 @@ npm install --save-dev typescript @types/node } ``` -### Add // @ts-check to the top of your JavaScript files to enable type checking:Step-by-Step Migration: +### Add // @ts-check to the top of your JavaScript files to enable type checking:Step-by-Step Migration ```ts // @ts-check @@ -1378,7 +1379,7 @@ name = 42; > [!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: +### Start with non-critical files and rename them from .js to .ts:Step-by-Step Migration ```ts # Rename a single file @@ -1388,7 +1389,7 @@ mv src/utils/helpers.js src/utils/helpers.ts find src/utils -name "*.js" -exec sh -c 'mv "$0" "${0%.js}.ts"' {} \; ``` -### Gradually add type annotations to your code:Step-by-Step Migration: +### Gradually add type annotations to your code:Step-by-Step Migration ```ts // Before @@ -1413,7 +1414,7 @@ function getUser(id: number): User { } ``` -### Modify your package.json to include TypeScript compilation:Step-by-Step Migration: +### Modify your package.json to include TypeScript compilation:Step-by-Step Migration ```ts { @@ -1428,7 +1429,7 @@ function getUser(id: number): User { > [!Note] > Make sure to update your test configuration to work with TypeScript files. -### Best Practices for Migration: +### Best Practices for Migration ```ts // Use type inference where possible @@ -1444,7 +1445,7 @@ function getUser(id: number): User { } ``` -### Common Challenges and Solutions: +### Common Challenges and Solutions ```ts // Before @@ -1453,7 +1454,7 @@ function getUser(id: number): User { // Error: Property 'name' does not exist ``` -### Common Challenges and Solutions: +### Common Challenges and Solutions ```ts // Option 1: Index signature @@ -1468,7 +1469,7 @@ function getUser(id: number): User { user.name = 'John'; // OK ``` -### Common Challenges and Solutions: +### Common Challenges and Solutions ```ts class Counter { @@ -1482,7 +1483,7 @@ function getUser(id: number): User { } ``` -### Common Challenges and Solutions: +### Common Challenges and Solutions ```ts // Solution 1: Arrow function @@ -1496,11 +1497,11 @@ function getUser(id: number): User { }.bind(this), 1000); ``` -## TypeScript Error Hanlding +## TypeScript Error Handling - Reference [Error Handling](https://www.w3schools.com/typescript/typescript_error_handling.php) -### Basic Error Handling: +### Basic Error Handling ```ts function divide(a: number, b: number): number { @@ -1518,7 +1519,7 @@ try { } ``` -### Custom Error Classes: +### Custom Error Classes ```ts class ValidationError extends Error { @@ -1549,7 +1550,7 @@ function validateUser(user: any) { } ``` -### Type Guards for Errors: +### Type Guards for Errors ```ts // Type guards @@ -1580,7 +1581,7 @@ try { } ``` -### Type Assertion Functions: +### Type Assertion Functions ```ts function assertIsError(error: unknown): asserts error is Error { @@ -1597,7 +1598,7 @@ try { } ``` -### Async Error Handling: +### Async Error Handling ```ts interface User { @@ -1638,7 +1639,7 @@ function fetchUserPosts(userId: number): Promise { } ``` -### Always Handle Promise Rejections: +### Always Handle Promise Rejections ```ts // Bad: Unhandled promise rejection @@ -1653,7 +1654,7 @@ fetchData() void fetchData().catch(console.error); ``` -### Error Boundaries in React: +### Error Boundaries in React ```tsx import React, { Component, ErrorInfo, ReactNode } from 'react'; @@ -1709,7 +1710,7 @@ function App() { } ``` -### Best Practices - Don't Swallow Errors: +### Best Practices - Don't Swallow Errors ```ts // Bad: Silent failure @@ -1721,7 +1722,7 @@ try { /* ... */ } catch (error) { } ``` -### Best Practices - Use Custom Error Types: +### Best Practices - Use Custom Error Types ```ts class NetworkError extends Error { @@ -1739,7 +1740,7 @@ class ValidationError extends Error { } ``` -### Best Practices - Handle Errors at Appropriate Layers: +### Best Practices - Handle Errors at Appropriate Layers ```ts // In a data access layer @@ -1774,7 +1775,7 @@ async function loadUser() { - Reference [Best Practices](https://www.w3schools.com/typescript/typescript_best_practices.php) -### Project Configuration - Enable Strict Mode: +### Project Configuration - Enable Strict Mode ```json // tsconfig.json @@ -1793,7 +1794,7 @@ async function loadUser() { } ``` -### Project Configuration - Additional Strict Checks: +### Project Configuration - Additional Strict Checks ```json { @@ -1810,7 +1811,7 @@ async function loadUser() { } ``` -### Type System - Let TypeScript Infer: +### Type System - Let TypeScript Infer ```ts // Bad: Redundant type annotation @@ -1830,7 +1831,7 @@ function add(a: number, b: number) { } ``` -### Type System - Be Explicit with Public APIs: +### Type System - Be Explicit with Public APIs ```ts // Bad: No type information @@ -1850,7 +1851,7 @@ function processUser(user: User): string { } ``` -### Type System - Interface vs Type: +### Type System - Interface vs Type ```ts // Use interface for object shapes that can be extended/implemented @@ -1877,7 +1878,7 @@ type ReadonlyUser = Readonly; type Point = [number, number]; ``` -### Type System - Prefer Specific Types Over 'any': +### Type System - Prefer Specific Types Over 'any' ```ts // Bad: Loses type safety @@ -1905,7 +1906,7 @@ function logUnknown(value: unknown) { } ``` -### Code Organization - Logical Modules: +### Code Organization - Logical Modules ```ts // user/user.model.ts @@ -1935,7 +1936,7 @@ export * from './user.model'; export * from './user.service'; ``` -### Code Organization - File Naming Patterns: +### Code Organization - File Naming Patterns ```ts // Good @@ -1952,7 +1953,7 @@ user_service.ts // Avoid snake_case userService.ts // Avoid camelCase for file names ``` -### Functions and Methods - Type-Safe Functions: +### Functions and Methods - Type-Safe Functions ```ts // Bad: No type information @@ -1983,7 +1984,7 @@ function sum(...numbers: number[]): number { } ``` -### Functions and Methods - Single Responsibility: +### Functions and Methods - Single Responsibility ```ts // Bad: Too many responsibilities @@ -2024,7 +2025,7 @@ function processUserData(userData: UserData): ProcessedUserData { } ``` -### Async/Await Patterns - Proper Error Handling: +### Async/Await Patterns - Proper Error Handling ```ts // Bad: Not handling errors @@ -2071,7 +2072,7 @@ async function getUserData(userId: string): Promise { } ``` -### Async/Await Patterns - Flatten Code: +### Async/Await Patterns - Flatten Code ```ts // Bad: Nested async/await (callback hell) @@ -2119,7 +2120,7 @@ async function processUser(userId: string) { } ``` -### Testing and Quality - Dependency Injection: +### Testing and Quality - Dependency Injection ```ts // Bad: Hard to test due to direct dependencies @@ -2172,7 +2173,7 @@ describe('PaymentProcessor', () => { }); ``` -### Testing and Quality - Type Testing: +### Testing and Quality - Type Testing ```ts // Using @ts-expect-error to test for type errors @@ -2201,7 +2202,7 @@ expectType(user.name); */ ``` -### Performance - Type-Only Imports: +### Performance - Type-Only Imports ```ts // Bad: Imports both type and value @@ -2224,7 +2225,7 @@ export { fetchUser }; // to ensure type-only imports are properly handled ``` -### Performance - Avoid Complex Types: +### Performance - Avoid Complex Types ```ts // Bad: Deeply nested mapped types can be slow @@ -2266,7 +2267,7 @@ interface User { } ``` -### Performance - Const Assertions: +### Performance - Const Assertions ```ts // Without const assertion (wider type) @@ -2295,7 +2296,7 @@ const config = { // } ``` -### Common Mistakes - Avoid 'any': +### Common Mistakes - Avoid 'any' ```ts // Bad: Loses all type safety @@ -2319,7 +2320,7 @@ function processUsers(users: User[]) { } ``` -### Common Mistakes - Enable Strict Mode: +### Common Mistakes - Enable Strict Mode ```json // tsconfig.json @@ -2338,7 +2339,7 @@ function processUsers(users: User[]) { } ``` -### Common Mistakes - Let TypeScript Infer: +### Common Mistakes - Let TypeScript Infer ```ts // Redundant type annotation @@ -2358,7 +2359,7 @@ function add(a: number, b: number) { } ``` -### Common Mistakes - Use Type Guards: +### Common Mistakes - Use Type Guards ```ts // Without type guard @@ -2385,7 +2386,7 @@ if (value instanceof Date) { /* value is Date */ } if ('id' in user) { /* user has id property */ } ``` -### Common Mistakes - Handle Null/Undefined: +### Common Mistakes - Handle Null/Undefined ```ts // Bad: Potential runtime error From b41cc6bc2220019680eb3da47fb5cba1544d5635 Mon Sep 17 00:00:00 2001 From: John Haugabook Date: Thu, 5 Mar 2026 21:26:33 -0500 Subject: [PATCH 03/11] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- skills/typescript-coder/SKILL.md | 2 +- skills/typescript-coder/references/typescript-basics.md | 8 ++++---- skills/typescript-coder/references/typescript-elements.md | 2 +- .../references/typescript-miscellaneous.md | 4 ++-- skills/typescript-coder/references/typescript-projects.md | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/skills/typescript-coder/SKILL.md b/skills/typescript-coder/SKILL.md index 7eba14c17..0bffebe0f 100644 --- a/skills/typescript-coder/SKILL.md +++ b/skills/typescript-coder/SKILL.md @@ -39,7 +39,7 @@ Use these shorthand commands to quickly invoke TypeScript expertise without leng - `typescript-coder --check "this code"` - `typescript-coder check this type guard` - `ts-coder migrate this file` -- `ts-coder --migrate project-to-javascript` +- `ts-coder --migrate project-to-typescript` ## Role and Expertise diff --git a/skills/typescript-coder/references/typescript-basics.md b/skills/typescript-coder/references/typescript-basics.md index 18757f9a9..77b1d17d3 100644 --- a/skills/typescript-coder/references/typescript-basics.md +++ b/skills/typescript-coder/references/typescript-basics.md @@ -152,16 +152,16 @@ console.log(obj[uniqueKey]); // "This is a unique property" ```ts // String -greeting: string = "Hello, TypeScript!"; +let greeting: string = "Hello, TypeScript!"; // Number -userCount: number = 42; +let userCount: number = 42; // Boolean -isLoading: boolean = true; +let isLoading: boolean = true; // Array of numbers -scores: number[] = [100, 95, 98]; +let scores: number[] = [100, 95, 98]; ``` ```ts diff --git a/skills/typescript-coder/references/typescript-elements.md b/skills/typescript-coder/references/typescript-elements.md index f59164f3c..1ea20cf0c 100644 --- a/skills/typescript-coder/references/typescript-elements.md +++ b/skills/typescript-coder/references/typescript-elements.md @@ -380,7 +380,7 @@ const negateFunction: Negate = (value) => value * -1; ## TypeScript Casting -- Reference [Casting](https://www.w3schools.com/typescript/typescript_functions.php) +- Reference [Casting](https://www.w3schools.com/typescript/typescript_casting.php) ### Casting with as: diff --git a/skills/typescript-coder/references/typescript-miscellaneous.md b/skills/typescript-coder/references/typescript-miscellaneous.md index 256cedbe4..e38a88f16 100644 --- a/skills/typescript-coder/references/typescript-miscellaneous.md +++ b/skills/typescript-coder/references/typescript-miscellaneous.md @@ -1266,7 +1266,7 @@ initialize(config); /** @typedef {import('./api').default as ApiClient} ApiClient */ ``` -### Create a types.d.ts file in your project:Type Imports +### Create a types.d.ts file in your project ```ts // types.d.ts @@ -1282,7 +1282,7 @@ declare module 'my-module' { } ``` -#### Then use it in your JavaScript files:Type Imports +#### Then use it in your JavaScript files ```ts // @ts-check diff --git a/skills/typescript-coder/references/typescript-projects.md b/skills/typescript-coder/references/typescript-projects.md index 2c8700a90..64b2fc477 100644 --- a/skills/typescript-coder/references/typescript-projects.md +++ b/skills/typescript-coder/references/typescript-projects.md @@ -767,7 +767,7 @@ module.exports = { testEnvironment: 'jsdom', setupFilesAfterEnv: ['@testing-library/jest-dom'], moduleNameMapper: { - '^@/(.*)$': '/src/$1', + '^@/(.*)$': '/src/$1', '\\\.(css|less|scss|sass)$': 'identity-obj-proxy', }, transform: { From d79ad853e4553a5ca0bb81a3542958e5f7576d7e Mon Sep 17 00:00:00 2001 From: John Haugabook Date: Thu, 5 Mar 2026 21:29:14 -0500 Subject: [PATCH 04/11] Apply suggestions from code review --- skills/typescript-coder/references/typescript-projects.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/skills/typescript-coder/references/typescript-projects.md b/skills/typescript-coder/references/typescript-projects.md index 64b2fc477..50d65a9b1 100644 --- a/skills/typescript-coder/references/typescript-projects.md +++ b/skills/typescript-coder/references/typescript-projects.md @@ -197,6 +197,14 @@ export const authenticate = (req: Request, res: Response, next: NextFunction) => } try { + /********************************************************************************************* + IMPORTANT + An attacker can exploit this by sending any arbitrary token to gain access to protected + routes that rely on authenticate/authorize, resulting in a complete authentication and + authorization bypass. Replace the mock decoded assignment with real JWT verification + (including signature, expiry, and claims checks) and ensure that invalid or missing tokens + never populate req.user or reach privileged handlers. + **********************************************************************************************/ // In a real app, verify the JWT token here const decoded = { id: 1, role: 'admin' }; // Mock decoded token req.user = decoded; From 0cd7f703bbae4c93ed76514163269b6095351854 Mon Sep 17 00:00:00 2001 From: John Haugabook Date: Thu, 5 Mar 2026 21:32:30 -0500 Subject: [PATCH 05/11] Apply suggestion from @jhauga My browsers' being buggy, but this should have been committed with the last batch of commits. --- .../references/typescript-miscellaneous.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/skills/typescript-coder/references/typescript-miscellaneous.md b/skills/typescript-coder/references/typescript-miscellaneous.md index e38a88f16..c3c257ca6 100644 --- a/skills/typescript-coder/references/typescript-miscellaneous.md +++ b/skills/typescript-coder/references/typescript-miscellaneous.md @@ -1251,21 +1251,6 @@ import { initialize } from 'my-module'; initialize(config); ``` -### Type Imports - -```ts -// @ts-check - -// Importing types from TypeScript files -/** @typedef {import('./types').User} User */ - -// Importing types from node_modules -/** @typedef {import('express').Request} ExpressRequest */ - -// Importing with renaming -/** @typedef {import('./api').default as ApiClient} ApiClient */ -``` - ### Create a types.d.ts file in your project ```ts From 1ba504a8a99b0052a11e69692bb8d26fe5b4ebee Mon Sep 17 00:00:00 2001 From: jhauga Date: Fri, 6 Mar 2026 19:42:44 -0500 Subject: [PATCH 06/11] changes to new skill typescript-coder --- docs/README.skills.md | 2 +- skills/typescript-coder/SKILL.md | 62 +- .../assets/select-from-the-type-menu.md | 1999 +++++++++++++++++ .../assets/typescript-algolia.md | 540 +++++ .../assets/typescript-angular-basic.md | 448 ++++ .../assets/typescript-aurelia.md | 439 ++++ .../assets/typescript-backbone.md | 415 ++++ .../assets/typescript-bitloops.md | 547 +++++ .../typescript-bscotch-template-modern.md | 455 ++++ .../typescript-coder/assets/typescript-ego.md | 421 ++++ .../assets/typescript-express-no-stress.md | 584 +++++ .../assets/typescript-gulp-angular.md | 373 +++ .../assets/typescript-kodly-react.md | 434 ++++ .../assets/typescript-lit-element.md | 513 +++++ .../assets/typescript-nestjs-boilerplate.md | 492 ++++ .../assets/typescript-ngx-rocket.md | 436 ++++ .../assets/typescript-node-boilerplate.md | 332 +++ .../assets/typescript-node-module.md | 365 +++ .../assets/typescript-node-tsnext.md | 230 ++ .../assets/typescript-package.md | 457 ++++ .../typescript-project-template-modern.md | 597 +++++ .../assets/typescript-react-lib.md | 371 +++ .../assets/typescript-rznode.md | 604 +++++ .../assets/typescript-tsx-adobe.md | 454 ++++ .../assets/typescript-tsx-docgen.md | 502 +++++ .../assets/typescript-xes-bdf.md | 455 ++++ .../assets/typescript-zotero-plugin.md | 397 ++++ .../{typescript-basics.md => basics.md} | 60 +- .../{typescript-classes.md => classes.md} | 13 +- .../{typescript-elements.md => elements.md} | 119 +- .../{typescript-keywords.md => keywords.md} | 32 +- ...ript-miscellaneous.md => miscellaneous.md} | 23 +- .../{typescript-projects.md => projects.md} | 110 +- .../{typescript-types.md => types.md} | 95 +- .../references/typescript-cheatsheet.md | 1 + .../references/typescript-configuration.md | 1054 +++++++++ .../references/typescript-d.ts-templates.md | 403 ++++ .../typescript-declaration-files.md | 651 ++++++ .../references/typescript-essentials.md | 887 ++++++++ .../references/typescript-getstarted.md | 609 +++++ .../references/typescript-handbook.md | 11 + .../typescript-module-references.md | 483 ++++ .../references/typescript-quickstart.md | 727 ++++++ .../references/typescript-releases.md | 269 +++ .../references/typescript-tools.md | 241 ++ .../references/typescript-tutorials.md | 724 ++++++ .../references/typescript-with-javascript.md | 583 +++++ .../typescript-coder/scripts/bun-workflow.js | 383 ++++ .../typescript-coder/scripts/bun-workflow.md | 682 ++++++ .../typescript-coder/scripts/health-check.js | 313 +++ .../typescript-coder/scripts/health-check.md | 947 ++++++++ .../typescript-coder/scripts/npm-workflow.js | 328 +++ .../typescript-coder/scripts/npm-workflow.md | 692 ++++++ .../scripts/yeoman-generator.js | 472 ++++ .../scripts/yeoman-generator.md | 738 ++++++ 55 files changed, 24382 insertions(+), 192 deletions(-) create mode 100644 skills/typescript-coder/assets/select-from-the-type-menu.md create mode 100644 skills/typescript-coder/assets/typescript-algolia.md create mode 100644 skills/typescript-coder/assets/typescript-angular-basic.md create mode 100644 skills/typescript-coder/assets/typescript-aurelia.md create mode 100644 skills/typescript-coder/assets/typescript-backbone.md create mode 100644 skills/typescript-coder/assets/typescript-bitloops.md create mode 100644 skills/typescript-coder/assets/typescript-bscotch-template-modern.md create mode 100644 skills/typescript-coder/assets/typescript-ego.md create mode 100644 skills/typescript-coder/assets/typescript-express-no-stress.md create mode 100644 skills/typescript-coder/assets/typescript-gulp-angular.md create mode 100644 skills/typescript-coder/assets/typescript-kodly-react.md create mode 100644 skills/typescript-coder/assets/typescript-lit-element.md create mode 100644 skills/typescript-coder/assets/typescript-nestjs-boilerplate.md create mode 100644 skills/typescript-coder/assets/typescript-ngx-rocket.md create mode 100644 skills/typescript-coder/assets/typescript-node-boilerplate.md create mode 100644 skills/typescript-coder/assets/typescript-node-module.md create mode 100644 skills/typescript-coder/assets/typescript-node-tsnext.md create mode 100644 skills/typescript-coder/assets/typescript-package.md create mode 100644 skills/typescript-coder/assets/typescript-project-template-modern.md create mode 100644 skills/typescript-coder/assets/typescript-react-lib.md create mode 100644 skills/typescript-coder/assets/typescript-rznode.md create mode 100644 skills/typescript-coder/assets/typescript-tsx-adobe.md create mode 100644 skills/typescript-coder/assets/typescript-tsx-docgen.md create mode 100644 skills/typescript-coder/assets/typescript-xes-bdf.md create mode 100644 skills/typescript-coder/assets/typescript-zotero-plugin.md rename skills/typescript-coder/references/{typescript-basics.md => basics.md} (70%) rename skills/typescript-coder/references/{typescript-classes.md => classes.md} (87%) rename skills/typescript-coder/references/{typescript-elements.md => elements.md} (65%) rename skills/typescript-coder/references/{typescript-keywords.md => keywords.md} (61%) rename skills/typescript-coder/references/{typescript-miscellaneous.md => miscellaneous.md} (96%) rename skills/typescript-coder/references/{typescript-projects.md => projects.md} (84%) rename skills/typescript-coder/references/{typescript-types.md => types.md} (92%) create mode 100644 skills/typescript-coder/references/typescript-configuration.md create mode 100644 skills/typescript-coder/references/typescript-d.ts-templates.md create mode 100644 skills/typescript-coder/references/typescript-declaration-files.md create mode 100644 skills/typescript-coder/references/typescript-essentials.md create mode 100644 skills/typescript-coder/references/typescript-getstarted.md create mode 100644 skills/typescript-coder/references/typescript-module-references.md create mode 100644 skills/typescript-coder/references/typescript-quickstart.md create mode 100644 skills/typescript-coder/references/typescript-releases.md create mode 100644 skills/typescript-coder/references/typescript-tools.md create mode 100644 skills/typescript-coder/references/typescript-tutorials.md create mode 100644 skills/typescript-coder/references/typescript-with-javascript.md create mode 100644 skills/typescript-coder/scripts/bun-workflow.js create mode 100644 skills/typescript-coder/scripts/bun-workflow.md create mode 100644 skills/typescript-coder/scripts/health-check.js create mode 100644 skills/typescript-coder/scripts/health-check.md create mode 100644 skills/typescript-coder/scripts/npm-workflow.js create mode 100644 skills/typescript-coder/scripts/npm-workflow.md create mode 100644 skills/typescript-coder/scripts/yeoman-generator.js create mode 100644 skills/typescript-coder/scripts/yeoman-generator.md diff --git a/docs/README.skills.md b/docs/README.skills.md index e70294a83..e6ee3436c 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -213,7 +213,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/typescript-basics.md`
`references/typescript-cheatsheet.md`
`references/typescript-classes.md`
`references/typescript-elements.md`
`references/typescript-handbook.md`
`references/typescript-keywords.md`
`references/typescript-miscellaneous.md`
`references/typescript-projects.md`
`references/typescript-types.md` | +| [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. | `assets/select-from-the-type-menu.md`
`assets/typescript-algolia.md`
`assets/typescript-angular-basic.md`
`assets/typescript-aurelia.md`
`assets/typescript-backbone.md`
`assets/typescript-bitloops.md`
`assets/typescript-bscotch-template-modern.md`
`assets/typescript-ego.md`
`assets/typescript-express-no-stress.md`
`assets/typescript-gulp-angular.md`
`assets/typescript-kodly-react.md`
`assets/typescript-lit-element.md`
`assets/typescript-nestjs-boilerplate.md`
`assets/typescript-ngx-rocket.md`
`assets/typescript-node-boilerplate.md`
`assets/typescript-node-module.md`
`assets/typescript-node-tsnext.md`
`assets/typescript-package.md`
`assets/typescript-project-template-modern.md`
`assets/typescript-react-lib.md`
`assets/typescript-rznode.md`
`assets/typescript-tsx-adobe.md`
`assets/typescript-tsx-docgen.md`
`assets/typescript-xes-bdf.md`
`assets/typescript-zotero-plugin.md`
`references/basics.md`
`references/classes.md`
`references/elements.md`
`references/keywords.md`
`references/miscellaneous.md`
`references/projects.md`
`references/types.md`
`references/typescript-cheatsheet.md`
`references/typescript-configuration.md`
`references/typescript-d.ts-templates.md`
`references/typescript-declaration-files.md`
`references/typescript-essentials.md`
`references/typescript-getstarted.md`
`references/typescript-handbook.md`
`references/typescript-module-references.md`
`references/typescript-quickstart.md`
`references/typescript-releases.md`
`references/typescript-tools.md`
`references/typescript-tutorials.md`
`references/typescript-with-javascript.md`
`scripts/bun-workflow.js`
`scripts/bun-workflow.md`
`scripts/health-check.js`
`scripts/health-check.md`
`scripts/npm-workflow.js`
`scripts/npm-workflow.md`
`scripts/yeoman-generator.js`
`scripts/yeoman-generator.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 index 0bffebe0f..8320cd6bb 100644 --- a/skills/typescript-coder/SKILL.md +++ b/skills/typescript-coder/SKILL.md @@ -768,26 +768,78 @@ When migrating a JavaScript project to TypeScript: ## References -This skill includes bundled reference documentation in the `references/` directory: +This skill includes bundled reference documentation, project templates, and workflow scripts. -- **[typescript-basics.md](references/typescript-basics.md)** - TypeScript fundamentals, simple types, type inference, and special types -- **[typescript-cheatsheet.md](references/typescript-cheatsheet.md)** - Quick reference for control flow, classes, interfaces, types, and common patterns +### Reference Documentation (`references/`) + +#### Getting Started & Core Concepts + +- **[typescript-getstarted.md](references/typescript-getstarted.md)** - Installation, first steps, and TypeScript tooling setup +- **[typescript-quickstart.md](references/typescript-quickstart.md)** - Rapid introduction to TypeScript syntax and key features +- **[typescript-essentials.md](references/typescript-essentials.md)** - Core TypeScript concepts every developer should know +- **[typescript-handbook.md](references/typescript-handbook.md)** - Comprehensive handbook covering core concepts from official TypeScript documentation (updated) +- **[typescript-cheatsheet.md](references/typescript-cheatsheet.md)** - Quick reference for control flow, classes, interfaces, types, and common patterns (updated) + +#### Type System & Language Features + +- **[typescript-types.md](references/typescript-types.md)** - Advanced types, conditional types, mapped types, type guards, and recursive types - **[typescript-classes.md](references/typescript-classes.md)** - Class syntax, inheritance, generics, and utility types - **[typescript-elements.md](references/typescript-elements.md)** - Arrays, tuples, objects, enums, functions, and casting -- **[typescript-handbook.md](references/typescript-handbook.md)** - Comprehensive handbook covering core concepts from official TypeScript documentation - **[typescript-keywords.md](references/typescript-keywords.md)** - keyof, null handling, optional chaining, and template literal types - **[typescript-miscellaneous.md](references/typescript-miscellaneous.md)** - Async programming, promises, decorators, and JSDoc integration + +#### Modules, Declarations & Configuration + +- **[typescript-module-references.md](references/typescript-module-references.md)** - Module systems, import/export patterns, and path resolution +- **[typescript-declaration-files.md](references/typescript-declaration-files.md)** - Writing and consuming `.d.ts` files +- **[typescript-d.ts-templates.md](references/typescript-d.ts-templates.md)** - Ready-to-use declaration file templates for common patterns +- **[typescript-with-javascript.md](references/typescript-with-javascript.md)** - Using TypeScript alongside JavaScript, `allowJs`, and JSDoc types +- **[typescript-configuration.md](references/typescript-configuration.md)** - Deep dive into `tsconfig.json` options and project setup - **[typescript-projects.md](references/typescript-projects.md)** - Project configuration, Node.js setup, Express integration, React TypeScript -- **[typescript-types.md](references/typescript-types.md)** - Advanced types, conditional types, mapped types, type guards, and recursive types + +#### Ecosystem & Learning + +- **[typescript-tools.md](references/typescript-tools.md)** - Editors, linters, formatters, and build tool integrations +- **[typescript-releases.md](references/typescript-releases.md)** - TypeScript version history and notable new features +- **[typescript-tutorials.md](references/typescript-tutorials.md)** - Step-by-step tutorials for common TypeScript use cases +- **[typescript-basics.md](references/typescript-basics.md)** - TypeScript fundamentals, simple types, type inference, and special types + +### Project Templates (`assets/`) + +The `assets/` folder contains ready-to-use TypeScript project templates: + +- Node.js library template (CJS + ESM dual output) +- React + TypeScript application template +- Express + TypeScript API template +- TypeScript monorepo template with npm workspaces + +### Workflow Scripts (`scripts/`) + +Step-by-step workflow guides and automation scripts for TypeScript development: + +- **[scripts/yeoman-typescript-generator.md](scripts/yeoman-typescript-generator.md)** - Create and publish custom Yeoman generators for scaffolding TypeScript projects. Covers generator structure, prompts, templates, composition, testing, and npm publishing. +- **[scripts/npm-typescript-workflow.md](scripts/npm-typescript-workflow.md)** - Comprehensive npm workflow for TypeScript projects: initialization, essential `package.json` scripts, `@types/*` management, publishing with declarations and exports map, npm workspaces for monorepos, and security auditing. +- **[scripts/bun-typescript-workflow.md](scripts/bun-typescript-workflow.md)** - Full Bun runtime workflow for TypeScript: running `.ts` files without compilation, `bun test`, `bun build`, `bun install`, `bunfig.toml` configuration, and migrating from Node.js/npm to Bun. +- **[scripts/typescript-health-check.md](scripts/typescript-health-check.md)** - TypeScript project health check with type coverage analysis, strict mode auditing, dead code detection, circular dependency checks, bundle size analysis, type performance profiling, declaration file validation, and a shell script (`health-check.sh`) that runs all checks automatically. ### 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 +- [Yeoman Generators](https://yeoman.io/generators/) - Browse community generators +- [Bun Documentation](https://bun.sh/docs) - Bun runtime reference +- [npm Documentation](https://docs.npmjs.com/) - npm CLI and registry reference ## 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. +This skill now includes: +- **Expanded reference material** across 13+ reference files covering the full TypeScript ecosystem +- **Project templates** in the `assets/` folder for common TypeScript project types +- **Workflow scripts** for project scaffolding (Yeoman), package management (npm and Bun), and automated health checking + **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/assets/select-from-the-type-menu.md b/skills/typescript-coder/assets/select-from-the-type-menu.md new file mode 100644 index 000000000..030535777 --- /dev/null +++ b/skills/typescript-coder/assets/select-from-the-type-menu.md @@ -0,0 +1,1999 @@ + + +# Select From the Type Menu + +> A modular TypeScript type-system template. Pick and combine ingredients to build a custom +> TypeScript project starter tailored to your project's scope, purpose, and goals. +> +> Based on the official [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/). +> TypeScript is licensed under Apache-2.0 by Microsoft Corporation. + +--- + +## How to Use This Template + +This file is a **modular ingredient library**, not a single fixed template. + +1. **Read the Base Setup** section — it is always required. Copy it into your project first. +2. **Browse the Menu Sections** below. Each section is independent. +3. **Select the sections that match your project's needs.** You can combine any number of them. +4. **Copy selected sections into a `src/types.ts` file** (or a `src/types/` directory for larger projects). +5. **Adapt the examples to your domain** — rename types, swap placeholder names, remove examples you don't need. +6. **Remove unselected sections entirely.** + +With 15 independent menu sections, each of which can be included or excluded, this template +supports over **32,000** possible combinations of type features. The Combination Examples section +at the end shows 8 common project archetypes and which sections to select for each. + +--- + +## Base Setup (Required for All Projects) + +Every project starts here. Copy this and customize as needed. + +### `package.json` shell + +```json +{ + "name": "my-project", + "version": "0.1.0", + "type": "module", + "scripts": { + "build": "tsc", + "type-check": "tsc --noEmit", + "test": "vitest run" + }, + "devDependencies": { + "typescript": "^5.7.0", + "@types/node": "^22.0.0" + } +} +``` + +### `tsconfig.json` (strict baseline) + +```json +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +### `src/index.ts` shell + +```typescript +/** + * Entry point. Import and re-export your types and logic here. + */ + +// Replace this with your actual exports +export const VERSION = "0.1.0"; +``` + +--- + +## Menu Sections + +Each section below is an independent "ingredient." Select the ones that apply to your project. + +--- + +### Primitive & Everyday Types + +**Choose these for:** any project — the foundation of all TypeScript + +**Reference:** [Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) + +```typescript +// ── Primitives ────────────────────────────────────────────────────────────── + +const message: string = "Hello, TypeScript"; +const count: number = 42; +const ratio: number = 3.14; +const isActive: boolean = true; +const nothing: null = null; +const notYet: undefined = undefined; + +// BigInt (for integers beyond Number.MAX_SAFE_INTEGER) +const bigNum: bigint = 9_007_199_254_740_993n; + +// Symbol (unique, non-enumerable identifier) +const sym: symbol = Symbol("description"); + +// ── Arrays ────────────────────────────────────────────────────────────────── + +const names: string[] = ["Alice", "Bob", "Carol"]; +const scores: Array = [95, 87, 72]; // Equivalent generic form + +// Readonly array — prevents mutation +const DAYS: ReadonlyArray = ["Mon", "Tue", "Wed", "Thu", "Fri"]; +// Or: const DAYS: readonly string[] = [...] + +// ── Tuples ────────────────────────────────────────────────────────────────── + +// Fixed-length, fixed-type array +type Coordinate = [x: number, y: number]; +const origin: Coordinate = [0, 0]; + +// Tuple with optional element +type HttpResponse = [statusCode: number, body: string, headers?: Record]; + +// Readonly tuple +type ImmutablePair = readonly [string, number]; + +// ── Special Types ─────────────────────────────────────────────────────────── + +// unknown — safer alternative to any; must narrow before use +function parseInput(raw: unknown): string { + if (typeof raw === "string") return raw; + if (typeof raw === "number") return String(raw); + throw new TypeError(`Cannot parse input of type ${typeof raw}`); +} + +// never — a value that can never occur (exhaustive checks, throwing functions) +function assertNever(value: never): never { + throw new Error(`Unhandled case: ${JSON.stringify(value)}`); +} + +// void — function return type when there is no meaningful return value +function logMessage(msg: string): void { + console.log(msg); +} + +// any — escape hatch; avoid in production code, prefer unknown +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function legacyInterop(data: any): void { + // Only use any when integrating with untyped third-party code + console.log(data); +} +``` + +--- + +### Object Types & Interfaces + +**Choose these for:** data models, API shapes, configuration objects, domain entities + +**Reference:** [Object Types](https://www.typescriptlang.org/docs/handbook/2/objects.html) + +```typescript +// ── Interface Declaration ──────────────────────────────────────────────────── + +interface User { + readonly id: string; // readonly — cannot be reassigned after creation + name: string; + email: string; + age?: number; // optional — may be undefined + readonly createdAt: Date; +} + +// ── Type Alias for Object Shape ────────────────────────────────────────────── + +// Use type for unions, intersections, and when you need a non-extensible shape +type Point = { + x: number; + y: number; +}; + +// ── Interface vs Type Alias ────────────────────────────────────────────────── +// Interface: can be extended (declaration merging), better error messages for objects +// Type: required for unions/intersections, cannot be re-declared + +// Interfaces are extendable via extends: +interface Animal { + name: string; +} + +interface Dog extends Animal { + breed: string; +} + +// Type aliases extend via intersection: +type Cat = Animal & { indoor: boolean }; + +// ── Optional and Readonly Properties ──────────────────────────────────────── + +interface Config { + readonly host: string; + readonly port: number; + timeout?: number; // Optional + retries?: number; // Optional + tls?: { // Nested optional object + cert: string; + key: string; + }; +} + +// ── Index Signatures ───────────────────────────────────────────────────────── +// Use when the property names are not known ahead of time + +interface StringMap { + [key: string]: string; +} + +interface NumberRecord { + [id: string]: number; +} + +// Mixed: known keys + index signature (index signature type must include known values) +interface UserRegistry { + admin: User; + [userId: string]: User; +} + +// ── Excess Property Checking ───────────────────────────────────────────────── +// TypeScript checks for excess properties on object literals assigned to a typed variable + +interface Options { + color?: string; + width?: number; +} + +// This would error — 'colour' is not in Options +// const opts: Options = { colour: "red" }; // Error! + +// Workaround for dynamic objects: use intermediate variable +const dynamicOpts = { colour: "red", width: 100 }; +const opts: Options = dynamicOpts; // OK — structural compatibility + +// ── Nested and Recursive Types ──────────────────────────────────────────────── + +interface TreeNode { + value: T; + children?: TreeNode[]; +} + +type JsonValue = + | string + | number + | boolean + | null + | JsonValue[] + | { [key: string]: JsonValue }; +``` + +--- + +### Union & Intersection Types + +**Choose these for:** flexible APIs, combining existing types, discriminated unions, multi-type parameters + +**Reference:** [Everyday Types — Union Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) + +```typescript +// ── Union Types ────────────────────────────────────────────────────────────── + +// A value that can be one of several types +type StringOrNumber = string | number; +type NullableString = string | null; +type MaybeUser = User | null | undefined; + +// Function accepting multiple types +function formatId(id: string | number): string { + if (typeof id === "number") { + return id.toString().padStart(8, "0"); + } + return id; +} + +// ── Intersection Types ─────────────────────────────────────────────────────── + +// Combine multiple types into one — the value must satisfy ALL constituent types +type Serializable = { serialize(): string }; +type Identifiable = { id: string }; +type SerializableEntity = Identifiable & Serializable; + +// Common pattern: Mixin / trait composition +interface Timestamped { + createdAt: Date; + updatedAt: Date; +} + +interface SoftDeletable { + deletedAt: Date | null; +} + +type AuditedEntity = Timestamped & SoftDeletable; + +// Intersection with an interface +interface BaseEntity { + id: string; +} + +type UserEntity = BaseEntity & { + name: string; + email: string; +}; + +// ── Combining Union + Intersection ──────────────────────────────────────────── + +type AdminUser = User & { role: "admin"; permissions: string[] }; +type GuestUser = { role: "guest"; sessionId: string }; +type AuthenticatedUser = AdminUser | GuestUser; + +// ── Narrowing Union Types ───────────────────────────────────────────────────── + +function processId(id: string | number): string { + // typeof guard — narrows to the specific primitive type + if (typeof id === "string") { + return id.toUpperCase(); // TypeScript knows: id is string here + } + return id.toFixed(2); // TypeScript knows: id is number here +} + +// instanceof guard — for class instances +function formatDate(value: Date | string): string { + if (value instanceof Date) { + return value.toISOString(); // TypeScript knows: value is Date here + } + return value; // TypeScript knows: value is string here +} +``` + +--- + +### Literal Types & Const Assertions + +**Choose these for:** state machines, option sets, fixed value sets, configuration enums, exhaustive handling + +**Reference:** [Everyday Types — Literal Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) + +```typescript +// ── String Literal Types ────────────────────────────────────────────────────── + +type Direction = "north" | "south" | "east" | "west"; +type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; +type LogLevel = "debug" | "info" | "warn" | "error"; + +function move(direction: Direction): void { + console.log(`Moving ${direction}`); +} + +move("north"); // OK +// move("up"); // Error: Argument of type '"up"' is not assignable to parameter of type 'Direction' + +// ── Numeric Literal Types ───────────────────────────────────────────────────── + +type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6; +type HttpStatusCode = 200 | 201 | 204 | 400 | 401 | 403 | 404 | 500; + +// ── Boolean Literal Types ───────────────────────────────────────────────────── + +type StrictTrue = true; +type StrictFalse = false; + +// Useful in discriminated unions: +type Loading = { status: "loading"; data: null }; +type Loaded = { status: "loaded"; data: T }; +type Failed = { status: "error"; error: Error; data: null }; + +// ── Const Assertions ───────────────────────────────────────────────────────── + +// Without `as const` — TypeScript widens the type +const mutableConfig = { + host: "localhost", // inferred: string (wide) + port: 3000, // inferred: number (wide) +}; + +// With `as const` — TypeScript narrows to literal types +const CONFIG = { + host: "localhost", // literal: "localhost" + port: 3000, // literal: 3000 + features: ["auth", "logging"] as const, +} as const; + +// CONFIG.port is now type 3000, not number +// CONFIG.features is now readonly ["auth", "logging"] + +type Config = typeof CONFIG; +// { readonly host: "localhost"; readonly port: 3000; readonly features: readonly ["auth", "logging"]; } + +// Deriving a union from an array literal using `as const` +const ALLOWED_ROLES = ["admin", "editor", "viewer"] as const; +type Role = (typeof ALLOWED_ROLES)[number]; // "admin" | "editor" | "viewer" + +// Deriving types from object values +const STATUS_CODES = { + ok: 200, + created: 201, + notFound: 404, + error: 500, +} as const; + +type StatusCode = (typeof STATUS_CODES)[keyof typeof STATUS_CODES]; +// 200 | 201 | 404 | 500 +``` + +--- + +### Generics + +**Choose these for:** reusable utilities, collections, wrappers, repository patterns, data transformation + +**Reference:** [Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) + +```typescript +// ── Generic Functions ──────────────────────────────────────────────────────── + +// The simplest generic — identity function +function identity(value: T): T { + return value; +} + +const strVal = identity("hello"); // T inferred as string +const numVal = identity(42); // T inferred as number + +// Multiple type parameters +function pair(first: A, second: B): [A, B] { + return [first, second]; +} + +const strNum = pair("hello", 42); // [string, number] + +// ── Generic Constraints ─────────────────────────────────────────────────────── + +// Constrain T to only types that have a `.length` property +function getLength(value: T): number { + return value.length; +} + +getLength("hello"); // OK — string has .length +getLength([1, 2, 3]); // OK — array has .length +// getLength(42); // Error — number has no .length + +// Constrain K to only keys that actually exist on T +function getProperty(obj: T, key: K): T[K] { + return obj[key]; +} + +const user = { name: "Alice", age: 30, email: "alice@example.com" }; +const name = getProperty(user, "name"); // Type: string +const age = getProperty(user, "age"); // Type: number +// getProperty(user, "phone"); // Error — 'phone' is not a key of user + +// ── Generic Interfaces ──────────────────────────────────────────────────────── + +interface Repository { + findById(id: ID): Promise; + findAll(): Promise; + create(item: Omit): Promise; + update(id: ID, item: Partial): Promise; + delete(id: ID): Promise; +} + +// Implementation example +interface UserEntity { + id: string; + name: string; + email: string; +} + +class InMemoryUserRepository implements Repository { + private store = new Map(); + + async findById(id: string): Promise { + return this.store.get(id) ?? null; + } + + async findAll(): Promise { + return Array.from(this.store.values()); + } + + async create(item: Omit): Promise { + const entity: UserEntity = { ...item, id: crypto.randomUUID() }; + this.store.set(entity.id, entity); + return entity; + } + + async update(id: string, item: Partial): Promise { + const existing = this.store.get(id); + if (!existing) throw new Error(`Entity ${id} not found`); + const updated = { ...existing, ...item }; + this.store.set(id, updated); + return updated; + } + + async delete(id: string): Promise { + return this.store.delete(id); + } +} + +// ── Generic Classes ─────────────────────────────────────────────────────────── + +class Stack { + private items: T[] = []; + + push(item: T): void { + this.items.push(item); + } + + pop(): T | undefined { + return this.items.pop(); + } + + peek(): T | undefined { + return this.items[this.items.length - 1]; + } + + get size(): number { + return this.items.length; + } + + isEmpty(): boolean { + return this.items.length === 0; + } +} + +const numberStack = new Stack(); +numberStack.push(1); +numberStack.push(2); +console.log(numberStack.pop()); // 2 + +// ── Default Type Parameters ─────────────────────────────────────────────────── + +// T defaults to unknown if not provided +interface ApiResponse { + data: T; + statusCode: number; + message: string; +} + +type UserResponse = ApiResponse; // Explicit +type GenericResponse = ApiResponse; // Uses default: unknown + +// ── Generic Utility Functions ───────────────────────────────────────────────── + +/** Filters an array to only items that match the predicate. Type-safe. */ +function filterDefined(array: Array): T[] { + return array.filter((item): item is T => item !== null && item !== undefined); +} + +const mixed = [1, null, 2, undefined, 3]; +const onlyNumbers = filterDefined(mixed); // number[] + +/** Groups items by a key derived from each item. */ +function groupBy( + items: readonly T[], + keySelector: (item: T) => K +): Partial> { + return items.reduce( + (acc, item) => { + const key = keySelector(item); + if (!acc[key]) acc[key] = []; + acc[key]!.push(item); + return acc; + }, + {} as Partial> + ); +} +``` + +--- + +### Keyof & Typeof Operators + +**Choose these for:** safe property access, dynamic key patterns, runtime reflection, type-safe object manipulation + +**Reference:** [keyof](https://www.typescriptlang.org/docs/handbook/2/keyof-types.html) | [typeof](https://www.typescriptlang.org/docs/handbook/2/typeof-types.html) + +```typescript +// ── keyof ───────────────────────────────────────────────────────────────────── + +interface Product { + id: string; + name: string; + price: number; + inStock: boolean; +} + +// keyof produces a union of all known keys as string/number/symbol literals +type ProductKeys = keyof Product; +// "id" | "name" | "price" | "inStock" + +// Common pattern: safe property getter +function getField(obj: T, key: K): T[K] { + return obj[key]; +} + +const p: Product = { id: "p1", name: "Widget", price: 9.99, inStock: true }; +const productName = getField(p, "name"); // Type: string +// getField(p, "sku"); // Error — 'sku' is not a key of Product + +// Use keyof with mapped types (see Mapped Types section) +type ProductUpdate = Partial>; + +// ── typeof ──────────────────────────────────────────────────────────────────── + +// In expression position: JavaScript typeof (returns string at runtime) +const x = "hello"; +console.log(typeof x); // "string" — runtime JavaScript + +// In type position: TypeScript typeof (captures the type of a variable) +const defaultConfig = { + apiUrl: "https://api.example.com", + timeout: 5000, + retries: 3, + features: { auth: true, logging: false } as const, +}; + +type DefaultConfig = typeof defaultConfig; +// { +// apiUrl: string; +// timeout: number; +// retries: number; +// features: { readonly auth: true; readonly logging: false }; +// } + +// Use typeof to avoid repeating a type definition +function mergeConfig(base: typeof defaultConfig, overrides: Partial) { + return { ...base, ...overrides }; +} + +// Capture the type of a function +function createUser(name: string, email: string) { + return { id: crypto.randomUUID(), name, email, createdAt: new Date() }; +} + +type CreatedUser = ReturnType; +// { id: string; name: string; email: string; createdAt: Date } + +// Capture the parameters of a function +type CreateUserParams = Parameters; +// [name: string, email: string] + +// ── keyof + typeof Together ──────────────────────────────────────────────────── + +const FEATURE_FLAGS = { + darkMode: false, + betaSignup: true, + analyticsV2: false, +} as const; + +type FeatureFlag = keyof typeof FEATURE_FLAGS; +// "darkMode" | "betaSignup" | "analyticsV2" + +function isFeatureEnabled(flag: FeatureFlag): boolean { + return FEATURE_FLAGS[flag]; +} + +isFeatureEnabled("darkMode"); // OK +// isFeatureEnabled("unknown"); // Error — not a valid flag +``` + +--- + +### Indexed Access Types + +**Choose these for:** extracting nested types, working with array element types, deeply nested configuration, API response shape extraction + +**Reference:** [Indexed Access Types](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html) + +```typescript +// ── Basic Indexed Access ────────────────────────────────────────────────────── + +interface UserProfile { + id: string; + name: string; + address: { + street: string; + city: string; + country: string; + zip: string; + }; + tags: string[]; + roles: ("admin" | "editor" | "viewer")[]; +} + +// Extract a single property type +type UserId = UserProfile["id"]; // string +type UserAddress = UserProfile["address"]; // { street: string; city: string; ... } + +// Extract a nested property type +type City = UserProfile["address"]["city"]; // string + +// Extract union from string literals +type UserRole = UserProfile["roles"][number]; +// "admin" | "editor" | "viewer" + +// Extract array element type +type TagType = UserProfile["tags"][number]; // string + +// ── Indexed Access with keyof ───────────────────────────────────────────────── + +// Get the type of any value in UserProfile +type AnyProfileValue = UserProfile[keyof UserProfile]; +// string | { street: string; ... } | string[] | ("admin" | "editor" | "viewer")[] + +// ── Practical: Extracting API Response Types ────────────────────────────────── + +interface ApiSchema { + "/users": { + GET: { response: UserProfile[] }; + POST: { body: Omit; response: UserProfile }; + }; + "/users/:id": { + GET: { response: UserProfile }; + PUT: { body: Partial; response: UserProfile }; + DELETE: { response: { success: boolean } }; + }; +} + +type GetUsersResponse = ApiSchema["/users"]["GET"]["response"]; +// UserProfile[] + +type CreateUserBody = ApiSchema["/users"]["POST"]["body"]; +// Omit + +// ── Indexed Access with Arrays ───────────────────────────────────────────────── + +const SORTED_COLUMNS = ["name", "email", "createdAt", "role"] as const; +type SortColumn = (typeof SORTED_COLUMNS)[number]; +// "name" | "email" | "createdAt" | "role" + +// Extract tuple element types +type Tuple = [string, number, boolean]; +type First = Tuple[0]; // string +type Second = Tuple[1]; // number +type TupleValues = Tuple[number]; // string | number | boolean +``` + +--- + +### Conditional Types + +**Choose these for:** type-level logic, utility type implementation, discriminating types by shape, advanced generics + +**Reference:** [Conditional Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) + +```typescript +// ── Basic Conditional Type ──────────────────────────────────────────────────── + +// Syntax: T extends U ? TrueType : FalseType +type IsString = T extends string ? true : false; + +type A = IsString; // true +type B = IsString; // false +type C = IsString<"hello">; // true — string literal extends string + +// ── Conditional Types with infer ────────────────────────────────────────────── + +// `infer` captures a type within a conditional — used to "pull out" inner types + +// Extract the return type of a function +type MyReturnType = T extends (...args: never[]) => infer R ? R : never; + +function fetchUser(): Promise { + return Promise.resolve({} as UserProfile); +} + +type FetchUserReturn = MyReturnType; +// Promise + +// Unwrap a Promise +type Awaited = T extends Promise ? Awaited : T; + +type ResolvedUser = Awaited>>; +// UserProfile + +// Extract the first element type from a tuple +type First = T extends [infer F, ...unknown[]] ? F : never; + +type Head = First<[string, number, boolean]>; // string + +// Extract the type of array/readonly array elements +type ElementType = T extends readonly (infer U)[] ? U : never; + +type StrElement = ElementType; // string +type NumElement = ElementType>; // number + +// ── Distributive Conditional Types ──────────────────────────────────────────── + +// When T is a naked type parameter, conditional types distribute over union members +type ToArray = T extends unknown ? T[] : never; + +type StrOrNumArray = ToArray; +// string[] | number[] (distributes! not (string | number)[]) + +// Preventing distribution with a tuple wrapper +type ToArrayNonDistributive = [T] extends [unknown] ? T[] : never; + +type Combined = ToArrayNonDistributive; +// (string | number)[] + +// ── Practical Utility Types Built with Conditionals ──────────────────────────── + +// Exclude types from a union +type MyExclude = T extends U ? never : T; + +type Colors = "red" | "green" | "blue" | "yellow"; +type WarmColors = MyExclude; +// "red" | "yellow" + +// Extract only types matching a shape +type MyExtract = T extends U ? T : never; + +type StringColors = MyExtract; +// "red" | "green" | "blue" | "yellow" + +// Non-nullable +type MyNonNullable = T extends null | undefined ? never : T; + +type SafeString = MyNonNullable; +// string + +// ── Conditional Types for Object Discrimination ──────────────────────────────── + +// Pick only the keys whose values are of a given type +type KeysOfType = { + [K in keyof T]: T[K] extends ValueType ? K : never; +}[keyof T]; + +interface MixedShape { + id: string; + name: string; + count: number; + isActive: boolean; + score: number; +} + +type StringKeys = KeysOfType; // "id" | "name" +type NumberKeys = KeysOfType; // "count" | "score" +``` + +--- + +### Mapped Types + +**Choose these for:** transforming existing types, making all-optional versions, readonly variants, API transformation layers + +**Reference:** [Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) + +```typescript +// ── Basic Mapped Types ──────────────────────────────────────────────────────── + +// Mapped types iterate over keys of an existing type and transform each property + +interface Task { + id: string; + title: string; + completed: boolean; + priority: number; +} + +// Make all properties optional (mirrors built-in Partial) +type MyPartial = { + [K in keyof T]?: T[K]; +}; + +type OptionalTask = MyPartial; +// { id?: string; title?: string; completed?: boolean; priority?: number; } + +// Make all properties readonly (mirrors built-in Readonly) +type MyReadonly = { + readonly [K in keyof T]: T[K]; +}; + +type ImmutableTask = MyReadonly; + +// Make all properties required (removes optionality) +type MyRequired = { + [K in keyof T]-?: T[K]; // -? removes optional modifier +}; + +// ── Modifiers: +/- readonly and +/- optional ────────────────────────────────── + +// Add readonly (+readonly or just readonly) +type AddReadonly = { +readonly [K in keyof T]: T[K] }; + +// Remove readonly +type RemoveReadonly = { -readonly [K in keyof T]: T[K] }; + +// Add optional +type AddOptional = { [K in keyof T]+?: T[K] }; + +// Remove optional (make required) +type RemoveOptional = { [K in keyof T]-?: T[K] }; + +// ── Remapping Keys ──────────────────────────────────────────────────────────── + +// Use `as` to remap property names +type Getters = { + [K in keyof T as `get${Capitalize}`]: () => T[K]; +}; + +type TaskGetters = Getters; +// { +// getId: () => string; +// getTitle: () => string; +// getCompleted: () => boolean; +// getPriority: () => number; +// } + +// Filter out keys using conditional type + never +type OnlyStrings = { + [K in keyof T as T[K] extends string ? K : never]: T[K]; +}; + +type StringTaskFields = OnlyStrings; +// { id: string; title: string; } + +// ── Mapped Type with Value Transformation ───────────────────────────────────── + +// Wrap each value in a Promise +type Promisified = { + [K in keyof T]: T[K] extends (...args: infer A) => infer R + ? (...args: A) => Promise + : T[K]; +}; + +// Nullable version of a type +type Nullable = { + [K in keyof T]: T[K] | null; +}; + +// ── Record Shorthand ────────────────────────────────────────────────────────── + +// Record is a common mapped type shorthand +type StatusMap = Record<"pending" | "active" | "archived", number>; + +const taskCounts: StatusMap = { pending: 5, active: 3, archived: 12 }; + +// More explicit equivalent: +type ExplicitStatusMap = { + [K in "pending" | "active" | "archived"]: number; +}; +``` + +--- + +### Template Literal Types + +**Choose these for:** string-based APIs, event naming, CSS-in-TS, route typing, property accessor naming + +**Reference:** [Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html) + +```typescript +// ── Basic Template Literal Types ────────────────────────────────────────────── + +type World = "world"; +type Greeting = `hello ${World}`; +// "hello world" + +// Combines with union types — produces the Cartesian product +type Color = "red" | "green" | "blue"; +type Shade = "light" | "dark"; +type ColorShade = `${Shade}-${Color}`; +// "light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue" + +// ── Event Name Patterns ──────────────────────────────────────────────────────── + +type EventName = "click" | "focus" | "blur" | "change" | "submit"; +type HandlerName = `on${Capitalize}`; +// "onClick" | "onFocus" | "onBlur" | "onChange" | "onSubmit" + +// Full event handler map +type EventHandlers = { + [K in EventName as `on${Capitalize}`]?: (event: Event) => void; +}; + +// ── CRUD Route Typing ────────────────────────────────────────────────────────── + +type Resource = "user" | "post" | "comment"; +type CrudAction = "create" | "read" | "update" | "delete" | "list"; +type PermissionString = `${Resource}:${CrudAction}`; +// "user:create" | "user:read" | "user:update" | "user:delete" | "user:list" | ... + +function hasPermission( + userPermissions: PermissionString[], + required: PermissionString +): boolean { + return userPermissions.includes(required); +} + +// ── Intrinsic String Manipulation Types ─────────────────────────────────────── + +// Built-in TypeScript string manipulation types +type U = Uppercase<"hello">; // "HELLO" +type L = Lowercase<"WORLD">; // "world" +type C = Capitalize<"hello">; // "Hello" +type UC = Uncapitalize<"Hello">; // "hello" + +// Practical: convert snake_case keys to camelCase at type level +type SnakeToCamel = + S extends `${infer Head}_${infer Tail}` + ? `${Head}${Capitalize>}` + : S; + +type CamelKey = SnakeToCamel<"created_at_utc">; // "createdAtUtc" + +// ── CSS-in-TS Property Typing ───────────────────────────────────────────────── + +type CSSUnit = "px" | "rem" | "em" | "%" | "vh" | "vw"; +type CSSValue = `${number}${CSSUnit}`; +// Partial — TypeScript can't enforce number in template literals, but documents intent + +type Side = "top" | "right" | "bottom" | "left"; +type MarginProp = `margin-${Side}` | "margin"; +type PaddingProp = `padding-${Side}` | "padding"; +type SpacingProp = MarginProp | PaddingProp; + +// ── Deeply Typed Object Keys ────────────────────────────────────────────────── + +// Typed getter/setter pair generation +type WithAccessors = T & { + [K in keyof T as `get${Capitalize}`]: () => T[K]; +} & { + [K in keyof T as `set${Capitalize}`]: (value: T[K]) => void; +}; +``` + +--- + +### Utility Types Toolkit + +**Choose these for:** any project — these are TypeScript's built-in transformation utilities + +```typescript +// This section documents and demonstrates the full suite of TypeScript built-in +// utility types. Use them freely — no import required. + +// ────────────────────────────────────────────────────────────────────────────── +// Object transformation utilities +// ────────────────────────────────────────────────────────────────────────────── + +interface FullUser { + id: string; + name: string; + email: string; + password: string; + age: number; + role: "admin" | "user"; + createdAt: Date; + deletedAt: Date | null; +} + +// Partial — makes all properties optional +type UserUpdate = Partial; +// { id?: string; name?: string; ... } + +// Required — makes all properties required (removes optional modifiers) +interface DraftPost { + title?: string; + body?: string; + tags?: string[]; +} +type PublishedPost = Required; +// { title: string; body: string; tags: string[]; } + +// Readonly — makes all properties readonly +type ImmutableUser = Readonly; +// { readonly id: string; readonly name: string; ... } + +// Pick — creates a type with only the specified keys +type UserPublicProfile = Pick; +// { id: string; name: string; role: "admin" | "user" } + +// Omit — creates a type without the specified keys +type UserWithoutSensitive = Omit; +// { id: string; name: string; email: string; age: number; role: ...; createdAt: Date; } + +// Record — creates a map type from keys K to values V +type RolePermissions = Record<"admin" | "user" | "guest", string[]>; +const permissions: RolePermissions = { + admin: ["read", "write", "delete"], + user: ["read", "write"], + guest: ["read"], +}; + +// ────────────────────────────────────────────────────────────────────────────── +// Union manipulation utilities +// ────────────────────────────────────────────────────────────────────────────── + +type AllColors = "red" | "green" | "blue" | "yellow" | "cyan" | "magenta"; + +// Exclude — removes types from T that are assignable to U +type PrimaryColors = Exclude; +// "red" | "green" | "blue" + +// Extract — keeps only types from T that are assignable to U +type WarmColors = Extract; +// "red" | "yellow" (orange isn't in AllColors) + +// NonNullable — removes null and undefined from T +type MaybeString = string | null | undefined; +type DefiniteString = NonNullable; +// string + +// ────────────────────────────────────────────────────────────────────────────── +// Function-related utilities +// ────────────────────────────────────────────────────────────────────────────── + +async function fetchUsers(page: number, limit: number): Promise { + return []; +} + +// ReturnType — extracts the return type of a function type +type FetchUsersReturn = ReturnType; +// Promise + +// Parameters — extracts the parameter types as a tuple +type FetchUsersParams = Parameters; +// [page: number, limit: number] + +// ConstructorParameters — extracts constructor parameter types +class UserService { + constructor( + private readonly apiUrl: string, + private readonly timeout: number + ) {} +} +type ServiceCtorParams = ConstructorParameters; +// [apiUrl: string, timeout: number] + +// InstanceType — gets the instance type of a constructor function/class +type ServiceInstance = InstanceType; +// UserService + +// ────────────────────────────────────────────────────────────────────────────── +// Promise utility +// ────────────────────────────────────────────────────────────────────────────── + +// Awaited — recursively unwraps Promise types +type DeepPromise = Promise>>; +type Resolved = Awaited; +// string + +type AwaitedUsers = Awaited>; +// FullUser[] + +// ────────────────────────────────────────────────────────────────────────────── +// Quick Reference Table +// ────────────────────────────────────────────────────────────────────────────── + +// | Utility | What it does | +// |---------------------|-------------------------------------------------------| +// | Partial | All properties optional | +// | Required | All properties required | +// | Readonly | All properties readonly | +// | Pick | Keep only keys K from T | +// | Omit | Remove keys K from T | +// | Record | Map from keys K to values V | +// | Exclude | Remove from union T anything assignable to U | +// | Extract | Keep from union T only types assignable to U | +// | NonNullable | Remove null and undefined from T | +// | ReturnType | Return type of function F | +// | Parameters | Parameter types of function F as a tuple | +// | ConstructorParameters | Constructor parameter types as a tuple | +// | InstanceType | Instance type of constructor C | +// | Awaited | Recursively unwrap Promise<...> to inner type | +``` + +--- + +### Function Signatures & Overloads + +**Choose these for:** libraries, complex APIs, callback-heavy code, multiple calling conventions + +**Reference:** [More on Functions](https://www.typescriptlang.org/docs/handbook/2/functions.html) + +```typescript +// ── Basic Typed Function Signatures ────────────────────────────────────────── + +// Named function with explicit types +function add(a: number, b: number): number { + return a + b; +} + +// Arrow function +const multiply = (a: number, b: number): number => a * b; + +// Function type expression +type Transformer = (value: T) => U; +const stringify: Transformer = (n) => n.toString(); + +// ── Optional and Default Parameters ────────────────────────────────────────── + +function greet(name: string, greeting?: string): string { + return `${greeting ?? "Hello"}, ${name}!`; +} + +function createTimeout(ms: number, label = "timeout"): NodeJS.Timeout { + return setTimeout(() => console.warn(`${label} expired`), ms); +} + +// ── Rest Parameters ─────────────────────────────────────────────────────────── + +function logAll(level: "info" | "warn" | "error", ...messages: string[]): void { + messages.forEach((msg) => console[level](msg)); +} + +logAll("info", "Starting", "Connecting", "Ready"); + +// ── Destructuring Parameters ────────────────────────────────────────────────── + +interface PaginationOptions { + page?: number; + pageSize?: number; + sortBy?: string; + sortOrder?: "asc" | "desc"; +} + +function paginate({ + page = 1, + pageSize = 20, + sortBy = "createdAt", + sortOrder = "desc", +}: PaginationOptions = {}): string { + return `page=${page}&limit=${pageSize}&sort=${sortBy}:${sortOrder}`; +} + +// ── Function Overloads ──────────────────────────────────────────────────────── + +// Overloads allow the same function to accept different argument combinations +// with different return types, while keeping a single implementation. + +// Overload signatures (declarations only — no body) +function formatDate(date: Date): string; +function formatDate(timestamp: number): string; +function formatDate(iso: string): string; +function formatDate(parts: { year: number; month: number; day: number }): string; + +// Implementation signature (must be compatible with all overloads) +function formatDate( + value: Date | number | string | { year: number; month: number; day: number } +): string { + if (value instanceof Date) { + return value.toISOString().slice(0, 10); + } + if (typeof value === "number") { + return new Date(value).toISOString().slice(0, 10); + } + if (typeof value === "string") { + return new Date(value).toISOString().slice(0, 10); + } + const { year, month, day } = value; + return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; +} + +// All these are type-safe: +formatDate(new Date()); +formatDate(Date.now()); +formatDate("2025-01-15"); +formatDate({ year: 2025, month: 1, day: 15 }); + +// ── Generic Functions with Constraints ──────────────────────────────────────── + +// Merge two objects, with right taking precedence +function merge(base: T, overrides: U): T & U { + return { ...base, ...overrides }; +} + +// Type-safe array first/last +function first(arr: readonly [T, ...T[]]): T; // Non-empty — always returns T +function first(arr: readonly T[]): T | undefined; // May be empty — may return undefined +function first(arr: readonly T[]): T | undefined { + return arr[0]; +} + +// ── Callable & Constructable Interfaces ────────────────────────────────────── + +// Callable interface (function with properties) +interface Formatter { + (value: string): string; + locale: string; + precision?: number; +} + +// Constructor signature +interface Constructable { + new (...args: unknown[]): T; +} + +// ── ThisParameterType ───────────────────────────────────────────────────────── + +// Type `this` explicitly in method functions (useful for mixin patterns) +function validate(this: { value: number; min: number; max: number }): boolean { + return this.value >= this.min && this.value <= this.max; +} +``` + +--- + +### Variable Declarations & Scoping + +**Choose these for:** any project — best practices for variable declarations + +**Reference:** [Variable Declarations](https://www.typescriptlang.org/docs/handbook/variable-declarations.html) + +```typescript +// ── let vs const ────────────────────────────────────────────────────────────── + +// const — preferred for values that won't be reassigned +const MAX_RETRIES = 3; // number +const API_URL = "https://api.example.com"; // string + +// let — for variables that will be reassigned +let currentPage = 1; +currentPage += 1; // OK + +// TypeScript infers types from initialization +const greeting = "hello"; // inferred: string +const count = 0; // inferred: number + +// Explicit annotations when the inference would be too wide +let status: "active" | "inactive" | "suspended" = "active"; + +// ── Destructuring Assignment ────────────────────────────────────────────────── + +// Object destructuring with type annotation +const { name, age, email }: { name: string; age: number; email: string } = getUser(); + +// Object destructuring with renaming +const { id: userId, name: userName } = getUser(); + +// Object destructuring with defaults +const { timeout = 5000, retries = 3 } = getConfig(); + +// Nested destructuring +const { + address: { city, country }, + preferences: { theme = "light" }, +} = getUserProfile(); + +// Array destructuring +const [first, second, ...rest] = getItems(); +const [, secondItem] = getTuple(); // skip first element with "," + +// Tuple destructuring with renaming +function getCoordinates(): [number, number] { + return [40.7128, -74.0060]; +} +const [latitude, longitude] = getCoordinates(); + +// ── Spread Operator ─────────────────────────────────────────────────────────── + +// Spread in array literals +const base = [1, 2, 3]; +const extended = [...base, 4, 5]; // [1, 2, 3, 4, 5] + +// Spread in object literals (shallow copy + merge) +const defaults = { timeout: 5000, retries: 3, debug: false }; +const overrides = { timeout: 10000, debug: true }; +const config = { ...defaults, ...overrides }; // overrides wins on conflict + +// Spread with type-safe defaults +function withDefaults( + partial: Partial, + defaults: T +): T { + return { ...defaults, ...partial }; +} + +// ── Type-Safe Destructuring from Function Returns ───────────────────────────── + +function useToggle(initialState: boolean) { + let state = initialState; + const toggle = () => { state = !state; }; + const reset = () => { state = initialState; }; + return [state, toggle, reset] as const; + // ^^^^^^^^^ const assertion preserves tuple types +} + +// Without `as const`, TypeScript would infer Array void)> +// With `as const`, TypeScript infers readonly [boolean, () => void, () => void] +const [isOpen, toggleOpen, resetOpen] = useToggle(false); +// isOpen: boolean +// toggleOpen: () => void +// resetOpen: () => void + +// ── Type Narrowing with Declarations ────────────────────────────────────────── + +// Declare a variable with a union type, then narrow it +declare function getApiResponse(): string | null | { error: string }; + +const response = getApiResponse(); + +if (response === null) { + console.log("No response"); +} else if (typeof response === "string") { + console.log("Success:", response.toUpperCase()); +} else { + console.error("Error:", response.error); +} +``` + +--- + +### Discriminated Unions & Type Narrowing + +**Choose these for:** state management, event systems, result types, request/response handling, FSMs + +**Reference:** [Everyday Types — Narrowing](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) + +```typescript +// ── Discriminated Unions ────────────────────────────────────────────────────── +// Every member has a common "discriminant" property (usually `kind` or `type`) +// that TypeScript uses to narrow the type in switch/if statements. + +// Async operation state machine +type IdleState = { status: "idle" }; +type LoadingState = { status: "loading"; startedAt: Date }; +type SuccessState = { status: "success"; data: T; loadedAt: Date }; +type ErrorState = { status: "error"; error: Error; retryCount: number }; + +type AsyncState = + | IdleState + | LoadingState + | SuccessState + | ErrorState; + +// Type-safe state handler with exhaustive switch +function handleState(state: AsyncState): string { + switch (state.status) { + case "idle": + return "Waiting to start"; + case "loading": + return `Loading since ${state.startedAt.toISOString()}`; + case "success": + return `Loaded at ${state.loadedAt.toISOString()}`; + case "error": + return `Error: ${state.error.message} (retry ${state.retryCount})`; + default: + // Exhaustive check — TypeScript errors if a case is unhandled + return assertNever(state); + } +} + +function assertNever(value: never): never { + throw new Error(`Unhandled discriminated union case: ${JSON.stringify(value)}`); +} + +// ── DOM/Input Event Discriminated Union ─────────────────────────────────────── + +type AppEvent = + | { type: "USER_LOGIN"; payload: { userId: string; sessionId: string } } + | { type: "USER_LOGOUT"; payload: { userId: string } } + | { type: "ITEM_ADDED"; payload: { itemId: string; quantity: number } } + | { type: "ITEM_REMOVED"; payload: { itemId: string } } + | { type: "ERROR"; payload: { code: string; message: string } }; + +function dispatch(event: AppEvent): void { + switch (event.type) { + case "USER_LOGIN": + console.log(`User ${event.payload.userId} logged in`); + break; + case "USER_LOGOUT": + console.log(`User ${event.payload.userId} logged out`); + break; + case "ITEM_ADDED": + console.log(`Added ${event.payload.quantity} of item ${event.payload.itemId}`); + break; + case "ITEM_REMOVED": + console.log(`Removed item ${event.payload.itemId}`); + break; + case "ERROR": + console.error(`[${event.payload.code}] ${event.payload.message}`); + break; + default: + assertNever(event); + } +} + +// ── Type Predicates (User-Defined Type Guards) ──────────────────────────────── + +// A type predicate narrows the type in the scope where it returns true +function isErrorState(state: AsyncState): state is ErrorState { + return state.status === "error"; +} + +function isSuccessState(state: AsyncState): state is SuccessState { + return state.status === "success"; +} + +// Using type predicates in filter +const states: AsyncState[] = []; +const errors = states.filter(isErrorState); // ErrorState[] +const successes = states.filter(isSuccessState); // SuccessState[] + +// ── in Operator Narrowing ───────────────────────────────────────────────────── + +interface Circle { + kind: "circle"; + radius: number; +} + +interface Rectangle { + kind: "rectangle"; + width: number; + height: number; +} + +interface Triangle { + kind: "triangle"; + base: number; + height: number; +} + +type Shape = Circle | Rectangle | Triangle; + +function getArea(shape: Shape): number { + // Discriminant-based narrowing (preferred) + switch (shape.kind) { + case "circle": + return Math.PI * shape.radius ** 2; + case "rectangle": + return shape.width * shape.height; + case "triangle": + return (shape.base * shape.height) / 2; + default: + return assertNever(shape); + } +} + +// `in` operator narrowing (when no discriminant is available) +type Bird = { fly(): void; layEggs(): void }; +type Fish = { swim(): void; layEggs(): void }; + +function move(animal: Bird | Fish): void { + if ("fly" in animal) { + animal.fly(); // TypeScript knows: animal is Bird + } else { + animal.swim(); // TypeScript knows: animal is Fish + } +} +``` + +--- + +### Declaration Merging & Module Augmentation + +**Choose these for:** extending third-party types, adding custom properties to Express `Request`, global type extensions + +```typescript +// ── Interface Declaration Merging ───────────────────────────────────────────── +// TypeScript merges multiple declarations of the same interface name. + +// Original interface (e.g., from a library) +interface Window { + title: string; +} + +// Your augmentation — adds to the existing Window interface +interface Window { + analytics?: { + track(event: string, properties?: Record): void; + }; + featureFlags: Record; +} + +// Now Window has all three properties. + +// ── Module Augmentation ─────────────────────────────────────────────────────── +// Extend types from external modules without modifying their source. + +// Augmenting Express Request (example: adding authenticated user) +// File: src/types/express.d.ts + +declare global { + namespace Express { + interface Request { + user?: { + id: string; + email: string; + role: "admin" | "user"; + }; + requestId?: string; + } + } +} + +// Now in your Express route handlers: +// req.user?.id and req.requestId are fully typed. + +// ── Global Augmentation ──────────────────────────────────────────────────────── + +// Add global variables that are injected at runtime (e.g., by webpack DefinePlugin) +declare global { + const __APP_VERSION__: string; + const __DEV__: boolean; + const __BUILD_TIMESTAMP__: number; +} + +// ── Extending Existing Module Types ─────────────────────────────────────────── + +// Augment a specific module's types +// File: src/types/some-library.d.ts +declare module "some-library" { + interface SomeLibraryConfig { + // Add a property that the library doesn't declare but your code needs + customTimeout?: number; + onError?: (error: Error) => void; + } +} + +// ── Declaration Files (.d.ts) for Plain JS Modules ──────────────────────────── + +// When a library has no types at all, write a minimal declaration: +// File: src/types/untyped-module.d.ts + +declare module "untyped-module" { + export interface Options { + apiKey: string; + timeout?: number; + } + + export function initialize(options: Options): void; + export function query(sql: string, params?: unknown[]): Promise; + + const untyped: { + initialize: typeof initialize; + query: typeof query; + }; + + export default untyped; +} + +// ── Namespace Merging ────────────────────────────────────────────────────────── + +// Add static methods to a class via namespace merging +class Validator { + constructor(public readonly value: unknown) {} + + isString(): this is Validator & { value: string } { + return typeof this.value === "string"; + } +} + +namespace Validator { + // Merge a factory method into the Validator namespace + export function fromEnv(key: string): Validator { + return new Validator(process.env[key]); + } +} + +const v = Validator.fromEnv("NODE_ENV"); +``` + +--- + +## Combination Examples + +Use this section to identify which menu sections to include for your project type. + +### 1. Simple CLI Tool + +A command-line script with argument parsing and file I/O. + +**Select:** +- Base Setup (required) +- Primitive & Everyday Types +- Variable Declarations & Scoping +- Function Signatures & Overloads +- Utility Types Toolkit (Partial, Required, Record) +- Discriminated Unions & Type Narrowing (for exit codes / result types) + +**Skip:** Template Literal Types, Mapped Types, Declaration Merging + +**Example shape:** +```typescript +// src/types.ts for a CLI tool +type ExitCode = 0 | 1 | 2; +type LogLevel = "silent" | "info" | "verbose" | "debug"; + +interface CliOptions { + input: string; + output?: string; + logLevel?: LogLevel; + dryRun?: boolean; +} + +type CliResult = + | { success: true; outputPath: string } + | { success: false; error: Error; exitCode: ExitCode }; +``` + +--- + +### 2. REST API (Node.js / Express / Fastify) + +A backend API with request/response typing, middleware, and database models. + +**Select:** +- Base Setup (required) +- Object Types & Interfaces (request/response bodies, DB entities) +- Union & Intersection Types (combining base entity with timestamps) +- Generics (generic Repository, ApiResponse wrapper) +- Utility Types Toolkit (Partial for PATCH bodies, Omit for create payloads) +- Discriminated Unions & Type Narrowing (result types, error handling) +- Template Literal Types (route strings, permission strings) +- Declaration Merging & Module Augmentation (Express Request augmentation) + +**Example shape:** +```typescript +// src/types.ts for a REST API +interface ApiResponse { + data: T; + meta?: { page: number; total: number }; +} + +type ApiError = { + status: "error"; + code: string; + message: string; + details?: Record; +}; + +type Resource = "user" | "post" | "comment"; +type CrudPermission = `${Resource}:${"create" | "read" | "update" | "delete"}`; +``` + +--- + +### 3. React Component Library + +A set of reusable UI components with typed props. + +**Select:** +- Base Setup (required) +- Primitive & Everyday Types +- Object Types & Interfaces (component props interfaces) +- Union & Intersection Types (variant props, composed prop types) +- Literal Types & Const Assertions (variant/size/color options) +- Generics (generic List, Select, Table components) +- Utility Types Toolkit (Partial for optional props, Omit to exclude HTML attrs) +- Template Literal Types (className generation, CSS custom property names) +- Function Signatures & Overloads (event handler types, render props) + +**Example shape:** +```typescript +// src/types.ts for a component library +type ButtonVariant = "primary" | "secondary" | "ghost" | "destructive"; +type ButtonSize = "sm" | "md" | "lg" | "icon"; + +interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: ButtonVariant; + size?: ButtonSize; + loading?: boolean; + leftIcon?: React.ReactNode; +} + +type CSSCustomProperty = `--${string}`; +``` + +--- + +### 4. State Machine / Domain Model + +A business logic layer with complex state transitions and domain entities. + +**Select:** +- Base Setup (required) +- Object Types & Interfaces (entity definitions) +- Literal Types & Const Assertions (state/event names) +- Discriminated Unions & Type Narrowing (states, transitions, events) +- Generics (generic state machine, generic event bus) +- Conditional Types (state guards, transition validators) +- Mapped Types (state-to-handler mapping, transition table) +- Keyof & Typeof Operators (dynamic state lookup) + +**Example shape:** +```typescript +// src/types.ts for a state machine +type OrderStatus = "draft" | "pending" | "confirmed" | "shipped" | "delivered" | "cancelled"; + +type OrderEvent = + | { type: "SUBMIT"; payload: { customerId: string } } + | { type: "CONFIRM"; payload: { confirmedBy: string } } + | { type: "SHIP"; payload: { trackingNumber: string } } + | { type: "DELIVER"; payload: { deliveredAt: Date } } + | { type: "CANCEL"; payload: { reason: string } }; + +type OrderTransitions = { + [S in OrderStatus]: Partial>; +}; +``` + +--- + +### 5. npm Utility Package + +A published library with a public API surface, no runtime, type-only exports. + +**Select:** +- Base Setup (required) +- Object Types & Interfaces (public API types) +- Generics (generic utilities) +- Conditional Types (utility type helpers) +- Mapped Types (type transformations) +- Indexed Access Types (extracting sub-types from API shapes) +- Template Literal Types (if string-based APIs) +- Utility Types Toolkit (building on built-ins) +- Function Signatures & Overloads (multiple calling conventions) + +**Example shape:** +```typescript +// src/types.ts for a utility library +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; + +export type DeepReadonly = { + readonly [P in keyof T]: T[P] extends object ? DeepReadonly : T[P]; +}; + +export type Prettify = { [K in keyof T]: T[K] } & {}; +``` + +--- + +### 6. Full-Stack App Shared Types + +A `packages/shared` or `src/shared/types` package shared between frontend and backend. + +**Select:** +- Base Setup (required) +- Object Types & Interfaces (shared domain entities, DTO shapes) +- Union & Intersection Types (combining entity variants) +- Literal Types & Const Assertions (status codes, enum-like constants) +- Generics (generic API wrappers, paginated responses) +- Utility Types Toolkit (Partial for updates, Pick/Omit for view models) +- Template Literal Types (route definitions, event names) +- Indexed Access Types (extracting API types from a central schema) + +**Example shape:** +```typescript +// packages/shared/src/types.ts +export interface Paginated { + items: T[]; + meta: { page: number; pageSize: number; total: number; totalPages: number }; +} + +export type ApiRoutes = { + "GET /users": { response: Paginated }; + "POST /users": { body: CreateUserDto; response: User }; + "GET /users/:id": { params: { id: string }; response: User }; +}; + +export type CreateUserDto = Omit; +``` + +--- + +### 7. Configuration-Heavy Project + +A project with complex, nested, validated configuration (e.g., CI system, build tool). + +**Select:** +- Base Setup (required) +- Object Types & Interfaces (config object shapes) +- Literal Types & Const Assertions (fixed option sets) +- Generics (generic config defaults merger) +- Keyof & Typeof Operators (deriving types from config objects) +- Indexed Access Types (extracting sub-config types) +- Mapped Types (config override/merge utilities) +- Discriminated Unions & Type Narrowing (different config modes) +- Variable Declarations & Scoping (const assertions for config) + +**Example shape:** +```typescript +// src/config/types.ts +const BASE_CONFIG = { + server: { host: "0.0.0.0", port: 8080, cors: true }, + database: { driver: "postgres", poolSize: 10, ssl: false }, + cache: { driver: "redis", ttl: 300 }, +} as const; + +type BaseConfig = typeof BASE_CONFIG; +type ServerConfig = BaseConfig["server"]; +type DatabaseDriver = BaseConfig["database"]["driver"]; +// "postgres" + +type EnvOverrides = DeepPartial<{ + [K in keyof BaseConfig]: { + [P in keyof BaseConfig[K]]: unknown; + }; +}>; +``` + +--- + +### 8. Type-Safe Event System + +A custom event emitter or pub/sub system with fully typed event maps. + +**Select:** +- Base Setup (required) +- Object Types & Interfaces (event payload shapes) +- Generics (generic emitter class) +- Keyof & Typeof Operators (deriving event name unions) +- Mapped Types (event handler map) +- Template Literal Types (namespaced event names like `user:created`) +- Discriminated Unions & Type Narrowing (event dispatcher) +- Conditional Types (extracting payload type from event name) + +**Example shape:** +```typescript +// src/events/types.ts +export interface EventMap { + "user:created": { id: string; email: string }; + "user:deleted": { id: string }; + "order:placed": { orderId: string; total: number }; + "order:shipped": { orderId: string; trackingNumber: string }; +} + +export type EventName = keyof EventMap; +// "user:created" | "user:deleted" | "order:placed" | "order:shipped" + +export type PayloadOf = EventMap[E]; +// PayloadOf<"user:created"> → { id: string; email: string } + +export type EventHandler = (payload: PayloadOf) => void | Promise; + +export type EventHandlerMap = { + [E in EventName]?: EventHandler[]; +}; +``` + +--- + +## Applying Your Selection + +1. **Create your types file.** For smaller projects, use a single `src/types.ts`. For larger projects, use a `src/types/` directory with one file per domain area (e.g., `src/types/user.ts`, `src/types/api.ts`, `src/types/events.ts`). + +2. **Copy your selected sections** from this template into your types file(s). + +3. **Adapt all placeholder names** to your domain: + - Replace `User`, `Task`, `Product`, `Order` with your actual entity names + - Replace `"admin" | "user"` role literals with your actual roles + - Replace `"GET" | "POST"` etc. with your actual HTTP methods or command types + +4. **Remove the explanatory comments** and alternative examples you didn't select — keep the types clean. + +5. **Export types** that need to be shared across modules: + ```typescript + // src/types.ts + export type { User, UserUpdate, UserId } from "./types/user.js"; + export type { ApiResponse, ApiError, Paginated } from "./types/api.js"; + export type { AppEvent, EventMap } from "./types/events.js"; + ``` + +6. **Use `noEmit` + `tsc` to validate** your types in CI: + ```bash + npx tsc --noEmit + ``` + +7. **Revisit this menu** as your project evolves — add sections when a new concern is introduced (e.g., add Template Literal Types when you introduce a typed event bus). diff --git a/skills/typescript-coder/assets/typescript-algolia.md b/skills/typescript-coder/assets/typescript-algolia.md new file mode 100644 index 000000000..3a84883eb --- /dev/null +++ b/skills/typescript-coder/assets/typescript-algolia.md @@ -0,0 +1,540 @@ +# TypeScript Algolia Integration Template + +> A TypeScript project template for integrating Algolia search into an application. Covers +> search client setup, index management, record indexing, and query construction using the +> official `algoliasearch` SDK and InstantSearch utilities. + +## License + +The Algolia JavaScript SDK (`algoliasearch`) is distributed under the MIT License. +See the [algoliasearch npm package](https://www.npmjs.com/package/algoliasearch) and +[Algolia GitHub repositories](https://github.com/algolia) for full license terms. + +## Source + +- [Algolia JavaScript API Client](https://github.com/algolia/algoliasearch-client-javascript) +- [Algolia InstantSearch.js](https://github.com/algolia/instantsearch) +- [Algolia Documentation](https://www.algolia.com/doc/) + +## Project Structure + +``` +my-algolia-app/ +├── src/ +│ ├── client/ +│ │ └── algoliaClient.ts # Initialized search client singleton +│ ├── indexing/ +│ │ ├── indexManager.ts # Create, configure, and delete indices +│ │ └── recordUploader.ts # Batch upload / save objects +│ ├── search/ +│ │ ├── searchService.ts # Query builder and search execution +│ │ └── facetService.ts # Faceted search helpers +│ ├── types/ +│ │ └── algolia.types.ts # Shared TypeScript interfaces +│ └── index.ts # Entry point / demo runner +├── .env # ALGOLIA_APP_ID, ALGOLIA_API_KEY +├── .env.example +├── package.json +├── tsconfig.json +└── README.md +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-algolia-app", + "version": "1.0.0", + "description": "TypeScript project with Algolia search integration", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "dev": "ts-node src/index.ts", + "start": "node dist/index.js", + "lint": "eslint 'src/**/*.ts'", + "test": "jest --coverage", + "index:upload": "ts-node src/indexing/recordUploader.ts", + "clean": "rimraf dist" + }, + "dependencies": { + "algoliasearch": "^5.0.0", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "@types/node": "^20.11.0", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0", + "jest": "^29.7.0", + "rimraf": "^5.0.5", + "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} +``` + +### `tsconfig.json` + +```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", "**/*.test.ts"] +} +``` + +### `src/types/algolia.types.ts` + +```typescript +export interface ProductRecord { + objectID: string; + name: string; + description: string; + price: number; + category: string; + brand: string; + rating: number; + inStock: boolean; + tags: string[]; + image?: string; + _highlightResult?: AlgoliaHighlightResult; +} + +export interface AlgoliaHighlightResult { + [key: string]: { + value: string; + matchLevel: 'none' | 'partial' | 'full'; + matchedWords: string[]; + }; +} + +export interface SearchParams { + query: string; + page?: number; + hitsPerPage?: number; + filters?: string; + facets?: string[]; + facetFilters?: string | string[][]; + numericFilters?: string[]; + attributesToRetrieve?: string[]; + attributesToHighlight?: string[]; +} + +export interface SearchResult { + hits: T[]; + nbHits: number; + page: number; + nbPages: number; + hitsPerPage: number; + processingTimeMS: number; + query: string; + facets?: Record>; +} + +export interface IndexSettings { + searchableAttributes?: string[]; + attributesForFaceting?: string[]; + customRanking?: string[]; + ranking?: string[]; + highlightPreTag?: string; + highlightPostTag?: string; + hitsPerPage?: number; + maxValuesPerFacet?: number; + typoTolerance?: boolean | 'min' | 'strict'; +} +``` + +### `src/client/algoliaClient.ts` + +```typescript +import { algoliasearch, SearchClient } from 'algoliasearch'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const APP_ID = process.env.ALGOLIA_APP_ID; +const API_KEY = process.env.ALGOLIA_API_KEY; + +if (!APP_ID || !API_KEY) { + throw new Error( + 'Missing required environment variables: ALGOLIA_APP_ID and ALGOLIA_API_KEY' + ); +} + +let clientInstance: SearchClient | null = null; + +export function getAlgoliaClient(): SearchClient { + if (!clientInstance) { + clientInstance = algoliasearch(APP_ID, API_KEY); + } + return clientInstance; +} + +export const SEARCH_APP_ID = APP_ID; +``` + +### `src/indexing/indexManager.ts` + +```typescript +import { getAlgoliaClient } from '../client/algoliaClient'; +import { IndexSettings } from '../types/algolia.types'; + +export async function configureIndex( + indexName: string, + settings: IndexSettings +): Promise { + const client = getAlgoliaClient(); + + await client.setSettings({ + indexName, + indexSettings: { + searchableAttributes: settings.searchableAttributes ?? [ + 'name', + 'description', + 'brand', + 'tags', + ], + attributesForFaceting: settings.attributesForFaceting ?? [ + 'filterOnly(category)', + 'filterOnly(brand)', + 'price', + 'rating', + ], + customRanking: settings.customRanking ?? [ + 'desc(rating)', + 'asc(price)', + ], + ranking: settings.ranking ?? [ + 'typo', + 'geo', + 'words', + 'filters', + 'proximity', + 'attribute', + 'exact', + 'custom', + ], + highlightPreTag: settings.highlightPreTag ?? '', + highlightPostTag: settings.highlightPostTag ?? '', + hitsPerPage: settings.hitsPerPage ?? 20, + maxValuesPerFacet: settings.maxValuesPerFacet ?? 100, + }, + }); + + console.log(`Index "${indexName}" configured successfully.`); +} + +export async function clearIndex(indexName: string): Promise { + const client = getAlgoliaClient(); + await client.clearObjects({ indexName }); + console.log(`Index "${indexName}" cleared.`); +} + +export async function deleteIndex(indexName: string): Promise { + const client = getAlgoliaClient(); + await client.deleteIndex({ indexName }); + console.log(`Index "${indexName}" deleted.`); +} +``` + +### `src/indexing/recordUploader.ts` + +```typescript +import { getAlgoliaClient } from '../client/algoliaClient'; +import { ProductRecord } from '../types/algolia.types'; + +const INDEX_NAME = process.env.ALGOLIA_INDEX_NAME ?? 'products'; + +const sampleProducts: ProductRecord[] = [ + { + objectID: 'prod-001', + name: 'Wireless Noise-Cancelling Headphones', + description: 'Over-ear headphones with 30-hour battery and active noise cancellation.', + price: 299.99, + category: 'Electronics', + brand: 'AudioTech', + rating: 4.7, + inStock: true, + tags: ['headphones', 'wireless', 'noise-cancelling', 'audio'], + }, + { + objectID: 'prod-002', + name: 'Mechanical Keyboard TKL', + description: 'Tenkeyless mechanical keyboard with Cherry MX Blue switches.', + price: 129.99, + category: 'Peripherals', + brand: 'KeyMaster', + rating: 4.5, + inStock: true, + tags: ['keyboard', 'mechanical', 'tkl', 'gaming'], + }, +]; + +export async function uploadRecords( + records: ProductRecord[], + indexName: string = INDEX_NAME +): Promise { + const client = getAlgoliaClient(); + + const { taskID } = await client.saveObjects({ + indexName, + objects: records, + }); + + await client.waitForTask({ indexName, taskID }); + console.log(`Uploaded ${records.length} records to "${indexName}".`); +} + +export async function deleteRecord( + objectID: string, + indexName: string = INDEX_NAME +): Promise { + const client = getAlgoliaClient(); + + const { taskID } = await client.deleteObject({ indexName, objectID }); + await client.waitForTask({ indexName, taskID }); + console.log(`Deleted record "${objectID}" from "${indexName}".`); +} + +// Run directly: ts-node src/indexing/recordUploader.ts +if (require.main === module) { + uploadRecords(sampleProducts).catch(console.error); +} +``` + +### `src/search/searchService.ts` + +```typescript +import { getAlgoliaClient } from '../client/algoliaClient'; +import { ProductRecord, SearchParams, SearchResult } from '../types/algolia.types'; + +const INDEX_NAME = process.env.ALGOLIA_INDEX_NAME ?? 'products'; + +export async function searchProducts( + params: SearchParams, + indexName: string = INDEX_NAME +): Promise> { + const client = getAlgoliaClient(); + + const response = await client.searchSingleIndex({ + indexName, + searchParams: { + query: params.query, + page: params.page ?? 0, + hitsPerPage: params.hitsPerPage ?? 20, + filters: params.filters, + facets: params.facets, + facetFilters: params.facetFilters, + numericFilters: params.numericFilters, + attributesToRetrieve: params.attributesToRetrieve, + attributesToHighlight: params.attributesToHighlight ?? ['name', 'description'], + }, + }); + + return { + hits: response.hits, + nbHits: response.nbHits ?? 0, + page: response.page ?? 0, + nbPages: response.nbPages ?? 0, + hitsPerPage: response.hitsPerPage ?? 20, + processingTimeMS: response.processingTimeMS, + query: response.query, + facets: response.facets, + }; +} + +export async function multiIndexSearch( + queries: Array<{ indexName: string; query: string; hitsPerPage?: number }> +): Promise>> { + const client = getAlgoliaClient(); + + const response = await client.search({ + requests: queries.map((q) => ({ + indexName: q.indexName, + query: q.query, + hitsPerPage: q.hitsPerPage ?? 5, + })), + }); + + return response.results.map((result) => { + if ('hits' in result) { + return { + hits: result.hits, + nbHits: result.nbHits ?? 0, + page: result.page ?? 0, + nbPages: result.nbPages ?? 0, + hitsPerPage: result.hitsPerPage ?? 5, + processingTimeMS: result.processingTimeMS, + query: result.query, + }; + } + throw new Error('Unexpected search result type'); + }); +} +``` + +### `src/search/facetService.ts` + +```typescript +import { getAlgoliaClient } from '../client/algoliaClient'; + +const INDEX_NAME = process.env.ALGOLIA_INDEX_NAME ?? 'products'; + +export async function searchForFacetValues( + facetName: string, + facetQuery: string, + indexName: string = INDEX_NAME +): Promise> { + const client = getAlgoliaClient(); + + const response = await client.searchForFacetValues({ + indexName, + facetName, + searchForFacetValuesParams: { facetQuery }, + }); + + return response.facetHits.map((hit) => ({ + value: hit.value, + count: hit.count, + })); +} + +export function buildPriceRangeFilter(min: number, max: number): string { + return `price >= ${min} AND price <= ${max}`; +} + +export function buildCategoryFilter(categories: string[]): string { + return categories.map((c) => `category:"${c}"`).join(' OR '); +} + +export function buildInStockFilter(): string { + return 'inStock:true'; +} +``` + +### `src/index.ts` + +```typescript +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { configureIndex } from './indexing/indexManager'; +import { uploadRecords } from './indexing/recordUploader'; +import { searchProducts } from './search/searchService'; +import { buildCategoryFilter, buildPriceRangeFilter } from './search/facetService'; +import { ProductRecord } from './types/algolia.types'; + +const INDEX_NAME = process.env.ALGOLIA_INDEX_NAME ?? 'products'; + +const sampleRecords: ProductRecord[] = [ + { + objectID: 'prod-001', + name: 'Wireless Noise-Cancelling Headphones', + description: 'Over-ear headphones with 30-hour battery.', + price: 299.99, + category: 'Electronics', + brand: 'AudioTech', + rating: 4.7, + inStock: true, + tags: ['headphones', 'wireless'], + }, + { + objectID: 'prod-002', + name: 'Mechanical Keyboard TKL', + description: 'Tenkeyless keyboard with Cherry MX switches.', + price: 129.99, + category: 'Peripherals', + brand: 'KeyMaster', + rating: 4.5, + inStock: true, + tags: ['keyboard', 'mechanical'], + }, +]; + +async function main(): Promise { + // 1. Configure index settings + await configureIndex(INDEX_NAME, { + searchableAttributes: ['name', 'description', 'brand', 'tags'], + attributesForFaceting: ['category', 'brand', 'price', 'rating'], + hitsPerPage: 20, + }); + + // 2. Upload records + await uploadRecords(sampleRecords, INDEX_NAME); + + // 3. Basic search + const basicResults = await searchProducts({ query: 'headphones' }); + console.log('Basic search results:', basicResults.hits.length, 'hits'); + + // 4. Filtered search + const filteredResults = await searchProducts({ + query: 'keyboard', + filters: buildPriceRangeFilter(50, 200), + facets: ['category', 'brand'], + }); + console.log('Filtered search results:', filteredResults.hits.length, 'hits'); + + // 5. Category search + const categoryResults = await searchProducts({ + query: '', + filters: buildCategoryFilter(['Electronics', 'Peripherals']), + }); + console.log('Category search results:', categoryResults.hits.length, 'hits'); +} + +main().catch(console.error); +``` + +### `.env.example` + +``` +ALGOLIA_APP_ID=YOUR_APP_ID +ALGOLIA_API_KEY=YOUR_ADMIN_API_KEY +ALGOLIA_SEARCH_ONLY_API_KEY=YOUR_SEARCH_ONLY_API_KEY +ALGOLIA_INDEX_NAME=products +``` + +## Getting Started + +```bash +# 1. Install dependencies +npm install + +# 2. Copy environment file and fill in your Algolia credentials +cp .env.example .env + +# 3. Run the demo (configure index, upload sample data, run queries) +npm run dev + +# 4. Build for production +npm run build +npm start +``` + +## Features + +- Singleton Algolia client with environment-based configuration +- Index configuration: searchable attributes, faceting, custom ranking +- Batch record upload with task completion waiting +- Single-index and multi-index search +- Filter builders for price ranges, categories, and stock status +- Facet value search helpers +- Strongly typed records and search results using TypeScript interfaces +- Full `strict` TypeScript compilation with source maps diff --git a/skills/typescript-coder/assets/typescript-angular-basic.md b/skills/typescript-coder/assets/typescript-angular-basic.md new file mode 100644 index 000000000..4f0cbd1c4 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-angular-basic.md @@ -0,0 +1,448 @@ +# Angular Basic TypeScript Template + +> A basic Angular application starter with ASP.NET Core hosting, Angular routing, HTTP services, and a clean component/service/interface structure. Suitable as a starting point for Angular SPAs with a .NET backend or as a standalone Angular project. + +## License + +MIT — See [source repository](https://github.com/MattJeanes/AngularBasic) for full license text. + +## Source + +- [MattJeanes/AngularBasic](https://github.com/MattJeanes/AngularBasic) + +## Project Structure + +``` +AngularBasic/ +├── ClientApp/ (Angular SPA) +│ ├── src/ +│ │ ├── app/ +│ │ │ ├── core/ +│ │ │ │ ├── services/ +│ │ │ │ │ └── item.service.ts +│ │ │ │ └── models/ +│ │ │ │ └── item.model.ts +│ │ │ ├── features/ +│ │ │ │ └── items/ +│ │ │ │ ├── items.component.ts +│ │ │ │ ├── items.component.html +│ │ │ │ └── items.component.css +│ │ │ ├── shared/ +│ │ │ │ └── components/ +│ │ │ │ └── nav/ +│ │ │ │ ├── nav.component.ts +│ │ │ │ └── nav.component.html +│ │ │ ├── app.component.ts +│ │ │ ├── app.component.html +│ │ │ ├── app.module.ts +│ │ │ └── app-routing.module.ts +│ │ ├── environments/ +│ │ │ ├── environment.ts +│ │ │ └── environment.prod.ts +│ │ ├── index.html +│ │ ├── main.ts +│ │ └── styles.css +│ ├── angular.json +│ ├── tsconfig.json +│ ├── tsconfig.app.json +│ ├── tsconfig.spec.json +│ └── package.json +├── Controllers/ (.NET API controllers) +│ └── ItemsController.cs +├── Program.cs +├── appsettings.json +└── AngularBasic.csproj +``` + +## Key Files + +### `ClientApp/angular.json` (excerpt) + +```json +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "angular-basic": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "css", + "changeDetection": "OnPush" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/angular-basic", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ] + }, + "development": { + "optimization": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { "buildTarget": "angular-basic:build:production" }, + "development": { "buildTarget": "angular-basic:build:development" } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["zone.js", "zone.js/testing"], + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js" + } + } + } + } + } +} +``` + +### `ClientApp/tsconfig.json` + +```json +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "Node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} +``` + +### `ClientApp/tsconfig.app.json` + +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} +``` + +### `ClientApp/package.json` + +```json +{ + "name": "angular-basic", + "version": "0.0.0", + "private": true, + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "lint": "ng lint" + }, + "dependencies": { + "@angular/animations": "^17.3.0", + "@angular/common": "^17.3.0", + "@angular/compiler": "^17.3.0", + "@angular/core": "^17.3.0", + "@angular/forms": "^17.3.0", + "@angular/platform-browser": "^17.3.0", + "@angular/platform-browser-dynamic": "^17.3.0", + "@angular/router": "^17.3.0", + "rxjs": "~7.8.0", + "tslib": "^2.6.0", + "zone.js": "~0.14.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.3.0", + "@angular/cli": "^17.3.0", + "@angular/compiler-cli": "^17.3.0", + "@types/jasmine": "~5.1.0", + "jasmine-core": "~5.1.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.4.0" + } +} +``` + +### `ClientApp/src/app/app.module.ts` + +```typescript +import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { ItemsComponent } from './features/items/items.component'; +import { NavComponent } from './shared/components/nav/nav.component'; + +@NgModule({ + declarations: [AppComponent, NavComponent, ItemsComponent], + imports: [BrowserModule, HttpClientModule, AppRoutingModule], + providers: [], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +### `ClientApp/src/app/app-routing.module.ts` + +```typescript +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { ItemsComponent } from './features/items/items.component'; + +const routes: Routes = [ + { path: '', redirectTo: '/items', pathMatch: 'full' }, + { path: 'items', component: ItemsComponent }, + { path: '**', redirectTo: '/items' }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], +}) +export class AppRoutingModule {} +``` + +### `ClientApp/src/app/core/models/item.model.ts` + +```typescript +export interface Item { + id: number; + name: string; + description: string; + createdAt: string; + isActive: boolean; +} + +export interface CreateItemRequest { + name: string; + description: string; +} + +export interface UpdateItemRequest { + name?: string; + description?: string; + isActive?: boolean; +} +``` + +### `ClientApp/src/app/core/services/item.service.ts` + +```typescript +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, catchError, throwError } from 'rxjs'; +import type { CreateItemRequest, Item, UpdateItemRequest } from '../models/item.model'; + +@Injectable({ providedIn: 'root' }) +export class ItemService { + private readonly apiUrl = '/api/items'; + + constructor(private readonly http: HttpClient) {} + + getAll(): Observable { + return this.http.get(this.apiUrl).pipe(catchError(this.handleError)); + } + + getById(id: number): Observable { + return this.http.get(`${this.apiUrl}/${id}`).pipe(catchError(this.handleError)); + } + + create(request: CreateItemRequest): Observable { + return this.http.post(this.apiUrl, request).pipe(catchError(this.handleError)); + } + + update(id: number, request: UpdateItemRequest): Observable { + return this.http.put(`${this.apiUrl}/${id}`, request).pipe(catchError(this.handleError)); + } + + delete(id: number): Observable { + return this.http.delete(`${this.apiUrl}/${id}`).pipe(catchError(this.handleError)); + } + + private handleError(error: HttpErrorResponse): Observable { + const message = + error.status === 0 + ? `Network error: ${error.error?.message ?? 'Unknown'}` + : `Server error ${error.status}: ${error.message}`; + console.error(message); + return throwError(() => new Error(message)); + } +} +``` + +### `ClientApp/src/app/features/items/items.component.ts` + +```typescript +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { Observable, catchError, of } from 'rxjs'; +import type { Item } from '../../core/models/item.model'; +import { ItemService } from '../../core/services/item.service'; + +@Component({ + selector: 'app-items', + templateUrl: './items.component.html', + styleUrls: ['./items.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ItemsComponent implements OnInit { + items$!: Observable; + error: string | null = null; + + constructor(private readonly itemService: ItemService) {} + + ngOnInit(): void { + this.loadItems(); + } + + loadItems(): void { + this.items$ = this.itemService.getAll().pipe( + catchError((err: Error) => { + this.error = err.message; + return of([]); + }) + ); + } + + trackById(_index: number, item: Item): number { + return item.id; + } +} +``` + +### `ClientApp/src/app/features/items/items.component.html` + +```html +
+

Items

+ + + + +

No items found.

+
    +
  • + {{ item.name }} + {{ item.description }} + + {{ item.isActive ? 'Active' : 'Inactive' }} + +
  • +
+
+ + +

Loading…

+
+
+``` + +### `ClientApp/src/environments/environment.ts` + +```typescript +export const environment = { + production: false, + apiBaseUrl: 'https://localhost:5001', +}; +``` + +## Getting Started + +```bash +# Prerequisites: Node.js 18+, Angular CLI 17+ + +# 1. Navigate to the ClientApp directory +cd ClientApp + +# 2. Install Angular dependencies +npm install + +# 3. Serve the Angular app (standalone, without .NET backend) +npm start +# Opens at http://localhost:4200 + +# 4. Build for production +npm run build +# Output goes to dist/angular-basic/ + +# (Optional) With .NET backend +# Restore and run from the project root +# dotnet restore +# dotnet run +``` + +## Features + +- Angular 17 with `OnPush` change detection strategy for performance +- `HttpClient`-based service with typed `Observable` returns and centralised error handling +- `AsyncPipe` in templates to automatically subscribe and unsubscribe from Observables +- Standalone `AppRoutingModule` with lazy-loadable route structure +- Strict Angular template type-checking via `strictTemplates: true` +- Interface-driven data models (`Item`, `CreateItemRequest`, `UpdateItemRequest`) +- Environment configuration files for development and production API URLs +- ASP.NET Core backend integration with a `/api/items` REST controller diff --git a/skills/typescript-coder/assets/typescript-aurelia.md b/skills/typescript-coder/assets/typescript-aurelia.md new file mode 100644 index 000000000..e2a005ecf --- /dev/null +++ b/skills/typescript-coder/assets/typescript-aurelia.md @@ -0,0 +1,439 @@ +# TypeScript Aurelia Framework Project Template + +> A TypeScript project starter based on patterns from `generator-aurelia-ts` by kristianmandrup. Produces an Aurelia 2 application with TypeScript, component/view model pairs, dependency injection, and `aurelia.json` / Webpack configuration. + +## License + +MIT License — See source repository for full license terms. + +## Source + +- [kristianmandrup/generator-aurelia-ts](https://github.com/kristianmandrup/generator-aurelia-ts) + +> Note: The original generator targeted Aurelia 1 (Classic). This template reflects Aurelia 2 (`@aurelia/`) conventions, which are the current stable release. + +## Project Structure + +``` +my-aurelia-app/ +├── src/ +│ ├── components/ +│ │ ├── app.ts ← Root app component +│ │ ├── app.html ← Root app template +│ │ ├── hello-world.ts ← Sample component view-model +│ │ └── hello-world.html ← Sample component template +│ ├── services/ +│ │ └── user.service.ts +│ ├── models/ +│ │ └── user.ts +│ ├── value-converters/ +│ │ └── date-format.ts +│ ├── resources/ +│ │ └── index.ts ← Global resource registration +│ ├── routes/ +│ │ └── index.ts ← Route configuration +│ └── main.ts ← Application bootstrap +├── public/ +│ └── index.html +├── tests/ +│ └── unit/ +│ └── hello-world.spec.ts +├── .eslintrc.json +├── .gitignore +├── aurelia.json ← Aurelia configuration +├── jest.config.ts +├── package.json +├── tsconfig.json +└── webpack.config.ts +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-aurelia-app", + "version": "1.0.0", + "description": "Aurelia 2 TypeScript application", + "scripts": { + "start": "webpack serve --mode development", + "build": "webpack --mode production", + "test": "jest", + "test:watch": "jest --watch", + "lint": "eslint src --ext .ts", + "clean": "rimraf dist" + }, + "dependencies": { + "@aurelia/fetch-client": "^2.0.0", + "@aurelia/kernel": "^2.0.0", + "@aurelia/router": "^2.0.0", + "@aurelia/runtime": "^2.0.0", + "@aurelia/runtime-html": "^2.0.0", + "aurelia": "^2.0.0" + }, + "devDependencies": { + "@aurelia/testing": "^2.0.0", + "@babel/core": "^7.23.0", + "@babel/preset-env": "^7.23.0", + "@babel/preset-typescript": "^7.23.0", + "@types/jest": "^29.5.7", + "@types/node": "^20.8.10", + "@typescript-eslint/eslint-plugin": "^6.9.1", + "@typescript-eslint/parser": "^6.9.1", + "babel-loader": "^9.1.3", + "css-loader": "^6.8.1", + "eslint": "^8.52.0", + "html-webpack-plugin": "^5.5.3", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "rimraf": "^5.0.5", + "style-loader": "^3.3.3", + "ts-loader": "^9.5.0", + "typescript": "^5.2.2", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "strict": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "baseUrl": "src" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} +``` + +### `aurelia.json` + +```json +{ + "name": "my-aurelia-app", + "type": "app", + "platform": "web", + "bundler": "webpack", + "transpiler": "typescript", + "cssProcessor": "none", + "unitTestRunner": "jest", + "integrationTestRunner": "none", + "features": { + "router": true, + "store": false + }, + "build": { + "options": { + "server": "dev", + "extract-css": "prod" + }, + "env": { + "development": { + "debug": true, + "logLevel": "debug" + }, + "production": { + "debug": false, + "logLevel": "warn" + } + } + } +} +``` + +### `webpack.config.ts` + +```typescript +import path from "path"; +import HtmlWebpackPlugin from "html-webpack-plugin"; +import { Configuration } from "webpack"; +import "webpack-dev-server"; + +const config: Configuration = { + entry: "./src/main.ts", + output: { + path: path.resolve(__dirname, "dist"), + filename: "[name].[contenthash].js", + clean: true, + }, + resolve: { + extensions: [".ts", ".js", ".html"], + alias: { "@": path.resolve(__dirname, "src") }, + }, + module: { + rules: [ + { + test: /\.ts$/, + use: "ts-loader", + exclude: /node_modules/, + }, + { + test: /\.html$/i, + use: "html-loader", + }, + { + test: /\.css$/, + use: ["style-loader", "css-loader"], + }, + ], + }, + plugins: [ + new HtmlWebpackPlugin({ template: "./public/index.html" }), + ], + devServer: { + static: "./dist", + port: 9000, + hot: true, + historyApiFallback: true, + }, +}; + +export default config; +``` + +### `src/main.ts` + +```typescript +import Aurelia, { RouterConfiguration } from "aurelia"; +import { App } from "./components/app"; + +Aurelia + .register(RouterConfiguration.customize({ useUrlFragmentHash: false })) + .app(App) + .start(); +``` + +### `src/components/app.ts` + +```typescript +import { customElement } from "@aurelia/runtime-html"; +import template from "./app.html"; + +@customElement({ name: "app", template }) +export class App { + public message = "Welcome to Aurelia 2!"; +} +``` + +### `src/components/app.html` + +```html + +``` + +### `src/components/hello-world.ts` + +```typescript +import { bindable, customElement } from "@aurelia/runtime-html"; +import { inject } from "@aurelia/kernel"; +import { UserService } from "../services/user.service"; +import template from "./hello-world.html"; + +@customElement({ name: "hello-world", template }) +@inject(UserService) +export class HelloWorld { + /** The name to display — bindable from outside. */ + @bindable public name = "World"; + + /** Track whether the greeting has been acknowledged. */ + public acknowledged = false; + + private userCount = 0; + + constructor(private readonly userService: UserService) {} + + async attached(): Promise { + const users = await this.userService.getUsers(); + this.userCount = users.length; + } + + acknowledge(): void { + this.acknowledged = true; + } + + get greeting(): string { + return `Hello, ${this.name}! (${this.userCount} users registered)`; + } +} +``` + +### `src/components/hello-world.html` + +```html + +``` + +### `src/models/user.ts` + +```typescript +export interface User { + id: number; + name: string; + email: string; + role: "admin" | "user" | "guest"; +} +``` + +### `src/services/user.service.ts` + +```typescript +import { singleton } from "@aurelia/kernel"; +import { User } from "../models/user"; + +@singleton() +export class UserService { + private users: User[] = [ + { id: 1, name: "Alice", email: "alice@example.com", role: "admin" }, + { id: 2, name: "Bob", email: "bob@example.com", role: "user" }, + ]; + + async getUsers(): Promise { + return Promise.resolve(this.users); + } + + async getUserById(id: number): Promise { + return Promise.resolve(this.users.find((u) => u.id === id)); + } +} +``` + +### `src/value-converters/date-format.ts` + +```typescript +import { valueConverter } from "@aurelia/runtime-html"; + +@valueConverter("dateFormat") +export class DateFormatValueConverter { + toView(value: string | Date, format = "short"): string { + if (!value) return ""; + const date = value instanceof Date ? value : new Date(value); + return date.toLocaleDateString("en-US", { + dateStyle: format as Intl.DateTimeFormatOptions["dateStyle"], + }); + } +} +``` + +### `tests/unit/hello-world.spec.ts` + +```typescript +import { createFixture } from "@aurelia/testing"; +import { HelloWorld } from "../../src/components/hello-world"; +import { UserService } from "../../src/services/user.service"; + +describe("HelloWorld component", () => { + it("renders greeting with default name", async () => { + const { getBy, tearDown } = createFixture( + ``, + [], + [HelloWorld, UserService] + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); // tick + const p = getBy("p"); + expect(p.textContent).toContain("Hello, World!"); + + await tearDown(); + }); + + it("reflects custom name binding", async () => { + const { getBy, tearDown } = createFixture( + ``, + [], + [HelloWorld, UserService] + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + const p = getBy("p"); + expect(p.textContent).toContain("Hello, Aurelia!"); + + await tearDown(); + }); +}); +``` + +### `jest.config.ts` + +```typescript +import type { Config } from "jest"; + +const config: Config = { + testEnvironment: "jsdom", + transform: { + "^.+\\.ts$": "babel-jest", + "^.+\\.html$": "/tests/html-transform.js", + }, + moduleFileExtensions: ["ts", "js", "html", "json"], + roots: ["/tests"], +}; + +export default config; +``` + +## Getting Started + +1. Install dependencies: + ```bash + npm install + ``` +2. Start the development server: + ```bash + npm start + ``` + The app will be available at `http://localhost:9000`. +3. Run unit tests: + ```bash + npm test + ``` +4. Build for production: + ```bash + npm run build + ``` + +## Features + +- Aurelia 2 (`@aurelia/`) with TypeScript decorator support (`experimentalDecorators`, `emitDecoratorMetadata`) +- Custom element components using the `@customElement` decorator with separate HTML templates +- `@bindable` properties for one-way and two-way data binding between components +- Dependency injection via `@inject` and `@singleton` decorators from `@aurelia/kernel` +- Value converters (e.g. `dateFormat`) for transforming bound values in templates +- Router integration with `` for single-page navigation +- Webpack 5 build with ts-loader and separate HTML template loading +- Jest + `@aurelia/testing` `createFixture` API for lightweight component unit tests +- `aurelia.json` for project-level configuration and feature flags +- Lifecycle hook `attached()` for async initialisation after DOM insertion diff --git a/skills/typescript-coder/assets/typescript-backbone.md b/skills/typescript-coder/assets/typescript-backbone.md new file mode 100644 index 000000000..aa0690ee6 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-backbone.md @@ -0,0 +1,415 @@ +# TypeScript Backbone.js MVC + +> A TypeScript Backbone.js application starter following the classic MVC pattern — Models, Collections, Views, and a Router — bundled with Webpack. Provides strongly-typed wrappers around Backbone primitives for maintainable single-page applications. + +## License + +See [source repository](https://gitlab.com/ridesz/generator-typescript-backbone-by-ridesz) for license terms. + +## Source + +- [ridesz/generator-typescript-backbone-by-ridesz](https://gitlab.com/ridesz/generator-typescript-backbone-by-ridesz) + +## Project Structure + +``` +my-backbone-app/ +├── src/ +│ ├── models/ +│ │ └── TodoModel.ts +│ ├── collections/ +│ │ └── TodoCollection.ts +│ ├── views/ +│ │ ├── TodoView.ts +│ │ └── TodoListView.ts +│ ├── router/ +│ │ └── AppRouter.ts +│ ├── templates/ +│ │ └── todo.html +│ └── app.ts (entry point) +├── dist/ (generated — do not edit) +├── index.html +├── package.json +├── tsconfig.json +├── webpack.config.js +├── .gitignore +└── README.md +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-backbone-app", + "version": "0.1.0", + "description": "TypeScript Backbone.js MVC application", + "license": "MIT", + "private": true, + "scripts": { + "start": "webpack serve --mode development", + "build": "webpack --mode production", + "build:dev": "webpack --mode development", + "clean": "rimraf dist", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "backbone": "^1.4.1", + "jquery": "^3.7.0", + "underscore": "^1.13.6" + }, + "devDependencies": { + "@types/backbone": "^1.4.15", + "@types/jquery": "^3.5.29", + "@types/underscore": "^1.11.15", + "css-loader": "^6.10.0", + "html-webpack-plugin": "^5.6.0", + "rimraf": "^5.0.0", + "style-loader": "^3.3.0", + "ts-loader": "^9.5.0", + "typescript": "^5.4.0", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.0", + "webpack-dev-server": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2018", + "module": "ES2020", + "moduleResolution": "Bundler", + "lib": ["ES2018", "DOM", "DOM.Iterable"], + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} +``` + +### `webpack.config.js` + +```javascript +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = (env, argv) => ({ + entry: './src/app.ts', + output: { + filename: 'bundle.[contenthash].js', + path: path.resolve(__dirname, 'dist'), + clean: true, + }, + resolve: { + extensions: ['.ts', '.js'], + }, + module: { + rules: [ + { + test: /\.ts$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + ], + }, + plugins: [ + new HtmlWebpackPlugin({ + template: './index.html', + title: 'Backbone TypeScript App', + }), + ], + devServer: { + port: 8080, + hot: true, + historyApiFallback: true, + }, + devtool: argv.mode === 'production' ? 'source-map' : 'eval-source-map', +}); +``` + +### `src/models/TodoModel.ts` + +```typescript +import Backbone from 'backbone'; + +export interface TodoAttributes { + id?: string; + title: string; + completed: boolean; + createdAt: Date; +} + +export class TodoModel extends Backbone.Model { + defaults(): Partial { + return { + title: '', + completed: false, + createdAt: new Date(), + }; + } + + validate(attrs: Partial): string | undefined { + if (!attrs.title || attrs.title.trim().length === 0) { + return 'Title must not be empty.'; + } + return undefined; + } + + toggle(): void { + this.save({ completed: !this.get('completed') }); + } +} +``` + +### `src/collections/TodoCollection.ts` + +```typescript +import Backbone from 'backbone'; +import { TodoModel } from '../models/TodoModel.js'; +import type { TodoAttributes } from '../models/TodoModel.js'; + +export class TodoCollection extends Backbone.Collection { + model = TodoModel; + + url = '/api/todos'; + + get remaining(): TodoModel[] { + return this.filter((todo) => !todo.get('completed')); + } + + get completed(): TodoModel[] { + return this.filter((todo) => todo.get('completed')); + } + + clearCompleted(): void { + const done = this.completed; + this.remove(done); + done.forEach((todo) => todo.destroy()); + } + + comparator(todo: TodoModel): Date { + return todo.get('createdAt') ?? new Date(0); + } +} +``` + +### `src/views/TodoView.ts` + +```typescript +import Backbone from 'backbone'; +import _ from 'underscore'; +import type { TodoModel } from '../models/TodoModel.js'; + +const TEMPLATE = _.template(` +
+ /> + + +
+`); + +export class TodoView extends Backbone.View { + tagName = 'li' as const; + + events(): Backbone.EventsHash { + return { + 'click .toggle': 'onToggle', + 'click .destroy': 'onDestroy', + 'dblclick .title': 'onEdit', + }; + } + + initialize(): void { + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.model, 'destroy', this.remove); + } + + render(): this { + this.$el.html(TEMPLATE(this.model.toJSON())); + this.$el.toggleClass('completed', !!this.model.get('completed')); + return this; + } + + private onToggle(): void { + this.model.toggle(); + } + + private onDestroy(): void { + this.model.destroy(); + } + + private onEdit(): void { + const newTitle = prompt('Edit todo:', this.model.get('title')); + if (newTitle !== null && newTitle.trim()) { + this.model.save({ title: newTitle.trim() }); + } + } +} +``` + +### `src/views/TodoListView.ts` + +```typescript +import Backbone from 'backbone'; +import type { TodoCollection } from '../collections/TodoCollection.js'; +import type { TodoModel } from '../models/TodoModel.js'; +import { TodoView } from './TodoView.js'; + +export class TodoListView extends Backbone.View { + declare collection: TodoCollection; + + events(): Backbone.EventsHash { + return { + 'keypress #new-todo': 'onKeyPress', + 'click #clear-completed': 'onClearCompleted', + }; + } + + initialize(): void { + this.listenTo(this.collection, 'add', this.addOne); + this.listenTo(this.collection, 'reset', this.addAll); + this.listenTo(this.collection, 'change remove', this.updateStatus); + this.collection.fetch(); + } + + render(): this { + this.$el.html(` +

Todos

+ +
    +
    + `); + this.addAll(); + return this; + } + + private addOne(todo: TodoModel): void { + const view = new TodoView({ model: todo }); + this.$('#todo-list').append(view.render().el); + } + + private addAll(): void { + this.$('#todo-list').empty(); + this.collection.each((todo) => this.addOne(todo)); + this.updateStatus(); + } + + private updateStatus(): void { + const remaining = this.collection.remaining.length; + this.$('#footer').html( + `${remaining} item${remaining !== 1 ? 's' : ''} left + ` + ); + } + + private onKeyPress(e: JQuery.KeyPressEvent): void { + const input = this.$('#new-todo'); + const title = (input.val() as string).trim(); + if (e.which === 13 && title) { + this.collection.create({ title, completed: false, createdAt: new Date() }); + input.val(''); + } + } + + private onClearCompleted(): void { + this.collection.clearCompleted(); + } +} +``` + +### `src/router/AppRouter.ts` + +```typescript +import Backbone from 'backbone'; + +export class AppRouter extends Backbone.Router { + routes(): Backbone.RoutesHash { + return { + '': 'home', + 'todos/:id': 'showTodo', + '*path': 'notFound', + }; + } + + home(): void { + console.log('Route: home'); + } + + showTodo(id: string): void { + console.log(`Route: showTodo — id=${id}`); + } + + notFound(path: string): void { + console.warn(`Route not found: ${path}`); + } +} +``` + +### `src/app.ts` + +```typescript +import $ from 'jquery'; +import { TodoCollection } from './collections/TodoCollection.js'; +import { AppRouter } from './router/AppRouter.js'; +import { TodoListView } from './views/TodoListView.js'; + +$(() => { + const todos = new TodoCollection(); + + const appView = new TodoListView({ + collection: todos, + el: '#app', + }); + appView.render(); + + const router = new AppRouter(); + Backbone.history.start({ pushState: true }); +}); +``` + +## Getting Started + +```bash +# 1. Create project directory +mkdir my-backbone-app && cd my-backbone-app + +# 2. Copy project files (see structure above) + +# 3. Install dependencies +npm install + +# 4. Start the development server (http://localhost:8080) +npm start + +# 5. Build for production +npm run build +``` + +## Features + +- Strongly-typed Backbone Models, Collections, Views, and Router using `@types/backbone` +- Webpack 5 bundling with `ts-loader`, content-hash output filenames, and dev-server HMR +- Underscore templates compiled inline — no extra template loader needed +- Collection comparator, filtering helpers (`remaining`, `completed`), and `clearCompleted` +- View event delegation using the standard Backbone `events()` hash with TypeScript method references +- `Backbone.history` with `pushState` for clean URLs +- Strict TypeScript mode with source maps in both development and production diff --git a/skills/typescript-coder/assets/typescript-bitloops.md b/skills/typescript-coder/assets/typescript-bitloops.md new file mode 100644 index 000000000..9a8b2abe5 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-bitloops.md @@ -0,0 +1,547 @@ +# TypeScript Bitloops DDD / Clean Architecture Template + +> A TypeScript project template based on the `generator-bitloops` Yeoman generator. Produces +> a Domain-Driven Design (DDD) application scaffold following clean/hexagonal architecture +> principles. Organises code into domain, application, and infrastructure layers with bounded +> contexts, strongly typed value objects, domain entities, aggregates, repositories, and +> use cases (application services). + +## License + +See the [generator-bitloops repository](https://github.com/bitloops/generator-bitloops) for +license terms. Bitloops open-source tooling is generally released under the MIT License. + +## Source + +- [generator-bitloops](https://github.com/bitloops/generator-bitloops) by Bitloops + +## Project Structure + +``` +my-bitloops-app/ +├── src/ +│ ├── bounded-contexts/ +│ │ └── iam/ # Identity & Access Management context +│ │ ├── domain/ +│ │ │ ├── entities/ +│ │ │ │ └── user.entity.ts +│ │ │ ├── value-objects/ +│ │ │ │ ├── email.value-object.ts +│ │ │ │ └── user-id.value-object.ts +│ │ │ ├── aggregates/ +│ │ │ │ └── user.aggregate.ts +│ │ │ ├── events/ +│ │ │ │ └── user-registered.event.ts +│ │ │ ├── errors/ +│ │ │ │ └── user.errors.ts +│ │ │ └── repositories/ +│ │ │ └── user.repository.ts # Port (interface) +│ │ ├── application/ +│ │ │ └── use-cases/ +│ │ │ └── register-user/ +│ │ │ ├── register-user.use-case.ts +│ │ │ ├── register-user.request.ts +│ │ │ └── register-user.response.ts +│ │ └── infrastructure/ +│ │ ├── persistence/ +│ │ │ └── mongo-user.repository.ts # Adapter +│ │ └── mappers/ +│ │ └── user.mapper.ts +│ ├── shared/ +│ │ ├── domain/ +│ │ │ ├── entity.base.ts +│ │ │ ├── aggregate-root.base.ts +│ │ │ ├── value-object.base.ts +│ │ │ ├── domain-event.base.ts +│ │ │ └── unique-entity-id.ts +│ │ └── result/ +│ │ └── result.ts +│ └── main.ts +├── tests/ +│ ├── unit/ +│ │ └── iam/ +│ │ └── user.entity.spec.ts +│ └── integration/ +│ └── iam/ +│ └── register-user.use-case.spec.ts +├── package.json +├── tsconfig.json +└── .env.example +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-bitloops-app", + "version": "1.0.0", + "description": "DDD / clean architecture TypeScript app via generator-bitloops", + "main": "dist/main.js", + "scripts": { + "build": "tsc", + "start": "node dist/main.js", + "dev": "ts-node-dev --respawn --transpile-only src/main.ts", + "test": "jest --coverage", + "test:unit": "jest tests/unit", + "test:integration": "jest tests/integration", + "lint": "eslint 'src/**/*.ts'", + "clean": "rimraf dist" + }, + "dependencies": { + "dotenv": "^16.3.1", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@types/jest": "^29.5.11", + "@types/node": "^20.11.0", + "@types/uuid": "^9.0.7", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0", + "jest": "^29.7.0", + "rimraf": "^5.0.5", + "ts-jest": "^29.1.4", + "ts-node-dev": "^2.0.0", + "typescript": "^5.3.3" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "paths": { + "@shared/*": ["src/shared/*"], + "@iam/*": ["src/bounded-contexts/iam/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} +``` + +### `src/shared/result/result.ts` + +```typescript +export type Either = Left | Right; + +export class Left { + readonly value: L; + + constructor(value: L) { + this.value = value; + } + + isLeft(): this is Left { + return true; + } + + isRight(): this is Right { + return false; + } +} + +export class Right { + readonly value: R; + + constructor(value: R) { + this.value = value; + } + + isLeft(): this is Left { + return false; + } + + isRight(): this is Right { + return true; + } +} + +export const left = (value: L): Either => new Left(value); +export const right = (value: R): Either => new Right(value); +``` + +### `src/shared/domain/value-object.base.ts` + +```typescript +interface ValueObjectProps { + [index: string]: unknown; +} + +export abstract class ValueObject { + protected readonly props: T; + + constructor(props: T) { + this.props = Object.freeze(props); + } + + equals(other?: ValueObject): boolean { + if (other === null || other === undefined) return false; + if (other.props === undefined) return false; + return JSON.stringify(this.props) === JSON.stringify(other.props); + } +} +``` + +### `src/shared/domain/entity.base.ts` + +```typescript +import { UniqueEntityId } from './unique-entity-id'; + +export abstract class Entity { + protected readonly _id: UniqueEntityId; + protected readonly props: T; + + constructor(props: T, id?: UniqueEntityId) { + this._id = id ?? new UniqueEntityId(); + this.props = props; + } + + get id(): UniqueEntityId { + return this._id; + } + + equals(entity?: Entity): boolean { + if (entity === null || entity === undefined) return false; + if (!(entity instanceof Entity)) return false; + return this._id.equals(entity._id); + } +} +``` + +### `src/shared/domain/aggregate-root.base.ts` + +```typescript +import { Entity } from './entity.base'; +import { DomainEvent } from './domain-event.base'; +import { UniqueEntityId } from './unique-entity-id'; + +export abstract class AggregateRoot extends Entity { + private _domainEvents: DomainEvent[] = []; + + constructor(props: T, id?: UniqueEntityId) { + super(props, id); + } + + get domainEvents(): DomainEvent[] { + return this._domainEvents; + } + + protected addDomainEvent(event: DomainEvent): void { + this._domainEvents.push(event); + } + + clearEvents(): void { + this._domainEvents = []; + } +} +``` + +### `src/shared/domain/unique-entity-id.ts` + +```typescript +import { v4 as uuidv4 } from 'uuid'; +import { ValueObject } from './value-object.base'; + +interface UniqueEntityIdProps { + value: string; +} + +export class UniqueEntityId extends ValueObject { + constructor(id?: string) { + super({ value: id ?? uuidv4() }); + } + + get value(): string { + return this.props.value; + } + + toString(): string { + return this.props.value; + } +} +``` + +### `src/bounded-contexts/iam/domain/value-objects/email.value-object.ts` + +```typescript +import { Either, left, right } from '../../../../shared/result/result'; +import { ValueObject } from '../../../../shared/domain/value-object.base'; + +interface EmailProps { + value: string; +} + +type EmailError = { type: 'INVALID_EMAIL'; message: string }; + +export class Email extends ValueObject { + private static readonly EMAIL_REGEX = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; + + private constructor(props: EmailProps) { + super(props); + } + + get value(): string { + return this.props.value; + } + + static create(email: string): Either { + if (!email || !Email.EMAIL_REGEX.test(email)) { + return left({ + type: 'INVALID_EMAIL', + message: `"${email}" is not a valid email address`, + }); + } + return right(new Email({ value: email.toLowerCase() })); + } +} +``` + +### `src/bounded-contexts/iam/domain/aggregates/user.aggregate.ts` + +```typescript +import { AggregateRoot } from '../../../../shared/domain/aggregate-root.base'; +import { UniqueEntityId } from '../../../../shared/domain/unique-entity-id'; +import { Email } from '../value-objects/email.value-object'; +import { UserRegisteredEvent } from '../events/user-registered.event'; +import { Either, left, right } from '../../../../shared/result/result'; + +interface UserProps { + email: Email; + passwordHash: string; + name: string; + isActive: boolean; + createdAt: Date; +} + +type UserCreateError = { type: 'USER_ALREADY_EXISTS' | 'INVALID_EMAIL'; message: string }; + +export class User extends AggregateRoot { + private constructor(props: UserProps, id?: UniqueEntityId) { + super(props, id); + } + + get email(): Email { + return this.props.email; + } + + get name(): string { + return this.props.name; + } + + get isActive(): boolean { + return this.props.isActive; + } + + static create( + props: { email: string; passwordHash: string; name: string }, + id?: UniqueEntityId, + ): Either { + const emailOrError = Email.create(props.email); + if (emailOrError.isLeft()) { + return left({ type: 'INVALID_EMAIL', message: emailOrError.value.message }); + } + + const user = new User( + { + email: emailOrError.value, + passwordHash: props.passwordHash, + name: props.name, + isActive: true, + createdAt: new Date(), + }, + id, + ); + + user.addDomainEvent( + new UserRegisteredEvent({ userId: user.id.value, email: props.email }), + ); + + return right(user); + } +} +``` + +### `src/bounded-contexts/iam/domain/repositories/user.repository.ts` + +```typescript +import { User } from '../aggregates/user.aggregate'; + +// Port — implemented in infrastructure layer +export interface IUserRepository { + findById(id: string): Promise; + findByEmail(email: string): Promise; + save(user: User): Promise; + delete(id: string): Promise; +} +``` + +### `src/bounded-contexts/iam/application/use-cases/register-user/register-user.use-case.ts` + +```typescript +import { IUserRepository } from '../../../domain/repositories/user.repository'; +import { User } from '../../../domain/aggregates/user.aggregate'; +import { RegisterUserRequest } from './register-user.request'; +import { RegisterUserResponse } from './register-user.response'; +import { Either, left, right } from '../../../../../../shared/result/result'; + +type RegisterUserError = + | { type: 'USER_ALREADY_EXISTS'; message: string } + | { type: 'INVALID_EMAIL'; message: string } + | { type: 'UNEXPECTED_ERROR'; message: string }; + +export class RegisterUserUseCase { + constructor(private readonly userRepository: IUserRepository) {} + + async execute( + request: RegisterUserRequest, + ): Promise> { + try { + const existing = await this.userRepository.findByEmail(request.email); + if (existing) { + return left({ + type: 'USER_ALREADY_EXISTS', + message: `A user with email "${request.email}" already exists`, + }); + } + + // NOTE: in production, hash with bcrypt before creating the aggregate + const userOrError = User.create({ + email: request.email, + passwordHash: request.passwordHash, + name: request.name, + }); + + if (userOrError.isLeft()) { + return left({ type: 'INVALID_EMAIL', message: userOrError.value.message }); + } + + const user = userOrError.value; + await this.userRepository.save(user); + + return right({ userId: user.id.value, email: user.email.value }); + } catch (err) { + return left({ + type: 'UNEXPECTED_ERROR', + message: err instanceof Error ? err.message : 'Unexpected error', + }); + } + } +} +``` + +### `src/bounded-contexts/iam/application/use-cases/register-user/register-user.request.ts` + +```typescript +export interface RegisterUserRequest { + name: string; + email: string; + passwordHash: string; +} +``` + +### `src/bounded-contexts/iam/application/use-cases/register-user/register-user.response.ts` + +```typescript +export interface RegisterUserResponse { + userId: string; + email: string; +} +``` + +### `src/bounded-contexts/iam/domain/events/user-registered.event.ts` + +```typescript +import { DomainEvent } from '../../../../shared/domain/domain-event.base'; + +interface UserRegisteredProps { + userId: string; + email: string; +} + +export class UserRegisteredEvent extends DomainEvent { + readonly userId: string; + readonly email: string; + + constructor(props: UserRegisteredProps) { + super({ aggregateId: props.userId, eventName: 'UserRegistered' }); + this.userId = props.userId; + this.email = props.email; + } +} +``` + +### `src/shared/domain/domain-event.base.ts` + +```typescript +interface DomainEventProps { + aggregateId: string; + eventName: string; +} + +export abstract class DomainEvent { + readonly aggregateId: string; + readonly eventName: string; + readonly occurredOn: Date; + + constructor(props: DomainEventProps) { + this.aggregateId = props.aggregateId; + this.eventName = props.eventName; + this.occurredOn = new Date(); + } +} +``` + +## Getting Started + +```bash +# 1. Install dependencies +npm install + +# 2. Run the domain tests +npm run test:unit + +# 3. Run integration tests +npm run test:integration + +# 4. Build +npm run build + +# 5. Start +npm start +``` + +## Features + +- Domain-Driven Design with bounded-context folder layout +- Aggregate roots, domain entities, and immutable value objects +- `Either` monad for explicit, type-safe error handling without exceptions +- Domain events attached to aggregates and cleared after persistence +- Repository interfaces (ports) in the domain layer; adapters in infrastructure +- Use cases (application services) orchestrating domain logic +- `UniqueEntityId` value object wrapping UUIDs for identity management +- Strict TypeScript with no implicit `any` and unused-variable enforcement +- Jest unit and integration tests targeting individual layers independently diff --git a/skills/typescript-coder/assets/typescript-bscotch-template-modern.md b/skills/typescript-coder/assets/typescript-bscotch-template-modern.md new file mode 100644 index 000000000..8454d7867 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-bscotch-template-modern.md @@ -0,0 +1,455 @@ + + +# TypeScript Template — Modernized (bscotch variation) + +> Based on [bscotch/typescript-template](https://github.com/bscotch/typescript-template) +> License: **MIT** +> This is a **modernized variation** — the original may be outdated. This version applies +> current best practices: ESM, Node.js 20+, TypeScript 5.x strict mode, and Vitest. + +## What Changed from the Original + +| Area | Original (bscotch) | This Modernized Variation | +|---|---|---| +| Module system | CommonJS or mixed | Pure ESM (`"type": "module"`) | +| Node.js target | Node 14/16 | Node.js 20+ | +| TypeScript | 4.x | 5.x strict mode | +| Test runner | Mocha or Jest | **Vitest** (ESM-native, fast) | +| tsconfig base | Permissive | `@tsconfig/node20` + strict overrides | +| Module resolution | `node` | `NodeNext` | + +## Project Structure + +``` +my-project/ +├── src/ +│ ├── index.ts # Main entry / public API +│ ├── lib/ +│ │ └── utils.ts # Internal utilities +│ └── types.ts # Shared type definitions +├── tests/ +│ ├── index.test.ts +│ └── utils.test.ts +├── dist/ # Compiled output (git-ignored) +├── .gitignore +├── package.json +├── tsconfig.json +├── tsconfig.build.json # Excludes test files for production build +├── vitest.config.ts +└── README.md +``` + +## `package.json` + +```json +{ + "name": "my-project", + "version": "1.0.0", + "description": "A TypeScript project", + "author": "Your Name ", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist", + "!dist/**/*.test.*", + "!dist/**/*.spec.*" + ], + "engines": { + "node": ">=20.0.0" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "build:watch": "tsc -p tsconfig.build.json --watch", + "clean": "rimraf dist coverage", + "prebuild": "npm run clean", + "dev": "node --watch --loader ts-node/esm src/index.ts", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "type-check": "tsc --noEmit", + "lint": "eslint src tests", + "lint:fix": "eslint src tests --fix", + "prepublishOnly": "npm run build && npm run type-check" + }, + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^2.1.0", + "rimraf": "^6.0.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + } +} +``` + +## `tsconfig.json` + +Used for editor support and type-checking (includes test files): + +```json +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +## `tsconfig.build.json` + +Used only for the production build — excludes test files: + +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "inlineSources": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests/**/*", "**/*.test.ts", "**/*.spec.ts"] +} +``` + +## `vitest.config.ts` + +```typescript +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + // Run tests in Node.js environment + environment: "node", + + // Glob patterns for test files + include: ["tests/**/*.test.ts", "src/**/*.test.ts"], + + // Coverage configuration + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/**/*.ts"], + exclude: [ + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/types.ts", + "node_modules/**", + ], + thresholds: { + lines: 80, + functions: 80, + branches: 70, + statements: 80, + }, + }, + + // Timeout per test in milliseconds + testTimeout: 10_000, + + // Reporter + reporter: "verbose", + }, +}); +``` + +## `src/types.ts` + +```typescript +/** + * Shared type definitions for the project. + * Export all public-facing types from here. + */ + +/** Generic result type — avoids throwing for expected error cases. */ +export type Result = + | { success: true; value: T } + | { success: false; error: E }; + +/** Creates a successful Result. */ +export function ok(value: T): Result { + return { success: true, value }; +} + +/** Creates a failed Result. */ +export function err(error: E): Result { + return { success: false, error }; +} + +/** A value that may be null or undefined. */ +export type Maybe = T | null | undefined; + +/** Deep readonly utility — makes all nested properties readonly. */ +export type DeepReadonly = { + readonly [P in keyof T]: T[P] extends object ? DeepReadonly : T[P]; +}; + +/** Unwrap a Promise type. */ +export type Awaited = T extends PromiseLike ? Awaited : T; +``` + +## `src/lib/utils.ts` + +```typescript +/** + * Internal utility functions. + */ + +import type { Maybe } from "../types.js"; + +/** + * Asserts that a value is non-null and non-undefined. + * Throws at runtime with a descriptive message if the assertion fails. + */ +export function assertDefined( + value: Maybe, + label = "value" +): asserts value is T { + if (value === null || value === undefined) { + throw new Error(`Expected ${label} to be defined, but got ${String(value)}`); + } +} + +/** + * Narrows an unknown value to string. + */ +export function isString(value: unknown): value is string { + return typeof value === "string"; +} + +/** + * Narrows an unknown value to number. + */ +export function isNumber(value: unknown): value is number { + return typeof value === "number" && !Number.isNaN(value); +} + +/** + * Returns the first defined value from a list of candidates. + */ +export function coalesce(...values: Array>): T | undefined { + return values.find((v) => v !== null && v !== undefined) as T | undefined; +} + +/** + * Groups an array of items by a key selector. + */ +export function groupBy( + items: readonly T[], + keySelector: (item: T) => K +): Record { + return items.reduce( + (acc, item) => { + const key = keySelector(item); + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(item); + return acc; + }, + {} as Record + ); +} +``` + +## `src/index.ts` + +```typescript +/** + * Public API entry point. + * Re-export anything that should be part of the public interface. + */ + +export type { Result, Maybe, DeepReadonly } from "./types.js"; +export { ok, err } from "./types.js"; +export { assertDefined, isString, isNumber, coalesce, groupBy } from "./lib/utils.js"; + +// Example: application-specific logic +export interface AppOptions { + readonly name: string; + readonly version: string; + readonly logLevel?: "debug" | "info" | "warn" | "error"; +} + +export class App { + readonly #name: string; + readonly #version: string; + readonly #logLevel: NonNullable; + + constructor(options: AppOptions) { + this.#name = options.name; + this.#version = options.version; + this.#logLevel = options.logLevel ?? "info"; + } + + get name(): string { + return this.#name; + } + + get version(): string { + return this.#version; + } + + info(message: string): void { + if (this.#logLevel !== "error" && this.#logLevel !== "warn") { + console.log(`[${this.#name}] ${message}`); + } + } + + toString(): string { + return `${this.#name}@${this.#version}`; + } +} +``` + +## `tests/index.test.ts` + +```typescript +import { describe, expect, it } from "vitest"; +import { App, ok, err } from "../src/index.js"; + +describe("App", () => { + it("creates an app with the provided name and version", () => { + const app = new App({ name: "test-app", version: "1.0.0" }); + expect(app.name).toBe("test-app"); + expect(app.version).toBe("1.0.0"); + }); + + it("has a meaningful toString representation", () => { + const app = new App({ name: "my-lib", version: "2.0.0" }); + expect(app.toString()).toBe("my-lib@2.0.0"); + }); +}); + +describe("Result helpers", () => { + it("ok() creates a successful result", () => { + const result = ok(42); + expect(result.success).toBe(true); + if (result.success) { + expect(result.value).toBe(42); + } + }); + + it("err() creates a failed result", () => { + const result = err(new Error("something went wrong")); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.message).toBe("something went wrong"); + } + }); +}); +``` + +## `tests/utils.test.ts` + +```typescript +import { describe, expect, it } from "vitest"; +import { assertDefined, coalesce, groupBy, isString, isNumber } from "../src/lib/utils.js"; + +describe("assertDefined", () => { + it("does not throw for a defined value", () => { + expect(() => assertDefined("hello", "greeting")).not.toThrow(); + }); + + it("throws for null", () => { + expect(() => assertDefined(null, "myVar")).toThrow( + "Expected myVar to be defined" + ); + }); + + it("throws for undefined", () => { + expect(() => assertDefined(undefined, "myVar")).toThrow( + "Expected myVar to be defined" + ); + }); +}); + +describe("coalesce", () => { + it("returns the first non-null/undefined value", () => { + expect(coalesce(null, undefined, 0, 1)).toBe(0); + }); + + it("returns undefined when all values are nullish", () => { + expect(coalesce(null, undefined)).toBeUndefined(); + }); +}); + +describe("groupBy", () => { + it("groups items by the key selector", () => { + const items = [ + { type: "fruit", name: "apple" }, + { type: "veggie", name: "carrot" }, + { type: "fruit", name: "banana" }, + ]; + const grouped = groupBy(items, (item) => item.type); + expect(grouped["fruit"]).toHaveLength(2); + expect(grouped["veggie"]).toHaveLength(1); + }); +}); + +describe("type guards", () => { + it("isString returns true for strings", () => { + expect(isString("hello")).toBe(true); + expect(isString(123)).toBe(false); + }); + + it("isNumber returns false for NaN", () => { + expect(isNumber(NaN)).toBe(false); + expect(isNumber(42)).toBe(true); + }); +}); +``` + +## Notable Modern TypeScript 5.x Features Used + +| Feature | Where | Notes | +|---|---|---| +| `exactOptionalPropertyTypes` | `tsconfig.json` | Prevents `undefined` being assigned to optional props accidentally | +| `noUncheckedIndexedAccess` | `tsconfig.json` | Index operations return `T \| undefined` for safety | +| `noImplicitOverride` | `tsconfig.json` | Subclass methods must use `override` keyword | +| Private class fields (`#`) | `src/index.ts` | True JS private, not just TypeScript-enforced | +| `satisfies` operator | Can be used in types.ts | Validates without widening the inferred type | +| `const` type parameters | Generic helpers | `function id<const T>(v: T): T` for narrower inference | + +## Vitest vs Jest — Why Vitest Here + +- **Native ESM support** — no `transform` config needed for ESM +- **Faster** — uses Vite's transform pipeline under the hood +- **`vitest.config.ts`** — single config file, TypeScript-first +- **Compatible API** — `describe/it/expect` are identical to Jest +- **Built-in coverage** — via `@vitest/coverage-v8`, no extra setup diff --git a/skills/typescript-coder/assets/typescript-ego.md b/skills/typescript-coder/assets/typescript-ego.md new file mode 100644 index 000000000..d1bde94ae --- /dev/null +++ b/skills/typescript-coder/assets/typescript-ego.md @@ -0,0 +1,421 @@ +# TypeScript Project Template (EgoDigital / generator-ego Style) + +> A TypeScript project starter based on patterns from EgoDigital's `generator-ego`. Produces an enterprise-grade Node.js/Express API application with structured middleware, logging, environment configuration, and modular route organisation. + +## License + +MIT License — See source repository for full license terms. + +## Source + +- [egodigital/generator-ego](https://github.com/egodigital/generator-ego) + +## Project Structure + +``` +my-ego-app/ +├── src/ +│ ├── controllers/ +│ │ ├── health.ts +│ │ └── users.ts +│ ├── middleware/ +│ │ ├── auth.ts +│ │ ├── errorHandler.ts +│ │ └── logger.ts +│ ├── models/ +│ │ └── user.ts +│ ├── routes/ +│ │ ├── index.ts +│ │ └── users.ts +│ ├── services/ +│ │ └── userService.ts +│ ├── types/ +│ │ └── index.ts +│ ├── utils/ +│ │ └── env.ts +│ ├── app.ts +│ └── index.ts +├── tests/ +│ ├── controllers/ +│ │ └── users.test.ts +│ └── services/ +│ └── userService.test.ts +├── .env +├── .env.example +├── .eslintrc.json +├── .gitignore +├── jest.config.ts +├── nodemon.json +├── package.json +└── tsconfig.json +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-ego-app", + "version": "1.0.0", + "description": "Enterprise TypeScript Node.js API", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "nodemon", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src --ext .ts", + "lint:fix": "eslint src --ext .ts --fix", + "clean": "rimraf dist" + }, + "dependencies": { + "compression": "^1.7.4", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-validator": "^7.0.1", + "helmet": "^7.0.0", + "morgan": "^1.10.0", + "winston": "^3.11.0" + }, + "devDependencies": { + "@types/compression": "^1.7.5", + "@types/cors": "^2.8.15", + "@types/express": "^4.17.20", + "@types/jest": "^29.5.7", + "@types/morgan": "^1.9.8", + "@types/node": "^20.8.10", + "@types/supertest": "^2.0.15", + "@typescript-eslint/eslint-plugin": "^6.9.1", + "@typescript-eslint/parser": "^6.9.1", + "eslint": "^8.52.0", + "jest": "^29.7.0", + "nodemon": "^3.0.1", + "rimraf": "^5.0.5", + "supertest": "^6.3.3", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "baseUrl": ".", + "paths": { + "@controllers/*": ["src/controllers/*"], + "@middleware/*": ["src/middleware/*"], + "@models/*": ["src/models/*"], + "@routes/*": ["src/routes/*"], + "@services/*": ["src/services/*"], + "@types/*": ["src/types/*"], + "@utils/*": ["src/utils/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} +``` + +### `nodemon.json` + +```json +{ + "watch": ["src"], + "ext": "ts", + "exec": "ts-node -r tsconfig-paths/register src/index.ts", + "env": { + "NODE_ENV": "development" + } +} +``` + +### `src/index.ts` + +```typescript +import dotenv from "dotenv"; +dotenv.config(); + +import { createApp } from "./app"; +import { getEnv } from "./utils/env"; +import { createLogger } from "./middleware/logger"; + +const logger = createLogger("bootstrap"); + +async function bootstrap(): Promise { + const port = getEnv("PORT", "3000"); + const app = createApp(); + + app.listen(Number(port), () => { + logger.info(`Server running on port ${port} [${process.env.NODE_ENV ?? "development"}]`); + }); +} + +bootstrap().catch((err) => { + console.error("Failed to start server:", err); + process.exit(1); +}); +``` + +### `src/app.ts` + +```typescript +import express, { Application } from "express"; +import cors from "cors"; +import helmet from "helmet"; +import compression from "compression"; +import morgan from "morgan"; +import { router } from "./routes"; +import { errorHandler } from "./middleware/errorHandler"; + +export function createApp(): Application { + const app = express(); + + // Security middleware + app.use(helmet()); + app.use(cors()); + + // Request parsing + app.use(express.json({ limit: "10mb" })); + app.use(express.urlencoded({ extended: true })); + app.use(compression()); + + // Logging + app.use(morgan("combined")); + + // Routes + app.use("/api/v1", router); + + // Error handling (must be last) + app.use(errorHandler); + + return app; +} +``` + +### `src/utils/env.ts` + +```typescript +/** + * Retrieve a required environment variable, throwing if absent. + */ +export function requireEnv(key: string): string { + const value = process.env[key]; + if (value === undefined || value === "") { + throw new Error(`Missing required environment variable: ${key}`); + } + return value; +} + +/** + * Retrieve an optional environment variable with a default fallback. + */ +export function getEnv(key: string, defaultValue: string): string { + return process.env[key] ?? defaultValue; +} + +/** + * Retrieve a boolean environment variable. + */ +export function getBoolEnv(key: string, defaultValue = false): boolean { + const value = process.env[key]; + if (value === undefined) return defaultValue; + return ["true", "1", "yes"].includes(value.toLowerCase()); +} +``` + +### `src/middleware/logger.ts` + +```typescript +import winston from "winston"; + +const { combine, timestamp, printf, colorize, errors } = winston.format; + +const logFormat = printf(({ level, message, timestamp, context, stack }) => { + const ctx = context ? ` [${context}]` : ""; + return stack + ? `${timestamp} ${level}${ctx}: ${message}\n${stack}` + : `${timestamp} ${level}${ctx}: ${message}`; +}); + +export function createLogger(context?: string): winston.Logger { + return winston.createLogger({ + level: process.env.LOG_LEVEL ?? "info", + format: combine( + colorize(), + timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + errors({ stack: true }), + logFormat + ), + defaultMeta: context ? { context } : {}, + transports: [ + new winston.transports.Console(), + new winston.transports.File({ + filename: "logs/error.log", + level: "error", + }), + ], + }); +} +``` + +### `src/middleware/errorHandler.ts` + +```typescript +import { Request, Response, NextFunction } from "express"; +import { createLogger } from "./logger"; + +const logger = createLogger("errorHandler"); + +export interface AppError extends Error { + statusCode?: number; + isOperational?: boolean; +} + +export function errorHandler( + err: AppError, + _req: Request, + res: Response, + _next: NextFunction +): void { + const statusCode = err.statusCode ?? 500; + const message = err.isOperational ? err.message : "Internal Server Error"; + + logger.error(err.message, { stack: err.stack }); + + res.status(statusCode).json({ + success: false, + error: { + message, + ...(process.env.NODE_ENV === "development" && { stack: err.stack }), + }, + }); +} +``` + +### `src/middleware/auth.ts` + +```typescript +import { Request, Response, NextFunction } from "express"; + +export interface AuthenticatedRequest extends Request { + user?: { + id: string; + email: string; + roles: string[]; + }; +} + +export function requireAuth( + req: AuthenticatedRequest, + res: Response, + next: NextFunction +): void { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + res.status(401).json({ success: false, error: { message: "Unauthorized" } }); + return; + } + + // TODO: Replace with real JWT verification + const token = authHeader.substring(7); + if (!token) { + res.status(401).json({ success: false, error: { message: "Invalid token" } }); + return; + } + + next(); +} +``` + +### `src/routes/index.ts` + +```typescript +import { Router } from "express"; +import { userRouter } from "./users"; + +export const router = Router(); + +router.get("/health", (_req, res) => { + res.json({ status: "ok", timestamp: new Date().toISOString() }); +}); + +router.use("/users", userRouter); +``` + +### `.env.example` + +``` +NODE_ENV=development +PORT=3000 +LOG_LEVEL=info +DATABASE_URL=postgres://user:password@localhost:5432/mydb +JWT_SECRET=change-me-in-production +``` + +## Getting Started + +1. Copy the template files into your project directory. +2. Install dependencies: + ```bash + npm install + ``` +3. Copy `.env.example` to `.env` and fill in real values: + ```bash + cp .env.example .env + ``` +4. Run in development mode with hot-reload: + ```bash + npm run dev + ``` +5. Build for production: + ```bash + npm run build + npm start + ``` +6. Run tests: + ```bash + npm test + ``` + +## Features + +- TypeScript 5.x with strict mode, decorators, and path aliases +- Express 4 with helmet, cors, compression, and morgan middleware +- Winston structured logging with per-module logger contexts +- Centralised error handler middleware with operational vs. programmer error distinction +- Environment variable utilities (`requireEnv`, `getEnv`, `getBoolEnv`) +- Auth middleware scaffold with Bearer token pattern +- Modular route and controller organisation +- Jest + ts-jest test setup with Supertest for HTTP integration tests +- nodemon-based development workflow with ts-node +- Path aliases (`@controllers/*`, `@services/*`, etc.) for clean imports diff --git a/skills/typescript-coder/assets/typescript-express-no-stress.md b/skills/typescript-coder/assets/typescript-express-no-stress.md new file mode 100644 index 000000000..8fa30f6fd --- /dev/null +++ b/skills/typescript-coder/assets/typescript-express-no-stress.md @@ -0,0 +1,584 @@ +# Express No-Stress TypeScript API + +> A production-ready Express.js TypeScript API starter with OpenAPI 3.0 request/response validation, Swagger UI, structured logging, helmet security headers, and a clean controller-per-resource layout. Requests are validated automatically against your OpenAPI spec before reaching controller logic. + +## License + +MIT — See [source repository](https://github.com/cdimascio/generator-express-no-stress-typescript) for full license text. + +## Source + +- [cdimascio/generator-express-no-stress-typescript](https://github.com/cdimascio/generator-express-no-stress-typescript) + +## Project Structure + +``` +my-api/ +├── server/ +│ ├── api/ +│ │ ├── controllers/ +│ │ │ └── examples/ +│ │ │ ├── controller.ts +│ │ │ └── router.ts +│ │ ├── middlewares/ +│ │ │ └── error.handler.ts +│ │ └── services/ +│ │ └── examples.service.ts +│ ├── common/ +│ │ └── server.ts +│ ├── routes.ts +│ └── index.ts +├── openapi.yml +├── package.json +├── tsconfig.json +├── nodemon.json +├── .env +├── .gitignore +└── README.md +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-api", + "version": "1.0.0", + "description": "Production-ready Express TypeScript API", + "license": "MIT", + "private": true, + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch", + "clean": "rimraf dist", + "dev": "nodemon", + "start": "node dist/server/index.js", + "lint": "eslint server --ext .ts", + "test": "jest --forceExit", + "test:watch": "jest --watch" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.0", + "express": "^4.19.0", + "express-openapi-validator": "^5.3.0", + "helmet": "^7.1.0", + "morgan": "^1.10.0", + "swagger-ui-express": "^5.0.0", + "yaml": "^2.4.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/morgan": "^1.9.9", + "@types/node": "^20.12.0", + "@types/swagger-ui-express": "^4.1.6", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.57.0", + "jest": "^29.7.0", + "nodemon": "^3.1.0", + "rimraf": "^5.0.0", + "supertest": "^6.3.0", + "ts-jest": "^29.1.0", + "typescript": "^5.4.0" + }, + "engines": { + "node": ">=20.0.0" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "Node", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": ".", + "resolveJsonModule": true, + "skipLibCheck": true + }, + "include": ["server"], + "exclude": ["node_modules", "dist"] +} +``` + +### `nodemon.json` + +```json +{ + "watch": ["server"], + "ext": "ts,yml,yaml,json", + "exec": "ts-node -r dotenv/config server/index.ts", + "env": { + "NODE_ENV": "development" + } +} +``` + +### `.env` + +``` +NODE_ENV=development +PORT=3000 +LOG_LEVEL=debug +REQUEST_LIMIT=100kb +OPENAPI_SPEC=/api/v1/spec +``` + +### `openapi.yml` + +```yaml +openapi: '3.0.3' +info: + title: My API + description: A production-ready Express TypeScript API + version: 1.0.0 +servers: + - url: /api/v1 + description: Local development server + +paths: + /examples: + get: + summary: List all examples + operationId: listExamples + tags: [examples] + parameters: + - in: query + name: limit + schema: + type: integer + minimum: 1 + maximum: 100 + default: 10 + responses: + '200': + description: A list of examples + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Example' + post: + summary: Create an example + operationId: createExample + tags: [examples] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateExampleRequest' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/Example' + '422': + $ref: '#/components/responses/UnprocessableEntity' + + /examples/{id}: + get: + summary: Get an example by ID + operationId: getExample + tags: [examples] + parameters: + - $ref: '#/components/parameters/IdParam' + responses: + '200': + description: The example + content: + application/json: + schema: + $ref: '#/components/schemas/Example' + '404': + $ref: '#/components/responses/NotFound' + +components: + parameters: + IdParam: + name: id + in: path + required: true + schema: + type: string + + schemas: + Example: + type: object + required: [id, name, createdAt] + properties: + id: + type: string + name: + type: string + minLength: 1 + maxLength: 100 + description: + type: string + createdAt: + type: string + format: date-time + + CreateExampleRequest: + type: object + required: [name] + properties: + name: + type: string + minLength: 1 + maxLength: 100 + description: + type: string + + responses: + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + UnprocessableEntity: + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + ErrorResponse: + type: object + required: [message] + properties: + message: + type: string + errors: + type: array + items: + type: object +``` + +### `server/common/server.ts` + +```typescript +import cors from 'cors'; +import * as dotenv from 'dotenv'; +import express, { Application } from 'express'; +import * as fs from 'fs'; +import helmet from 'helmet'; +import morgan from 'morgan'; +import * as OpenApiValidator from 'express-openapi-validator'; +import * as path from 'path'; +import swaggerUi from 'swagger-ui-express'; +import * as yaml from 'yaml'; +import { errorHandler } from '../api/middlewares/error.handler'; +import routes from '../routes'; + +dotenv.config(); + +export default class Server { + private readonly app: Application; + + constructor() { + this.app = express(); + this.middleware(); + this.routes(); + this.swagger(); + this.openApiValidator(); + this.errorHandler(); + } + + private middleware(): void { + this.app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev')); + this.app.use(express.json({ limit: process.env.REQUEST_LIMIT ?? '100kb' })); + this.app.use(express.urlencoded({ extended: true })); + this.app.use( + helmet({ + contentSecurityPolicy: process.env.NODE_ENV === 'production', + }) + ); + this.app.use(cors()); + } + + private routes(): void { + const apiBase = process.env.OPENAPI_SPEC ?? '/api/v1'; + this.app.use(apiBase, routes); + } + + private swagger(): void { + const specPath = path.resolve('openapi.yml'); + const specContent = yaml.parse(fs.readFileSync(specPath, 'utf8')) as object; + this.app.use('/api-explorer', swaggerUi.serve, swaggerUi.setup(specContent)); + this.app.get('/api/v1/spec', (_req, res) => { + res.sendFile(specPath); + }); + } + + private openApiValidator(): void { + const apiSpec = path.resolve('openapi.yml'); + this.app.use( + OpenApiValidator.middleware({ + apiSpec, + validateRequests: true, + validateResponses: process.env.NODE_ENV !== 'production', + operationHandlers: false, + }) + ); + } + + private errorHandler(): void { + this.app.use(errorHandler); + } + + listen(port: number): Application { + this.app.listen(port, () => { + console.log(`Server listening on port ${port}`); + console.log(`Swagger UI: http://localhost:${port}/api-explorer`); + }); + return this.app; + } +} +``` + +### `server/index.ts` + +```typescript +import Server from './common/server'; + +const port = parseInt(process.env.PORT ?? '3000', 10); +export default new Server().listen(port); +``` + +### `server/routes.ts` + +```typescript +import { Router } from 'express'; +import examplesRouter from './api/controllers/examples/router'; + +const router = Router(); + +router.use('/examples', examplesRouter); + +export default router; +``` + +### `server/api/controllers/examples/router.ts` + +```typescript +import { Router } from 'express'; +import controller from './controller'; + +const router = Router(); + +router.get('/', controller.list); +router.post('/', controller.create); +router.get('/:id', controller.get); +router.put('/:id', controller.update); +router.delete('/:id', controller.delete); + +export default router; +``` + +### `server/api/controllers/examples/controller.ts` + +```typescript +import { NextFunction, Request, Response } from 'express'; +import ExamplesService from '../../services/examples.service'; + +export class ExamplesController { + async list(req: Request, res: Response, next: NextFunction): Promise { + try { + const limit = Number(req.query['limit'] ?? 10); + const examples = await ExamplesService.list(limit); + res.json(examples); + } catch (err) { + next(err); + } + } + + async get(req: Request, res: Response, next: NextFunction): Promise { + try { + const example = await ExamplesService.get(req.params['id']!); + if (!example) { + res.status(404).json({ message: `Example ${req.params['id']} not found` }); + return; + } + res.json(example); + } catch (err) { + next(err); + } + } + + async create(req: Request, res: Response, next: NextFunction): Promise { + try { + const example = await ExamplesService.create(req.body); + res.status(201).json(example); + } catch (err) { + next(err); + } + } + + async update(req: Request, res: Response, next: NextFunction): Promise { + try { + const example = await ExamplesService.update(req.params['id']!, req.body); + res.json(example); + } catch (err) { + next(err); + } + } + + async delete(req: Request, res: Response, next: NextFunction): Promise { + try { + await ExamplesService.delete(req.params['id']!); + res.status(204).send(); + } catch (err) { + next(err); + } + } +} + +export default new ExamplesController(); +``` + +### `server/api/services/examples.service.ts` + +```typescript +import { randomUUID } from 'crypto'; + +export interface Example { + id: string; + name: string; + description?: string; + createdAt: string; +} + +interface CreateRequest { + name: string; + description?: string; +} + +// In-memory store (replace with a database in production) +const store = new Map(); + +const ExamplesService = { + async list(limit: number): Promise { + return Array.from(store.values()).slice(0, limit); + }, + + async get(id: string): Promise { + return store.get(id); + }, + + async create(data: CreateRequest): Promise { + const example: Example = { + id: randomUUID(), + name: data.name, + description: data.description, + createdAt: new Date().toISOString(), + }; + store.set(example.id, example); + return example; + }, + + async update(id: string, data: Partial): Promise { + const existing = store.get(id); + if (!existing) return undefined; + const updated = { ...existing, ...data }; + store.set(id, updated); + return updated; + }, + + async delete(id: string): Promise { + store.delete(id); + }, +}; + +export default ExamplesService; +``` + +### `server/api/middlewares/error.handler.ts` + +```typescript +import { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; + +interface ApiError { + status?: number; + message: string; + errors?: unknown[]; +} + +export const errorHandler: ErrorRequestHandler = ( + err: ApiError, + _req: Request, + res: Response, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _next: NextFunction +): void => { + const status = err.status ?? 500; + const message = err.message ?? 'Internal Server Error'; + + if (process.env.NODE_ENV !== 'production') { + console.error(`[${status}] ${message}`, err.errors ?? ''); + } + + res.status(status).json({ + message, + ...(err.errors ? { errors: err.errors } : {}), + }); +}; +``` + +## Getting Started + +```bash +# 1. Create project directory and initialise +mkdir my-api && cd my-api +npm init -y + +# 2. Copy all project files (see structure above) + +# 3. Install dependencies +npm install + +# 4. Copy .env.example to .env and configure +cp .env .env.local + +# 5. Start in development mode (hot-reload via nodemon) +npm run dev + +# 6. Browse the Swagger UI +open http://localhost:3000/api-explorer + +# 7. Build for production +npm run build + +# 8. Start in production mode +npm start +``` + +## Features + +- OpenAPI 3.0 spec-first development — define once, validate automatically +- `express-openapi-validator` rejects invalid requests before they hit controller code +- Response validation in non-production environments catches API contract drift +- Swagger UI served at `/api-explorer` for interactive API exploration +- Helmet security headers enabled by default +- Morgan structured request logging (dev format locally, combined in production) +- Centralised error handler normalises all errors to a consistent JSON shape +- Nodemon with `ts-node` for zero-build development hot-reload +- Controller/Service/Router separation for clean, testable architecture diff --git a/skills/typescript-coder/assets/typescript-gulp-angular.md b/skills/typescript-coder/assets/typescript-gulp-angular.md new file mode 100644 index 000000000..c5ccbb837 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-gulp-angular.md @@ -0,0 +1,373 @@ +# TypeScript Gulp + Angular Project Template (Modernized) + +> A modernized TypeScript project starter inspired by `generator-gulp-angular`. The original generator targeted Angular 1.x (AngularJS) with Gulp 3; this template updates the approach for Angular 17+ and TypeScript 5.x while preserving the Gulp task-runner philosophy for builds, serving, and asset pipelines. + +## License + +MIT License — See source repository for full license terms. + +> Note: The original `swiip/generator-gulp-angular` generator is no longer actively maintained and targeted AngularJS (Angular 1.x). This template modernises its patterns for current Angular and TypeScript. + +## Source + +- [swiip/generator-gulp-angular](https://github.com/swiip/generator-gulp-angular) (original, AngularJS era) + +## Project Structure + +``` +my-gulp-angular-app/ +├── src/ +│ ├── app/ +│ │ ├── components/ +│ │ │ └── hero-card/ +│ │ │ ├── hero-card.component.ts +│ │ │ ├── hero-card.component.html +│ │ │ └── hero-card.component.css +│ │ ├── services/ +│ │ │ └── hero.service.ts +│ │ ├── models/ +│ │ │ └── hero.model.ts +│ │ ├── app.component.ts +│ │ ├── app.component.html +│ │ ├── app.module.ts +│ │ └── app-routing.module.ts +│ ├── assets/ +│ │ └── images/ +│ ├── environments/ +│ │ ├── environment.ts +│ │ └── environment.prod.ts +│ ├── styles/ +│ │ ├── _variables.css +│ │ └── main.css +│ ├── index.html +│ └── main.ts +├── tests/ +│ └── hero-card.component.spec.ts +├── dist/ ← Gulp build output +├── .eslintrc.json +├── .gitignore +├── gulpfile.ts +├── karma.conf.js +├── package.json +└── tsconfig.json +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-gulp-angular-app", + "version": "1.0.0", + "description": "Angular 17 + TypeScript application with Gulp build pipeline", + "scripts": { + "start": "gulp serve", + "build": "gulp build", + "build:prod": "gulp build --env=production", + "test": "gulp test", + "lint": "eslint src --ext .ts", + "clean": "gulp clean" + }, + "dependencies": { + "@angular/animations": "^17.0.0", + "@angular/common": "^17.0.0", + "@angular/compiler": "^17.0.0", + "@angular/core": "^17.0.0", + "@angular/forms": "^17.0.0", + "@angular/platform-browser": "^17.0.0", + "@angular/platform-browser-dynamic": "^17.0.0", + "@angular/router": "^17.0.0", + "rxjs": "^7.8.1", + "tslib": "^2.6.2", + "zone.js": "^0.14.2" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.0.0", + "@angular/cli": "^17.0.0", + "@angular/compiler-cli": "^17.0.0", + "@types/jasmine": "^5.1.1", + "@types/node": "^20.8.10", + "@typescript-eslint/eslint-plugin": "^6.9.1", + "@typescript-eslint/parser": "^6.9.1", + "browser-sync": "^3.0.2", + "del": "^7.1.0", + "eslint": "^8.52.0", + "eslint-plugin-angular": "^4.1.0", + "fancy-log": "^2.0.0", + "gulp": "^5.0.0", + "gulp-clean-css": "^4.3.0", + "gulp-concat": "^2.6.0", + "gulp-htmlmin": "^5.0.1", + "gulp-if": "^3.0.0", + "gulp-rev": "^10.0.0", + "gulp-sourcemaps": "^3.0.0", + "gulp-typescript": "^6.0.0-alpha.1", + "gulp-uglify": "^3.0.2", + "jasmine-core": "^5.1.1", + "karma": "^6.4.2", + "karma-chrome-launcher": "^3.2.0", + "karma-jasmine": "^5.1.0", + "typescript": "^5.2.2" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "useDefineForClassFields": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": true, + "declaration": false, + "outDir": "dist/app", + "baseUrl": "src" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} +``` + +### `gulpfile.ts` + +```typescript +import gulp from "gulp"; +import ts from "gulp-typescript"; +import cleanCSS from "gulp-clean-css"; +import htmlmin from "gulp-htmlmin"; +import sourcemaps from "gulp-sourcemaps"; +import browserSync from "browser-sync"; +import { deleteAsync } from "del"; + +const bs = browserSync.create(); +const tsProject = ts.createProject("tsconfig.json"); + +const paths = { + ts: "src/**/*.ts", + html: ["src/**/*.html", "src/index.html"], + css: "src/**/*.css", + assets: "src/assets/**/*", + dist: "dist/", +}; + +// --- Clean --- +export async function clean(): Promise { + await deleteAsync([paths.dist]); +} + +// --- TypeScript --- +export function scripts(): NodeJS.ReadWriteStream { + return gulp + .src(paths.ts) + .pipe(sourcemaps.init()) + .pipe(tsProject()) + .js.pipe(sourcemaps.write(".")) + .pipe(gulp.dest(paths.dist)) + .pipe(bs.stream()); +} + +// --- CSS --- +export function styles(): NodeJS.ReadWriteStream { + return gulp + .src(paths.css) + .pipe(sourcemaps.init()) + .pipe(cleanCSS({ compatibility: "ie11" })) + .pipe(sourcemaps.write(".")) + .pipe(gulp.dest(paths.dist + "styles/")) + .pipe(bs.stream()); +} + +// --- HTML --- +export function templates(): NodeJS.ReadWriteStream { + return gulp + .src(paths.html) + .pipe(htmlmin({ collapseWhitespace: true, removeComments: true })) + .pipe(gulp.dest(paths.dist)) + .pipe(bs.stream()); +} + +// --- Assets --- +export function assets(): NodeJS.ReadWriteStream { + return gulp.src(paths.assets).pipe(gulp.dest(paths.dist + "assets/")); +} + +// --- Dev server --- +export function serve(done: () => void): void { + bs.init({ server: { baseDir: paths.dist }, port: 4200, open: false }); + done(); +} + +// --- Watch --- +export function watchFiles(): void { + gulp.watch(paths.ts, scripts); + gulp.watch(paths.css, styles); + gulp.watch(paths.html, templates); + gulp.watch(paths.assets, assets); +} + +// --- Composite tasks --- +export const build = gulp.series( + clean, + gulp.parallel(scripts, styles, templates, assets) +); + +export default gulp.series( + build, + serve, + watchFiles +); +``` + +### `src/app/models/hero.model.ts` + +```typescript +export interface Hero { + id: number; + name: string; + power: string; + alterEgo?: string; +} +``` + +### `src/app/services/hero.service.ts` + +```typescript +import { Injectable } from "@angular/core"; +import { Observable, of } from "rxjs"; +import { Hero } from "../models/hero.model"; + +@Injectable({ + providedIn: "root", +}) +export class HeroService { + private heroes: Hero[] = [ + { id: 1, name: "Windstorm", power: "Meteorology" }, + { id: 2, name: "Bombasto", power: "Super Strength", alterEgo: "Bob" }, + { id: 3, name: "Magneta", power: "Magnetism" }, + ]; + + getHeroes(): Observable { + return of(this.heroes); + } + + getHero(id: number): Observable { + return of(this.heroes.find((h) => h.id === id)); + } +} +``` + +### `src/app/components/hero-card/hero-card.component.ts` + +```typescript +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Hero } from "../../models/hero.model"; + +@Component({ + selector: "app-hero-card", + standalone: true, + imports: [CommonModule], + templateUrl: "./hero-card.component.html", + styleUrls: ["./hero-card.component.css"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HeroCardComponent { + @Input({ required: true }) hero!: Hero; + @Output() selected = new EventEmitter(); + + onSelect(): void { + this.selected.emit(this.hero); + } +} +``` + +### `src/app/components/hero-card/hero-card.component.html` + +```html +
    +

    {{ hero.name }}

    +

    Power: {{ hero.power }}

    +

    Alter Ego: {{ hero.alterEgo }}

    +
    +``` + +### `src/app/app.module.ts` + +```typescript +import { NgModule } from "@angular/core"; +import { BrowserModule } from "@angular/platform-browser"; +import { AppRoutingModule } from "./app-routing.module"; +import { AppComponent } from "./app.component"; + +@NgModule({ + declarations: [AppComponent], + imports: [BrowserModule, AppRoutingModule], + providers: [], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +### `src/environments/environment.ts` + +```typescript +export const environment = { + production: false, + apiUrl: "http://localhost:3000/api", +}; +``` + +### `src/environments/environment.prod.ts` + +```typescript +export const environment = { + production: true, + apiUrl: "https://api.my-app.com", +}; +``` + +## Getting Started + +1. Install dependencies: + ```bash + npm install + ``` +2. Start the development server with Gulp (compiles TS, serves with BrowserSync, watches for changes): + ```bash + npm start + ``` + The app will be available at `http://localhost:4200`. +3. Build for production: + ```bash + npm run build:prod + ``` +4. Run tests with Karma/Jasmine: + ```bash + npm test + ``` + +## Features + +- Angular 17 with standalone components and `ChangeDetectionStrategy.OnPush` +- TypeScript 5.x with strict mode and Angular decorator support +- Gulp 5 build pipeline replacing the Angular CLI build tooling +- BrowserSync for live-reloading development server +- CSS minification via `gulp-clean-css` +- HTML minification via `gulp-htmlmin` +- TypeScript compilation via `gulp-typescript` +- Source maps in development for debuggable compiled output +- Environment-specific configuration files (`environment.ts` / `environment.prod.ts`) +- Modular component structure with standalone component pattern +- RxJS 7 Observable-based service layer diff --git a/skills/typescript-coder/assets/typescript-kodly-react.md b/skills/typescript-coder/assets/typescript-kodly-react.md new file mode 100644 index 000000000..12ed4d4ea --- /dev/null +++ b/skills/typescript-coder/assets/typescript-kodly-react.md @@ -0,0 +1,434 @@ +# TypeScript React App (Kodly) + +> A TypeScript React application starter with React Router, a component-per-feature folder layout, CSS Modules styling, and Vitest for unit testing. Produces a Vite-powered SPA ready for modern React development. + +## License + +See [source repository](https://github.com/thepanther-io/kodly-react-yo-generator) for license terms. + +## Source + +- [thepanther-io/kodly-react-yo-generator](https://github.com/thepanther-io/kodly-react-yo-generator) + +## Project Structure + +``` +my-react-app/ +├── src/ +│ ├── app/ +│ │ ├── App.tsx +│ │ ├── App.module.css +│ │ └── router.tsx +│ ├── components/ +│ │ └── shared/ +│ │ ├── Button/ +│ │ │ ├── Button.tsx +│ │ │ ├── Button.module.css +│ │ │ └── Button.test.tsx +│ │ └── index.ts +│ ├── features/ +│ │ ├── home/ +│ │ │ ├── HomePage.tsx +│ │ │ └── HomePage.module.css +│ │ └── about/ +│ │ └── AboutPage.tsx +│ ├── hooks/ +│ │ └── useLocalStorage.ts +│ ├── types/ +│ │ └── global.d.ts +│ ├── main.tsx +│ └── vite-env.d.ts +├── public/ +│ └── favicon.svg +├── index.html +├── package.json +├── tsconfig.json +├── tsconfig.node.json +├── vite.config.ts +├── vitest.config.ts +├── .gitignore +└── README.md +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-react-app", + "version": "0.1.0", + "description": "TypeScript React application", + "license": "MIT", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint src --ext .ts,.tsx", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-router-dom": "^6.23.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.4.0", + "@testing-library/react": "^15.0.0", + "@testing-library/user-event": "^14.5.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "@vitejs/plugin-react": "^4.2.0", + "@vitest/coverage-v8": "^1.0.0", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.0", + "jsdom": "^24.0.0", + "typescript": "^5.4.0", + "vite": "^5.2.0", + "vitest": "^1.0.0" + }, + "engines": { + "node": ">=20.0.0" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} +``` + +### `tsconfig.node.json` + +```json +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts", "vitest.config.ts"] +} +``` + +### `vite.config.ts` + +```typescript +import react from '@vitejs/plugin-react'; +import path from 'path'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + port: 3000, + open: true, + }, + build: { + outDir: 'dist', + sourcemap: true, + rollupOptions: { + output: { + manualChunks: { + vendor: ['react', 'react-dom', 'react-router-dom'], + }, + }, + }, + }, +}); +``` + +### `vitest.config.ts` + +```typescript +import react from '@vitejs/plugin-react'; +import path from 'path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + test: { + environment: 'jsdom', + setupFiles: ['./src/test-setup.ts'], + include: ['src/**/*.test.{ts,tsx}'], + coverage: { + provider: 'v8', + reporter: ['text', 'lcov', 'html'], + include: ['src/**/*.{ts,tsx}'], + exclude: ['src/**/*.test.*', 'src/main.tsx', 'src/vite-env.d.ts'], + }, + }, +}); +``` + +### `src/main.tsx` + +```tsx +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { RouterProvider } from 'react-router-dom'; +import { router } from './app/router'; +import './index.css'; + +const root = document.getElementById('root'); +if (!root) throw new Error('Root element not found'); + +ReactDOM.createRoot(root).render( + + + +); +``` + +### `src/app/router.tsx` + +```tsx +import React from 'react'; +import { createBrowserRouter } from 'react-router-dom'; +import { App } from './App'; +import { AboutPage } from '@/features/about/AboutPage'; +import { HomePage } from '@/features/home/HomePage'; + +export const router = createBrowserRouter([ + { + path: '/', + element: , + children: [ + { index: true, element: }, + { path: 'about', element: }, + ], + }, +]); +``` + +### `src/app/App.tsx` + +```tsx +import React from 'react'; +import { Link, Outlet } from 'react-router-dom'; +import styles from './App.module.css'; + +export function App(): JSX.Element { + return ( +
    +
    + +
    +
    + +
    +
    +

    © {new Date().getFullYear()} My App

    +
    +
    + ); +} +``` + +### `src/features/home/HomePage.tsx` + +```tsx +import React from 'react'; +import { Button } from '@/components/shared'; +import styles from './HomePage.module.css'; + +export function HomePage(): JSX.Element { + const [count, setCount] = React.useState(0); + + return ( +
    +

    Welcome

    +

    Count: {count}

    + + +
    + ); +} +``` + +### `src/components/shared/Button/Button.tsx` + +```tsx +import React, { ButtonHTMLAttributes } from 'react'; +import styles from './Button.module.css'; + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'danger'; +} + +export function Button({ variant = 'primary', className = '', children, ...rest }: ButtonProps): JSX.Element { + return ( + + ); +} +``` + +### `src/components/shared/Button/Button.test.tsx` + +```tsx +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { Button } from './Button'; + +describe('Button', () => { + it('renders with children', () => { + render(); + expect(screen.getByRole('button', { name: 'Click' })).toBeInTheDocument(); + }); + + it('calls onClick when clicked', async () => { + const handleClick = vi.fn(); + render(); + await userEvent.click(screen.getByRole('button')); + expect(handleClick).toHaveBeenCalledOnce(); + }); + + it('applies the secondary class when variant is secondary', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('secondary'); + }); +}); +``` + +### `src/components/shared/index.ts` + +```typescript +export { Button } from './Button/Button'; +export type { ButtonProps } from './Button/Button'; +``` + +### `src/hooks/useLocalStorage.ts` + +```typescript +import { useCallback, useState } from 'react'; + +export function useLocalStorage( + key: string, + initialValue: T +): [T, (value: T | ((prev: T) => T)) => void] { + const [storedValue, setStoredValue] = useState(() => { + try { + const item = window.localStorage.getItem(key); + return item !== null ? (JSON.parse(item) as T) : initialValue; + } catch { + return initialValue; + } + }); + + const setValue = useCallback( + (value: T | ((prev: T) => T)) => { + setStoredValue((prev) => { + const next = value instanceof Function ? value(prev) : value; + try { + window.localStorage.setItem(key, JSON.stringify(next)); + } catch { + console.warn(`useLocalStorage: failed to persist key "${key}"`); + } + return next; + }); + }, + [key] + ); + + return [storedValue, setValue]; +} +``` + +## Getting Started + +```bash +# Prerequisites: Node.js 20+ + +# 1. Create project directory +mkdir my-react-app && cd my-react-app + +# 2. Copy project files (see structure above) + +# 3. Install dependencies +npm install + +# 4. Start the development server (http://localhost:3000) +npm run dev + +# 5. Run unit tests +npm test + +# 6. Build for production +npm run build + +# 7. Preview the production build locally +npm run preview +``` + +## Features + +- Vite for near-instant dev server startup and hot module replacement +- React Router v6 with `createBrowserRouter` and nested layouts +- CSS Modules for scoped, collision-free component styles +- Path alias `@/` mapped to `src/` for cleaner imports +- Vitest with `jsdom` environment for component testing (uses same Vite config) +- React Testing Library + `@testing-library/user-event` for user-interaction tests +- Vendor chunk splitting in production build for better caching +- Custom `useLocalStorage` hook as an example of composable, typed custom hooks +- Strict TypeScript with `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess` diff --git a/skills/typescript-coder/assets/typescript-lit-element.md b/skills/typescript-coder/assets/typescript-lit-element.md new file mode 100644 index 000000000..3728fdc62 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-lit-element.md @@ -0,0 +1,513 @@ +# TypeScript Lit Web Component Template (lit-element-next) + +> A TypeScript web component project template based on `generator-lit-element-next` by +> motss. Produces a modern Lit 3 web component package with TypeScript decorators, Rollup +> bundling, Web Test Runner browser tests, custom element registration, and a publishable +> NPM package structure following open-wc conventions. + +## License + +MIT License. See [https://github.com/motss/generator-lit-element-next](https://github.com/motss/generator-lit-element-next) for full license terms. + +## Source + +- [generator-lit-element-next](https://github.com/motss/generator-lit-element-next) by motss + +## Project Structure + +``` +my-lit-component/ +├── src/ +│ ├── my-counter.ts # Main component implementation +│ ├── my-counter.styles.ts # Lit css`` tagged template styles +│ ├── types.ts # Shared TypeScript interfaces +│ └── index.ts # Package entry point / re-exports +├── test/ +│ ├── my-counter.test.ts # Web Test Runner specs +│ └── helpers.ts # Test helpers / fixtures +├── custom-elements.json # Custom Elements Manifest (CEM) +├── web-test-runner.config.mjs +├── rollup.config.mjs +├── package.json +├── tsconfig.json +└── .eslintrc.cjs +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-lit-component", + "version": "1.0.0", + "description": "A TypeScript Lit web component", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "customElements": "custom-elements.json", + "files": ["dist", "custom-elements.json"], + "scripts": { + "build": "tsc && rollup -c", + "build:watch": "tsc -w", + "test": "wtr", + "test:watch": "wtr --watch", + "lint": "eslint 'src/**/*.ts' 'test/**/*.ts'", + "format": "prettier --write 'src/**/*.ts'", + "analyze": "cem analyze --litelement", + "clean": "rimraf dist" + }, + "dependencies": { + "lit": "^3.1.2" + }, + "devDependencies": { + "@custom-elements-manifest/analyzer": "^0.9.3", + "@esm-bundle/chai": "^4.3.4-fix.0", + "@open-wc/testing": "^4.0.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-typescript": "^11.1.6", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "@web/test-runner": "^0.18.1", + "@web/test-runner-playwright": "^0.11.0", + "eslint": "^8.56.0", + "eslint-plugin-lit": "^1.11.0", + "eslint-plugin-wc": "^2.1.0", + "prettier": "^3.2.4", + "rimraf": "^5.0.5", + "rollup": "^4.9.5", + "tslib": "^2.6.2", + "typescript": "^5.3.3" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2021", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "experimentalDecorators": true, + "useDefineForClassFields": false + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "test"] +} +``` + +### `src/types.ts` + +```typescript +export interface CounterChangedDetail { + /** Current counter value after the change */ + value: number; + /** Direction of the last change */ + direction: 'increment' | 'decrement' | 'reset'; +} +``` + +### `src/my-counter.styles.ts` + +```typescript +import { css } from 'lit'; + +export const styles = css` + :host { + display: inline-block; + font-family: system-ui, sans-serif; + --my-counter-bg: #f0f4ff; + --my-counter-color: #1a1a2e; + --my-counter-accent: #4361ee; + --my-counter-radius: 8px; + } + + :host([hidden]) { + display: none; + } + + .container { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: var(--my-counter-bg); + border-radius: var(--my-counter-radius); + color: var(--my-counter-color); + } + + button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 2px solid var(--my-counter-accent); + border-radius: 50%; + background: transparent; + color: var(--my-counter-accent); + font-size: 1.25rem; + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease; + } + + button:hover:not([disabled]) { + background: var(--my-counter-accent); + color: #fff; + } + + button[disabled] { + opacity: 0.4; + cursor: not-allowed; + } + + .value { + min-width: 3ch; + text-align: center; + font-size: 1.5rem; + font-weight: 600; + } + + .reset { + font-size: 0.75rem; + border-radius: 4px; + width: auto; + padding: 0 8px; + height: 28px; + } +`; +``` + +### `src/my-counter.ts` + +```typescript +import { LitElement, html } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { styles } from './my-counter.styles'; +import { CounterChangedDetail } from './types'; + +/** + * An accessible counter web component built with Lit and TypeScript. + * + * @fires {CustomEvent} counter-changed - Fired whenever + * the counter value changes. + * + * @csspart container - The outer wrapper element. + * @csspart value - The element displaying the current count. + * + * @cssprop --my-counter-bg - Background colour (default: #f0f4ff). + * @cssprop --my-counter-color - Text colour (default: #1a1a2e). + * @cssprop --my-counter-accent - Accent colour for buttons (default: #4361ee). + * @cssprop --my-counter-radius - Border radius (default: 8px). + * + * @example + * ```html + * + * ``` + */ +@customElement('my-counter') +export class MyCounter extends LitElement { + static override styles = styles; + + /** Starting value for the counter */ + @property({ type: Number, attribute: 'initial-value' }) + initialValue = 0; + + /** Minimum allowed value (inclusive) */ + @property({ type: Number }) + min = -Infinity; + + /** Maximum allowed value (inclusive) */ + @property({ type: Number }) + max = Infinity; + + /** Step size for increment / decrement */ + @property({ type: Number }) + step = 1; + + @state() + private _value = 0; + + override connectedCallback(): void { + super.connectedCallback(); + this._value = this.initialValue; + } + + private _emit(direction: CounterChangedDetail['direction']): void { + this.dispatchEvent( + new CustomEvent('counter-changed', { + detail: { value: this._value, direction }, + bubbles: true, + composed: true, + }), + ); + } + + private _increment(): void { + if (this._value + this.step <= this.max) { + this._value += this.step; + this._emit('increment'); + } + } + + private _decrement(): void { + if (this._value - this.step >= this.min) { + this._value -= this.step; + this._emit('decrement'); + } + } + + private _reset(): void { + this._value = this.initialValue; + this._emit('reset'); + } + + override render() { + const atMin = this._value - this.step < this.min; + const atMax = this._value + this.step > this.max; + + return html` +
    + + + + ${this._value} + + + + + +
    + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'my-counter': MyCounter; + } +} +``` + +### `src/index.ts` + +```typescript +export { MyCounter } from './my-counter'; +export type { CounterChangedDetail } from './types'; +``` + +### `test/my-counter.test.ts` + +```typescript +import { html, fixture, expect } from '@open-wc/testing'; +import { MyCounter } from '../src/my-counter'; + +describe('MyCounter', () => { + it('renders with default value of 0', async () => { + const el = await fixture(html``); + const value = el.shadowRoot!.querySelector('.value'); + expect(value?.textContent?.trim()).to.equal('0'); + }); + + it('respects initial-value attribute', async () => { + const el = await fixture( + html``, + ); + const value = el.shadowRoot!.querySelector('.value'); + expect(value?.textContent?.trim()).to.equal('5'); + }); + + it('increments on + button click', async () => { + const el = await fixture(html``); + const incBtn = el.shadowRoot!.querySelectorAll('button')[1] as HTMLButtonElement; + incBtn.click(); + await el.updateComplete; + const value = el.shadowRoot!.querySelector('.value'); + expect(value?.textContent?.trim()).to.equal('1'); + }); + + it('decrements on - button click', async () => { + const el = await fixture(html``); + const decBtn = el.shadowRoot!.querySelector('button') as HTMLButtonElement; + decBtn.click(); + await el.updateComplete; + const value = el.shadowRoot!.querySelector('.value'); + expect(value?.textContent?.trim()).to.equal('2'); + }); + + it('fires counter-changed event on increment', async () => { + const el = await fixture(html``); + let eventDetail: { value: number; direction: string } | undefined; + el.addEventListener('counter-changed', (e) => { + eventDetail = (e as CustomEvent).detail; + }); + const incBtn = el.shadowRoot!.querySelectorAll('button')[1] as HTMLButtonElement; + incBtn.click(); + await el.updateComplete; + expect(eventDetail).to.deep.equal({ value: 1, direction: 'increment' }); + }); + + it('disables decrement button at min boundary', async () => { + const el = await fixture( + html``, + ); + const decBtn = el.shadowRoot!.querySelector('button') as HTMLButtonElement; + expect(decBtn.disabled).to.be.true; + }); + + it('resets to initial value', async () => { + const el = await fixture( + html``, + ); + const incBtn = el.shadowRoot!.querySelectorAll('button')[1] as HTMLButtonElement; + incBtn.click(); + await el.updateComplete; + const resetBtn = el.shadowRoot!.querySelectorAll('button')[2] as HTMLButtonElement; + resetBtn.click(); + await el.updateComplete; + const value = el.shadowRoot!.querySelector('.value'); + expect(value?.textContent?.trim()).to.equal('5'); + }); + + it('passes accessibility audit', async () => { + const el = await fixture(html``); + await expect(el).to.be.accessible(); + }); +}); +``` + +### `web-test-runner.config.mjs` + +```js +import { playwrightLauncher } from '@web/test-runner-playwright'; + +export default { + files: 'test/**/*.test.ts', + nodeResolve: true, + browsers: [ + playwrightLauncher({ product: 'chromium' }), + ], + plugins: [], + esbuildTarget: 'auto', +}; +``` + +### `rollup.config.mjs` + +```js +import resolve from '@rollup/plugin-node-resolve'; +import typescript from '@rollup/plugin-typescript'; + +export default { + input: 'src/index.ts', + output: { + dir: 'dist', + format: 'esm', + sourcemap: true, + preserveModules: true, + preserveModulesRoot: 'src', + }, + plugins: [ + resolve(), + typescript({ tsconfig: './tsconfig.json' }), + ], + external: ['lit', /^lit\//], +}; +``` + +### Usage in HTML + +```html + + + + + My Lit Counter Demo + + + + + + + + +``` + +## Getting Started + +```bash +# 1. Install dependencies +npm install + +# 2. Build the component +npm run build + +# 3. Run tests (requires Playwright — installed automatically) +npm test + +# 4. Run tests in watch mode during development +npm run test:watch + +# 5. Build in watch mode for live TypeScript compilation +npm run build:watch + +# 6. Generate the Custom Elements Manifest +npm run analyze + +# 7. Lint +npm run lint +``` + +## Features + +- Lit 3 with TypeScript class decorators (`@customElement`, `@property`, `@state`) +- `useDefineForClassFields: false` to ensure Lit decorators work correctly with TypeScript +- Scoped Shadow DOM styles using Lit's `css` tagged template literal +- CSS custom properties (CSS variables) for consumer-side theming +- CSS `part` attributes for structural styling from outside the shadow root +- Accessible ARIA attributes: `aria-live`, `aria-atomic`, descriptive `aria-label` on buttons +- Custom event (`counter-changed`) with typed `CustomEvent` detail +- `HTMLElementTagNameMap` augmentation for correct TypeScript types in consuming projects +- `@open-wc/testing` test utilities with accessibility audit via `axe-core` +- Web Test Runner + Playwright for real-browser testing +- Rollup ESM bundle with `preserveModules` for tree-shakeable output +- Custom Elements Manifest (`custom-elements.json`) for IDE tooling and documentation diff --git a/skills/typescript-coder/assets/typescript-nestjs-boilerplate.md b/skills/typescript-coder/assets/typescript-nestjs-boilerplate.md new file mode 100644 index 000000000..fa5d3a706 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-nestjs-boilerplate.md @@ -0,0 +1,492 @@ +# TypeScript NestJS Boilerplate Template + +> A production-ready NestJS boilerplate built with TypeScript. Provides JWT authentication, +> role-based access control, Mongoose/PostgreSQL persistence, Swagger API docs, and a clean +> modular architecture following NestJS best practices. Based on the Onix-Systems +> `nest-js-boilerplate` project. + +## License + +MIT License. See [https://github.com/Onix-Systems/nest-js-boilerplate](https://github.com/Onix-Systems/nest-js-boilerplate) for full license terms. + +## Source + +- [nest-js-boilerplate](https://github.com/Onix-Systems/nest-js-boilerplate) by Onix-Systems + +## Project Structure + +``` +nest-js-boilerplate/ +├── src/ +│ ├── auth/ +│ │ ├── auth.controller.ts +│ │ ├── auth.module.ts +│ │ ├── auth.service.ts +│ │ ├── dto/ +│ │ │ ├── sign-in.dto.ts +│ │ │ └── sign-up.dto.ts +│ │ └── strategies/ +│ │ ├── jwt.strategy.ts +│ │ └── local.strategy.ts +│ ├── users/ +│ │ ├── users.controller.ts +│ │ ├── users.module.ts +│ │ ├── users.service.ts +│ │ ├── schemas/ +│ │ │ └── users.schema.ts +│ │ └── dto/ +│ │ ├── create-user.dto.ts +│ │ └── update-user.dto.ts +│ ├── common/ +│ │ ├── decorators/ +│ │ │ └── roles.decorator.ts +│ │ ├── guards/ +│ │ │ ├── jwt-auth.guard.ts +│ │ │ └── roles.guard.ts +│ │ ├── interceptors/ +│ │ │ └── transform.interceptor.ts +│ │ └── filters/ +│ │ └── http-exception.filter.ts +│ ├── config/ +│ │ ├── configuration.ts +│ │ └── database.config.ts +│ ├── app.module.ts +│ └── main.ts +├── test/ +│ ├── app.e2e-spec.ts +│ └── jest-e2e.json +├── .env +├── .env.example +├── Dockerfile +├── docker-compose.yml +├── package.json +└── tsconfig.json +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "nest-js-boilerplate", + "version": "1.0.0", + "description": "NestJS TypeScript boilerplate with authentication and persistence", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.1.1", + "@nestjs/mongoose": "^10.0.2", + "@nestjs/passport": "^10.0.2", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.1.12", + "@nestjs/config": "^3.1.1", + "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "mongoose": "^8.0.1", + "passport": "^0.6.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1", + "swagger-ui-express": "^5.0.0" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/bcrypt": "^5.0.2", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.11", + "@types/node": "^20.11.0", + "@types/passport-jwt": "^3.0.13", + "@types/passport-local": "^1.0.38", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.7.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^6.3.4", + "ts-jest": "^29.1.1", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "paths": { + "@/*": ["src/*"] + } + } +} +``` + +### `src/main.ts` + +```typescript +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { AppModule } from './app.module'; +import { HttpExceptionFilter } from './common/filters/http-exception.filter'; +import { TransformInterceptor } from './common/interceptors/transform.interceptor'; + +async function bootstrap(): Promise { + const app = await NestFactory.create(AppModule); + + // Global validation pipe + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + }), + ); + + // Global exception filter + app.useGlobalFilters(new HttpExceptionFilter()); + + // Global response transform interceptor + app.useGlobalInterceptors(new TransformInterceptor()); + + // CORS + app.enableCors(); + + // Swagger documentation + const config = new DocumentBuilder() + .setTitle('NestJS Boilerplate API') + .setDescription('REST API built with NestJS and TypeScript') + .setVersion('1.0') + .addBearerAuth( + { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, + 'access-token', + ) + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api/docs', app, document); + + const port = process.env.PORT ?? 3000; + await app.listen(port); + console.log(`Application running on http://localhost:${port}`); + console.log(`Swagger docs at http://localhost:${port}/api/docs`); +} + +bootstrap(); +``` + +### `src/app.module.ts` + +```typescript +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { MongooseModule } from '@nestjs/mongoose'; +import { AuthModule } from './auth/auth.module'; +import { UsersModule } from './users/users.module'; +import configuration from './config/configuration'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [configuration], + }), + MongooseModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (configService: ConfigService) => ({ + uri: configService.get('database.uri'), + }), + inject: [ConfigService], + }), + AuthModule, + UsersModule, + ], +}) +export class AppModule {} +``` + +### `src/auth/auth.controller.ts` + +```typescript +import { + Controller, + Post, + Body, + HttpCode, + HttpStatus, + UseGuards, + Get, + Request, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { AuthService } from './auth.service'; +import { SignInDto } from './dto/sign-in.dto'; +import { SignUpDto } from './dto/sign-up.dto'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; + +@ApiTags('auth') +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Post('sign-up') + @ApiOperation({ summary: 'Register a new user' }) + @ApiResponse({ status: 201, description: 'User created successfully.' }) + @ApiResponse({ status: 409, description: 'Email already in use.' }) + async signUp(@Body() signUpDto: SignUpDto) { + return this.authService.signUp(signUpDto); + } + + @Post('sign-in') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Authenticate a user and return a JWT' }) + @ApiResponse({ status: 200, description: 'Authentication successful.' }) + @ApiResponse({ status: 401, description: 'Invalid credentials.' }) + async signIn(@Body() signInDto: SignInDto) { + return this.authService.signIn(signInDto); + } + + @Get('profile') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth('access-token') + @ApiOperation({ summary: 'Get current user profile' }) + @ApiResponse({ status: 200, description: 'User profile returned.' }) + getProfile(@Request() req: any) { + return req.user; + } +} +``` + +### `src/auth/auth.service.ts` + +```typescript +import { + Injectable, + ConflictException, + UnauthorizedException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import * as bcrypt from 'bcrypt'; +import { UsersService } from '../users/users.service'; +import { SignUpDto } from './dto/sign-up.dto'; +import { SignInDto } from './dto/sign-in.dto'; + +@Injectable() +export class AuthService { + constructor( + private readonly usersService: UsersService, + private readonly jwtService: JwtService, + ) {} + + async signUp(signUpDto: SignUpDto) { + const existing = await this.usersService.findByEmail(signUpDto.email); + if (existing) { + throw new ConflictException('Email already registered'); + } + const hashedPassword = await bcrypt.hash(signUpDto.password, 10); + const user = await this.usersService.create({ + ...signUpDto, + password: hashedPassword, + }); + const { password: _pw, ...result } = user.toObject(); + return result; + } + + async signIn(signInDto: SignInDto) { + const user = await this.usersService.findByEmail(signInDto.email); + if (!user) { + throw new UnauthorizedException('Invalid credentials'); + } + const isMatch = await bcrypt.compare(signInDto.password, user.password); + if (!isMatch) { + throw new UnauthorizedException('Invalid credentials'); + } + const payload = { sub: user._id, email: user.email, roles: user.roles }; + return { + accessToken: this.jwtService.sign(payload), + }; + } +} +``` + +### `src/auth/dto/sign-up.dto.ts` + +```typescript +import { IsEmail, IsString, MinLength, IsOptional, IsArray } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class SignUpDto { + @ApiProperty({ example: 'Jane Doe' }) + @IsString() + readonly name: string; + + @ApiProperty({ example: 'jane@example.com' }) + @IsEmail() + readonly email: string; + + @ApiProperty({ example: 'strongPassword123', minLength: 8 }) + @IsString() + @MinLength(8) + readonly password: string; + + @ApiPropertyOptional({ example: ['user'] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + readonly roles?: string[]; +} +``` + +### `src/users/schemas/users.schema.ts` + +```typescript +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document, HydratedDocument } from 'mongoose'; + +export type UserDocument = HydratedDocument; + +@Schema({ timestamps: true }) +export class User extends Document { + @Prop({ required: true }) + name: string; + + @Prop({ required: true, unique: true, lowercase: true }) + email: string; + + @Prop({ required: true }) + password: string; + + @Prop({ type: [String], default: ['user'] }) + roles: string[]; + + @Prop({ default: true }) + isActive: boolean; +} + +export const UserSchema = SchemaFactory.createForClass(User); +``` + +### `src/common/guards/jwt-auth.guard.ts` + +```typescript +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} +``` + +### `src/common/decorators/roles.decorator.ts` + +```typescript +import { SetMetadata } from '@nestjs/common'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); +``` + +### `src/config/configuration.ts` + +```typescript +export default () => ({ + port: parseInt(process.env.PORT ?? '3000', 10), + database: { + uri: process.env.MONGODB_URI ?? 'mongodb://localhost:27017/nestjs-boilerplate', + }, + jwt: { + secret: process.env.JWT_SECRET ?? 'super-secret-change-in-production', + expiresIn: process.env.JWT_EXPIRES_IN ?? '7d', + }, +}); +``` + +### `.env.example` + +``` +PORT=3000 +MONGODB_URI=mongodb://localhost:27017/nestjs-boilerplate +JWT_SECRET=change-this-to-a-long-random-secret +JWT_EXPIRES_IN=7d +``` + +## Getting Started + +```bash +# 1. Install dependencies +npm install + +# 2. Copy and configure environment variables +cp .env.example .env + +# 3. Start MongoDB (or use Docker) +docker-compose up -d mongo + +# 4. Run in development mode (with hot reload) +npm run start:dev + +# 5. Open Swagger docs +# http://localhost:3000/api/docs + +# 6. Run tests +npm test +npm run test:e2e + +# 7. Build for production +npm run build +npm run start:prod +``` + +## Features + +- Modular NestJS architecture with feature-based folder structure +- JWT authentication with Passport.js (local and JWT strategies) +- Role-based access control via custom guards and decorators +- MongoDB persistence with Mongoose and typed schemas +- Class-validator DTO validation with whitelist enforcement +- Swagger/OpenAPI documentation auto-generated from decorators +- Global HTTP exception filter with consistent error response shape +- Global response transform interceptor +- Docker Compose setup for local MongoDB +- Unit and end-to-end tests with Jest and Supertest +- Environment configuration via `@nestjs/config` with typed access diff --git a/skills/typescript-coder/assets/typescript-ngx-rocket.md b/skills/typescript-coder/assets/typescript-ngx-rocket.md new file mode 100644 index 000000000..ec35a1b07 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-ngx-rocket.md @@ -0,0 +1,436 @@ +# TypeScript Angular Enterprise Template (ngx-rocket) + +> An enterprise-grade Angular TypeScript application scaffold generated by +> `generator-ngx-rocket`. Includes authentication, internationalization (i18n), lazy-loaded +> feature modules, environment-based configuration, SCSS theming, and a comprehensive +> testing setup with Jest and Cypress. + +## License + +MIT License. See [https://github.com/ngx-rocket/generator-ngx-rocket](https://github.com/ngx-rocket/generator-ngx-rocket) for full license terms. + +## Source + +- [generator-ngx-rocket](https://github.com/ngx-rocket/generator-ngx-rocket) by ngx-rocket + +## Project Structure + +``` +my-ngx-app/ +├── src/ +│ ├── app/ +│ │ ├── core/ +│ │ │ ├── authentication/ +│ │ │ │ ├── authentication.service.ts +│ │ │ │ └── credentials.service.ts +│ │ │ ├── http/ +│ │ │ │ ├── api-prefix.interceptor.ts +│ │ │ │ └── error-handler.interceptor.ts +│ │ │ ├── i18n/ +│ │ │ │ └── i18n.module.ts +│ │ │ ├── shell/ +│ │ │ │ ├── shell.component.ts +│ │ │ │ ├── shell.component.html +│ │ │ │ └── shell-routing.module.ts +│ │ │ └── core.module.ts +│ │ ├── shared/ +│ │ │ ├── shared.module.ts +│ │ │ └── loader/ +│ │ │ ├── loader.component.ts +│ │ │ └── loader.component.html +│ │ ├── home/ +│ │ │ ├── home.component.ts +│ │ │ ├── home.component.html +│ │ │ ├── home.component.scss +│ │ │ ├── home.module.ts +│ │ │ └── home-routing.module.ts +│ │ ├── login/ +│ │ │ ├── login.component.ts +│ │ │ ├── login.component.html +│ │ │ ├── login.module.ts +│ │ │ └── login-routing.module.ts +│ │ ├── app.component.ts +│ │ ├── app.component.html +│ │ ├── app.module.ts +│ │ └── app-routing.module.ts +│ ├── assets/ +│ │ ├── i18n/ +│ │ │ ├── en-US.json +│ │ │ └── fr-FR.json +│ │ └── images/ +│ ├── environments/ +│ │ ├── environment.ts +│ │ └── environment.prod.ts +│ ├── styles.scss +│ ├── index.html +│ └── main.ts +├── e2e/ +│ └── src/ +│ └── app.e2e-spec.ts +├── angular.json +├── package.json +├── tsconfig.json +├── tsconfig.app.json +├── tsconfig.spec.json +└── jest.config.js +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-ngx-app", + "version": "0.0.1", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "build:prod": "ng build --configuration production", + "test": "jest", + "test:watch": "jest --watch", + "test:ci": "jest --runInBand --no-coverage", + "e2e": "ng e2e", + "lint": "ng lint", + "extract-i18n": "ngx-translate-extract --input ./src --output ./src/assets/i18n --clean --sort --format namespaced-json", + "format": "prettier --write \"src/**/*.{ts,html,scss}\"" + }, + "dependencies": { + "@angular/animations": "^17.0.0", + "@angular/common": "^17.0.0", + "@angular/compiler": "^17.0.0", + "@angular/core": "^17.0.0", + "@angular/forms": "^17.0.0", + "@angular/platform-browser": "^17.0.0", + "@angular/platform-browser-dynamic": "^17.0.0", + "@angular/router": "^17.0.0", + "@ngx-translate/core": "^15.0.0", + "@ngx-translate/http-loader": "^8.0.0", + "rxjs": "~7.8.0", + "tslib": "^2.6.0", + "zone.js": "~0.14.2" + }, + "devDependencies": { + "@angular-builders/jest": "^17.0.0", + "@angular-devkit/build-angular": "^17.0.0", + "@angular/cli": "^17.0.0", + "@angular/compiler-cli": "^17.0.0", + "@types/jest": "^29.5.11", + "@types/node": "^20.11.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "prettier": "^3.1.1", + "ts-jest": "^29.1.4", + "typescript": "~5.2.2" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"], + "paths": { + "@app/*": ["src/app/*"], + "@env/*": ["src/environments/*"] + } + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} +``` + +### `tsconfig.app.json` + +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} +``` + +### `src/environments/environment.ts` + +```typescript +export const environment = { + production: false, + serverUrl: 'http://localhost:3000', + defaultLanguage: 'en-US', + supportedLanguages: ['en-US', 'fr-FR'], +}; +``` + +### `src/environments/environment.prod.ts` + +```typescript +export const environment = { + production: true, + serverUrl: '/api', + defaultLanguage: 'en-US', + supportedLanguages: ['en-US', 'fr-FR'], +}; +``` + +### `src/app/app-routing.module.ts` + +```typescript +import { NgModule } from '@angular/core'; +import { Routes, RouterModule, PreloadAllModules } from '@angular/router'; +import { ShellComponent } from './core/shell/shell.component'; +import { AuthGuard } from './core/authentication/auth.guard'; + +const routes: Routes = [ + { path: 'login', loadChildren: () => import('./login/login.module').then((m) => m.LoginModule) }, + { + path: '', + component: ShellComponent, + canActivate: [AuthGuard], + children: [ + { + path: 'home', + loadChildren: () => import('./home/home.module').then((m) => m.HomeModule), + data: { title: 'Home' }, + }, + { path: '', redirectTo: 'home', pathMatch: 'full' }, + ], + }, + { path: '**', redirectTo: '', pathMatch: 'full' }, +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })], + exports: [RouterModule], + providers: [], +}) +export class AppRoutingModule {} +``` + +### `src/app/core/authentication/authentication.service.ts` + +```typescript +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, of } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; +import { CredentialsService } from './credentials.service'; + +export interface LoginContext { + username: string; + password: string; + remember?: boolean; +} + +export interface LoginResponse { + token: string; + username: string; +} + +@Injectable({ providedIn: 'root' }) +export class AuthenticationService { + constructor( + private httpClient: HttpClient, + private credentialsService: CredentialsService, + ) {} + + login(context: LoginContext): Observable { + return this.httpClient.post('/auth/sign-in', context).pipe( + map((response) => { + this.credentialsService.setCredentials( + { username: response.username, token: response.token }, + context.remember, + ); + return response; + }), + ); + } + + logout(): Observable { + this.credentialsService.setCredentials(); + return of(true); + } +} +``` + +### `src/app/home/home.component.ts` + +```typescript +import { Component, OnInit } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CredentialsService } from '../core/authentication/credentials.service'; + +export interface Quote { + text: string; + author: string; +} + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'], +}) +export class HomeComponent implements OnInit { + quote: Quote | undefined; + isLoading = true; + currentUser: string | null | undefined; + + constructor( + private translate: TranslateService, + private credentialsService: CredentialsService, + ) {} + + ngOnInit(): void { + this.currentUser = this.credentialsService.credentials?.username; + this.loadQuote(); + } + + private loadQuote(): void { + // Simulate async data loading + setTimeout(() => { + this.quote = { + text: 'First, solve the problem. Then, write the code.', + author: 'John Johnson', + }; + this.isLoading = false; + }, 500); + } + + setLanguage(language: string): void { + this.translate.use(language); + } +} +``` + +### `src/app/core/http/api-prefix.interceptor.ts` + +```typescript +import { Injectable } from '@angular/core'; +import { + HttpRequest, + HttpHandler, + HttpEvent, + HttpInterceptor, +} from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { environment } from '@env/environment'; + +@Injectable() +export class ApiPrefixInterceptor implements HttpInterceptor { + intercept(request: HttpRequest, next: HttpHandler): Observable> { + // Do not prefix requests to external domains + if (!/^(http|https):/i.test(request.url)) { + request = request.clone({ url: environment.serverUrl + request.url }); + } + return next.handle(request); + } +} +``` + +### `src/assets/i18n/en-US.json` + +```json +{ + "APP_NAME": "My Angular App", + "HOME": { + "TITLE": "Home", + "WELCOME": "Welcome, {{username}}!", + "QUOTE_TITLE": "Quote of the Day" + }, + "LOGIN": { + "TITLE": "Sign In", + "USERNAME": "Username", + "PASSWORD": "Password", + "REMEMBER_ME": "Remember me", + "BUTTON": "Sign In", + "ERROR": "Incorrect username or password." + }, + "LOGOUT": "Sign Out" +} +``` + +### `jest.config.js` + +```js +module.exports = { + preset: 'jest-preset-angular', + setupFilesAfterFramework: ['/setup-jest.ts'], + globalSetup: 'jest-preset-angular/global-setup', + testPathIgnorePatterns: ['/node_modules/', '/dist/'], + coverageDirectory: 'coverage', + coverageReporters: ['html', 'text'], + collectCoverageFrom: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/environments/**'], +}; +``` + +## Getting Started + +```bash +# 1. Install the Angular CLI +npm install -g @angular/cli + +# 2. Install dependencies +npm install + +# 3. Start the development server +npm start +# Open http://localhost:4200 + +# 4. Run unit tests +npm test + +# 5. Run end-to-end tests +npm run e2e + +# 6. Build for production +npm run build:prod + +# 7. Extract i18n strings +npm run extract-i18n +``` + +## Features + +- Angular 17 with standalone components support and lazy-loaded feature modules +- JWT-based authentication with credential persistence (localStorage / sessionStorage) +- HTTP interceptors: API prefix injection and global error handling +- Internationalization via `@ngx-translate/core` with JSON translation files +- Path aliases: `@app/*` for application code, `@env/*` for environment files +- SCSS global theming with component-level style encapsulation +- Shell/layout pattern separating authenticated routes from public routes +- Route guards protecting authenticated areas +- Jest unit tests with `jest-preset-angular` +- Strict TypeScript and strict Angular template checking enabled +- Prettier code formatting diff --git a/skills/typescript-coder/assets/typescript-node-boilerplate.md b/skills/typescript-coder/assets/typescript-node-boilerplate.md new file mode 100644 index 000000000..241ded8e3 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-node-boilerplate.md @@ -0,0 +1,332 @@ + + +# TypeScript Node.js Boilerplate + +> Based on [jsynowiec/node-typescript-boilerplate](https://github.com/jsynowiec/node-typescript-boilerplate) +> License: **Apache-2.0** +> A minimalistic, actively-maintained Node.js + TypeScript project template using ESM, Jest, and modern ESLint flat config. + +## Project Structure + +``` +node-typescript-boilerplate/ +├── src/ +│ ├── index.ts +│ └── __tests__/ +│ └── index.test.ts +├── dist/ # Compiled output (git-ignored) +├── .editorconfig +├── .gitignore +├── .nvmrc +├── eslint.config.mjs +├── jest.config.ts +├── package.json +├── tsconfig.json +└── README.md +``` + +## `package.json` + +```json +{ + "name": "node-typescript-boilerplate", + "version": "1.0.0", + "description": "Minimalistic project template to build a Node.js back-end application with TypeScript", + "author": "Your Name ", + "license": "Apache-2.0", + "type": "module", + "main": "./dist/index.js", + "exports": { + ".": "./dist/index.js" + }, + "typings": "./dist/index.d.ts", + "engines": { + "node": ">= 20" + }, + "scripts": { + "start": "node ./dist/index.js", + "build": "tsc -p tsconfig.json", + "clean": "rimraf ./dist ./coverage", + "prebuild": "npm run clean", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "type-check": "tsc --noEmit" + }, + "devDependencies": { + "@eslint/js": "^9.15.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.9.0", + "eslint": "^9.15.0", + "globals": "^15.12.0", + "jest": "^29.7.0", + "rimraf": "^6.0.1", + "ts-jest": "^29.2.5", + "typescript": "^5.7.2", + "typescript-eslint": "^8.15.0" + } +} +``` + +## `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "coverage", "**/*.test.ts"] +} +``` + +## `jest.config.ts` + +```typescript +import type { Config } from "jest"; + +const config: Config = { + displayName: "node-typescript-boilerplate", + preset: "ts-jest/presets/default-esm", + testEnvironment: "node", + extensionsToTreatAsEsm: [".ts"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + useESM: true, + }, + ], + }, + testMatch: ["**/src/__tests__/**/*.test.ts"], + coverageDirectory: "coverage", + collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts", "!src/__tests__/**"], +}; + +export default config; +``` + +## `eslint.config.mjs` + +```javascript +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import globals from "globals"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.es2022, + }, + }, + }, + { + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "no-console": "warn", + }, + }, + { + ignores: ["dist/**", "coverage/**", "node_modules/**"], + } +); +``` + +## `src/index.ts` + +```typescript +/** + * Main entry point for the application. + * Replace this with your actual application logic. + */ + +export interface AppConfig { + name: string; + version: string; + debug?: boolean; +} + +export function createApp(config: AppConfig): string { + const { name, version, debug = false } = config; + + if (debug) { + console.debug(`[DEBUG] Initializing ${name} v${version}`); + } + + return `${name} v${version} is running`; +} + +// Entry point — only runs when executed directly, not when imported +const config: AppConfig = { + name: "my-app", + version: "1.0.0", + debug: process.env.NODE_ENV === "development", +}; + +console.log(createApp(config)); +``` + +## `src/__tests__/index.test.ts` + +```typescript +import { describe, expect, it } from "@jest/globals"; +import { createApp } from "../index.js"; + +describe("createApp", () => { + it("returns a startup message with the app name and version", () => { + const result = createApp({ name: "test-app", version: "0.1.0" }); + expect(result).toBe("test-app v0.1.0 is running"); + }); + + it("includes the name and version in the returned string", () => { + const result = createApp({ name: "my-service", version: "2.3.1" }); + expect(result).toContain("my-service"); + expect(result).toContain("2.3.1"); + }); + + it("uses debug=false by default", () => { + // Should not throw even when debug is omitted + expect(() => + createApp({ name: "silent-app", version: "1.0.0" }) + ).not.toThrow(); + }); +}); +``` + +## `.editorconfig` + +```ini +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false +``` + +## `.nvmrc` + +``` +20 +``` + +## `.gitignore` + +``` +# Compiled output +/dist +/coverage + +# Dependencies +/node_modules + +# Build artifacts +*.tsbuildinfo +.tsbuildinfo + +# Environment +.env +.env.* +!.env.example + +# OS artifacts +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.code-workspace +``` + +## Key Scripts Reference + +| Script | Command | Purpose | +|---|---|---| +| `build` | `tsc -p tsconfig.json` | Compile TypeScript to `dist/` | +| `clean` | `rimraf ./dist ./coverage` | Remove build artifacts | +| `start` | `node ./dist/index.js` | Run compiled output | +| `test` | `jest` | Run all tests | +| `test:coverage` | `jest --coverage` | Run tests with coverage report | +| `lint` | `eslint src` | Lint all TypeScript sources | +| `lint:fix` | `eslint src --fix` | Auto-fix lint issues | +| `type-check` | `tsc --noEmit` | Type-check without emitting files | + +## Quick Start + +```bash +# Clone or copy this template +git clone https://github.com/jsynowiec/node-typescript-boilerplate my-project +cd my-project + +# Install dependencies +npm install + +# Run type-checking +npm run type-check + +# Run tests +npm test + +# Build for production +npm run build + +# Start the app +npm start +``` + +## Notes on ESM + NodeNext + +This boilerplate uses `"module": "NodeNext"` and `"type": "module"` in `package.json`. + +- **Imports must use `.js` extensions** in TypeScript source, even though the files are `.ts`: + ```typescript + // Correct — TypeScript resolves this to index.ts at compile time + import { helper } from "./helper.js"; + + // Wrong — will fail at runtime + import { helper } from "./helper"; + ``` +- Jest is configured with `extensionsToTreatAsEsm` and a `moduleNameMapper` to handle `.js` → source file resolution during tests. +- If you prefer CommonJS, change `"module"` to `"CommonJS"` and remove `"type": "module"` from `package.json`. diff --git a/skills/typescript-coder/assets/typescript-node-module.md b/skills/typescript-coder/assets/typescript-node-module.md new file mode 100644 index 000000000..913827770 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-node-module.md @@ -0,0 +1,365 @@ +# TypeScript Node.js Module / Library Template + +> A TypeScript Node.js module starter based on patterns from `generator-node-module-typescript`. Produces an npm-publishable library with CJS and ESM dual output via Rollup, Jest testing, and full TypeScript type declarations. + +## License + +MIT License — See source repository for full license terms. + +## Source + +- [codejamninja/generator-node-module-typescript](https://github.com/codejamninja/generator-node-module-typescript) + +## Project Structure + +``` +my-ts-module/ +├── src/ +│ ├── index.ts +│ ├── greet.ts +│ └── types.ts +├── tests/ +│ ├── greet.test.ts +│ └── index.test.ts +├── dist/ +│ ├── cjs/ ← CommonJS output +│ │ ├── index.js +│ │ └── index.d.ts +│ └── esm/ ← ES Module output +│ ├── index.js +│ └── index.d.ts +├── .eslintrc.json +├── .gitignore +├── .npmignore +├── babel.config.js +├── jest.config.ts +├── package.json +├── rollup.config.ts +├── tsconfig.json +└── tsconfig.cjs.json +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-ts-module", + "version": "1.0.0", + "description": "A TypeScript Node.js module", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/cjs/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + }, + "files": [ + "dist", + "src" + ], + "sideEffects": false, + "scripts": { + "build": "rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript", + "build:watch": "rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript --watch", + "clean": "rimraf dist", + "prebuild": "npm run clean", + "test": "jest", + "test:watch": "jest --watchAll", + "test:coverage": "jest --coverage", + "lint": "eslint src --ext .ts", + "lint:fix": "eslint src --ext .ts --fix", + "typecheck": "tsc --noEmit", + "prepublishOnly": "npm run build && npm run test" + }, + "keywords": ["typescript", "node", "module"], + "engines": { + "node": ">=18.0.0" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-typescript": "^11.1.5", + "@types/jest": "^29.5.7", + "@types/node": "^20.8.10", + "@typescript-eslint/eslint-plugin": "^6.9.1", + "@typescript-eslint/parser": "^6.9.1", + "eslint": "^8.52.0", + "jest": "^29.7.0", + "rimraf": "^5.0.5", + "rollup": "^4.3.0", + "rollup-plugin-dts": "^6.1.0", + "ts-jest": "^29.1.1", + "tslib": "^2.6.2", + "typescript": "^5.2.2" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "importHelpers": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "tests"] +} +``` + +### `tsconfig.cjs.json` + +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "node", + "outDir": "dist/cjs" + } +} +``` + +### `rollup.config.ts` + +```typescript +import { defineConfig } from "rollup"; +import resolve from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; +import typescript from "@rollup/plugin-typescript"; +import dts from "rollup-plugin-dts"; + +const external = (id: string) => + !id.startsWith(".") && !id.startsWith("/") && id !== "tslib"; + +export default defineConfig([ + // ESM build + { + input: "src/index.ts", + output: { + dir: "dist/esm", + format: "es", + sourcemap: true, + preserveModules: true, + preserveModulesRoot: "src", + }, + external, + plugins: [ + resolve(), + commonjs(), + typescript({ + tsconfig: "./tsconfig.json", + declarationDir: "dist/esm", + declaration: true, + declarationMap: true, + }), + ], + }, + // CJS build + { + input: "src/index.ts", + output: { + dir: "dist/cjs", + format: "cjs", + sourcemap: true, + exports: "named", + preserveModules: true, + preserveModulesRoot: "src", + }, + external, + plugins: [ + resolve(), + commonjs(), + typescript({ + tsconfig: "./tsconfig.cjs.json", + declarationDir: "dist/cjs", + declaration: true, + declarationMap: true, + }), + ], + }, + // Type declarations bundle (optional — for single-file .d.ts) + { + input: "src/index.ts", + output: { file: "dist/index.d.ts", format: "es" }, + external, + plugins: [dts()], + }, +]); +``` + +### `jest.config.ts` + +```typescript +import type { Config } from "jest"; + +const config: Config = { + preset: "ts-jest", + testEnvironment: "node", + roots: ["/tests"], + testMatch: ["**/*.test.ts"], + transform: { + "^.+\\.ts$": ["ts-jest", { tsconfig: "./tsconfig.json" }], + }, + collectCoverageFrom: ["src/**/*.ts", "!src/types.ts"], + coverageDirectory: "coverage", + coverageReporters: ["text", "lcov"], +}; + +export default config; +``` + +### `src/types.ts` + +```typescript +export interface GreetOptions { + /** The name to greet. */ + name: string; + /** Optional greeting prefix. Defaults to "Hello". */ + prefix?: string; + /** Whether to use formal capitalisation. */ + formal?: boolean; +} + +export type GreetResult = { + message: string; + timestamp: Date; +}; +``` + +### `src/greet.ts` + +```typescript +import { GreetOptions, GreetResult } from "./types"; + +/** + * Produce a greeting message. + * + * @example + * ```ts + * const result = greet({ name: "World" }); + * console.log(result.message); // "Hello, World!" + * ``` + */ +export function greet(options: GreetOptions): GreetResult { + const { name, prefix = "Hello", formal = false } = options; + + const displayName = formal + ? name.charAt(0).toUpperCase() + name.slice(1) + : name; + + return { + message: `${prefix}, ${displayName}!`, + timestamp: new Date(), + }; +} +``` + +### `src/index.ts` + +```typescript +export { greet } from "./greet"; +export type { GreetOptions, GreetResult } from "./types"; +``` + +### `tests/greet.test.ts` + +```typescript +import { greet } from "../src/greet"; + +describe("greet()", () => { + it("returns a message with the default prefix", () => { + const result = greet({ name: "World" }); + expect(result.message).toBe("Hello, World!"); + }); + + it("respects a custom prefix", () => { + const result = greet({ name: "Alice", prefix: "Hi" }); + expect(result.message).toBe("Hi, Alice!"); + }); + + it("capitalises the name when formal is true", () => { + const result = greet({ name: "alice", formal: true }); + expect(result.message).toBe("Hello, Alice!"); + }); + + it("includes a timestamp", () => { + const before = Date.now(); + const result = greet({ name: "Test" }); + const after = Date.now(); + expect(result.timestamp.getTime()).toBeGreaterThanOrEqual(before); + expect(result.timestamp.getTime()).toBeLessThanOrEqual(after); + }); +}); +``` + +### `.npmignore` + +``` +src/ +tests/ +*.config.ts +*.config.js +tsconfig*.json +.eslintrc.json +coverage/ +.github/ +``` + +## Getting Started + +1. Copy the template into your new module directory. +2. Update `name`, `description`, and `keywords` in `package.json`. +3. Install dependencies: + ```bash + npm install + ``` +4. Run tests to verify setup: + ```bash + npm test + ``` +5. Build dual CJS + ESM output: + ```bash + npm run build + ``` +6. Publish to npm: + ```bash + npm publish --access public + ``` + +## Features + +- TypeScript 5.x with strict mode and `importHelpers` (tslib) +- Dual CJS + ESM output via Rollup 4 with `preserveModules` for tree-shaking +- Separate `tsconfig.cjs.json` for the CommonJS build +- `exports` map in `package.json` with type-safe `import`/`require` conditions +- `rollup-plugin-dts` for bundling a single `.d.ts` entry-point declaration file +- Jest + ts-jest for native TypeScript test execution without pre-compilation +- `sideEffects: false` declared for optimal tree-shaking by bundlers +- `prepublishOnly` script enforces a passing build and test suite before publish +- `.npmignore` to keep the published package lean (only `dist/` and `src/`) diff --git a/skills/typescript-coder/assets/typescript-node-tsnext.md b/skills/typescript-coder/assets/typescript-node-tsnext.md new file mode 100644 index 000000000..495f6ceb9 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-node-tsnext.md @@ -0,0 +1,230 @@ +# TypeScript Node.js Module (tsnext) + +> A modern TypeScript Node.js module starter with ESM-first output, strict compiler settings targeting the latest Node.js LTS, and Vitest for testing. Designed to produce dual-publishable packages (ESM + type declarations) using `NodeNext` module resolution. + +## License + +MIT — See [source repository](https://github.com/motss/generator-node-tsnext) for full license text. + +## Source + +- [motss/generator-node-tsnext](https://github.com/motss/generator-node-tsnext) + +## Project Structure + +``` +my-module/ +├── src/ +│ ├── index.ts +│ ├── lib/ +│ │ └── my-feature.ts +│ └── test/ +│ ├── index.test.ts +│ └── my-feature.test.ts +├── dist/ (generated — do not edit) +├── package.json +├── tsconfig.json +├── tsconfig.build.json +├── vitest.config.ts +├── .eslintrc.cjs +├── .gitignore +└── README.md +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-module", + "version": "0.1.0", + "description": "My TypeScript Node.js module", + "license": "MIT", + "author": "Your Name ", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist", + "!dist/**/*.test.*", + "!dist/**/*.spec.*" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "build:watch": "tsc -p tsconfig.build.json --watch", + "clean": "rimraf dist", + "lint": "eslint src --ext .ts", + "prebuild": "npm run clean", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "prepublishOnly": "npm run build" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "@vitest/coverage-v8": "^1.0.0", + "eslint": "^8.0.0", + "rimraf": "^5.0.0", + "typescript": "^5.4.0", + "vitest": "^1.0.0" + }, + "engines": { + "node": ">=20.0.0" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "src/test"] +} +``` + +### `tsconfig.build.json` + +```json +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "src/test", "**/*.test.ts", "**/*.spec.ts"] +} +``` + +### `vitest.config.ts` + +```typescript +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/test/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'lcov', 'html'], + include: ['src/**/*.ts'], + exclude: ['src/test/**'], + }, + }, +}); +``` + +### `src/index.ts` + +```typescript +export { greet } from './lib/my-feature.js'; +export type { GreetOptions } from './lib/my-feature.js'; +``` + +### `src/lib/my-feature.ts` + +```typescript +export interface GreetOptions { + name: string; + greeting?: string; +} + +export function greet(options: GreetOptions): string { + const { name, greeting = 'Hello' } = options; + + if (!name.trim()) { + throw new TypeError('name must not be empty'); + } + + return `${greeting}, ${name}!`; +} +``` + +### `src/test/my-feature.test.ts` + +```typescript +import { describe, expect, it } from 'vitest'; +import { greet } from '../lib/my-feature.js'; + +describe('greet()', () => { + it('returns a greeting with the default prefix', () => { + expect(greet({ name: 'World' })).toBe('Hello, World!'); + }); + + it('returns a greeting with a custom prefix', () => { + expect(greet({ name: 'Alice', greeting: 'Hi' })).toBe('Hi, Alice!'); + }); + + it('throws when name is empty', () => { + expect(() => greet({ name: ' ' })).toThrow(TypeError); + }); +}); +``` + +### `.gitignore` + +``` +node_modules/ +dist/ +coverage/ +*.tsbuildinfo +.env +``` + +## Getting Started + +```bash +# 1. Initialise a new directory +mkdir my-module && cd my-module + +# 2. Copy / scaffold project files (see structure above) + +# 3. Install dependencies +npm install + +# 4. Run tests +npm test + +# 5. Build for publishing +npm run build + +# 6. Inspect the dist output +ls dist/ +``` + +## Features + +- ESM-first output with `"type": "module"` and `exports` map +- `NodeNext` module resolution for accurate Node.js ESM behaviour +- Strict TypeScript with `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess` +- Declaration files and source maps emitted alongside JS +- Vitest for fast, native-ESM unit testing with V8 coverage +- Separate `tsconfig.build.json` to exclude test files from the published build +- `engines` field enforces Node.js 20 LTS or later diff --git a/skills/typescript-coder/assets/typescript-package.md b/skills/typescript-coder/assets/typescript-package.md new file mode 100644 index 000000000..2cfab71c1 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-package.md @@ -0,0 +1,457 @@ +# TypeScript npm Package Template + +> A TypeScript npm package starter based on patterns from `generator-typescript-package` by EricCrosson. Produces a modern, publishable npm package with a full `exports` map, npm provenance, semantic release, strict TypeScript, and GitHub Actions CI/CD. + +## License + +MIT License — See source repository for full license terms. + +## Source + +- [EricCrosson/generator-typescript-package](https://github.com/EricCrosson/generator-typescript-package) + +## Project Structure + +``` +my-ts-package/ +├── .github/ +│ └── workflows/ +│ ├── ci.yml ← Test and typecheck on every PR/push +│ └── release.yml ← Semantic release on merge to main +├── src/ +│ ├── index.ts ← Public API barrel +│ ├── core.ts ← Core implementation +│ └── types.ts ← Exported types +├── tests/ +│ └── core.test.ts +├── dist/ ← Build output (gitignored) +│ ├── cjs/ +│ └── esm/ +├── .eslintrc.json +├── .gitignore +├── .npmignore +├── .releaserc.json ← Semantic release config +├── package.json +├── tsconfig.json +├── tsconfig.cjs.json +└── tsup.config.ts ← tsup bundler config (replaces manual Rollup) +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "@yourscope/my-ts-package", + "version": "0.0.0", + "description": "A modern TypeScript npm package", + "license": "MIT", + "author": "Your Name ", + "repository": { + "type": "git", + "url": "https://github.com/yourname/my-ts-package.git" + }, + "keywords": ["typescript"], + "type": "module", + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + } + } + }, + "files": [ + "dist", + "src" + ], + "sideEffects": false, + "engines": { + "node": ">=18.0.0" + }, + "scripts": { + "build": "tsup", + "clean": "rimraf dist", + "prebuild": "npm run clean", + "test": "jest", + "test:watch": "jest --watchAll", + "test:coverage": "jest --coverage", + "typecheck": "tsc --noEmit", + "lint": "eslint src --ext .ts", + "lint:fix": "eslint src --ext .ts --fix", + "prepublishOnly": "npm run build && npm run test && npm run typecheck" + }, + "devDependencies": { + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/git": "^10.0.1", + "@types/jest": "^29.5.7", + "@types/node": "^20.8.10", + "@typescript-eslint/eslint-plugin": "^6.9.1", + "@typescript-eslint/parser": "^6.9.1", + "eslint": "^8.52.0", + "jest": "^29.7.0", + "rimraf": "^5.0.5", + "semantic-release": "^22.0.8", + "ts-jest": "^29.1.1", + "tsup": "^8.0.0", + "typescript": "^5.2.2" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} +``` + +### `tsup.config.ts` + +```typescript +import { defineConfig } from "tsup"; + +export default defineConfig([ + // ESM build + { + entry: ["src/index.ts"], + format: ["esm"], + outDir: "dist/esm", + dts: true, + sourcemap: true, + clean: false, + splitting: false, + treeshake: true, + }, + // CJS build + { + entry: ["src/index.ts"], + format: ["cjs"], + outDir: "dist/cjs", + dts: true, + sourcemap: true, + clean: false, + splitting: false, + }, +]); +``` + +### `src/types.ts` + +```typescript +export interface ParseOptions { + /** Trim whitespace from string values. Default: true. */ + trim?: boolean; + /** Throw on invalid input instead of returning undefined. Default: false. */ + strict?: boolean; +} + +export type ParseResult = + | { success: true; value: T } + | { success: false; error: string }; +``` + +### `src/core.ts` + +```typescript +import { ParseOptions, ParseResult } from "./types.js"; + +/** + * Parse a raw string value into a number. + * + * @example + * ```ts + * parseNumber(" 42 ") // => { success: true, value: 42 } + * parseNumber("abc") // => { success: false, error: "Not a number: abc" } + * ``` + */ +export function parseNumber( + raw: string, + options: ParseOptions = {} +): ParseResult { + const { trim = true, strict = false } = options; + const input = trim ? raw.trim() : raw; + const parsed = Number(input); + + if (Number.isNaN(parsed) || input === "") { + const error = `Not a number: ${raw}`; + if (strict) throw new TypeError(error); + return { success: false, error }; + } + + return { success: true, value: parsed }; +} + +/** + * Parse a raw string value into a boolean. + * Accepts "true"/"false" (case-insensitive) and "1"/"0". + */ +export function parseBoolean( + raw: string, + options: ParseOptions = {} +): ParseResult { + const { trim = true, strict = false } = options; + const input = (trim ? raw.trim() : raw).toLowerCase(); + + if (input === "true" || input === "1") return { success: true, value: true }; + if (input === "false" || input === "0") return { success: true, value: false }; + + const error = `Not a boolean: ${raw}`; + if (strict) throw new TypeError(error); + return { success: false, error }; +} +``` + +### `src/index.ts` + +```typescript +export { parseNumber, parseBoolean } from "./core.js"; +export type { ParseOptions, ParseResult } from "./types.js"; +``` + +### `tests/core.test.ts` + +```typescript +import { parseNumber, parseBoolean } from "../src/index.js"; + +describe("parseNumber()", () => { + it("parses a valid integer string", () => { + const result = parseNumber("42"); + expect(result).toEqual({ success: true, value: 42 }); + }); + + it("parses a float string", () => { + const result = parseNumber("3.14"); + expect(result).toEqual({ success: true, value: 3.14 }); + }); + + it("trims whitespace by default", () => { + const result = parseNumber(" 7 "); + expect(result).toEqual({ success: true, value: 7 }); + }); + + it("returns failure for non-numeric input", () => { + const result = parseNumber("abc"); + expect(result.success).toBe(false); + }); + + it("throws in strict mode for non-numeric input", () => { + expect(() => parseNumber("abc", { strict: true })).toThrow(TypeError); + }); +}); + +describe("parseBoolean()", () => { + it.each([["true", true], ["1", true], ["false", false], ["0", false]])( + 'parses "%s" as %s', + (input, expected) => { + const result = parseBoolean(input); + expect(result).toEqual({ success: true, value: expected }); + } + ); + + it("is case-insensitive", () => { + expect(parseBoolean("TRUE")).toEqual({ success: true, value: true }); + }); + + it("returns failure for unknown values", () => { + expect(parseBoolean("yes").success).toBe(false); + }); +}); +``` + +### `.releaserc.json` + +```json +{ + "branches": ["main"], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/changelog", + { "changelogFile": "CHANGELOG.md" } + ], + [ + "@semantic-release/npm", + { "npmPublish": true } + ], + [ + "@semantic-release/git", + { + "assets": ["CHANGELOG.md", "package.json"], + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + } + ], + "@semantic-release/github" + ] +} +``` + +### `.github/workflows/ci.yml` + +```yaml +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Test + run: npm run test:coverage + + - name: Upload coverage + uses: codecov/codecov-action@v3 + if: matrix.node-version == '20.x' +``` + +### `.github/workflows/release.yml` + +```yaml +name: Release + +on: + push: + branches: [main] + +permissions: + contents: write + issues: write + pull-requests: write + id-token: write # Required for npm provenance + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: actions/setup-node@v4 + with: + node-version: 20.x + registry-url: https://registry.npmjs.org + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: true # Enables npm provenance attestation + run: npx semantic-release +``` + +### `.npmignore` + +``` +src/ +tests/ +*.config.ts +tsconfig*.json +.eslintrc.json +.releaserc.json +.github/ +coverage/ +CHANGELOG.md +``` + +## Getting Started + +1. Copy the template and update `name`, `description`, `author`, and `repository` in `package.json`. +2. Install dependencies: + ```bash + npm install + ``` +3. Implement your library in `src/core.ts` and update `src/index.ts` to export the public API. +4. Run tests: + ```bash + npm test + ``` +5. Typecheck: + ```bash + npm run typecheck + ``` +6. Build both CJS and ESM outputs: + ```bash + npm run build + ``` +7. To publish, add `NPM_TOKEN` as a GitHub Actions secret and push to `main`. Semantic release will version, tag, and publish automatically. + +## Features + +- TypeScript 5.x with the strictest practical settings: `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `noImplicitOverride` +- `"type": "module"` in `package.json` with `NodeNext` module resolution for correct ESM/CJS interop +- Dual CJS + ESM output via `tsup` with declaration files (`.d.ts` / `.d.cts`) per format +- Full `exports` map with `import`/`require` conditions and `types` subpaths +- `sideEffects: false` for optimal tree-shaking +- `npm provenance` attestation via `NPM_CONFIG_PROVENANCE: true` in the release workflow +- Semantic release with conventional commits for automatic versioning and changelog generation +- GitHub Actions CI matrix across Node.js 18/20/22 +- Codecov integration for coverage reporting +- `prepublishOnly` guard to prevent publishing a broken build +- `.npmignore` to keep the published tarball lean diff --git a/skills/typescript-coder/assets/typescript-project-template-modern.md b/skills/typescript-coder/assets/typescript-project-template-modern.md new file mode 100644 index 000000000..26bd46bba --- /dev/null +++ b/skills/typescript-coder/assets/typescript-project-template-modern.md @@ -0,0 +1,597 @@ + + +# TypeScript Project Template — Modernized (NivaldoFarias variation) + +> Based on [NivaldoFarias/typescript-project-template](https://github.com/NivaldoFarias/typescript-project-template) +> License: **MIT** +> This is a **modernized variation** applying current best practices: +> TypeScript 5.x, optional Bun runtime, modern ESLint flat config, Prettier 3.x, +> Husky v9 + lint-staged, and Conventional Commits enforcement. + +## What Changed from the Original + +| Area | Original (NivaldoFarias) | This Modernized Variation | +|---|---|---| +| TypeScript | 4.x | **5.x** with all strict flags | +| ESLint config | `.eslintrc.json` / `.eslintrc.js` | **ESLint 9 flat config** (`eslint.config.ts`) | +| Husky | v4/v8 | **Husky v9** (`.husky/` scripts) | +| Runtime option | Node.js only | Node.js 20+ **or Bun 1.x** | +| Commit enforcement | Not present | **commitlint** + Conventional Commits | +| Formatting | Prettier 2.x | **Prettier 3.x** | +| Module system | CommonJS | **ESM** (`"type": "module"`) | + +## Project Structure + +``` +my-project/ +├── src/ +│ ├── index.ts # Application entry point +│ ├── config/ +│ │ └── index.ts # Environment configuration +│ ├── types/ +│ │ └── index.ts # Shared type definitions +│ └── utils/ +│ └── index.ts # Utility functions +├── tests/ +│ ├── unit/ +│ │ └── utils.test.ts +│ └── integration/ +│ └── app.test.ts +├── dist/ # Compiled output (git-ignored) +├── .husky/ +│ ├── pre-commit # Runs lint-staged +│ └── commit-msg # Runs commitlint +├── .gitignore +├── .nvmrc # Node version pin +├── .prettierrc # Prettier configuration +├── .prettierignore +├── commitlint.config.ts +├── eslint.config.ts +├── package.json +├── tsconfig.json +└── README.md +``` + +## `package.json` + +```json +{ + "name": "my-project", + "version": "0.1.0", + "description": "A TypeScript project", + "author": "Your Name ", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "engines": { + "node": ">=20.0.0" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "build:watch": "tsc -p tsconfig.json --watch", + "clean": "rimraf dist coverage", + "prebuild": "npm run clean", + "start": "node dist/index.js", + "dev": "tsx watch src/index.ts", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "type-check": "tsc --noEmit", + "prepare": "husky", + "validate": "npm run type-check && npm run lint && npm run format:check && npm run test" + }, + "devDependencies": { + "@commitlint/cli": "^19.5.0", + "@commitlint/config-conventional": "^19.5.0", + "@eslint/js": "^9.15.0", + "@types/node": "^22.9.0", + "@vitest/coverage-v8": "^2.1.0", + "eslint": "^9.15.0", + "eslint-config-prettier": "^9.1.0", + "globals": "^15.12.0", + "husky": "^9.1.7", + "lint-staged": "^15.2.10", + "prettier": "^3.3.3", + "rimraf": "^6.0.1", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "typescript-eslint": "^8.15.0", + "vitest": "^2.1.0" + }, + "lint-staged": { + "*.{ts,tsx,mts,cts}": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,md,yaml,yml}": [ + "prettier --write" + ] + } +} +``` + +## `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noFallthroughCasesInSwitch": true, + + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "incremental": true, + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests/**/*"] +} +``` + +## `eslint.config.ts` + +Modern ESLint 9 flat config in TypeScript: + +```typescript +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import globals from "globals"; +import eslintConfigPrettier from "eslint-config-prettier"; + +export default tseslint.config( + // Ignore patterns (replaces .eslintignore) + { + ignores: ["dist/**", "coverage/**", "node_modules/**", "*.config.js"], + }, + + // Base JS rules + eslint.configs.recommended, + + // TypeScript rules + ...tseslint.configs.strictTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + + // Type-aware linting requires parserOptions.project + { + languageOptions: { + globals: { + ...globals.node, + ...globals.es2022, + }, + parserOptions: { + project: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + + // Custom rule overrides + { + rules: { + // Allow unused vars prefixed with _ + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + // Prefer const assertions over explicit type annotations where possible + "@typescript-eslint/prefer-as-const": "error", + // Require explicit return types on exported functions + "@typescript-eslint/explicit-module-boundary-types": "warn", + // Disallow floating promises + "@typescript-eslint/no-floating-promises": "error", + // Require await in async functions + "@typescript-eslint/require-await": "error", + // No console in production code (warn, not error) + "no-console": ["warn", { allow: ["warn", "error"] }], + }, + }, + + // Disable formatting rules that conflict with Prettier (must be last) + eslintConfigPrettier, +); +``` + +## `.prettierrc` + +```json +{ + "semi": true, + "singleQuote": false, + "quoteProps": "as-needed", + "trailingComma": "all", + "tabWidth": 2, + "useTabs": false, + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "overrides": [ + { + "files": "*.md", + "options": { + "printWidth": 80, + "proseWrap": "always" + } + } + ] +} +``` + +## `.prettierignore` + +``` +dist/ +coverage/ +node_modules/ +.tsbuildinfo +*.lock +``` + +## `commitlint.config.ts` + +```typescript +import type { UserConfig } from "@commitlint/types"; + +const config: UserConfig = { + extends: ["@commitlint/config-conventional"], + rules: { + // Type must be one of these + "type-enum": [ + 2, + "always", + [ + "build", // Changes that affect the build system or external dependencies + "chore", // Other changes that don't modify src or test files + "ci", // Changes to CI configuration files and scripts + "docs", // Documentation only changes + "feat", // A new feature + "fix", // A bug fix + "perf", // A code change that improves performance + "refactor",// A code change that neither fixes a bug nor adds a feature + "revert", // Reverts a previous commit + "style", // Changes that don't affect the meaning of the code + "test", // Adding missing tests or correcting existing tests + ], + ], + // Scope is optional but must be lowercase if provided + "scope-case": [2, "always", "lower-case"], + // Subject must not end with a period + "subject-full-stop": [2, "never", "."], + // Subject must start with lowercase + "subject-case": [2, "always", "lower-case"], + // Body must start after a blank line + "body-leading-blank": [2, "always"], + // Footer must start after a blank line + "footer-leading-blank": [2, "always"], + // Max header length + "header-max-length": [2, "always", 100], + }, +}; + +export default config; +``` + +## `.husky/pre-commit` + +```sh +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged +``` + +## `.husky/commit-msg` + +```sh +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no -- commitlint --edit "$1" +``` + +## `src/config/index.ts` + +```typescript +/** + * Environment configuration — validated at startup. + * All environment variables are accessed through this module, never directly. + */ + +function requireEnv(key: string): string { + const value = process.env[key]; + if (!value) { + throw new Error( + `Missing required environment variable: ${key}. ` + + `Check your .env file or deployment configuration.` + ); + } + return value; +} + +function optionalEnv(key: string, defaultValue: string): string { + return process.env[key] ?? defaultValue; +} + +export const config = { + app: { + name: optionalEnv("APP_NAME", "my-project"), + version: optionalEnv("APP_VERSION", "0.1.0"), + port: Number(optionalEnv("PORT", "3000")), + nodeEnv: optionalEnv("NODE_ENV", "development") as + | "development" + | "production" + | "test", + }, + log: { + level: optionalEnv("LOG_LEVEL", "info") as + | "debug" + | "info" + | "warn" + | "error", + }, +} as const; + +export type AppConfig = typeof config; +``` + +## `src/types/index.ts` + +```typescript +/** + * Shared type definitions. Export all public types from here. + */ + +/** Discriminated union result type for error handling without exceptions. */ +export type Result = + | { readonly ok: true; readonly value: T } + | { readonly ok: false; readonly error: E }; + +export const Result = { + ok(value: T): Result { + return { ok: true, value }; + }, + err(error: E): Result { + return { ok: false, error }; + }, + isOk(result: Result): result is { ok: true; value: T } { + return result.ok; + }, +} as const; + +/** Represents a value that may not yet exist. */ +export type Option = { readonly some: true; readonly value: T } | { readonly some: false }; + +export const Option = { + some(value: T): Option { + return { some: true, value }; + }, + none(): Option { + return { some: false }; + }, + isSome(opt: Option): opt is { some: true; value: T } { + return opt.some; + }, +} as const; + +/** Pagination metadata for list responses. */ +export interface PaginationMeta { + readonly page: number; + readonly pageSize: number; + readonly total: number; + readonly totalPages: number; +} + +/** A paginated response wrapping a list of items. */ +export interface Paginated { + readonly items: readonly T[]; + readonly meta: PaginationMeta; +} +``` + +## `src/utils/index.ts` + +```typescript +/** + * General-purpose utility functions. + */ + +/** + * Sleeps for the given number of milliseconds. + */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Chunks an array into sub-arrays of the specified size. + */ +export function chunk(array: readonly T[], size: number): T[][] { + if (size <= 0) throw new RangeError("Chunk size must be greater than 0"); + const result: T[][] = []; + for (let i = 0; i < array.length; i += size) { + result.push(array.slice(i, i + size) as T[]); + } + return result; +} + +/** + * Returns a new object with only the specified keys from the source. + */ +export function pick( + obj: T, + keys: readonly K[] +): Pick { + return keys.reduce( + (acc, key) => { + acc[key] = obj[key]; + return acc; + }, + {} as Pick + ); +} + +/** + * Returns a new object without the specified keys. + */ +export function omit( + obj: T, + keys: readonly K[] +): Omit { + const keysSet = new Set(keys); + return Object.fromEntries( + Object.entries(obj).filter(([k]) => !keysSet.has(k)) + ) as Omit; +} +``` + +## `src/index.ts` + +```typescript +/** + * Application entry point. + */ + +import { config } from "./config/index.js"; +import { Result } from "./types/index.js"; + +async function main(): Promise { + const { app, log } = config; + + if (log.level === "debug") { + console.warn( + `[DEBUG] Starting ${app.name} v${app.version} in ${app.nodeEnv} mode` + ); + } + + const result = await runApp(); + + if (!Result.isOk(result)) { + console.error("Fatal error:", result.error.message); + process.exit(1); + } + + console.warn(`${app.name} started successfully on port ${app.port}`); +} + +async function runApp(): Promise> { + try { + // Replace with your application logic + await Promise.resolve(); + return Result.ok(undefined); + } catch (error) { + return Result.err( + error instanceof Error ? error : new Error(String(error)) + ); + } +} + +main().catch((error: unknown) => { + console.error("Unhandled error:", error); + process.exit(1); +}); +``` + +## Bun Runtime Option + +To use **Bun** instead of Node.js: + +1. Install Bun: `curl -fsSL https://bun.sh/install | bash` +2. Replace the `dev` script in `package.json`: + ```json + "dev": "bun --watch src/index.ts" + ``` +3. Replace `tsx` with `bun` in the dev workflow — Bun runs TypeScript natively. +4. For testing, replace Vitest with Bun's built-in test runner: + ```json + "test": "bun test" + ``` + And rename test files to `*.test.ts` (Bun picks them up automatically). +5. The `tsconfig.json` target can be adjusted: `"target": "ESNext"` for Bun. + +> Note: Bun is not 100% compatible with all npm packages. Validate your dependencies before switching. + +## Conventional Commits Reference + +Valid commit message format: + +``` +(): + +[optional body] + +[optional footer(s)] +``` + +Examples: +``` +feat(auth): add JWT refresh token rotation + +fix(api): resolve race condition in user lookup + +chore: upgrade typescript to 5.7.2 + +docs(readme): add bun runtime setup instructions + +refactor(utils): extract pagination helper into separate module + +BREAKING CHANGE: rename Config interface to AppConfig +``` + +## Quick Setup Script + +```bash +# 1. Clone / scaffold +git init my-project && cd my-project + +# 2. Install dependencies +npm install + +# 3. Initialize Husky +npm run prepare + +# 4. Make Husky hooks executable (Linux/macOS) +chmod +x .husky/pre-commit .husky/commit-msg + +# 5. Verify everything works +npm run validate +``` diff --git a/skills/typescript-coder/assets/typescript-react-lib.md b/skills/typescript-coder/assets/typescript-react-lib.md new file mode 100644 index 000000000..3257418c9 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-react-lib.md @@ -0,0 +1,371 @@ +# TypeScript React Component Library + +> A TypeScript React component library starter with Rollup bundling (CJS + ESM dual output), Jest and React Testing Library for tests, and proper `exports` map for tree-shaking. Produces a publishable npm package with type declarations. + +## License + +MIT — See [source repository](https://github.com/tanem/generator-typescript-react-lib) for full license text. + +## Source + +- [tanem/generator-typescript-react-lib](https://github.com/tanem/generator-typescript-react-lib) + +## Project Structure + +``` +my-react-lib/ +├── src/ +│ ├── index.ts (public API re-exports) +│ └── components/ +│ ├── Button/ +│ │ ├── Button.tsx +│ │ ├── Button.types.ts +│ │ └── __tests__/ +│ │ └── Button.test.tsx +│ └── index.ts (component barrel) +├── dist/ (generated — do not edit) +│ ├── cjs/ +│ │ ├── index.js +│ │ └── index.d.ts +│ └── esm/ +│ ├── index.js +│ └── index.d.ts +├── package.json +├── tsconfig.json +├── tsconfig.cjs.json +├── tsconfig.esm.json +├── rollup.config.js +├── jest.config.js +├── babel.config.js +├── .gitignore +└── README.md +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-react-lib", + "version": "0.1.0", + "description": "My TypeScript React component library", + "license": "MIT", + "author": "Your Name ", + "sideEffects": false, + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + }, + "files": ["dist"], + "scripts": { + "build": "npm run build:cjs && npm run build:esm", + "build:cjs": "rollup -c --environment BUILD:cjs", + "build:esm": "rollup -c --environment BUILD:esm", + "clean": "rimraf dist", + "lint": "eslint src --ext .ts,.tsx", + "prebuild": "npm run clean", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "prepublishOnly": "npm run build" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + }, + "devDependencies": { + "@babel/core": "^7.24.0", + "@babel/preset-env": "^7.24.0", + "@babel/preset-react": "^7.23.0", + "@babel/preset-typescript": "^7.23.0", + "@testing-library/jest-dom": "^6.4.0", + "@testing-library/react": "^15.0.0", + "@testing-library/user-event": "^14.5.0", + "@types/jest": "^29.5.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "rimraf": "^5.0.0", + "rollup": "^4.13.0", + "@rollup/plugin-node-resolve": "^15.2.0", + "@rollup/plugin-commonjs": "^25.0.0", + "rollup-plugin-typescript2": "^0.36.0", + "typescript": "^5.4.0" + }, + "engines": { + "node": ">=18.0.0" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2018", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2018", "DOM"], + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} +``` + +### `tsconfig.cjs.json` + +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist/cjs" + }, + "exclude": ["node_modules", "dist", "**/__tests__/**", "**/*.test.*"] +} +``` + +### `tsconfig.esm.json` + +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "outDir": "dist/esm" + }, + "exclude": ["node_modules", "dist", "**/__tests__/**", "**/*.test.*"] +} +``` + +### `rollup.config.js` + +```javascript +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import typescript from 'rollup-plugin-typescript2'; + +const isEsm = process.env.BUILD === 'esm'; + +export default { + input: 'src/index.ts', + output: { + dir: isEsm ? 'dist/esm' : 'dist/cjs', + format: isEsm ? 'esm' : 'cjs', + preserveModules: true, + preserveModulesRoot: 'src', + sourcemap: true, + }, + external: ['react', 'react-dom', 'react/jsx-runtime'], + plugins: [ + resolve(), + commonjs(), + typescript({ + tsconfig: isEsm ? './tsconfig.esm.json' : './tsconfig.cjs.json', + useTsconfigDeclarationDir: true, + }), + ], +}; +``` + +### `jest.config.js` + +```javascript +/** @type {import('jest').Config} */ +export default { + testEnvironment: 'jsdom', + transform: { + '^.+\\.(ts|tsx)$': 'babel-jest', + }, + setupFilesAfterFramework: ['@testing-library/jest-dom'], + testMatch: ['**/__tests__/**/*.test.(ts|tsx)'], + collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/__tests__/**'], +}; +``` + +### `babel.config.js` + +```javascript +export default { + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + ['@babel/preset-react', { runtime: 'automatic' }], + '@babel/preset-typescript', + ], +}; +``` + +### `src/components/Button/Button.types.ts` + +```typescript +import type { ButtonHTMLAttributes, ReactNode } from 'react'; + +export type ButtonVariant = 'primary' | 'secondary' | 'ghost'; +export type ButtonSize = 'sm' | 'md' | 'lg'; + +export interface ButtonProps extends ButtonHTMLAttributes { + /** Visual style variant */ + variant?: ButtonVariant; + /** Size preset */ + size?: ButtonSize; + /** Whether the button is in a loading state */ + isLoading?: boolean; + /** Button content */ + children: ReactNode; +} +``` + +### `src/components/Button/Button.tsx` + +```tsx +import React from 'react'; +import type { ButtonProps } from './Button.types.js'; + +const sizeClasses: Record, string> = { + sm: 'btn--sm', + md: 'btn--md', + lg: 'btn--lg', +}; + +const variantClasses: Record, string> = { + primary: 'btn--primary', + secondary: 'btn--secondary', + ghost: 'btn--ghost', +}; + +export function Button({ + variant = 'primary', + size = 'md', + isLoading = false, + disabled, + children, + className = '', + ...rest +}: ButtonProps): JSX.Element { + const classes = [ + 'btn', + variantClasses[variant], + sizeClasses[size], + isLoading ? 'btn--loading' : '', + className, + ] + .filter(Boolean) + .join(' '); + + return ( + + ); +} +``` + +### `src/components/Button/__tests__/Button.test.tsx` + +```tsx +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { Button } from '../Button.js'; + +describe('Button', () => { + it('renders children', () => { + render(); + expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument(); + }); + + it('applies the primary variant class by default', () => { + render(); + expect(screen.getByRole('button')).toHaveClass('btn--primary'); + }); + + it('is disabled and aria-busy when isLoading is true', () => { + render(); + const btn = screen.getByRole('button'); + expect(btn).toBeDisabled(); + expect(btn).toHaveAttribute('aria-busy', 'true'); + }); + + it('calls onClick when clicked', async () => { + const handleClick = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); +}); +``` + +### `src/components/index.ts` + +```typescript +export { Button } from './Button/Button.js'; +export type { ButtonProps, ButtonVariant, ButtonSize } from './Button/Button.types.js'; +``` + +### `src/index.ts` + +```typescript +export * from './components/index.js'; +``` + +## Getting Started + +```bash +# 1. Create project directory +mkdir my-react-lib && cd my-react-lib + +# 2. Initialise and copy project files (see structure above) +npm init -y + +# 3. Install dependencies +npm install + +# 4. Run tests +npm test + +# 5. Build library (produces dist/cjs and dist/esm) +npm run build + +# 6. Publish (runs build automatically via prepublishOnly) +npm publish +``` + +## Features + +- Dual CJS and ESM build output via Rollup +- TypeScript strict mode with full declaration file emission +- `exports` map for modern Node.js and bundler resolution +- `sideEffects: false` for optimal tree-shaking +- Jest + React Testing Library + jest-dom for component testing +- Babel transform for Jest (separate from the Rollup build pipeline) +- React listed as a peer dependency — consumers supply their own React +- `preserveModules` keeps one output file per source file for fine-grained tree-shaking diff --git a/skills/typescript-coder/assets/typescript-rznode.md b/skills/typescript-coder/assets/typescript-rznode.md new file mode 100644 index 000000000..def63a87c --- /dev/null +++ b/skills/typescript-coder/assets/typescript-rznode.md @@ -0,0 +1,604 @@ +# TypeScript Node.js REST API (rznode) + +> A TypeScript Node.js REST API starter built on Express.js with a clean route/controller separation, typed middleware patterns, environment-based configuration, and Jest for integration testing. Ready to extend with a database layer or additional resource routes. + +## License + +MIT — See [source repository](https://github.com/odedlevy02/rznode) for full license text. + +## Source + +- [odedlevy02/rznode](https://github.com/odedlevy02/rznode) + +## Project Structure + +``` +my-node-api/ +├── src/ +│ ├── controllers/ +│ │ └── items.controller.ts +│ ├── routes/ +│ │ ├── items.routes.ts +│ │ └── index.ts +│ ├── middleware/ +│ │ ├── auth.middleware.ts +│ │ ├── error.middleware.ts +│ │ ├── logger.middleware.ts +│ │ └── validate.middleware.ts +│ ├── models/ +│ │ └── item.model.ts +│ ├── services/ +│ │ └── items.service.ts +│ ├── config/ +│ │ └── env.ts +│ └── app.ts +├── tests/ +│ └── items.test.ts +├── package.json +├── tsconfig.json +├── nodemon.json +├── jest.config.ts +├── .env +├── .env.example +├── .gitignore +└── README.md +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-node-api", + "version": "1.0.0", + "description": "TypeScript Node.js REST API", + "license": "MIT", + "private": true, + "main": "dist/app.js", + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch", + "clean": "rimraf dist", + "dev": "nodemon", + "start": "node dist/app.js", + "lint": "eslint src tests --ext .ts", + "test": "jest --forceExit", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage --forceExit" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.0", + "express": "^4.19.0", + "helmet": "^7.1.0", + "morgan": "^1.10.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.0", + "@types/morgan": "^1.9.9", + "@types/node": "^20.12.0", + "@types/supertest": "^6.0.2", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.57.0", + "jest": "^29.7.0", + "nodemon": "^3.1.0", + "rimraf": "^5.0.0", + "supertest": "^6.3.0", + "ts-jest": "^29.1.0", + "ts-node": "^10.9.0", + "typescript": "^5.4.0" + }, + "engines": { + "node": ">=20.0.0" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "Node", + "lib": ["ES2022"], + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitReturns": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": ".", + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "tests"] +} +``` + +### `nodemon.json` + +```json +{ + "watch": ["src"], + "ext": "ts,json", + "exec": "ts-node -r dotenv/config src/app.ts", + "env": { + "NODE_ENV": "development" + } +} +``` + +### `.env.example` + +``` +NODE_ENV=development +PORT=3000 +HOST=0.0.0.0 +LOG_LEVEL=dev +CORS_ORIGINS=http://localhost:3000,http://localhost:5173 +``` + +### `jest.config.ts` + +```typescript +import type { Config } from 'jest'; + +const config: Config = { + preset: 'ts-jest', + testEnvironment: 'node', + rootDir: '.', + testMatch: ['/tests/**/*.test.ts'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + collectCoverageFrom: ['src/**/*.ts', '!src/app.ts'], + coverageDirectory: 'coverage', +}; + +export default config; +``` + +### `src/config/env.ts` + +```typescript +import * as dotenv from 'dotenv'; + +dotenv.config(); + +function requireEnv(key: string): string { + const val = process.env[key]; + if (!val) throw new Error(`Missing required environment variable: ${key}`); + return val; +} + +export const config = { + nodeEnv: (process.env['NODE_ENV'] ?? 'development') as 'development' | 'test' | 'production', + port: parseInt(process.env['PORT'] ?? '3000', 10), + host: process.env['HOST'] ?? '0.0.0.0', + corsOrigins: (process.env['CORS_ORIGINS'] ?? '').split(',').filter(Boolean), +} as const; +``` + +### `src/models/item.model.ts` + +```typescript +export interface Item { + id: string; + name: string; + description?: string; + tags: string[]; + createdAt: Date; + updatedAt: Date; +} + +export interface CreateItemDto { + name: string; + description?: string; + tags?: string[]; +} + +export interface UpdateItemDto { + name?: string; + description?: string; + tags?: string[]; +} + +export interface PaginatedResult { + data: T[]; + total: number; + page: number; + pageSize: number; +} +``` + +### `src/services/items.service.ts` + +```typescript +import { randomUUID } from 'crypto'; +import type { + CreateItemDto, + Item, + PaginatedResult, + UpdateItemDto, +} from '../models/item.model'; + +// In-memory store — swap for a real DB client in production +const items = new Map(); + +export const ItemsService = { + findAll(page = 1, pageSize = 10): PaginatedResult { + const all = Array.from(items.values()); + const start = (page - 1) * pageSize; + return { + data: all.slice(start, start + pageSize), + total: all.length, + page, + pageSize, + }; + }, + + findById(id: string): Item | undefined { + return items.get(id); + }, + + create(dto: CreateItemDto): Item { + const now = new Date(); + const item: Item = { + id: randomUUID(), + name: dto.name, + description: dto.description, + tags: dto.tags ?? [], + createdAt: now, + updatedAt: now, + }; + items.set(item.id, item); + return item; + }, + + update(id: string, dto: UpdateItemDto): Item | undefined { + const existing = items.get(id); + if (!existing) return undefined; + const updated: Item = { + ...existing, + ...dto, + updatedAt: new Date(), + }; + items.set(id, updated); + return updated; + }, + + delete(id: string): boolean { + return items.delete(id); + }, +}; +``` + +### `src/controllers/items.controller.ts` + +```typescript +import { NextFunction, Request, Response } from 'express'; +import type { CreateItemDto, UpdateItemDto } from '../models/item.model'; +import { ItemsService } from '../services/items.service'; + +export const ItemsController = { + getAll(req: Request, res: Response, next: NextFunction): void { + try { + const page = parseInt(String(req.query['page'] ?? '1'), 10); + const pageSize = parseInt(String(req.query['pageSize'] ?? '10'), 10); + res.json(ItemsService.findAll(page, pageSize)); + } catch (err) { + next(err); + } + }, + + getById(req: Request, res: Response, next: NextFunction): void { + try { + const item = ItemsService.findById(req.params['id']!); + if (!item) { + res.status(404).json({ message: `Item '${req.params['id']}' not found` }); + return; + } + res.json(item); + } catch (err) { + next(err); + } + }, + + create(req: Request, res: Response, next: NextFunction): void { + try { + const dto = req.body as CreateItemDto; + if (!dto.name?.trim()) { + res.status(400).json({ message: 'name is required' }); + return; + } + res.status(201).json(ItemsService.create(dto)); + } catch (err) { + next(err); + } + }, + + update(req: Request, res: Response, next: NextFunction): void { + try { + const item = ItemsService.update(req.params['id']!, req.body as UpdateItemDto); + if (!item) { + res.status(404).json({ message: `Item '${req.params['id']}' not found` }); + return; + } + res.json(item); + } catch (err) { + next(err); + } + }, + + delete(req: Request, res: Response, next: NextFunction): void { + try { + const deleted = ItemsService.delete(req.params['id']!); + if (!deleted) { + res.status(404).json({ message: `Item '${req.params['id']}' not found` }); + return; + } + res.status(204).send(); + } catch (err) { + next(err); + } + }, +}; +``` + +### `src/routes/items.routes.ts` + +```typescript +import { Router } from 'express'; +import { ItemsController } from '../controllers/items.controller'; + +const router = Router(); + +router.get('/', ItemsController.getAll); +router.post('/', ItemsController.create); +router.get('/:id', ItemsController.getById); +router.patch('/:id', ItemsController.update); +router.delete('/:id', ItemsController.delete); + +export default router; +``` + +### `src/routes/index.ts` + +```typescript +import { Router } from 'express'; +import itemsRouter from './items.routes'; + +const router = Router(); + +router.use('/items', itemsRouter); + +// Health-check +router.get('/health', (_req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +export default router; +``` + +### `src/middleware/error.middleware.ts` + +```typescript +import { ErrorRequestHandler } from 'express'; + +export interface AppError extends Error { + statusCode?: number; + errors?: unknown[]; +} + +export const errorMiddleware: ErrorRequestHandler = ( + err: AppError, + _req, + res, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _next +) => { + const statusCode = err.statusCode ?? 500; + const message = err.message || 'Internal Server Error'; + + if (statusCode >= 500) { + console.error('[ERROR]', err); + } + + res.status(statusCode).json({ + message, + ...(err.errors ? { errors: err.errors } : {}), + ...(process.env['NODE_ENV'] !== 'production' ? { stack: err.stack } : {}), + }); +}; +``` + +### `src/middleware/logger.middleware.ts` + +```typescript +import morgan, { StreamOptions } from 'morgan'; + +const stream: StreamOptions = { + write: (message: string) => console.info(message.trim()), +}; + +export const loggerMiddleware = morgan( + process.env['NODE_ENV'] === 'production' ? 'combined' : 'dev', + { stream } +); +``` + +### `src/middleware/auth.middleware.ts` + +```typescript +import { NextFunction, Request, Response } from 'express'; + +// Extend the Express Request type with an authenticated user field +declare global { + namespace Express { + interface Request { + user?: { id: string; roles: string[] }; + } + } +} + +export function requireAuth(req: Request, res: Response, next: NextFunction): void { + const authHeader = req.headers['authorization']; + if (!authHeader?.startsWith('Bearer ')) { + res.status(401).json({ message: 'Missing or invalid Authorization header' }); + return; + } + + // Decode/verify your JWT here (e.g. using jsonwebtoken) + const token = authHeader.slice(7); + if (!token) { + res.status(401).json({ message: 'Empty token' }); + return; + } + + // Attach the decoded user to the request + req.user = { id: 'placeholder-id', roles: ['user'] }; + next(); +} +``` + +### `src/app.ts` + +```typescript +import cors from 'cors'; +import * as dotenv from 'dotenv'; +import express from 'express'; +import helmet from 'helmet'; +import { config } from './config/env'; +import { errorMiddleware } from './middleware/error.middleware'; +import { loggerMiddleware } from './middleware/logger.middleware'; +import routes from './routes'; + +dotenv.config(); + +const app = express(); + +// Security & parsing +app.use(helmet()); +app.use(cors({ origin: config.corsOrigins.length ? config.corsOrigins : '*' })); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); + +// Logging +app.use(loggerMiddleware); + +// Routes +app.use('/api/v1', routes); + +// Error handling (must be last) +app.use(errorMiddleware); + +if (require.main === module) { + app.listen(config.port, config.host, () => { + console.log(`API running at http://${config.host}:${config.port}/api/v1`); + console.log(`Health check: http://${config.host}:${config.port}/api/v1/health`); + }); +} + +export default app; +``` + +### `tests/items.test.ts` + +```typescript +import request from 'supertest'; +import app from '../src/app'; + +describe('Items API', () => { + describe('GET /api/v1/items', () => { + it('returns 200 with a paginated result', async () => { + const res = await request(app).get('/api/v1/items'); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ data: expect.any(Array), total: expect.any(Number) }); + }); + }); + + describe('POST /api/v1/items', () => { + it('creates an item and returns 201', async () => { + const res = await request(app) + .post('/api/v1/items') + .send({ name: 'Widget', description: 'A test widget', tags: ['test'] }); + expect(res.status).toBe(201); + expect(res.body).toMatchObject({ name: 'Widget', tags: ['test'] }); + expect(res.body.id).toBeTruthy(); + }); + + it('returns 400 when name is missing', async () => { + const res = await request(app).post('/api/v1/items').send({ description: 'no name' }); + expect(res.status).toBe(400); + }); + }); + + describe('GET /api/v1/items/:id', () => { + it('returns 404 for unknown id', async () => { + const res = await request(app).get('/api/v1/items/does-not-exist'); + expect(res.status).toBe(404); + }); + }); + + describe('GET /api/v1/health', () => { + it('returns status ok', async () => { + const res = await request(app).get('/api/v1/health'); + expect(res.status).toBe(200); + expect(res.body.status).toBe('ok'); + }); + }); +}); +``` + +## Getting Started + +```bash +# 1. Create project directory +mkdir my-node-api && cd my-node-api + +# 2. Copy project files (see structure above) + +# 3. Install dependencies +npm install + +# 4. Configure environment +cp .env.example .env + +# 5. Start in development mode (hot-reload) +npm run dev + +# 6. Verify the health endpoint +curl http://localhost:3000/api/v1/health + +# 7. Run integration tests +npm test + +# 8. Build for production +npm run build + +# 9. Start production server +npm start +``` + +## Features + +- Express 4 with full TypeScript types via `@types/express` +- Clean Controller/Service/Route separation — controllers handle HTTP, services handle logic +- Typed middleware: error handler, Morgan logger, CORS, Helmet, and a bearer-token auth stub +- `declare global` augmentation of `Express.Request` for attaching an authenticated user +- `ts-node` + Nodemon for zero-build development hot-reload +- Supertest integration tests running directly against the Express app instance +- `ts-jest` preset for native TypeScript Jest execution without a separate build step +- Environment config loaded via `dotenv` with a helper that throws on missing required variables +- `app.ts` is importable for testing AND runnable as the server entry point via `require.main === module` +- Health-check endpoint at `/api/v1/health` for container liveness probes diff --git a/skills/typescript-coder/assets/typescript-tsx-adobe.md b/skills/typescript-coder/assets/typescript-tsx-adobe.md new file mode 100644 index 000000000..5717aa878 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-tsx-adobe.md @@ -0,0 +1,454 @@ +# TypeScript React/TSX Project Template (Adobe Style) + +> A TypeScript + React project starter based on patterns from Adobe's `generator-tsx`. Produces a component-library-ready React application using TSX, with Webpack bundling, Jest testing, and Storybook documentation support. + +## License + +Apache License 2.0 — See source repository for full license terms. + +## Source + +- [adobe/generator-tsx](https://github.com/adobe/generator-tsx) + +## Project Structure + +``` +my-tsx-app/ +├── .storybook/ +│ ├── main.ts +│ └── preview.ts +├── src/ +│ ├── components/ +│ │ ├── Button/ +│ │ │ ├── Button.tsx +│ │ │ ├── Button.test.tsx +│ │ │ ├── Button.stories.tsx +│ │ │ └── index.ts +│ │ └── index.ts +│ ├── hooks/ +│ │ └── useTheme.ts +│ ├── types/ +│ │ └── index.ts +│ ├── index.ts +│ └── setupTests.ts +├── public/ +│ └── index.html +├── .eslintrc.json +├── .prettierrc +├── babel.config.js +├── jest.config.ts +├── package.json +├── tsconfig.json +├── tsconfig.build.json +└── webpack.config.ts +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-tsx-app", + "version": "1.0.0", + "description": "TypeScript React component library", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/types/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "webpack --config webpack.config.ts", + "build:types": "tsc --project tsconfig.build.json --emitDeclarationOnly", + "start": "webpack serve --config webpack.config.ts --mode development", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "lint": "eslint src --ext .ts,.tsx", + "lint:fix": "eslint src --ext .ts,.tsx --fix", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + }, + "devDependencies": { + "@babel/core": "^7.23.0", + "@babel/preset-env": "^7.23.0", + "@babel/preset-react": "^7.22.15", + "@babel/preset-typescript": "^7.23.0", + "@storybook/addon-essentials": "^7.5.0", + "@storybook/react": "^7.5.0", + "@storybook/react-webpack5": "^7.5.0", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.1.0", + "@testing-library/user-event": "^14.5.1", + "@types/jest": "^29.5.7", + "@types/react": "^18.2.33", + "@types/react-dom": "^18.2.14", + "@typescript-eslint/eslint-plugin": "^6.9.1", + "@typescript-eslint/parser": "^6.9.1", + "babel-loader": "^9.1.3", + "css-loader": "^6.8.1", + "eslint": "^8.52.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "prettier": "^3.0.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "style-loader": "^3.3.3", + "ts-node": "^10.9.1", + "typescript": "^5.2.2", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["DOM", "DOM.Iterable", "ES2020"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} +``` + +### `tsconfig.build.json` + +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationDir": "dist/types", + "outDir": "dist/esm", + "rootDir": "src" + }, + "exclude": ["node_modules", "dist", "**/*.test.tsx", "**/*.stories.tsx"] +} +``` + +### `webpack.config.ts` + +```typescript +import path from "path"; +import { Configuration } from "webpack"; +import "webpack-dev-server"; + +const config: Configuration = { + entry: "./src/index.ts", + output: { + path: path.resolve(__dirname, "dist"), + filename: "bundle.js", + library: { + name: "MyTsxApp", + type: "umd", + }, + globalObject: "this", + clean: true, + }, + resolve: { + extensions: [".tsx", ".ts", ".js"], + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, + module: { + rules: [ + { + test: /\.(ts|tsx)$/, + use: "babel-loader", + exclude: /node_modules/, + }, + { + test: /\.css$/, + use: ["style-loader", "css-loader"], + }, + ], + }, + devServer: { + static: "./public", + port: 3000, + hot: true, + }, +}; + +export default config; +``` + +### `babel.config.js` + +```javascript +module.exports = { + presets: [ + ["@babel/preset-env", { targets: { node: "current" } }], + ["@babel/preset-react", { runtime: "automatic" }], + "@babel/preset-typescript", + ], +}; +``` + +### `jest.config.ts` + +```typescript +import type { Config } from "jest"; + +const config: Config = { + preset: "ts-jest", + testEnvironment: "jsdom", + setupFilesAfterFramework: ["/src/setupTests.ts"], + moduleNameMapper: { + "^@/(.*)$": "/src/$1", + "\\.(css|less|scss)$": "identity-obj-proxy", + }, + transform: { + "^.+\\.(ts|tsx)$": "babel-jest", + }, + collectCoverageFrom: [ + "src/**/*.{ts,tsx}", + "!src/**/*.stories.{ts,tsx}", + "!src/index.ts", + ], +}; + +export default config; +``` + +### `src/components/Button/Button.tsx` + +```tsx +import React, { ButtonHTMLAttributes, forwardRef } from "react"; + +export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger"; +export type ButtonSize = "sm" | "md" | "lg"; + +export interface ButtonProps extends ButtonHTMLAttributes { + variant?: ButtonVariant; + size?: ButtonSize; + isLoading?: boolean; + leftIcon?: React.ReactNode; + rightIcon?: React.ReactNode; +} + +export const Button = forwardRef( + ( + { + variant = "primary", + size = "md", + isLoading = false, + leftIcon, + rightIcon, + children, + disabled, + className = "", + ...rest + }, + ref + ) => { + const baseClasses = "btn"; + const variantClass = `btn--${variant}`; + const sizeClass = `btn--${size}`; + const loadingClass = isLoading ? "btn--loading" : ""; + + return ( + + ); + } +); + +Button.displayName = "Button"; + +export default Button; +``` + +### `src/components/Button/Button.test.tsx` + +```tsx +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Button } from "./Button"; + +describe("Button", () => { + it("renders with default props", () => { + render(); + expect(screen.getByRole("button", { name: /click me/i })).toBeInTheDocument(); + }); + + it("calls onClick when clicked", async () => { + const handleClick = jest.fn(); + render(); + await userEvent.click(screen.getByRole("button")); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it("is disabled when isLoading is true", () => { + render(); + expect(screen.getByRole("button")).toBeDisabled(); + expect(screen.getByRole("button")).toHaveAttribute("aria-busy", "true"); + }); + + it("applies variant class", () => { + render(); + expect(screen.getByRole("button")).toHaveClass("btn--secondary"); + }); +}); +``` + +### `src/components/Button/Button.stories.tsx` + +```tsx +import type { Meta, StoryObj } from "@storybook/react"; +import { Button } from "./Button"; + +const meta: Meta = { + title: "Components/Button", + component: Button, + tags: ["autodocs"], + argTypes: { + variant: { + control: { type: "select" }, + options: ["primary", "secondary", "ghost", "danger"], + }, + size: { + control: { type: "select" }, + options: ["sm", "md", "lg"], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + variant: "primary", + children: "Button", + }, +}; + +export const Secondary: Story = { + args: { + variant: "secondary", + children: "Button", + }, +}; + +export const Loading: Story = { + args: { + isLoading: true, + children: "Loading...", + }, +}; +``` + +### `src/setupTests.ts` + +```typescript +import "@testing-library/jest-dom"; +``` + +### `.storybook/main.ts` + +```typescript +import type { StorybookConfig } from "@storybook/react-webpack5"; + +const config: StorybookConfig = { + stories: ["../src/**/*.stories.@(ts|tsx|mdx)"], + addons: [ + "@storybook/addon-essentials", + "@storybook/addon-interactions", + ], + framework: { + name: "@storybook/react-webpack5", + options: {}, + }, + docs: { + autodocs: "tag", + }, + typescript: { + check: true, + }, +}; + +export default config; +``` + +## Getting Started + +1. Copy this template or clone from the source generator. +2. Install dependencies: + ```bash + npm install + ``` +3. Start the development server: + ```bash + npm start + ``` +4. Run tests: + ```bash + npm test + ``` +5. Start Storybook for component documentation: + ```bash + npm run storybook + ``` +6. Build the library for distribution: + ```bash + npm run build && npm run build:types + ``` + +## Features + +- TypeScript 5.x with strict mode enabled +- React 18 with JSX transform (`react-jsx`) — no need to import React in every file +- Webpack 5 bundling with hot module replacement in development +- Babel transpilation supporting modern JS/TS/TSX +- Jest + React Testing Library for unit and component testing +- Storybook 7 for interactive component documentation +- ESLint + Prettier for code quality and formatting +- `forwardRef` pattern on components for ref forwarding +- Path alias `@/` mapped to `src/` for clean imports +- UMD output for broad compatibility as a distributable library +- CSS Modules support via css-loader diff --git a/skills/typescript-coder/assets/typescript-tsx-docgen.md b/skills/typescript-coder/assets/typescript-tsx-docgen.md new file mode 100644 index 000000000..917590ee7 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-tsx-docgen.md @@ -0,0 +1,502 @@ +# TypeScript TSX Component Library with Documentation (tsx-docgen) + +> A TypeScript React component library template based on `generator-tsx-docgen`. Produces a +> publishable NPM component library with Rollup bundling, TypeDoc API documentation +> generation, Storybook interactive component explorer, Jest + React Testing Library tests, +> and strict TypeScript throughout. + +## License + +See the [generator-tsx-docgen npm package](https://www.npmjs.com/package/generator-tsx-docgen) +for license terms. + +## Source + +- [generator-tsx-docgen on npm](https://www.npmjs.com/package/generator-tsx-docgen) + +## Project Structure + +``` +my-component-lib/ +├── src/ +│ ├── components/ +│ │ ├── Button/ +│ │ │ ├── Button.tsx +│ │ │ ├── Button.types.ts +│ │ │ ├── Button.module.css +│ │ │ ├── Button.stories.tsx +│ │ │ ├── Button.test.tsx +│ │ │ └── index.ts +│ │ ├── Input/ +│ │ │ ├── Input.tsx +│ │ │ ├── Input.types.ts +│ │ │ ├── Input.test.tsx +│ │ │ └── index.ts +│ │ └── index.ts # Barrel — re-exports all components +│ ├── hooks/ +│ │ └── useDebounce.ts +│ ├── types/ +│ │ └── common.types.ts +│ └── index.ts # Library entry point +├── .storybook/ +│ ├── main.ts +│ └── preview.ts +├── docs/ # Generated TypeDoc output (gitignored) +├── dist/ # Rollup build output (gitignored) +├── jest.config.ts +├── package.json +├── rollup.config.mjs +├── tsconfig.json +├── tsconfig.build.json +└── typedoc.json +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-component-lib", + "version": "1.0.0", + "description": "TypeScript React component library with documentation generation", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts" + } + }, + "files": ["dist"], + "sideEffects": false, + "scripts": { + "build": "rollup -c", + "build:types": "tsc --project tsconfig.build.json --emitDeclarationOnly", + "dev": "rollup -c --watch", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "docs": "typedoc", + "lint": "eslint 'src/**/*.{ts,tsx}'", + "lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix", + "format": "prettier --write 'src/**/*.{ts,tsx,css}'" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + }, + "devDependencies": { + "@storybook/addon-actions": "^7.6.7", + "@storybook/addon-docs": "^7.6.7", + "@storybook/addon-essentials": "^7.6.7", + "@storybook/react": "^7.6.7", + "@storybook/react-vite": "^7.6.7", + "@testing-library/jest-dom": "^6.2.0", + "@testing-library/react": "^14.1.2", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.11", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "prettier": "^3.2.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rollup": "^4.9.5", + "rollup-plugin-dts": "^6.1.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-postcss": "^4.0.2", + "storybook": "^7.6.7", + "ts-jest": "^29.1.4", + "typedoc": "^0.25.7", + "typescript": "^5.3.3", + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-typescript": "^11.1.6" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "bundler", + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "outDir": "./dist" + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "**/*.stories.tsx", "**/*.test.tsx"] +} +``` + +### `tsconfig.build.json` + +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist/types", + "declaration": true, + "emitDeclarationOnly": true + }, + "exclude": ["node_modules", "dist", "**/*.stories.tsx", "**/*.test.tsx", "**/*.spec.tsx"] +} +``` + +### `rollup.config.mjs` + +```js +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import typescript from '@rollup/plugin-typescript'; +import postcss from 'rollup-plugin-postcss'; +import peerDepsExternal from 'rollup-plugin-peer-deps-external'; +import dts from 'rollup-plugin-dts'; +import { readFileSync } from 'fs'; + +const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')); + +export default [ + // Main bundle: ESM + CJS + { + input: 'src/index.ts', + output: [ + { + file: pkg.module, + format: 'esm', + sourcemap: true, + exports: 'named', + }, + { + file: pkg.main, + format: 'cjs', + sourcemap: true, + exports: 'named', + }, + ], + plugins: [ + peerDepsExternal(), + resolve(), + commonjs(), + typescript({ tsconfig: './tsconfig.json', exclude: ['**/*.test.*', '**/*.stories.*'] }), + postcss({ modules: true, extract: false }), + ], + external: ['react', 'react-dom'], + }, + // Type declarations + { + input: 'dist/types/index.d.ts', + output: [{ file: 'dist/types/index.d.ts', format: 'esm' }], + plugins: [dts()], + external: [/\.css$/], + }, +]; +``` + +### `typedoc.json` + +```json +{ + "entryPoints": ["./src/index.ts"], + "out": "./docs", + "tsconfig": "./tsconfig.json", + "name": "My Component Library", + "readme": "./README.md", + "plugin": [], + "excludePrivate": true, + "excludeProtected": false, + "excludeExternals": true, + "categorizeByGroup": true, + "categoryOrder": ["Components", "Hooks", "Types", "*"], + "sort": ["alphabetical"] +} +``` + +### `src/components/Button/Button.types.ts` + +```typescript +import { ButtonHTMLAttributes, ReactNode } from 'react'; + +export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost'; +export type ButtonSize = 'sm' | 'md' | 'lg'; + +/** + * Props for the Button component. + * @category Components + */ +export interface ButtonProps extends ButtonHTMLAttributes { + /** Visual style variant */ + variant?: ButtonVariant; + /** Size preset */ + size?: ButtonSize; + /** Show a loading spinner and disable interaction */ + isLoading?: boolean; + /** Render button as full width */ + fullWidth?: boolean; + /** Left-aligned icon node */ + leftIcon?: ReactNode; + /** Right-aligned icon node */ + rightIcon?: ReactNode; + /** Button label */ + children: ReactNode; +} +``` + +### `src/components/Button/Button.tsx` + +```typescript +import React, { forwardRef } from 'react'; +import styles from './Button.module.css'; +import { ButtonProps } from './Button.types'; + +/** + * Primary UI element for triggering actions. + * + * @example + * ```tsx + * + * ``` + * @category Components + */ +export const Button = forwardRef( + ( + { + variant = 'primary', + size = 'md', + isLoading = false, + fullWidth = false, + leftIcon, + rightIcon, + children, + disabled, + className, + ...rest + }, + ref, + ) => { + const classes = [ + styles.button, + styles[variant], + styles[size], + fullWidth ? styles.fullWidth : '', + isLoading ? styles.loading : '', + className ?? '', + ] + .filter(Boolean) + .join(' '); + + return ( + + ); + }, +); + +Button.displayName = 'Button'; +``` + +### `src/components/Button/Button.test.tsx` + +```typescript +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import { Button } from './Button'; + +describe('Button', () => { + it('renders children', () => { + render(); + expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument(); + }); + + it('is disabled when isLoading is true', () => { + render(); + expect(screen.getByRole('button')).toBeDisabled(); + expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true'); + }); + + it('calls onClick when clicked', async () => { + const handleClick = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button')); + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('does not call onClick when disabled', async () => { + const handleClick = jest.fn(); + render(); + await userEvent.click(screen.getByRole('button')); + expect(handleClick).not.toHaveBeenCalled(); + }); +}); +``` + +### `src/components/Button/Button.stories.tsx` + +```typescript +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { Button } from './Button'; + +const meta: Meta = { + title: 'Components/Button', + component: Button, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + variant: { + control: 'select', + options: ['primary', 'secondary', 'danger', 'ghost'], + }, + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Primary: Story = { + args: { variant: 'primary', children: 'Button' }, +}; + +export const Secondary: Story = { + args: { variant: 'secondary', children: 'Button' }, +}; + +export const Loading: Story = { + args: { isLoading: true, children: 'Loading…' }, +}; + +export const FullWidth: Story = { + args: { fullWidth: true, children: 'Full Width Button' }, +}; +``` + +### `src/index.ts` + +```typescript +// Components +export { Button } from './components/Button'; +export type { ButtonProps, ButtonVariant, ButtonSize } from './components/Button/Button.types'; + +export { Input } from './components/Input'; +export type { InputProps } from './components/Input/Input.types'; + +// Hooks +export { useDebounce } from './hooks/useDebounce'; + +// Types +export type { Size, Variant } from './types/common.types'; +``` + +### `jest.config.ts` + +```typescript +import type { Config } from 'jest'; + +const config: Config = { + testEnvironment: 'jsdom', + setupFilesAfterFramework: ['/jest.setup.ts'], + transform: { + '^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: 'tsconfig.json' }], + }, + moduleNameMapper: { + '\\.module\\.css$': 'identity-obj-proxy', + '\\.css$': '/__mocks__/styleMock.js', + }, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.stories.tsx', + '!src/**/index.ts', + '!src/types/**', + ], +}; + +export default config; +``` + +## Getting Started + +```bash +# 1. Install dependencies +npm install + +# 2. Start Storybook for component development +npm run storybook +# Open http://localhost:6006 + +# 3. Run tests +npm test + +# 4. Generate TypeDoc API documentation +npm run docs +# Open docs/index.html + +# 5. Build the library for distribution +npm run build + +# 6. Build static Storybook site +npm run build-storybook +``` + +## Features + +- Rollup bundling producing both ESM and CJS outputs with source maps +- TypeDoc API documentation generated directly from TSDoc comments and TypeScript types +- Storybook 7 with `autodocs` tag for zero-config component documentation pages +- CSS Modules for scoped component styles, processed by `rollup-plugin-postcss` +- `forwardRef` pattern for all components to support ref forwarding +- Jest + React Testing Library + `@testing-library/user-event` for accessible test queries +- Strict TypeScript with `noUnusedLocals` and `noUnusedParameters` +- Barrel exports from `src/index.ts` for a clean public API surface +- Peer dependency configuration so consumers supply their own React version +- Separate `tsconfig.build.json` for declaration-only emission diff --git a/skills/typescript-coder/assets/typescript-xes-bdf.md b/skills/typescript-coder/assets/typescript-xes-bdf.md new file mode 100644 index 000000000..68f4f2547 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-xes-bdf.md @@ -0,0 +1,455 @@ +# TypeScript XES BDF Project Template + +> A TypeScript project template based on the `generator-xes-bdf` Yeoman generator. Produces +> a structured TypeScript application scaffold with Express-based REST API, dependency +> injection, service/controller layering, and a testing setup. The generator targets teams +> seeking an opinionated, batteries-included TypeScript backend starter. + +## License + +See the [generator-xes-bdf npm package](https://www.npmjs.com/package/generator-xes-bdf) and +its linked repository for license terms. + +## Source + +- [generator-xes-bdf on npm](https://www.npmjs.com/package/generator-xes-bdf) + +## Project Structure + +``` +my-xes-bdf-app/ +├── src/ +│ ├── controllers/ +│ │ └── item.controller.ts +│ ├── services/ +│ │ └── item.service.ts +│ ├── models/ +│ │ └── item.model.ts +│ ├── middleware/ +│ │ ├── auth.middleware.ts +│ │ └── error.middleware.ts +│ ├── config/ +│ │ └── app.config.ts +│ ├── types/ +│ │ └── index.d.ts +│ ├── app.ts +│ └── server.ts +├── tests/ +│ ├── unit/ +│ │ └── item.service.spec.ts +│ └── integration/ +│ └── item.controller.spec.ts +├── .env +├── .env.example +├── .eslintrc.json +├── .prettierrc +├── package.json +├── tsconfig.json +└── README.md +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "my-xes-bdf-app", + "version": "1.0.0", + "description": "TypeScript application scaffolded with generator-xes-bdf", + "main": "dist/server.js", + "scripts": { + "build": "tsc", + "start": "node dist/server.js", + "dev": "ts-node-dev --respawn --transpile-only src/server.ts", + "lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'", + "lint:fix": "eslint 'src/**/*.ts' 'tests/**/*.ts' --fix", + "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'", + "test": "jest --coverage", + "test:watch": "jest --watch", + "test:unit": "jest tests/unit", + "test:integration": "jest tests/integration", + "clean": "rimraf dist" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-validator": "^7.0.1", + "http-status-codes": "^2.3.0" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jest": "^29.5.11", + "@types/node": "^20.11.0", + "@types/supertest": "^6.0.2", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.1.3", + "jest": "^29.7.0", + "prettier": "^3.2.4", + "rimraf": "^5.0.5", + "supertest": "^6.3.4", + "ts-jest": "^29.1.4", + "ts-node-dev": "^2.0.0", + "typescript": "^5.3.3" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} +``` + +### `src/app.ts` + +```typescript +import express, { Application } from 'express'; +import cors from 'cors'; +import { json, urlencoded } from 'express'; +import { errorMiddleware } from './middleware/error.middleware'; +import { ItemController } from './controllers/item.controller'; + +export function createApp(): Application { + const app: Application = express(); + + // Body parsing + app.use(json()); + app.use(urlencoded({ extended: true })); + + // CORS + app.use(cors()); + + // Health check + app.get('/health', (_req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); + }); + + // Routes + const itemController = new ItemController(); + app.use('/api/items', itemController.router); + + // Global error handler (must be last) + app.use(errorMiddleware); + + return app; +} +``` + +### `src/server.ts` + +```typescript +import * as dotenv from 'dotenv'; +dotenv.config(); + +import { createApp } from './app'; +import { AppConfig } from './config/app.config'; + +const app = createApp(); +const config = new AppConfig(); + +app.listen(config.port, () => { + console.log(`Server running on http://localhost:${config.port}`); + console.log(`Environment: ${config.nodeEnv}`); +}); +``` + +### `src/config/app.config.ts` + +```typescript +export class AppConfig { + readonly port: number; + readonly nodeEnv: string; + readonly apiPrefix: string; + + constructor() { + this.port = parseInt(process.env.PORT ?? '3000', 10); + this.nodeEnv = process.env.NODE_ENV ?? 'development'; + this.apiPrefix = process.env.API_PREFIX ?? '/api'; + } + + get isDevelopment(): boolean { + return this.nodeEnv === 'development'; + } + + get isProduction(): boolean { + return this.nodeEnv === 'production'; + } +} +``` + +### `src/models/item.model.ts` + +```typescript +export interface Item { + id: string; + name: string; + description: string; + price: number; + quantity: number; + createdAt: Date; + updatedAt: Date; +} + +export interface CreateItemDto { + name: string; + description: string; + price: number; + quantity: number; +} + +export interface UpdateItemDto { + name?: string; + description?: string; + price?: number; + quantity?: number; +} +``` + +### `src/services/item.service.ts` + +```typescript +import { v4 as uuidv4 } from 'uuid'; +import { Item, CreateItemDto, UpdateItemDto } from '../models/item.model'; + +export class ItemService { + private items: Map = new Map(); + + findAll(): Item[] { + return Array.from(this.items.values()); + } + + findById(id: string): Item | undefined { + return this.items.get(id); + } + + create(dto: CreateItemDto): Item { + const now = new Date(); + const item: Item = { + id: uuidv4(), + ...dto, + createdAt: now, + updatedAt: now, + }; + this.items.set(item.id, item); + return item; + } + + update(id: string, dto: UpdateItemDto): Item | null { + const existing = this.items.get(id); + if (!existing) return null; + const updated: Item = { ...existing, ...dto, updatedAt: new Date() }; + this.items.set(id, updated); + return updated; + } + + delete(id: string): boolean { + return this.items.delete(id); + } +} +``` + +### `src/controllers/item.controller.ts` + +```typescript +import { Router, Request, Response, NextFunction } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { ItemService } from '../services/item.service'; +import { CreateItemDto, UpdateItemDto } from '../models/item.model'; + +export class ItemController { + readonly router: Router; + private readonly service: ItemService; + + constructor() { + this.router = Router(); + this.service = new ItemService(); + this.initRoutes(); + } + + private initRoutes(): void { + this.router.get('/', this.getAll.bind(this)); + this.router.get('/:id', this.getById.bind(this)); + this.router.post('/', this.create.bind(this)); + this.router.patch('/:id', this.update.bind(this)); + this.router.delete('/:id', this.remove.bind(this)); + } + + private getAll(_req: Request, res: Response): void { + res.json(this.service.findAll()); + } + + private getById(req: Request, res: Response): void { + const item = this.service.findById(req.params.id); + if (!item) { + res.status(StatusCodes.NOT_FOUND).json({ message: 'Item not found' }); + return; + } + res.json(item); + } + + private create(req: Request, res: Response): void { + const dto = req.body as CreateItemDto; + const item = this.service.create(dto); + res.status(StatusCodes.CREATED).json(item); + } + + private update(req: Request, res: Response): void { + const dto = req.body as UpdateItemDto; + const item = this.service.update(req.params.id, dto); + if (!item) { + res.status(StatusCodes.NOT_FOUND).json({ message: 'Item not found' }); + return; + } + res.json(item); + } + + private remove(req: Request, res: Response): void { + const deleted = this.service.delete(req.params.id); + if (!deleted) { + res.status(StatusCodes.NOT_FOUND).json({ message: 'Item not found' }); + return; + } + res.status(StatusCodes.NO_CONTENT).send(); + } +} +``` + +### `src/middleware/error.middleware.ts` + +```typescript +import { Request, Response, NextFunction } from 'express'; +import { StatusCodes } from 'http-status-codes'; + +export interface AppError extends Error { + statusCode?: number; +} + +export function errorMiddleware( + err: AppError, + _req: Request, + res: Response, + _next: NextFunction, +): void { + const statusCode = err.statusCode ?? StatusCodes.INTERNAL_SERVER_ERROR; + res.status(statusCode).json({ + message: err.message ?? 'Internal Server Error', + ...(process.env.NODE_ENV === 'development' && { stack: err.stack }), + }); +} +``` + +### `tests/unit/item.service.spec.ts` + +```typescript +import { ItemService } from '../../src/services/item.service'; + +describe('ItemService', () => { + let service: ItemService; + + beforeEach(() => { + service = new ItemService(); + }); + + it('should return an empty list initially', () => { + expect(service.findAll()).toHaveLength(0); + }); + + it('should create a new item', () => { + const dto = { name: 'Test', description: 'Desc', price: 9.99, quantity: 5 }; + const item = service.create(dto); + expect(item.id).toBeDefined(); + expect(item.name).toBe('Test'); + expect(service.findAll()).toHaveLength(1); + }); + + it('should return undefined for a missing item', () => { + expect(service.findById('nonexistent')).toBeUndefined(); + }); + + it('should update an item', () => { + const item = service.create({ name: 'Old', description: '', price: 1, quantity: 1 }); + const updated = service.update(item.id, { name: 'New' }); + expect(updated?.name).toBe('New'); + }); + + it('should delete an item', () => { + const item = service.create({ name: 'Del', description: '', price: 1, quantity: 1 }); + expect(service.delete(item.id)).toBe(true); + expect(service.findAll()).toHaveLength(0); + }); +}); +``` + +### `.env.example` + +``` +PORT=3000 +NODE_ENV=development +API_PREFIX=/api +``` + +## Getting Started + +```bash +# 1. Install dependencies +npm install + +# 2. Copy and configure environment variables +cp .env.example .env + +# 3. Start in development mode (hot reload) +npm run dev + +# 4. Run tests +npm test + +# 5. Build for production +npm run build + +# 6. Start production server +npm start +``` + +## Features + +- Express 4 REST API with typed request/response handlers +- Controller/Service/Model layered architecture +- UUID-based entity identity generation +- Global error middleware with environment-aware stack trace output +- Health check endpoint +- CORS and body parsing pre-configured +- `http-status-codes` for readable HTTP status references +- ESLint + Prettier enforced code style +- Jest unit and integration tests with Supertest +- `ts-node-dev` for fast development reloading +- Strict TypeScript compilation with unused-variable enforcement diff --git a/skills/typescript-coder/assets/typescript-zotero-plugin.md b/skills/typescript-coder/assets/typescript-zotero-plugin.md new file mode 100644 index 000000000..bc60c14c1 --- /dev/null +++ b/skills/typescript-coder/assets/typescript-zotero-plugin.md @@ -0,0 +1,397 @@ +# TypeScript Zotero Plugin Template + +> A TypeScript project starter for building Zotero 7 plugins. Produces a Zotero-compatible plugin using the bootstrap (non-overlay) plugin API, TypeScript compilation, and a structured manifest. Inspired by patterns from the Zotero plugin ecosystem, including projects hosted at retorque.re (Better BibTeX for Zotero by Emiliano Heyns). + +## License + +AGPL-3.0 — The Better BibTeX for Zotero project (retorque.re) is licensed under AGPL-3.0. Review licensing carefully before distributing a plugin based on its patterns. For a permissive alternative, consider MIT; consult the source project's license file. + +## Source + +- [retorque.re (Better BibTeX for Zotero)](https://retorque.re/) +- [Better BibTeX GitHub](https://github.com/retorquere/zotero-better-bibtex) + +## Project Structure + +``` +zotero-my-plugin/ +├── src/ +│ ├── bootstrap.ts ← Plugin lifecycle entry point +│ ├── plugin.ts ← Main plugin class +│ ├── prefs.ts ← Preferences/settings management +│ ├── ui.ts ← Menu and UI registration +│ └── types/ +│ └── zotero.d.ts ← Zotero global type declarations +├── addon/ +│ ├── manifest.json ← Plugin manifest (Zotero 7 / WebExtension-style) +│ ├── prefs/ +│ │ └── prefs.xhtml ← Preferences pane (XUL) +│ └── locale/ +│ └── en-US/ +│ └── addon.ftl ← Fluent localisation strings +├── scripts/ +│ └── build.mjs ← Build/zip script +├── .gitignore +├── package.json +├── tsconfig.json +└── README.md +``` + +## Key Files + +### `package.json` + +```json +{ + "name": "zotero-my-plugin", + "version": "1.0.0", + "description": "A Zotero 7 plugin built with TypeScript", + "scripts": { + "build": "tsc && node scripts/build.mjs", + "watch": "tsc --watch", + "package": "node scripts/build.mjs --zip", + "typecheck": "tsc --noEmit", + "clean": "rimraf build dist" + }, + "devDependencies": { + "@types/node": "^20.8.10", + "rimraf": "^5.0.5", + "typescript": "^5.2.2", + "zotero-types": "^1.3.22" + } +} +``` + +### `tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "build", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "sourceMap": false, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "build", "dist"] +} +``` + +### `addon/manifest.json` + +```json +{ + "manifest_version": 2, + "name": "My Zotero Plugin", + "version": "1.0.0", + "description": "A sample Zotero 7 plugin", + "homepage_url": "https://github.com/yourname/zotero-my-plugin", + "author": "Your Name ", + "applications": { + "zotero": { + "id": "my-plugin@example.com", + "update_url": "https://raw.githubusercontent.com/yourname/zotero-my-plugin/main/update.json", + "strict_min_version": "7.0.0" + } + }, + "icons": { + "32": "icons/icon32.png", + "48": "icons/icon48.png" + }, + "permissions": [], + "browser_specific_settings": { + "zotero": { + "id": "my-plugin@example.com" + } + } +} +``` + +### `src/bootstrap.ts` + +```typescript +/** + * Zotero 7 Bootstrap Plugin Entry Point. + * + * Zotero 7 uses a WebExtension-like bootstrap model. The plugin must export + * these lifecycle functions, which Zotero calls at the appropriate times. + */ + +import { MyPlugin } from "./plugin"; + +let plugin: MyPlugin | null = null; + +/** + * Called when the plugin is first installed. + */ +export function install(_data: { version: string; reason: string }): void { + Zotero.log("my-plugin: install"); +} + +/** + * Called each time Zotero starts (or the plugin is enabled). + * The plugin should register its UI elements and listeners here. + */ +export async function startup(data: { + id: string; + version: string; + rootURI: string; +}): Promise { + Zotero.log(`my-plugin: startup v${data.version}`); + + // Wait for Zotero to be fully initialised before registering UI + await Zotero.initializationPromise; + + plugin = new MyPlugin(data.rootURI); + await plugin.init(); +} + +/** + * Called when Zotero quits or the plugin is disabled. + */ +export function shutdown(_data: { id: string; version: string; reason: string }): void { + Zotero.log("my-plugin: shutdown"); + plugin?.unload(); + plugin = null; +} + +/** + * Called when the plugin is uninstalled. + */ +export function uninstall(_data: { version: string; reason: string }): void { + Zotero.log("my-plugin: uninstall"); +} +``` + +### `src/plugin.ts` + +```typescript +import { registerPrefs } from "./prefs"; +import { registerUI, unregisterUI } from "./ui"; + +export class MyPlugin { + private rootURI: string; + private registeredWindows: Set = new Set(); + + constructor(rootURI: string) { + this.rootURI = rootURI; + } + + async init(): Promise { + Zotero.log("my-plugin: initialising"); + + // Register preferences defaults + registerPrefs(); + + // Register UI elements in all open windows + for (const win of Zotero.getMainWindows()) { + this.addToWindow(win); + } + + // Listen for new windows opening + Zotero.uiReadyPromise.then(() => { + Services.wm.addListener(this.windowListener); + }); + } + + addToWindow(win: Window): void { + if (this.registeredWindows.has(win)) return; + this.registeredWindows.add(win); + registerUI(win, this.rootURI); + } + + removeFromWindow(win: Window): void { + unregisterUI(win); + this.registeredWindows.delete(win); + } + + unload(): void { + Services.wm.removeListener(this.windowListener); + for (const win of this.registeredWindows) { + this.removeFromWindow(win); + } + this.registeredWindows.clear(); + } + + private windowListener = { + onOpenWindow: (xulWindow: Zotero.XULWindow) => { + const win = xulWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) as Window; + win.addEventListener("load", () => this.addToWindow(win), { once: true }); + }, + onCloseWindow: (xulWindow: Zotero.XULWindow) => { + const win = xulWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) as Window; + this.removeFromWindow(win); + }, + }; +} +``` + +### `src/prefs.ts` + +```typescript +const PREF_PREFIX = "extensions.my-plugin"; + +export function registerPrefs(): void { + const defaults = Services.prefs.getDefaultBranch(""); + defaults.setBoolPref(`${PREF_PREFIX}.enabled`, true); + defaults.setCharPref(`${PREF_PREFIX}.apiKey`, ""); + defaults.setIntPref(`${PREF_PREFIX}.maxResults`, 100); +} + +export function getPref(key: string): T { + const branch = Services.prefs.getBranch(`${PREF_PREFIX}.`); + const type = branch.getPrefType(key); + + if (type === branch.PREF_BOOL) return branch.getBoolPref(key) as T; + if (type === branch.PREF_INT) return branch.getIntPref(key) as T; + if (type === branch.PREF_STRING) return branch.getCharPref(key) as T; + + throw new Error(`Unknown pref type for key: ${key}`); +} + +export function setPref(key: string, value: T): void { + const branch = Services.prefs.getBranch(`${PREF_PREFIX}.`); + if (typeof value === "boolean") branch.setBoolPref(key, value); + else if (typeof value === "number") branch.setIntPref(key, value); + else branch.setCharPref(key, value); +} +``` + +### `src/ui.ts` + +```typescript +export function registerUI(win: Window, rootURI: string): void { + const doc = win.document; + + // Add a menu item to the Tools menu + const toolsMenu = doc.getElementById("menu_ToolsPopup"); + if (!toolsMenu) return; + + const menuItem = doc.createXULElement("menuitem"); + menuItem.id = "my-plugin-menu-item"; + menuItem.setAttribute("label", "My Plugin"); + menuItem.setAttribute("oncommand", ""); + menuItem.addEventListener("command", () => { + openPluginDialog(win, rootURI); + }); + + toolsMenu.appendChild(menuItem); +} + +export function unregisterUI(win: Window): void { + const doc = win.document; + doc.getElementById("my-plugin-menu-item")?.remove(); +} + +function openPluginDialog(win: Window, rootURI: string): void { + win.openDialog( + `${rootURI}content/dialog.xhtml`, + "my-plugin-dialog", + "chrome,centerscreen,resizable=yes" + ); +} +``` + +### `src/types/zotero.d.ts` + +```typescript +/** + * Minimal ambient declarations for Zotero globals. + * For comprehensive typings, install the `zotero-types` package. + */ + +declare const Zotero: { + log: (msg: string) => void; + initializationPromise: Promise; + uiReadyPromise: Promise; + getMainWindows: () => Window[]; + XULWindow: unknown; +}; + +declare const Services: { + prefs: { + getDefaultBranch: (root: string) => mozIBranch; + getBranch: (root: string) => mozIBranch; + }; + wm: { + addListener: (listener: unknown) => void; + removeListener: (listener: unknown) => void; + }; +}; + +declare const Ci: { + nsIInterfaceRequestor: unknown; + nsIDOMWindow: unknown; +}; + +interface mozIBranch { + PREF_BOOL: number; + PREF_INT: number; + PREF_STRING: number; + getPrefType: (key: string) => number; + getBoolPref: (key: string) => boolean; + getIntPref: (key: string) => number; + getCharPref: (key: string) => string; + setBoolPref: (key: string, value: boolean) => void; + setIntPref: (key: string, value: number) => void; + setCharPref: (key: string, value: string) => void; +} +``` + +### `addon/locale/en-US/addon.ftl` + +``` +# Fluent localisation file + +my-plugin-menu-label = My Plugin +my-plugin-prefs-title = My Plugin Preferences +my-plugin-prefs-enabled = Enable My Plugin +my-plugin-prefs-api-key = API Key: +``` + +## Getting Started + +1. Install dependencies: + ```bash + npm install + ``` +2. Install the `zotero-types` package for comprehensive Zotero API typings: + ```bash + npm install --save-dev zotero-types + ``` +3. Compile TypeScript: + ```bash + npm run build + ``` +4. Package into a `.xpi` file for installation: + ```bash + npm run package + ``` +5. In Zotero 7, install via **Tools > Add-ons > Install Add-on From File** and select the `.xpi`. + +## Features + +- Zotero 7 bootstrap (non-overlay) plugin architecture — no legacy XUL overlay required +- TypeScript 5.x compilation to ES2020 for Zotero's SpiderMonkey runtime +- Plugin lifecycle hooks: `install`, `startup`, `shutdown`, `uninstall` +- Window listener pattern to register UI in all existing and future main windows +- XUL menu item registration and cleanup via `registerUI` / `unregisterUI` +- Preferences management via `Services.prefs` with typed `getPref` / `setPref` helpers +- Fluent (`.ftl`) localisation file for translatable strings +- WebExtension-style `manifest.json` for Zotero 7 plugin metadata +- Ambient type declarations for Zotero globals (supplemented by `zotero-types`) diff --git a/skills/typescript-coder/references/typescript-basics.md b/skills/typescript-coder/references/basics.md similarity index 70% rename from skills/typescript-coder/references/typescript-basics.md rename to skills/typescript-coder/references/basics.md index 77b1d17d3..90138f8f0 100644 --- a/skills/typescript-coder/references/typescript-basics.md +++ b/skills/typescript-coder/references/basics.md @@ -2,7 +2,8 @@ ## TypeScript Tutorial -- Reference [Tutorial](https://www.w3schools.com/typescript/index.php) +- 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 @@ -12,9 +13,14 @@ console.log('Hello World!'); ``` -- Reference [Getting Started](https://www.w3schools.com/typescript/typescript_getstarted.php) +- 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: +### Installing Compiler ```bash npm install typescript --save-dev @@ -34,7 +40,7 @@ Version 4.5.5 tsc: The TypeScript Compiler - Version 4.5.5 ``` -### Configuring compiler: +### Configuring compiler ```bash npx tsc --init @@ -50,7 +56,7 @@ Created a new tsconfig.json with: forceConsistentCasingInFileNames: true ``` -### Configuration example: +### Configuration example ```json { @@ -61,7 +67,7 @@ Created a new tsconfig.json with: } ``` -### Your First Program: +### Your First Program ```ts function greet(name: string): string { @@ -72,13 +78,13 @@ const message: string = greet("World"); console.log(message); ``` -### Compile and run: +### Compile and run ```bash npx tsc hello.ts ``` -### Compiled JavaScript output: +### Compiled JavaScript output ```js function greet(name) { @@ -99,16 +105,18 @@ Hello, World! ## TypeScript Simple Types -- Reference [Simple Types](https://www.w3schools.com/typescript/typescript_simple_types.php) +- 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: +### Boolean ```ts let isActive: boolean = true; let hasPermission = false; // TypeScript infers 'boolean' type ``` -### Number: +### Number ```ts let decimal: number = 6; @@ -118,7 +126,7 @@ let octal: number = 0o744; // Octal let float: number = 3.14; // Floating point ``` -### String: +### String ```ts let color: string = "blue"; @@ -127,14 +135,14 @@ let age: number = 30; let sentence: string = `Hello, my name is ${fullName} and I'll be ${age + 1} next year.`; ``` -### BigInt (ES2020+): +### BigInt (ES2020+) ```ts const bigNumber: bigint = 9007199254740991n; const hugeNumber = BigInt(9007199254740991); // Alternative syntax ``` -### Symbol: +### Symbol ```ts const uniqueKey: symbol = Symbol('description'); @@ -146,9 +154,11 @@ console.log(obj[uniqueKey]); // "This is a unique property" ## TypeScript Explicit Types and Inference -- Reference [Explicit Types and Inference](https://www.w3schools.com/typescript/typescript_explicit_inference.php) +- 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: +### Explicit Type Annotations ```ts // String @@ -176,8 +186,7 @@ greet(42); // Error: Argument of type '42' is not assignable to parameter of type 'string' ``` - -### Type Inference: +### Type Inference ```ts // TypeScript infers 'string' @@ -209,7 +218,7 @@ console.log(user.email); // Error: Property 'email' does not exist ``` -### Type Safety in Action: +### Type Safety in Action ```ts let username: string = "alice"; @@ -253,9 +262,11 @@ something = 42; // No error ## TypeScript Special Types -- Reference [Special Types](https://www.w3schools.com/typescript/typescript_special_types.php) +- 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: +### Type: any ```ts let u = true; @@ -271,7 +282,7 @@ v = "string"; // no error as it can be "any" type Math.round(v); // no error as it can be "any" type ``` -### Type: unknown: +### Type: unknown ```ts let w: unknown = 1; @@ -290,7 +301,8 @@ if(typeof w === 'object' && w !== null) { // 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: + +### Type: never ```ts function throwError(message: string): never { @@ -320,7 +332,7 @@ let x: never = true; // Error: Type 'boolean' is not assignable to type 'never'. ``` -### Type: undefined & null: +### Type: undefined & null ```ts let y: undefined = undefined; diff --git a/skills/typescript-coder/references/typescript-classes.md b/skills/typescript-coder/references/classes.md similarity index 87% rename from skills/typescript-coder/references/typescript-classes.md rename to skills/typescript-coder/references/classes.md index 8a1fa7c35..c541008a2 100644 --- a/skills/typescript-coder/references/typescript-classes.md +++ b/skills/typescript-coder/references/classes.md @@ -2,7 +2,9 @@ ## TypeScript Classes -- Reference [Classes](https://www.w3schools.com/typescript/typescript_classes.php) +- 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 @@ -175,9 +177,12 @@ class Rectangle extends Polygon { } } ``` + ## TypeScript Basic Generics -- Reference [Basic Generics](https://www.w3schools.com/typescript/typescript_basic_generics.php) +- 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 @@ -256,9 +261,11 @@ function createLoggedPair( return [v1, v2]; } ``` + ## TypeScript Utility Types -- Reference [Utility Types](https://www.w3schools.com/typescript/typescript_utility_types.php) +- 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 diff --git a/skills/typescript-coder/references/typescript-elements.md b/skills/typescript-coder/references/elements.md similarity index 65% rename from skills/typescript-coder/references/typescript-elements.md rename to skills/typescript-coder/references/elements.md index 1ea20cf0c..5653298bf 100644 --- a/skills/typescript-coder/references/typescript-elements.md +++ b/skills/typescript-coder/references/elements.md @@ -2,22 +2,23 @@ ## TypeScript Arrays -- Reference [Arrays](https://www.w3schools.com/typescript/typescript_arrays.php) +- 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: +### Elements Syntax ```ts const names: string[] = []; names.push("Dylan"); // no error -// names.push(3); +// names.push(3); // Error: Argument of type 'number' is not assignable to parameter of type 'string'. ``` -### Readonly: +### Readonly ```ts const names: readonly string[] = ["Dylan"]; -names.push("Jack"); +names.push("Jack"); // Error: Property 'push' does not exist on type 'readonly string[]'. // try removing the readonly modifier and see if it works? ``` @@ -25,16 +26,18 @@ names.push("Jack"); ```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"); +// 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 [Tuples](https://www.w3schools.com/typescript/typescript_tuples.php) +- 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: +### Typed Arrays ```ts // define our tuple @@ -52,7 +55,7 @@ let ourTuple: [number, boolean, string]; ourTuple = [false, 'Coding God was mistaken', 5]; ``` -### Readonly Tuple: +### Readonly Tuple ```ts // define our tuple @@ -66,13 +69,13 @@ console.log(ourTuple); ```ts // define our readonly tuple -const ourReadonlyTuple: readonly [number, boolean, string] = +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: +### Named Tuples ```ts const graph: [x: number, y: number] = [55.2, 41.3]; @@ -82,11 +85,14 @@ const graph: [x: number, y: number] = [55.2, 41.3]; const graph: [number, number] = [55.2, 41.3]; const [x, y] = graph; ``` + ## TypeScript Object Types -- Reference [Object Types](https://www.w3schools.com/typescript/typescript_object_types.php) +- 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: +### Elements Syntax ```ts const car: { type: string, model: string, year: number } = { @@ -96,22 +102,22 @@ const car: { type: string, model: string, year: number } = { }; ``` -### Type Inference: +### Type Inference ```ts const car = { type: "Toyota", }; car.type = "Ford"; // no error -car.type = 2; +car.type = 2; // Error: Type 'number' is not assignable to type 'string'. ``` -### Optional Properties: +### Optional Properties ```ts -const car: { type: string, mileage: number } = { - // Error: Property 'mileage' is missing in type '{ type: string;}' +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", }; @@ -125,19 +131,21 @@ const car: { type: string, mileage?: number } = { // no error car.mileage = 2000; ``` -### Index Signatures: +### Index Signatures ```ts const nameAgeMap: { [index: string]: number } = {}; nameAgeMap.Jack = 25; // no error -nameAgeMap.Mark = "Fifty"; +nameAgeMap.Mark = "Fifty"; // Error: Type 'string' is not assignable to type 'number'. ``` + ## TypeScript Enums -- Reference [Enums](https://www.w3schools.com/typescript/typescript_enums.php) +- 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: +### Numeric Enums - Default ```ts enum CardinalDirections { @@ -150,11 +158,11 @@ let currentDirection = CardinalDirections.North; // logs 0 console.log(currentDirection); // throws error as 'North' is not a valid enum -currentDirection = 'North'; +currentDirection = 'North'; // Error: "North" is not assignable to type 'CardinalDirections'. ``` -### Numeric Enums - Initialized: +### Numeric Enums - Initialized ```ts enum CardinalDirections { @@ -169,7 +177,7 @@ console.log(CardinalDirections.North); console.log(CardinalDirections.West); ``` -### Numeric Enums - Fully Initialized: +### Numeric Enums - Fully Initialized ```ts enum StatusCodes { @@ -184,7 +192,7 @@ console.log(StatusCodes.NotFound); console.log(StatusCodes.Success); ``` -### String Enums: +### String Enums ```ts enum CardinalDirections { @@ -198,11 +206,14 @@ console.log(CardinalDirections.North); // logs "West" console.log(CardinalDirections.West); ``` + ## TypeScript Type Aliases and Interfaces -- Reference [Type Aliases and Interfaces](https://www.w3schools.com/typescript/typescript_aliases_and_interfaces.php) +- 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: +### Type Aliases ```ts type CarYear = number @@ -233,7 +244,7 @@ type Status = "success" | "error"; let response: Status = "success"; ``` -### Interfaces: +### Interfaces ```ts interface Rectangle { @@ -249,10 +260,10 @@ const rectangle: Rectangle = { ```ts interface Animal { - name: string; + name: string; } interface Animal { - age: number; + age: number; } const dog: Animal = { name: "Fido", @@ -260,7 +271,7 @@ const dog: Animal = { }; ``` -### Extending Interfaces: +### Extending Interfaces ```ts interface Rectangle { @@ -281,9 +292,11 @@ const coloredRectangle: ColoredRectangle = { ## TypeScript Union Types -- Reference [Union Types](https://www.w3schools.com/typescript/typescript_union_types.php) +- 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): +### Union | (OR) ```ts function printStatusCode(code: string | number) { @@ -293,12 +306,12 @@ printStatusCode(404); printStatusCode('404'); ``` -### Type Guards: +### 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'. + 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' } ``` @@ -308,9 +321,10 @@ function printStatusCode(code: string | number) { ## TypeScript Functions -- Reference [Functions](https://www.w3schools.com/typescript/typescript_functions.php) +- 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: +### Return Type ```ts // the `: number` here specifies that this function returns a number @@ -319,7 +333,7 @@ function getTime(): number { } ``` -### Void Return Type: +### Void Return Type ```ts function printHello(): void { @@ -327,7 +341,7 @@ function printHello(): void { } ``` -### Parameters: +### Parameters ```ts function multiply(a: number, b: number) { @@ -335,7 +349,7 @@ function multiply(a: number, b: number) { } ``` -### Optional Parameters: +### Optional Parameters ```ts // the `?` operator here marks parameter `c` as optional @@ -344,7 +358,7 @@ function add(a: number, b: number, c?: number) { } ``` -### Default Parameters: +### Default Parameters ```ts function pow(value: number, exponent: number = 10) { @@ -352,7 +366,7 @@ function pow(value: number, exponent: number = 10) { } ``` -### Named Parameters: +### Named Parameters ```ts function divide({ dividend, divisor }: { dividend: number, divisor: number }) { @@ -360,7 +374,7 @@ function divide({ dividend, divisor }: { dividend: number, divisor: number }) { } ``` -### Rest Parameters: +### Rest Parameters ```ts function add(a: number, b: number, ...rest: number[]) { @@ -368,7 +382,7 @@ function add(a: number, b: number, ...rest: number[]) { } ``` -### Type Alias: +### Type Alias ```ts type Negate = (value: number) => number; @@ -380,26 +394,27 @@ const negateFunction: Negate = (value) => value * -1; ## TypeScript Casting -- Reference [Casting](https://www.w3schools.com/typescript/typescript_casting.php) +- 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: +### Casting with as ```ts let x: unknown = 'hello'; console.log((x as string).length); ``` -### Casting with <>: +### Casting with <> ```ts let x: unknown = 'hello'; console.log((x).length); ``` -### Force Casting: +### Force Casting ```ts let x = 'hello'; -console.log(((x as unknown) as number).length); +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/typescript-keywords.md b/skills/typescript-coder/references/keywords.md similarity index 61% rename from skills/typescript-coder/references/typescript-keywords.md rename to skills/typescript-coder/references/keywords.md index a4619a07b..618b96cf1 100644 --- a/skills/typescript-coder/references/typescript-keywords.md +++ b/skills/typescript-coder/references/keywords.md @@ -2,9 +2,11 @@ ## TypeScript Keyof -- Reference [Keyof](https://www.w3schools.com/typescript/typescript_keyof.php) +- 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: +### Parameters ```ts interface Person { @@ -30,11 +32,14 @@ function createStringPair(property: keyof StringMap, value: string): StringMap { return { [property]: value }; } ``` + ## TypeScript Null & Undefined -- Reference [Null & Undefined](https://www.w3schools.com/typescript/typescript_null.php) +- 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: +### Types ```ts let value: string | undefined | null = null; @@ -42,7 +47,7 @@ value = 'hello'; value = undefined; ``` -### Optional Chaining: +### Optional Chaining ```ts interface House { @@ -67,7 +72,7 @@ let home: House = { printYardSize(home); // Prints 'No yard' ``` -### Nullish Coalescing: +### Nullish Coalescing ```ts function printMileage(mileage: number | null | undefined) { @@ -78,7 +83,7 @@ printMileage(null); // Prints 'Mileage: Not Available' printMileage(0); // Prints 'Mileage: 0' ``` -### Null Assertion: +### Null Assertion ```ts function getValue(): string | undefined { @@ -93,11 +98,13 @@ let array: number[] = [1, 2, 3]; let value = array[0]; // with `noUncheckedIndexedAccess` this has the type `number | undefined` ``` + ## TypeScript Definitely Typed -- Reference [Definitely Typed](https://www.w3schools.com/typescript/typescript_definitely_typed.php) +- 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: +### Installation ```bash npm install --save-dev @types/jquery @@ -105,9 +112,10 @@ npm install --save-dev @types/jquery ## TypeScript 5.x Update -- Reference [5.x Update](https://www.w3schools.com/typescript/typescript_5_updates.php) +- 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: +### Template Literal Types ```ts type Color = "red" | "green" | "blue"; @@ -117,7 +125,7 @@ type HexColor = `#${string}`; let myColor: HexColor<"blue"> = "#0000FF"; ``` -### Index Signature Labels: +### Index Signature Labels ```ts type DynamicObject = { [key: `dynamic_${string}`]: string }; diff --git a/skills/typescript-coder/references/typescript-miscellaneous.md b/skills/typescript-coder/references/miscellaneous.md similarity index 96% rename from skills/typescript-coder/references/typescript-miscellaneous.md rename to skills/typescript-coder/references/miscellaneous.md index c3c257ca6..2eba82c9b 100644 --- a/skills/typescript-coder/references/typescript-miscellaneous.md +++ b/skills/typescript-coder/references/miscellaneous.md @@ -2,7 +2,9 @@ ## TypeScript Async Programming -- Reference [Async Programming](https://www.w3schools.com/typescript/typescript_async.php) +- 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 @@ -411,7 +413,9 @@ async function consumeNumbers() { ## TypeScript Decorators -- Reference [Decorators](https://www.w3schools.com/typescript/typescript_decorators.php) +- 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 @@ -982,7 +986,9 @@ class Demo { ## TypeScript in JavaScript Projects (*JSDoc*) -- Reference [JavaScript Projects (*JSDoc*)](https://www.w3schools.com/typescript/typescript_jsdoc.php) +- 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 @@ -1285,7 +1291,8 @@ initialize(config); ## TypeScript Migration -- Reference [Migration](https://www.w3schools.com/typescript/typescript_migration.php) +- 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 @@ -1484,7 +1491,9 @@ function getUser(id: number): User { ## TypeScript Error Handling -- Reference [Error Handling](https://www.w3schools.com/typescript/typescript_error_handling.php) +- 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 @@ -1758,7 +1767,9 @@ async function loadUser() { ## TypeScript Best Practices -- Reference [Best Practices](https://www.w3schools.com/typescript/typescript_best_practices.php) +- 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 diff --git a/skills/typescript-coder/references/typescript-projects.md b/skills/typescript-coder/references/projects.md similarity index 84% rename from skills/typescript-coder/references/typescript-projects.md rename to skills/typescript-coder/references/projects.md index 50d65a9b1..6b1cac9df 100644 --- a/skills/typescript-coder/references/typescript-projects.md +++ b/skills/typescript-coder/references/projects.md @@ -2,9 +2,12 @@ ## TypeScript Configuration -- Reference [Configuration](https://www.w3schools.com/typescript/typescript_config.php) +- Reference material for [Configuration](https://www.w3schools.com/typescript/typescript_config.php) +- See [What is a tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) for additional information +- See [TSConfig Reference](https://www.typescriptlang.org/tsconfig/) for additional information +- See [Integrating with Build Tools](https://www.typescriptlang.org/docs/handbook/integrating-with-build-tools.html) for additional information -### Basic Configuration: +### Basic Configuration ```json { @@ -16,7 +19,7 @@ } ``` -### Advanced Configuration: +### Advanced Configuration ```json { @@ -36,7 +39,7 @@ } ``` -### Initialize Configuration: +### Initialize Configuration ```bash tsc --init @@ -44,9 +47,11 @@ tsc --init ## TypeScript Node.js -- Reference [Node.js](https://www.w3schools.com/typescript/typescript_nodejs.php) +- Reference material for [Node.js](https://www.w3schools.com/typescript/typescript_nodejs.php) +- See [TypeScript Tooling in 5 minutes](https://www.typescriptlang.org/docs/handbook/typescript-tooling-in-5-minutes.html) for additional information +- See [Modules](https://www.typescriptlang.org/docs/handbook/2/modules.html) for additional information -### Setting Up Node.js Project: +### Setting Up Node.js Project ```bash mkdir my-ts-node-app @@ -56,14 +61,14 @@ npm install typescript @types/node --save-dev npx tsc --init ``` -### Create Project Structure: +### Create Project Structure ```bash mkdir src # later add files like: src/server.ts, src/middleware/auth.ts ``` -### TypeScript Configuration: +### TypeScript Configuration ```json { @@ -85,14 +90,14 @@ mkdir src } ``` -### Install Dependencies: +### Install Dependencies ```bash npm install express body-parser npm install --save-dev ts-node nodemon @types/express ``` -### Project Structure: +### Project Structure ``` my-ts-node-app/ @@ -110,7 +115,7 @@ my-ts-node-app/ tsconfig.json ``` -### Basic Express Server Example: +### Basic Express Server Example ```ts import express, { Request, Response, NextFunction } from 'express'; @@ -175,7 +180,7 @@ app.listen(PORT, () => { }); ``` -### Express Middleware with Authentication: +### Express Middleware with Authentication ```ts import { Request, Response, NextFunction } from 'express'; @@ -203,7 +208,7 @@ export const authenticate = (req: Request, res: Response, next: NextFunction) => routes that rely on authenticate/authorize, resulting in a complete authentication and authorization bypass. Replace the mock decoded assignment with real JWT verification (including signature, expiry, and claims checks) and ensure that invalid or missing tokens - never populate req.user or reach privileged handlers. + never populate req.user or reach privileged handlers. **********************************************************************************************/ // In a real app, verify the JWT token here const decoded = { id: 1, role: 'admin' }; // Mock decoded token @@ -229,7 +234,7 @@ export const authorize = (roles: string[]) => { }; ``` -### Using Middleware in Routes: +### Using Middleware in Routes ```ts // src/server.ts @@ -240,7 +245,7 @@ app.get('/api/admin', authenticate, authorize(['admin']), (req, res) => { }); ``` -### Database Integration with TypeORM - Entity: +### Database Integration with TypeORM - Entity ```ts import { Entity, PrimaryGeneratedColumn, Column, @@ -271,7 +276,7 @@ export class User { } ``` -### Database Configuration: +### Database Configuration ```ts import 'reflect-metadata'; @@ -293,7 +298,7 @@ export const AppDataSource = new DataSource({ }); ``` -### Initialize Database: +### Initialize Database ```ts // src/server.ts @@ -309,7 +314,7 @@ AppDataSource.initialize() }); ``` -### Package Scripts: +### Package Scripts ```json { @@ -323,29 +328,32 @@ AppDataSource.initialize() } ``` -### Development Mode: +### Development Mode ```bash npm run dev ``` -### Production Build: +### Production Build ```bash npm run build npm start ``` -### Run with Source Maps: +### Run with Source Maps ```bash node --enable-source-maps dist/server.js ``` + ## TypeScript React -- Reference [React](https://www.w3schools.com/typescript/typescript_react.php) +- Reference material for [React](https://www.w3schools.com/typescript/typescript_react.php) +- See [JSX](https://www.typescriptlang.org/docs/handbook/jsx.html) for additional information +- See [React & Webpack](https://webpack.js.org/guides/typescript/) for additional information -### Getting Started: +### Getting Started ```bash npm create vite@latest my-app -- --template react-ts @@ -354,7 +362,7 @@ npm install npm run dev ``` -### TypeScript Configuration for React: +### TypeScript Configuration for React ```json { @@ -376,7 +384,7 @@ npm run dev } ``` -### Component Typing: +### Component Typing ```tsx // Greeting.tsx @@ -395,7 +403,7 @@ export function Greeting({ name, age }: GreetingProps) { } ``` -### Event Handlers: +### Event Handlers ```tsx // Input change @@ -415,7 +423,7 @@ function SaveButton() { } ``` -### useState Hook: +### useState Hook ```tsx const [count, setCount] = React.useState(0); @@ -425,7 +433,7 @@ type User = { id: string; name: string }; const [user, setUser] = React.useState(null); ``` -### useRef Hook: +### useRef Hook ```tsx function FocusInput() { @@ -434,7 +442,7 @@ function FocusInput() { } ``` -### Children Props: +### Children Props ```tsx type CardProps = { title: string; children?: React.ReactNode }; @@ -448,7 +456,7 @@ function Card({ title, children }: CardProps) { } ``` -### Generic Fetch Function: +### Generic Fetch Function ```tsx async function fetchJson(url: string): Promise { @@ -465,7 +473,7 @@ async function loadPosts() { } ``` -### Context API with TypeScript: +### Context API with TypeScript ```tsx type Theme = 'light' | 'dark'; @@ -486,14 +494,14 @@ function useTheme() { } ``` -### Vite Environment Types: +### Vite Environment Types ```ts // src/vite-env.d.ts /// ``` -### TypeScript Config for Vite Types: +### TypeScript Config for Vite Types ```json { @@ -503,7 +511,7 @@ function useTheme() { } ``` -### Path Aliases: +### Path Aliases ```json // tsconfig.json @@ -527,16 +535,18 @@ import { formatDate } from '@utils/date'; ## TypeScript Tooling -- Reference [Tooling](https://www.w3schools.com/typescript/typescript_tooling.php) +- Reference material for [Tooling](https://www.w3schools.com/typescript/typescript_tooling.php) +- See [Integrating with Build Tools](https://www.typescriptlang.org/docs/handbook/integrating-with-build-tools.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 -### Install ESLint: +### Install ESLint ```bash # Install ESLint with TypeScript support npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin ``` -### ESLint Configuration: +### ESLint Configuration ```json // .eslintrc.json @@ -562,7 +572,7 @@ npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslin } ``` -### ESLint Scripts: +### ESLint Scripts ```json // package.json @@ -575,14 +585,14 @@ npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslin } ``` -### Install Prettier: +### Install Prettier ```bash # Install Prettier and related packages npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier ``` -### Prettier Configuration: +### Prettier Configuration ```json // .prettierrc @@ -597,7 +607,7 @@ npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier } ``` -### Prettier Ignore File: +### Prettier Ignore File ``` // .prettierignore @@ -608,7 +618,7 @@ dist .vscode ``` -### Integrate Prettier with ESLint: +### Integrate Prettier with ESLint ```json // .eslintrc.json @@ -620,7 +630,7 @@ dist } ``` -### Setup with Vite: +### Setup with Vite ```bash # Create a new project with React + TypeScript @@ -636,7 +646,7 @@ npm install npm run dev ``` -### Webpack Configuration: +### Webpack Configuration ```js // webpack.config.js @@ -679,7 +689,7 @@ module.exports = { }; ``` -### TypeScript Configuration for Build Tools: +### TypeScript Configuration for Build Tools ```json // tsconfig.json @@ -709,7 +719,7 @@ module.exports = { } ``` -### VS Code Settings: +### VS Code Settings ```json // .vscode/settings.json @@ -726,7 +736,7 @@ module.exports = { } ``` -### VS Code Launch Configuration: +### VS Code Launch Configuration ```json // .vscode/launch.json @@ -759,14 +769,14 @@ module.exports = { } ``` -### Install Testing Dependencies: +### Install Testing Dependencies ```bash # Install testing dependencies npm install --save-dev jest @types/jest ts-jest @testing-library/react @testing-library/jest-dom @testing-library/user-event ``` -### Jest Configuration: +### Jest Configuration ```js // jest.config.js @@ -785,7 +795,7 @@ module.exports = { }; ``` -### Example Test File: +### Example Test File ```tsx // src/__tests__/Button.test.tsx diff --git a/skills/typescript-coder/references/typescript-types.md b/skills/typescript-coder/references/types.md similarity index 92% rename from skills/typescript-coder/references/typescript-types.md rename to skills/typescript-coder/references/types.md index 8a3258ba3..e1dbb828a 100644 --- a/skills/typescript-coder/references/typescript-types.md +++ b/skills/typescript-coder/references/types.md @@ -2,9 +2,11 @@ ## TypeScript Advanced Types -- Reference [Advanced Types](https://www.w3schools.com/typescript/typescript_advanced_types.php) +- 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: +### Mapped Types ```ts // Convert all properties to boolean @@ -69,9 +71,7 @@ type MethodsOnly = { }; ``` -## TypeScript Conditional Types - -### Conditional Types: +### Conditional Types ```ts type IsString = T extends string ? true : false; @@ -116,7 +116,7 @@ type FilterStrings = T extends string ? T : never; type Letters = FilterStrings<'a' | 'b' | 1 | 2 | 'c'>; // 'a' | 'b' | 'c' ``` -### Template Literal Types: +### Template Literal Types ```ts type Greeting = `Hello, ${string}`; @@ -171,7 +171,7 @@ type EventHandlers = { }; ``` -### Utility Types: +### Utility Types ```ts // Basic types @@ -224,7 +224,7 @@ type Mutable = { }; ``` -### Recursive Types: +### Recursive Types ```ts // Simple binary tree @@ -286,9 +286,11 @@ type RecursiveFunction = (x: T | RecursiveFunction) => void; ## TypeScript Type Guards -- Reference [Type Guards](https://www.w3schools.com/typescript/typescript_type_guards.php) +- 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: +### typeof Type Guards ```ts // Simple type guard with typeof @@ -307,7 +309,7 @@ const result1 = formatValue(' hello '); // "HELLO" const result2 = formatValue(42.1234); // "42.12" ``` -### instanceof Type Guards: +### instanceof Type Guards ```ts class Bird { @@ -333,7 +335,7 @@ function move(animal: Bird | Fish) { } ``` -### User-Defined Type Guards: +### User-Defined Type Guards ```ts interface Car { @@ -367,7 +369,7 @@ function displayVehicleInfo(vehicle: Car | Motorcycle) { } ``` -### Discriminated Unions: +### Discriminated Unions ```ts interface Circle { @@ -394,7 +396,7 @@ function calculateArea(shape: Shape) { } ``` -### 'in' Operator Type Guards: +### 'in' Operator Type Guards ```ts interface Dog { @@ -416,7 +418,7 @@ function makeSound(animal: Dog | Cat) { } ``` -### Assertion Functions: +### Assertion Functions ```ts // Type assertion function @@ -448,11 +450,12 @@ function processNumber(value: unknown): number { } ``` -## TypeScript Conditional Types +## Advanced Conditional Types -- Reference [Conditional Types](https://www.w3schools.com/typescript/typescript_conditional_types.php) +- 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: +### Basic Conditional Type Syntax ```ts type IsString = T extends string ? true : false; @@ -467,7 +470,7 @@ let a: IsString; // a has type 'true' let b: IsString; // b has type 'false' ``` -### Conditional Types with Unions: +### Conditional Types with Unions ```ts type ToArray = T extends any ? T[] : never; @@ -483,7 +486,7 @@ type StringsOnly = ExtractString; // Result: string | "hello" ``` -### Infer keyword with conditional types: +### Infer keyword with conditional types ```ts // Extract the return type of a function type @@ -502,7 +505,7 @@ type NumberArrayElement = ElementType; // number type StringArrayElement = ElementType; // string ``` -### Built-in Conditional Types: +### Built-in Conditional Types ```ts // Extract - Extracts types from T that are assignable to U @@ -521,7 +524,7 @@ type Params = Parameters<(a: string, b: number) => void>; // [string, number] type Return = ReturnType<() => string>; // string ``` -### Advanced Patterns and Techniques: +### Advanced Patterns and Techniques ```ts // Deeply unwrap Promise types @@ -533,7 +536,7 @@ type B = UnwrapPromise>>; // number type C = UnwrapPromise; // boolean ``` -### Type name mapping: +### Type name mapping ```ts type TypeName = @@ -552,7 +555,7 @@ type T3 = TypeName<() => void>; // "function" type T4 = TypeName; // "object" ``` -### Conditional return types: +### Conditional return types ```ts // A function that returns different types based on input type @@ -581,11 +584,13 @@ const numberResult = processValue(10); // Returns 20 (type is number) const boolResult = processValue(true); // Returns false (type is boolean) ``` -## TypeScript Mapped Types +## Advanced Mapped Types -- Reference [Mapped Types](https://www.w3schools.com/typescript/typescript_mapped_types.php) +- 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: +### Type Syntax Example ```ts // Small example @@ -594,7 +599,7 @@ type PartialPerson = { [P in keyof Person]?: Person[P] }; type ReadonlyPerson = { readonly [P in keyof Person]: Person[P] }; ``` -### Basic Mapped Type Syntax: +### Basic Mapped Type Syntax ```ts // Define an object type @@ -631,7 +636,7 @@ const readonlyPerson: ReadonlyPerson = { // Error: Cannot assign to 'age' because it is a read-only property ``` -### Built-in Mapped Types: +### Built-in Mapped Types ```ts interface User { @@ -666,7 +671,7 @@ type UserRoles = Record<"admin" | "user" | "guest", string>; // Equivalent to: { admin: string; user: string; guest: string; } ``` -### Creating Custom Mapped Types: +### Creating Custom Mapped Types ```ts // Base interface @@ -700,7 +705,7 @@ const productValidator: Validator = { }; ``` -### Modifying Property Modifiers: +### Modifying Property Modifiers ```ts // Base interface with some readonly and optional properties @@ -744,7 +749,7 @@ type RequiredConfig = RequiredProps; */ ``` -### Conditional Mapped Types: +### Conditional Mapped Types ```ts // Base interface @@ -783,7 +788,8 @@ type ApiResponseStringProps = StringPropsOnly; ## TypeScript Type Inference -- Reference [Type Inference](https://www.w3schools.com/typescript/typescript_type_inference.php) +- 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 @@ -986,9 +992,11 @@ const config: { }; ``` -## TypeScript Literal Types +## Advanced Literal Types -- Reference [Literal Types](https://www.w3schools.com/typescript/typescript_literal_types.php) +- 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 @@ -1221,7 +1229,9 @@ const userQuery = query('users', { ## TypeScript Namespaces -- Reference [Namespaces](https://www.w3schools.com/typescript/typescript_namespaces.php) +- 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 @@ -1342,6 +1352,7 @@ button3.display(); ### Multi-file Namespaces **validators.ts:** + ```ts namespace Validation { export interface StringValidator { @@ -1351,6 +1362,7 @@ namespace Validation { ``` **letters-validator.ts:** + ```ts /// namespace Validation { @@ -1365,6 +1377,7 @@ namespace Validation { ``` **zipcode-validator.ts:** + ```ts /// namespace Validation { @@ -1379,6 +1392,7 @@ namespace Validation { ``` **main.ts:** + ```ts /// /// @@ -1403,6 +1417,7 @@ strings.forEach(s => { ``` **Compile:** + ```bash tsc --outFile sample.js main.ts ``` @@ -1532,7 +1547,9 @@ const userService = new UserService(); ## TypeScript Index Signatures -- Reference [Index Signatures](https://www.w3schools.com/typescript/typescript_index_signatures.php) +- 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 @@ -1682,7 +1699,8 @@ interface FixedTypes { ## TypeScript Declaration Merging -- Reference [Declaration Merging](https://www.w3schools.com/typescript/typescript_declaration_merging.php) +- 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 @@ -1878,4 +1896,3 @@ console.log(user.preferences?.theme); const prefs = LibraryModule.getUserPreferences(123); console.log(prefs.notifications); ``` - diff --git a/skills/typescript-coder/references/typescript-cheatsheet.md b/skills/typescript-coder/references/typescript-cheatsheet.md index 9dbe2c01a..35da60575 100644 --- a/skills/typescript-coder/references/typescript-cheatsheet.md +++ b/skills/typescript-coder/references/typescript-cheatsheet.md @@ -528,3 +528,4 @@ type Style = `${Color}-${Size}`; - [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/typescript-configuration.md b/skills/typescript-coder/references/typescript-configuration.md new file mode 100644 index 000000000..f85754c78 --- /dev/null +++ b/skills/typescript-coder/references/typescript-configuration.md @@ -0,0 +1,1054 @@ +# TypeScript Project Configuration + +Reference material for configuring TypeScript projects, the TypeScript compiler, and build tool integration. + +## What is a tsconfig.json + +- Reference material for [What is a tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) + +The presence of a `tsconfig.json` file in a directory indicates that the directory is the root of a TypeScript project. The file specifies the root files and the compiler options required to compile the project. + +**How TypeScript locates a `tsconfig.json`:** + +- When `tsc` is invoked without input files, the compiler searches for `tsconfig.json` starting in the current directory, then each parent directory +- When `tsc` is invoked with input files directly, `tsconfig.json` is ignored +- Use `tsc --project` (or `tsc -p`) to specify an explicit path to a config file + +**Top-level `tsconfig.json` fields:** + +```json +{ + "compilerOptions": { }, + "files": ["src/main.ts", "src/utils.ts"], + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.spec.ts"], + "extends": "./tsconfig.base.json", + "references": [{ "path": "../shared" }] +} +``` + +**`files`** — an explicit list of files to include. If any file cannot be found, an error occurs: + +```json +{ + "compilerOptions": { "outDir": "dist" }, + "files": [ + "core.ts", + "app.ts" + ] +} +``` + +**`include`** — glob patterns for files to include (supports `*`, `**`, `?`): + +```json +{ + "include": ["src/**/*", "tests/**/*"] +} +``` + +- `*` matches any file segment (excludes directory separators) +- `**` matches any directory nesting depth +- `?` matches any single character + +Supported extensions automatically: `.ts`, `.tsx`, `.d.ts`. With `allowJs`: `.js`, `.jsx`. + +**`exclude`** — glob patterns for files to exclude. Defaults to `node_modules`, `bower_components`, `jspm_packages`, and the `outDir` if set: + +```json +{ + "exclude": ["node_modules", "**/*.test.ts", "dist"] +} +``` + +Note: `exclude` only prevents files from being *included* automatically — a file referenced via `import` or a triple-slash directive will still be included. + +**`extends`** — inherit configuration from a base file: + +```json +{ + "extends": "@tsconfig/node18/tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"] +} +``` + +All relative paths in the base file are resolved relative to the base file. Fields from the inheriting config override the base. Arrays (`files`, `include`, `exclude`) are not merged — they replace the base values entirely. + +**`jsconfig.json`** — TypeScript recognizes `jsconfig.json` files as well; they are equivalent to a `tsconfig.json` with `"allowJs": true` set by default. + +## Compiler Options in MSBuild + +- Reference material for [Compiler Options in MSBuild](https://www.typescriptlang.org/docs/handbook/compiler-options-in-msbuild.html) + +TypeScript can be integrated into MSBuild projects (Visual Studio, `.csproj`, `.vbproj`) via the `Microsoft.TypeScript.MSBuild` NuGet package. + +**Install the NuGet package:** + +```xml + +``` + +**TypeScript compiler options in an MSBuild `.csproj` file** are set inside a ``: + +```xml + + + ES2020 + ES2020 + true + true + wwwroot/js + true + false + true + + +``` + +**Common MSBuild TypeScript properties:** + +| MSBuild Property | tsconfig Equivalent | Description | +|---|---|---| +| `TypeScriptTarget` | `target` | ECMAScript output version | +| `TypeScriptModuleKind` | `module` | Module system | +| `TypeScriptOutDir` | `outDir` | Output directory | +| `TypeScriptSourceMap` | `sourceMap` | Emit source maps | +| `TypeScriptStrict` | `strict` | Enable all strict checks | +| `TypeScriptNoImplicitAny` | `noImplicitAny` | Error on implicit `any` | +| `TypeScriptRemoveComments` | `removeComments` | Strip comments | +| `TypeScriptNoEmitOnError` | `noEmitOnError` | Don't emit on errors | +| `TypeScriptDeclaration` | `declaration` | Emit `.d.ts` files | +| `TypeScriptExperimentalDecorators` | `experimentalDecorators` | Enable decorators | +| `TypeScriptjsx` | `jsx` | JSX compilation mode | +| `TypeScriptNoResolve` | `noResolve` | Disable module resolution | +| `TypeScriptPreserveConstEnums` | `preserveConstEnums` | Keep const enum declarations | +| `TypeScriptSuppressImplicitAnyIndexErrors` | `suppressImplicitAnyIndexErrors` | Suppress index implicit any | + +**Using a `tsconfig.json` with MSBuild** (preferred over individual properties): + +```xml + + true + + + + +``` + +**Compile TypeScript as part of the build** automatically by including `.ts` files: + +```xml + + + +``` + +## TSConfig Reference + +- Reference material for [TSConfig Reference](https://www.typescriptlang.org/tsconfig/) + +Comprehensive reference for all TypeScript compiler options available in `tsconfig.json`. + +### Type Checking Options + +```json +{ + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "useUnknownInCatchVariables": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false + } +} +``` + +| Option | Default | Description | +|---|---|---| +| `strict` | `false` | Enable all strict type-checking options | +| `noImplicitAny` | `false` | Error on expressions with an implied `any` type | +| `strictNullChecks` | `false` | `null`/`undefined` not assignable to other types | +| `strictFunctionTypes` | `false` | Stricter checking of function parameter types (contravariant) | +| `strictBindCallApply` | `false` | Strict checking of `bind`, `call`, and `apply` methods | +| `strictPropertyInitialization` | `false` | Properties must be initialized in the constructor | +| `noImplicitThis` | `false` | Error on `this` expressions with an implied `any` type | +| `useUnknownInCatchVariables` | `false` | Catch clause variables are `unknown` instead of `any` | +| `alwaysStrict` | `false` | Parse in strict mode; emit `"use strict"` in output | +| `noUnusedLocals` | `false` | Report errors on unused local variables | +| `noUnusedParameters` | `false` | Report errors on unused function parameters | +| `exactOptionalPropertyTypes` | `false` | Distinguish between `undefined` value and missing key | +| `noImplicitReturns` | `false` | Report error when not all code paths return a value | +| `noFallthroughCasesInSwitch` | `false` | Report errors for fallthrough cases in `switch` | +| `noUncheckedIndexedAccess` | `false` | Include `undefined` in index signature return type | +| `noImplicitOverride` | `false` | Require `override` keyword for overridden methods | + +### Module Options + +```json +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "baseUrl": "./", + "paths": { "@app/*": ["src/*"] }, + "rootDirs": ["src", "generated"], + "typeRoots": ["./typings", "./node_modules/@types"], + "types": ["node", "jest"], + "allowUmdGlobalAccess": false, + "moduleSuffixes": [".ios", ".native", ""], + "allowImportingTsExtensions": true, + "resolvePackageJsonExports": true, + "resolvePackageJsonImports": true, + "customConditions": ["my-condition"], + "resolveJsonModule": true, + "allowArbitraryExtensions": true, + "noResolve": false + } +} +``` + +| Option | Description | +|---|---| +| `module` | Module system: `CommonJS`, `ES2015`–`ES2022`, `ESNext`, `Node16`, `NodeNext`, `Preserve` | +| `moduleResolution` | Resolution strategy: `classic`, `node`, `node16`, `nodenext`, `bundler` | +| `baseUrl` | Base directory for non-relative module names | +| `paths` | Re-map module names to different locations | +| `rootDirs` | Virtual merged directory for module resolution | +| `typeRoots` | Directories to include type definitions from | +| `types` | Only include these `@types` packages globally | +| `resolveJsonModule` | Allow importing `.json` files | +| `esModuleInterop` | Emit compatibility helpers for CommonJS/ES module interop | +| `allowSyntheticDefaultImports` | Allow `import x from 'module'` even without a default export | + +### Emit Options + +```json +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": false, + "sourceMap": true, + "inlineSourceMap": false, + "outFile": "./output.js", + "outDir": "./dist", + "removeComments": false, + "noEmit": false, + "importHelpers": true, + "importsNotUsedAsValues": "remove", + "downlevelIteration": false, + "sourceRoot": "", + "mapRoot": "", + "inlineSources": false, + "emitBOM": false, + "newLine": "lf", + "stripInternal": false, + "noEmitHelpers": false, + "noEmitOnError": true, + "preserveConstEnums": false, + "declarationDir": "./types", + "preserveValueImports": false + } +} +``` + +| Option | Default | Description | +|---|---|---| +| `declaration` | `false` | Generate `.d.ts` files from TS/JS files | +| `declarationMap` | `false` | Generate source maps for `.d.ts` files | +| `emitDeclarationOnly` | `false` | Only output `.d.ts` files, no JS | +| `sourceMap` | `false` | Generate `.js.map` source map files | +| `outDir` | — | Redirect output structure to the directory | +| `outFile` | — | Concatenate and emit output to a single file | +| `removeComments` | `false` | Strip all comments from TypeScript files | +| `noEmit` | `false` | Do not emit compiler output files | +| `noEmitOnError` | `false` | Do not emit if any type checking errors were reported | +| `importHelpers` | `false` | Import helper functions from `tslib` | +| `downlevelIteration` | `false` | Emit more compliant, but verbose JS for iterables | +| `declarationDir` | — | Output directory for generated declaration files | + +### JavaScript Support Options + +```json +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "maxNodeModuleJsDepth": 0 + } +} +``` + +| Option | Default | Description | +|---|---|---| +| `allowJs` | `false` | Allow JavaScript files to be imported in the project | +| `checkJs` | `false` | Enable error reporting in JS files (requires `allowJs`) | +| `maxNodeModuleJsDepth` | `0` | Max depth to search `node_modules` for JS files | + +### Language and Environment Options + +```json +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "jsxFactory": "React.createElement", + "jsxFragmentFactory": "React.Fragment", + "jsxImportSource": "react", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "useDefineForClassFields": true, + "moduleDetection": "force" + } +} +``` + +| Option | Default | Description | +|---|---|---| +| `target` | `ES3` | Set the JS language version: `ES3`, `ES5`, `ES2015`–`ES2022`, `ESNext` | +| `lib` | (based on `target`) | Bundled library declaration files to include | +| `jsx` | — | JSX code generation: `preserve`, `react`, `react-jsx`, `react-jsxdev`, `react-native` | +| `experimentalDecorators` | `false` | Enable experimental support for decorators | +| `emitDecoratorMetadata` | `false` | Emit design-type metadata for decorated declarations | +| `useDefineForClassFields` | `true` (ES2022+) | Use `Object.defineProperty` for class fields | +| `moduleDetection` | `auto` | Strategy for detecting if a file is a script or module | + +### Interop Constraints + +```json +{ + "compilerOptions": { + "isolatedModules": true, + "verbatimModuleSyntax": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "preserveSymlinks": false, + "forceConsistentCasingInFileNames": true + } +} +``` + +| Option | Default | Description | +|---|---|---| +| `isolatedModules` | `false` | Ensure each file can be safely transpiled in isolation | +| `verbatimModuleSyntax` | `false` | Do not transform/elide any imports or exports not marked `type` | +| `esModuleInterop` | `false` | Emit additional JS to ease support for CJS modules | +| `allowSyntheticDefaultImports` | `false` | Allow default imports from non-default-exporting modules | +| `forceConsistentCasingInFileNames` | `false` | Ensure imports have consistent casing | + +### Completeness / Performance Options + +```json +{ + "compilerOptions": { + "skipDefaultLibCheck": false, + "skipLibCheck": true + } +} +``` + +| Option | Default | Description | +|---|---|---| +| `skipLibCheck` | `false` | Skip type checking of all declaration files (`.d.ts`) | +| `skipDefaultLibCheck` | `false` | Skip type checking of default TypeScript lib files | + +### Projects / Incremental Compilation + +```json +{ + "compilerOptions": { + "incremental": true, + "composite": true, + "tsBuildInfoFile": "./.tsbuildinfo", + "disableSourceOfProjectReferenceRedirect": false, + "disableSolutionSearching": false, + "disableReferencedProjectLoad": false + } +} +``` + +| Option | Default | Description | +|---|---|---| +| `incremental` | `false` | Save information about last compilation to speed up future builds | +| `composite` | `false` | Required for project references; enforces constraints for project structure | +| `tsBuildInfoFile` | `.tsbuildinfo` | File path to store incremental compilation information | + +### Output Formatting / Diagnostics + +```json +{ + "compilerOptions": { + "noErrorTruncation": false, + "diagnostics": false, + "extendedDiagnostics": false, + "listFiles": false, + "listEmittedFiles": false, + "traceResolution": false, + "explainFiles": false + } +} +``` + +## tsc CLI Options + +- Reference material for [tsc CLI Options](https://www.typescriptlang.org/docs/handbook/compiler-options.html) + +The TypeScript compiler `tsc` can be invoked from the command line to compile TypeScript files. Most `tsconfig.json` options can also be passed as CLI flags. + +**Basic usage:** + +```bash +# Compile a single file +tsc app.ts + +# Use a specific tsconfig +tsc --project tsconfig.json +tsc -p tsconfig.json + +# Watch mode +tsc --watch +tsc -w + +# Build mode (project references) +tsc --build +tsc -b + +# Type check only (no emit) +tsc --noEmit + +# Show version +tsc --version +tsc -v + +# Show help +tsc --help +tsc -h + +# Print diagnostic information +tsc --diagnostics +``` + +**Override tsconfig from CLI:** + +```bash +# Compile to ES2020, CommonJS modules, strict mode +tsc --target ES2020 --module commonjs --strict + +# Enable source maps and output to dist/ +tsc --sourceMap --outDir dist + +# Check JS files +tsc --allowJs --checkJs +``` + +**Important CLI-only flags (not valid in tsconfig):** + +| Flag | Description | +|---|---| +| `--build` / `-b` | Build project references in the correct dependency order | +| `--clean` | Delete output files from a `--build` invocation | +| `--dry` | Show what `--build` would do without actually building | +| `--force` | Force a rebuild in `--build` mode | +| `--watch` / `-w` | Watch input files for changes | +| `--project` / `-p` | Compile the project in the given directory or tsconfig | +| `--version` / `-v` | Print the compiler's version | +| `--help` / `-h` | Print help message | +| `--listFilesOnly` | Print names of files that would be part of compilation | +| `--generateTrace` | Generate an event trace and types list | + +**Key compiler flags (also available in tsconfig):** + +```bash +tsc --target ES2020 +tsc --module NodeNext +tsc --moduleResolution NodeNext +tsc --strict +tsc --noImplicitAny +tsc --strictNullChecks +tsc --noEmit +tsc --noEmitOnError +tsc --declaration +tsc --sourceMap +tsc --outDir ./dist +tsc --rootDir ./src +tsc --allowJs +tsc --checkJs +tsc --esModuleInterop +tsc --resolveJsonModule +tsc --skipLibCheck +tsc --lib ES2020,DOM +tsc --jsx react-jsx +tsc --incremental +tsc --composite +``` + +**Build a project using `--build`:** + +```bash +# Build with project references +tsc --build tsconfig.json + +# Clean build artifacts +tsc --build --clean + +# Rebuild everything (ignore incremental cache) +tsc --build --force + +# Dry run to see what would be built +tsc --build --dry --verbose +``` + +## Project References + +- Reference material for [Project References](https://www.typescriptlang.org/docs/handbook/project-references.html) + +Project References (introduced in TypeScript 3.0) allow TypeScript programs to be structured into smaller pieces. This improves build times, enforces logical separation between components, and enables code to be organized into new and better ways. + +**Basic project reference configuration:** + +```json +// tsconfig.json (in the root or a consuming project) +{ + "compilerOptions": { + "declaration": true + }, + "references": [ + { "path": "../shared" }, + { "path": "../utils", "prepend": true } + ] +} +``` + +**Referenced project requirements (`composite: true` is required):** + +```json +// shared/tsconfig.json +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "declarationMap": true, + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +} +``` + +**What `composite: true` enforces:** + +- `rootDir` must be set (defaults to directory containing `tsconfig.json`) +- All implementation files must be matched by an `include` pattern or listed in `files` +- `declaration` must be `true` +- `.tsbuildinfo` files are generated for incremental builds + +**Building with project references:** + +```bash +# Build all projects in dependency order +tsc --build + +# Build a specific project +tsc --build src/tsconfig.json + +# Clean all project reference build artifacts +tsc --build --clean + +# Force full rebuild +tsc --build --force + +# Verbose output +tsc --build --verbose +``` + +**Monorepo `tsconfig.json` layout example:** + +``` +project/ + tsconfig.json ← solution-level config + shared/ + tsconfig.json ← composite: true + src/ + index.ts + server/ + tsconfig.json ← references shared + src/ + main.ts + client/ + tsconfig.json ← references shared + src/ + app.ts +``` + +**Solution-level config (no `include`, only `references`):** + +```json +// tsconfig.json +{ + "files": [], + "references": [ + { "path": "shared" }, + { "path": "server" }, + { "path": "client" } + ] +} +``` + +**`prepend` option** — concatenate a project's output before the current project's output (for `outFile` bundling only): + +```json +{ + "references": [ + { "path": "../utils", "prepend": true } + ] +} +``` + +**`disableSourceOfProjectReferenceRedirect`** — for very large projects, use `.d.ts` instead of source files when following project references (faster but less accurate error reporting): + +```json +{ + "compilerOptions": { + "disableSourceOfProjectReferenceRedirect": true + } +} +``` + +**Key benefits of project references:** + +- Faster incremental builds — only rebuild changed projects +- Strong logical boundaries between packages +- Better editor responsiveness in large monorepos +- Supports `--build` mode with dependency ordering +- `declarationMap` enables "go to source" navigation across project boundaries + +## Integrating with Build Tools + +- Reference material for [Integrating with Build Tools](https://www.typescriptlang.org/docs/handbook/integrating-with-build-tools.html) + +TypeScript can be integrated with various JavaScript build tools and bundlers. + +### Babel + +Use `@babel/preset-typescript` to strip TypeScript types using Babel (no type checking — use `tsc --noEmit` separately): + +```bash +npm install --save-dev @babel/preset-typescript @babel/core @babel/cli +``` + +```json +// babel.config.json +{ + "presets": ["@babel/preset-typescript"] +} +``` + +Compile TypeScript with Babel: + +```bash +babel --extensions '.ts,.tsx' src --out-dir dist +``` + +Note: Babel does not type-check. Run `tsc --noEmit` separately for type checking. + +### Webpack + +Using `ts-loader`: + +```bash +npm install --save-dev ts-loader webpack webpack-cli +``` + +```js +// webpack.config.js +module.exports = { + entry: "./src/index.ts", + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: [".tsx", ".ts", ".js"], + }, + output: { + filename: "bundle.js", + path: __dirname + "/dist", + }, +}; +``` + +Using `babel-loader` with `@babel/preset-typescript` (type checking separate): + +```bash +npm install --save-dev babel-loader @babel/core @babel/preset-env @babel/preset-typescript +``` + +```js +// webpack.config.js +module.exports = { + module: { + rules: [ + { + test: /\.(ts|tsx)$/, + use: { + loader: "babel-loader", + options: { + presets: ["@babel/preset-env", "@babel/preset-typescript"], + }, + }, + }, + ], + }, +}; +``` + +### Vite + +Vite uses `esbuild` for TypeScript transpilation (no type checking during build — use `tsc --noEmit` separately): + +```bash +npm create vite@latest my-app -- --template vanilla-ts +``` + +```json +// tsconfig.json (Vite-recommended settings) +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true + } +} +``` + +### Rollup + +Using `@rollup/plugin-typescript`: + +```bash +npm install --save-dev @rollup/plugin-typescript tslib +``` + +```js +// rollup.config.js +import typescript from "@rollup/plugin-typescript"; + +export default { + input: "src/index.ts", + output: { + file: "dist/bundle.js", + format: "cjs", + }, + plugins: [typescript()], +}; +``` + +### Gulp + +Using `gulp-typescript`: + +```bash +npm install --save-dev gulp-typescript +``` + +```js +const gulp = require("gulp"); +const ts = require("gulp-typescript"); + +const tsProject = ts.createProject("tsconfig.json"); + +gulp.task("typescript", function () { + return tsProject.src().pipe(tsProject()).js.pipe(gulp.dest("dist")); +}); +``` + +### Grunt + +Using `grunt-ts`: + +```bash +npm install --save-dev grunt-ts +``` + +```js +// Gruntfile.js +module.exports = function (grunt) { + grunt.initConfig({ + ts: { + default: { + tsconfig: "./tsconfig.json", + }, + }, + }); + grunt.loadNpmTasks("grunt-ts"); + grunt.registerTask("default", ["ts"]); +}; +``` + +### Browserify + +Using `tsify`: + +```bash +npm install --save-dev tsify browserify +``` + +```bash +browserify main.ts -p tsify --debug > bundle.js +``` + +### Jest + +Using `ts-jest` for running TypeScript tests: + +```bash +npm install --save-dev ts-jest @types/jest +``` + +```json +// jest.config.json +{ + "preset": "ts-jest", + "testEnvironment": "node" +} +``` + +Using `@swc/jest` for faster transforms: + +```bash +npm install --save-dev @swc/jest @swc/core +``` + +```json +// jest.config.json +{ + "transform": { + "^.+\\.(t|j)sx?$": "@swc/jest" + } +} +``` + +### MSBuild + +See the [Compiler Options in MSBuild](#compiler-options-in-msbuild) section above. + +## Configuring Watch + +- Reference material for [Configuring Watch](https://www.typescriptlang.org/docs/handbook/configuring-watch.html) + +TypeScript 3.8+ supports `watchOptions` in `tsconfig.json` to configure how the compiler watches files and directories. + +**`watchOptions` configuration:** + +```json +{ + "watchOptions": { + "watchFile": "useFsEvents", + "watchDirectory": "useFsEvents", + "fallbackPolling": "dynamicPriority", + "synchronousWatchDirectory": true, + "excludeDirectories": ["**/node_modules", "_build"], + "excludeFiles": ["build/fileWhichChangesOften.ts"] + } +} +``` + +**`watchFile` strategies** — controls how individual files are watched: + +| Strategy | Description | +|---|---| +| `fixedPollingInterval` | Check files for changes at a fixed interval | +| `priorityPollingInterval` | Check files at different intervals based on heuristics | +| `dynamicPriorityPolling` | Polling interval adjusts dynamically based on change frequency | +| `useFsEvents` (default) | Use OS file system events (`inotify`, `FSEvents`, `ReadDirectoryChangesW`) | +| `useFsEventsOnParentDirectory` | Watch the parent directory instead of individual files | + +**`watchDirectory` strategies** — controls how directory trees are watched: + +| Strategy | Description | +|---|---| +| `fixedPollingInterval` | Check directory for changes at a fixed interval | +| `dynamicPriorityPolling` | Polling interval adjusts dynamically | +| `useFsEvents` (default) | Use OS file system events for directory watching | + +**`fallbackPolling`** — polling strategy to use when OS file system events are unavailable: + +| Value | Description | +|---|---| +| `fixedPollingInterval` | Fixed interval polling | +| `priorityPollingInterval` | Priority-based polling | +| `dynamicPriorityPolling` | Dynamic interval polling (default) | + +**`synchronousWatchDirectory`** — disable deferred watching on directories (useful on systems that don't support recursive watching natively): + +```json +{ + "watchOptions": { + "synchronousWatchDirectory": true + } +} +``` + +**`excludeDirectories`** — reduce the number of directories watched (avoids watching large directories like `node_modules`): + +```json +{ + "watchOptions": { + "excludeDirectories": ["**/node_modules", "dist", ".git"] + } +} +``` + +**`excludeFiles`** — exclude specific files from being watched: + +```json +{ + "watchOptions": { + "excludeFiles": ["src/generated/**/*.ts"] + } +} +``` + +**Environment variable configuration** — TypeScript watch behavior can also be influenced by the `TSC_WATCHFILE` and `TSC_WATCHDIRECTORY` environment variables: + +```bash +# Force polling for file watching +TSC_WATCHFILE=DynamicPriorityPolling tsc --watch + +# Force polling for directory watching +TSC_WATCHDIRECTORY=FixedPollingInterval tsc --watch +``` + +**Recommended watch config for large projects:** + +```json +{ + "watchOptions": { + "watchFile": "useFsEvents", + "watchDirectory": "useFsEvents", + "fallbackPolling": "dynamicPriority", + "excludeDirectories": ["**/node_modules", "dist", "build", ".git"] + } +} +``` + +## Nightly Builds + +- Reference material for [Nightly Builds](https://www.typescriptlang.org/docs/handbook/nightly-builds.html) + +TypeScript publishes nightly builds to npm as the `typescript@next` package, allowing developers to test upcoming features and report bugs before official releases. + +**Install the TypeScript nightly build:** + +```bash +# Install globally +npm install -g typescript@next + +# Install locally in a project +npm install --save-dev typescript@next +``` + +**Verify the installed version:** + +```bash +tsc --version +# Example output: Version 5.x.0-dev.20240115 +``` + +**Using nightly builds in Visual Studio Code:** + +1. Install `typescript@next` locally in the project: + ```bash + npm install --save-dev typescript@next + ``` + +2. Open the VS Code Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) + +3. Run **"TypeScript: Select TypeScript Version..."** + +4. Choose **"Use Workspace Version"** + +This makes VS Code use the project's local TypeScript (the nightly build) instead of the bundled version. + +**Setting workspace TypeScript version via `.vscode/settings.json`:** + +```json +{ + "typescript.tsdk": "node_modules/typescript/lib" +} +``` + +**Nightly builds in CI pipelines:** + +```json +// package.json +{ + "devDependencies": { + "typescript": "next" + }, + "scripts": { + "typecheck": "tsc --noEmit" + } +} +``` + +```yaml +# GitHub Actions example +- name: Install nightly TypeScript + run: npm install typescript@next +- name: Type check + run: npx tsc --noEmit +``` + +**Switching back to a stable release:** + +```bash +# Install specific version +npm install --save-dev typescript@5.3.3 + +# Install latest stable +npm install --save-dev typescript@latest +``` + +**`@next` vs `@rc`:** + +| Tag | Description | +|---|---| +| `typescript@next` | Nightly build — latest development snapshot | +| `typescript@rc` | Release Candidate — nearly stable, pre-release | +| `typescript@latest` | Stable release | +| `typescript@beta` | Beta release — significant features, may have bugs | + +**Checking available versions:** + +```bash +npm dist-tag ls typescript +``` diff --git a/skills/typescript-coder/references/typescript-d.ts-templates.md b/skills/typescript-coder/references/typescript-d.ts-templates.md new file mode 100644 index 000000000..aa9f9959a --- /dev/null +++ b/skills/typescript-coder/references/typescript-d.ts-templates.md @@ -0,0 +1,403 @@ +# TypeScript d.ts Templates + +Template patterns for TypeScript declaration files (.d.ts) from the official TypeScript documentation. + +## Modules .d.ts + +- Reference material for [Modules .d.ts](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-d-ts.html) + +Use this template when writing a declaration file for a module that is consumed via `import` or `require`. This is the most common template for npm packages. + +### Determining What Kind of Module + +Before writing a module declaration, look at the JavaScript source for clues: + +- Uses `module.exports = ...` or `exports.foo = ...` — use `export =` syntax +- Uses `export default` or named `export` statements — use ES module syntax +- Both accessible as a global and via `require` — use `export as namespace` (UMD) + +### Module Template + +```ts +// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] +// Project: [~THE PROJECT NAME~] +// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> + +/*~ This is the module template file. You should rename it to index.d.ts + *~ and place it in a folder with the same name as the module. + *~ For example, if you were writing a file for "super-greeter", this + *~ file should be 'super-greeter/index.d.ts' + */ + +/*~ If this module is a UMD module that exposes a global variable 'myLib' + *~ when loaded outside a module loader environment, declare that global here. + *~ Remove this section if your library is not UMD. + */ +export as namespace myLib; + +/*~ If this module exports functions, declare them like so. + */ +export function myFunction(a: string): string; +export function myOtherFunction(a: number): number; + +/*~ You can declare types that are available via importing the module. + */ +export interface someType { + name: string; + length: number; + extras?: string[]; +} + +/*~ You can declare properties of the module using const, let, or var. + */ +export const myField: number; + +/*~ If there are types, properties, or methods inside dotted names in your + *~ module, declare them inside a 'namespace'. + */ +export namespace subProp { + /*~ For example, given this definition, someone could write: + *~ import { subProp } from 'yourModule'; + *~ subProp.foo(); + *~ or + *~ import * as yourModule from 'yourModule'; + *~ yourModule.subProp.foo(); + */ + export function foo(): void; +} +``` + +### Exporting a Class and Namespace Together + +When the module exports a class as well as related types: + +```ts +export = MyClass; + +declare class MyClass { + constructor(someParam?: string); + someProperty: string[]; + myMethod(opts: MyClass.MyClassMethodOptions): number; +} + +declare namespace MyClass { + export interface MyClassMethodOptions { + width?: number; + height?: number; + } +} +``` + +## Module: Plugin + +- Reference material for [Module: Plugin](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-plugin-d-ts.html) + +Use this template when writing a declaration file for a module that augments another module. A plugin imports a base module and extends its types. + +### When to Use + +- Your library is imported alongside another library and adds capabilities to it +- You call `require("super-greeter")` and then your plugin augments its behavior +- Example: a charting plugin that adds methods to a base chart library + +### Plugin Template + +```ts +// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] +// Project: [~THE PROJECT NAME~] +// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> + +/*~ This is the module plugin template file. You should rename it to index.d.ts + *~ and place it in a folder with the same name as the module. + *~ For example, if you were writing a file for "super-greeter", this + *~ file should be 'super-greeter/index.d.ts' + */ + +/*~ On this line, import the module which this module adds to */ +import { greeter } from "super-greeter"; + +/*~ Here, declare the same module as the one you imported above, + *~ then expand the existing declaration of the greeter function. + */ +declare module "super-greeter" { + /*~ Here, declare the things that are added by your plugin. + *~ You can add new members to existing types. + */ + interface Greeter { + printHello(): void; + } +} +``` + +### Key Points + +- The `import` at the top is required to make this file a module (not a script), which enables `declare module` augmentation. +- The `declare module "super-greeter"` block must exactly match the original module's specifier string. +- Only exported members can be augmented; you cannot add new exports to an existing module. + +## Module: Class + +- Reference material for [Module: Class](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-class-d-ts.html) + +Use this template when the module's primary export is a class (constructor function). The library is used like `new Greeter("hello")`. + +### Usage Example (JavaScript) + +```js +const Greeter = require("super-greeter"); +const greeter = new Greeter("Hello, world"); +greeter.sayHello(); +``` + +### Module: Class Template + +```ts +// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] +// Project: [~THE PROJECT NAME~] +// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> + +/*~ This is the module-class template file. You should rename it to index.d.ts + *~ and place it in a folder with the same name as the module. + *~ For example, if you were writing a file for "super-greeter", this + *~ file should be 'super-greeter/index.d.ts' + */ + +// Note that ES6 modules cannot directly export class objects. +// This file should be imported using the CommonJS-style: +// import x = require('[~THE MODULE~]'); +// +// Alternatively, if --allowSyntheticDefaultImports or +// --esModuleInterop is turned on, this file can also be +// imported as a default import: +// import x from '[~THE MODULE~]'; +// +// Refer to the TypeScript documentation at +// https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require +// to understand common workarounds for this limitation of ES6 modules. + +/*~ This declaration specifies that the class constructor function + *~ is the exported object from the file. + */ +export = MyClass; + +/*~ Write your module's methods and properties in this class */ +declare class MyClass { + constructor(someParam?: string); + someProperty: string[]; + myMethod(opts: MyClass.MyClassMethodOptions): number; +} + +/*~ If you want to expose types from your module as well, you can + *~ place them in this block. Note that if you include this namespace, + *~ the module can be temporarily used as a namespace, although this + *~ isn't a recommended use. + */ +declare namespace MyClass { + /** Documentation comment */ + export interface MyClassMethodOptions { + width?: number; + height?: number; + } +} +``` + +### Key Points + +- `export =` is used because the library uses `module.exports = MyClass` (CommonJS). +- To use this with `import MyClass from "..."` syntax, enable `esModuleInterop` in `tsconfig.json`. +- The `declare namespace MyClass` block lets you export nested types that are accessible as `MyClass.MyClassMethodOptions`. + +## Module: Function + +- Reference material for [Module: Function](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-function-d-ts.html) + +Use this template when the module's primary export is a callable function. The library is used like `myFn(42)` after requiring it. + +### Usage Example (JavaScript) + +```js +const x = require("super-greeter"); +const y = x(42); +const z = x("hello"); +``` + +### Module: Function Template + +```ts +// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] +// Project: [~THE PROJECT NAME~] +// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> + +/*~ This is the module-function template file. You should rename it to index.d.ts + *~ and place it in a folder with the same name as the module. + *~ For example, if you were writing a file for "super-greeter", this + *~ file should be 'super-greeter/index.d.ts' + */ + +/*~ This declaration specifies that the function + *~ is the exported object from the file. + */ +export = MyFunction; + +/*~ This example shows how to have multiple overloads of your function */ +declare function MyFunction(name: string): MyFunction.NamespaceName; +declare function MyFunction(name: string, greeting: string): MyFunction.NamespaceName; + +/*~ If you want to expose types from your module as well, you can + *~ place them in this block. Often you will want to describe the + *~ shape of the return type of the function; that type should + *~ be declared in here as shown. + */ +declare namespace MyFunction { + export interface NamespaceName { + firstName: string; + lastName: string; + } +} +``` + +### Key Points + +- `export =` is required when the module uses `module.exports = myFunction`. +- Overloads are declared as separate `declare function` statements before the namespace. +- The `declare namespace MyFunction` block is merged with the function declaration, letting the function have properties. + +## Global .d.ts + +- Reference material for [Global .d.ts](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-d-ts.html) + +Use this template for libraries loaded via a `` as the installation method +- Usage examples with no `import`/`require` statement +- References to a global like `$` or `_` with no import + +### Global Template + +```ts +// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] +// Project: [~THE PROJECT NAME~] +// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> + +/*~ If this library is callable (e.g. can be invoked as myLib(3)), + *~ include those call signatures here. + *~ Otherwise, delete this section. + */ +declare function myLib(a: string): string; +declare function myLib(a: number): number; + +/*~ If you want the name of this library to be a valid type name, + *~ you can do so here. + *~ + *~ For example, this allows us to write 'var x: myLib'. + *~ Be sure this actually makes sense! If it doesn't, just + *~ delete this declaration and add types inside the namespace below. + */ +interface myLib { + name: string; + length: number; + extras?: string[]; +} + +/*~ If your library has properties exposed on a global variable, + *~ place them here. + *~ You should also place types (interfaces and type aliases) here. + */ +declare namespace myLib { + //~ We can write 'myLib.timeout = 50' + let timeout: number; + //~ We can access 'myLib.version', but not change it + const version: string; + //~ There's some class we can create via 'let c = new myLib.Cat(42)' + //~ Or reference, e.g. 'function f(c: myLib.Cat) { ... }' + class Cat { + constructor(n: number); + //~ We can read 'c.age' from a 'Cat' instance + readonly age: number; + //~ We can invoke 'c.purr()' from a 'Cat' instance + purr(): void; + } + //~ We can declare a variable as + //~ 'var s: myLib.CatSettings = { weight: 5, name: "Maru" };' + interface CatSettings { + weight: number; + name: string; + tailLength?: number; + } + //~ We can write 'myLib.VetID = 42' or 'myLib.VetID = "bob"' + type VetID = string | number; + //~ We can invoke 'myLib.checkCat(c)' or 'myLib.checkCat(c, v)' + function checkCat(c: Cat, s?: VetID): boolean; +} +``` + +### Key Points + +- Do not include `export` or `import` statements — that would make this a module file, not a global script file. +- Top-level `declare` statements describe what exists in the global scope. +- `declare namespace myLib` groups all properties and types accessible under the `myLib` global. +- An `interface myLib` at the top level (merged with the namespace) allows `myLib` to be used as a type directly. + +## Global: Modifying Module + +- Reference material for [Global: Modifying Module](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-modifying-module-d-ts.html) + +Use this template when a library is imported as a module but has the side effect of modifying the global scope (e.g., adding methods to built-in prototypes like `String.prototype`). + +### When to Use + +- The library is loaded with `require("my-lib")` or `import "my-lib"` for its side effects +- The import adds new methods to existing global types (e.g., `String`, `Array`, `Promise`) +- Usage example: `require("moment"); moment().format("YYYY")` combined with `String.prototype.toDate()` + +### Global-Modifying Module Template + +```ts +// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] +// Project: [~THE PROJECT NAME~] +// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> + +/*~ This is the global-modifying module template file. You should rename it + *~ to index.d.ts and place it in a folder with the same name as the module. + *~ For example, if you were writing a file for "super-greeter", this + *~ file should be 'super-greeter/index.d.ts' + */ + +/*~ Note: If your global-modifying module is callable or constructable, you'll + *~ need to combine the patterns here with those in the module-class or + *~ module-function template files. + */ +declare global { + /*~ Here, declare things in the normal global namespace */ + interface String { + fancyFormat(opts: StringFormatOptions): string; + } +} + +/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */ +export {}; + +/*~ Mark the types that are referenced inside the 'declare global' block here */ +export interface StringFormatOptions { + fancinessLevel: number; +} +``` + +### Key Points + +- The file must be a **module** (contain at least one `export` or `import`) for `declare global` to work. The `export {}` statement at the bottom satisfies this requirement without exporting anything. +- `declare global { }` is the mechanism for adding to the global scope from within a module file. +- If the module also has callable or constructable exports, combine this template with the module-function or module-class template. +- Consumers install the type augmentation simply by importing the module: + +```ts +import "super-greeter"; + +const s = "hello"; +s.fancyFormat({ fancinessLevel: 3 }); // OK — added by the module +``` diff --git a/skills/typescript-coder/references/typescript-declaration-files.md b/skills/typescript-coder/references/typescript-declaration-files.md new file mode 100644 index 000000000..5a7a87e07 --- /dev/null +++ b/skills/typescript-coder/references/typescript-declaration-files.md @@ -0,0 +1,651 @@ +# TypeScript Declaration Files + +Reference material for writing and consuming TypeScript declaration files (.d.ts) from the official TypeScript documentation. + +## Introduction + +- Reference material for [Introduction](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) + +Declaration files (`.d.ts`) describe the shape of JavaScript code to TypeScript. They allow the TypeScript compiler to type-check usage of JavaScript libraries that were not written in TypeScript. + +There are two primary sources of declaration files: + +- **Bundled with a package** — the library author includes `.d.ts` files in the npm package alongside the JavaScript output. +- **DefinitelyTyped (`@types`)** — a community-maintained repository of declaration files for libraries that don't ship their own. Install via `npm install --save-dev @types/`. + +TypeScript resolves declaration files automatically when using `import` or `require`, looking first at the package's `types` or `typings` field in `package.json`, then for an `index.d.ts` at the package root. + +The declaration file guide covers: + +1. Writing declarations by example (common patterns) +2. Identifying the correct library structure (global, module, UMD) +3. Do's and Don'ts for avoiding common mistakes +4. A deep dive into how declarations work internally +5. How to publish and consume declaration files + +## Declaration Reference + +- Reference material for [Declaration Reference](https://www.typescriptlang.org/docs/handbook/declaration-files/by-example.html) + +Common patterns for writing declaration files, organized by the kind of thing being declared. + +### Global Variables + +```ts +/** The number of widgets present */ +declare var foo: number; +``` + +### Global Functions + +```ts +declare function greet(greeting: string): void; +``` + +Functions can have overloads: + +```ts +declare function getWidget(n: number): Widget; +declare function getWidget(s: string): Widget[]; +``` + +### Objects with Properties + +Use `declare namespace` to describe objects accessed via dotted notation: + +```ts +declare namespace myLib { + function makeGreeting(s: string): string; + let numberOfGreetings: number; +} +``` + +Usage: `myLib.makeGreeting("hello")` and `myLib.numberOfGreetings`. + +### Reusable Types — Interfaces + +```ts +interface GreetingSettings { + greeting: string; + duration?: number; + color?: string; +} + +declare function greet(setting: GreetingSettings): void; +``` + +### Reusable Types — Type Aliases + +```ts +type GreetingLike = string | (() => string) | MyGreeter; + +declare function greet(g: GreetingLike): void; +``` + +### Organizing Types with Nested Namespaces + +```ts +declare namespace GreetingLib { + interface LogOptions { + verbose?: boolean; + } + interface AlertOptions { + modal: boolean; + title?: string; + color?: string; + } +} +``` + +These are referenced as `GreetingLib.LogOptions` and `GreetingLib.AlertOptions`. + +Namespaces can be nested: + +```ts +declare namespace GreetingLib.Options { + interface Log { + verbose?: boolean; + } + interface Alert { + modal: boolean; + title?: string; + color?: string; + } +} +``` + +### Classes + +```ts +declare class Greeter { + constructor(greeting: string); + greeting: string; + showGreeting(): void; +} +``` + +### Enums + +```ts +declare enum Shading { + None, + Streamline, + Matte, + Glossy, +} +``` + +### Modules (CommonJS / ESM exports) + +Use `export =` for CommonJS-style modules (when the library uses `module.exports = ...`): + +```ts +export = MyModule; + +declare function MyModule(): void; +declare namespace MyModule { + let version: string; +} +``` + +Use named exports for ESM-style: + +```ts +export function myFunction(a: string): string; +export const myField: number; +export interface SomeType { + name: string; + length: number; +} +``` + +### UMD Modules + +Libraries usable both as globals and as modules use `export as namespace`: + +```ts +export as namespace myLib; +export function makeGreeting(s: string): string; +export let numberOfGreetings: number; +``` + +## Library Structures + +- Reference material for [Library Structures](https://www.typescriptlang.org/docs/handbook/declaration-files/library-structures.html) + +Choosing the correct declaration file structure depends on how the library is consumed. + +### Global Libraries + +A global library is accessible from the global scope — no `import` or `require` needed. Examples: older jQuery (`$()`), Underscore.js (old style). + +Identifying characteristics in JavaScript source: + +- Top-level `var` statements or `function` declarations +- `window.myLib = ...` assignments +- References to `document` or `window` + +Use the `global.d.ts` template. + +```ts +declare function myLib(a: string): string; +declare namespace myLib { + let timeout: number; +} +``` + +### Module Libraries + +Libraries consumed via `require()` or `import`. Most modern npm packages are module libraries. + +Identifying characteristics: + +- `const x = require("foo")` or `import x from "foo"` in usage examples +- `exports.myFn = ...` or `module.exports = ...` in the source +- `define(...)` (AMD) in the source + +Use the `module.d.ts` template. + +### UMD Libraries + +Libraries that can be used as a global (via a ` + + +``` + +### TypeScript-aware Editors + +TypeScript's language service powers rich editor tooling in: + +- **Visual Studio Code** — built-in TypeScript support +- **WebStorm / IntelliJ IDEA** — built-in TypeScript support +- **Vim / Neovim** — via tsserver language server +- **Emacs** — via tide or lsp-mode + +All major editors support: +- Inline error reporting +- Autocompletion (IntelliSense) +- Go-to-definition and find-all-references +- Rename refactoring +- Hover documentation diff --git a/skills/typescript-coder/references/typescript-handbook.md b/skills/typescript-coder/references/typescript-handbook.md index 453ba3495..dfb269c37 100644 --- a/skills/typescript-coder/references/typescript-handbook.md +++ b/skills/typescript-coder/references/typescript-handbook.md @@ -2,6 +2,17 @@ Comprehensive TypeScript reference based on the official TypeScript Handbook. This document covers core concepts and patterns. +## Reference Sources + +- [The TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) +- [The Basics](https://www.typescriptlang.org/docs/handbook/2/basic-types.html) +- [Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) +- [Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) +- [More on Functions](https://www.typescriptlang.org/docs/handbook/2/functions.html) +- [Object Types](https://www.typescriptlang.org/docs/handbook/2/objects.html) +- [Classes](https://www.typescriptlang.org/docs/handbook/2/classes.html) +- [Modules](https://www.typescriptlang.org/docs/handbook/2/modules.html) + ## Getting Started TypeScript is a strongly typed programming language that builds on JavaScript. It adds optional static typing to JavaScript, enabling better tooling, error detection, and code quality. diff --git a/skills/typescript-coder/references/typescript-module-references.md b/skills/typescript-coder/references/typescript-module-references.md new file mode 100644 index 000000000..c138c4af4 --- /dev/null +++ b/skills/typescript-coder/references/typescript-module-references.md @@ -0,0 +1,483 @@ +# TypeScript Modules Reference + +Reference material for TypeScript module systems, theory, and configuration options. + +## Introduction + +- Reference material for [Introduction](https://www.typescriptlang.org/docs/handbook/modules/introduction.html) + +A file is a **module** in TypeScript if it contains at least one top-level `import` or `export`. Files without these are treated as **scripts** whose declarations exist in the global scope. + +```ts +// module.ts — IS a module (has export); isolated scope +export const name = "TypeScript"; +export function greet(n: string) { return `Hello, ${n}`; } + +// script.ts — NOT a module (no import/export); global scope +const name = "TypeScript"; + +// Force a file to be a module with no exports +export {}; +``` + +TypeScript uses the same ES Module syntax as JavaScript for imports and exports: + +```ts +// Named exports and imports +export function add(a: number, b: number): number { return a + b; } +import { add } from "./math"; + +// Default export and import +export default class User { constructor(public name: string) {} } +import User from "./user"; + +// Re-export +export { add as sum } from "./math"; +export * from "./utils"; + +// Type-only imports — fully erased at runtime (no runtime cost) +import type { User } from "./types"; +import { type Config, loadConfig } from "./config"; +``` + +Key distinctions: + +| Concept | Description | +|---|---| +| Module | File with `import`/`export`; isolated scope | +| Script | File without them; shares global scope | +| `moduleResolution` | Controls how TypeScript finds imported modules | +| `module` compiler option | Controls the output format of the emitted JS | + +The `module` compiler option and `moduleResolution` option are distinct settings that work together: +- `module` determines the output format (CommonJS, ESNext, NodeNext, etc.) +- `moduleResolution` determines how import paths are resolved to files + +## Theory + +- Reference material for [Theory](https://www.typescriptlang.org/docs/handbook/modules/theory.html) + +### The Host + +TypeScript code always runs in a **host environment** that determines what global APIs are available and what module system is in use. Common hosts: Node.js, browsers, Deno, bundlers (Webpack, esbuild, Vite, Rollup). + +### Module Systems + +| System | Syntax | Used By | +|---|---|---| +| ESM (ECMAScript Modules) | `import` / `export` | Browsers, modern Node.js | +| CommonJS | `require()` / `module.exports` | Node.js (default) | +| AMD | `define()` | RequireJS | +| UMD | CJS + AMD fallback | Libraries | +| SystemJS | `System.register` | Legacy bundlers | + +### How TypeScript Determines Module Format + +The `module` compiler option is the primary signal. For Node.js, the `package.json` `"type"` field and file extension also matter: + +```json +// package.json +{ "type": "module" } // .js files treated as ESM +{ "type": "commonjs" } // .js files treated as CJS (default) +``` + +File extension overrides `"type"` field: +- `.mjs` / `.mts` — always ESM +- `.cjs` / `.cts` — always CJS +- `.js` / `.ts` — determined by `"type"` field + +### Module Resolution + +Controls how TypeScript resolves module specifiers to files: + +```ts +// With moduleResolution: "nodenext" — extension required +import { foo } from "./foo.js"; + +// With moduleResolution: "bundler" — extension optional +import { foo } from "./foo"; +``` + +### Module Output + +TypeScript transpiles module syntax based on the `module` setting: + +```ts +// Input (TypeScript) +import { x } from "./mod"; +export const y = x + 1; +``` + +```js +// Output with module: "commonjs" +"use strict"; +const mod_1 = require("./mod"); +exports.y = mod_1.x + 1; +``` + +```js +// Output with module: "esnext" +import { x } from "./mod"; +export const y = x + 1; +``` + +### Important Notes + +```ts +// isolatedModules: true — each file must be independently transpilable (required by esbuild, Babel) +// verbatimModuleSyntax — TS 5.0+: import type is always erased; inline type imports allowed +import type { Foo } from "./foo"; // always erased +import { type Bar, baz } from "./bar"; // Bar erased, baz kept + +// A file with no imports/exports is a script — pollutes global scope +const x = 1; // global + +// Make it a module explicitly +export {}; // now isolated scope +``` + +### Structural Module Resolution + +TypeScript uses a two-pass approach: +1. **Syntactic analysis** — determine if a file is a module or script +2. **Semantic analysis** — resolve imports and type-check + +The module format affects how TypeScript handles: +- Top-level `await` (ESM only) +- `import.meta` (ESM only) +- `require()` calls (CJS only) +- Dynamic `import()` (available in both) + +## Reference + +- Reference material for [Reference](https://www.typescriptlang.org/docs/handbook/modules/reference.html) + +### `module` + +Specifies the module code generation format for emitted JavaScript. + +```json +{ + "compilerOptions": { + "module": "NodeNext" + } +} +``` + +| Value | Use Case | +|-------|----------| +| `CommonJS` | Traditional Node.js | +| `ESNext` / `ES2020` / `ES2022` | Modern bundlers | +| `Node16` / `NodeNext` | Modern Node.js with ESM support | +| `Preserve` | Pass-through (TS 5.4+); keeps input module syntax | +| `None` | No module system | +| `AMD` | AMD / RequireJS | +| `UMD` | Universal module (CJS + AMD) | +| `System` | SystemJS | + +### `moduleResolution` + +Controls how TypeScript resolves module imports. + +```json +{ "compilerOptions": { "moduleResolution": "bundler" } } +``` + +| Value | Description | +|-------|-------------| +| `node` | Mimics Node.js CommonJS resolution (legacy) | +| `node16` / `nodenext` | Node.js ESM + CJS hybrid resolution | +| `bundler` | For bundler tools (Vite, esbuild); extensionless imports allowed | +| `classic` | Legacy TypeScript behavior (not recommended) | + +### `baseUrl` + +Sets the base directory for resolving non-relative module names. + +```json +{ "compilerOptions": { "baseUrl": "./src" } } +``` + +```ts +// With baseUrl: "./src" +import { utils } from "utils/helpers"; // resolves to ./src/utils/helpers +``` + +### `paths` + +Maps module names to file locations. Requires `baseUrl`. + +```json +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@app/*": ["src/app/*"], + "@utils/*": ["src/utils/*"] + } + } +} +``` + +```ts +import { Component } from "@app/components"; // → src/app/components +import { debounce } from "@utils/debounce"; // → src/utils/debounce +``` + +Note: `paths` affects type resolution only. Bundlers need their own alias configuration. + +### `rootDirs` + +Treats multiple directories as a single virtual root for module resolution. + +```json +{ "compilerOptions": { "rootDirs": ["src", "generated"] } } +``` + +```ts +// src/views/main.ts can import from generated/views/ as if same folder +import { template } from "./templates"; // resolves to generated/views/templates.ts +``` + +### `resolveJsonModule` + +Allows importing `.json` files with full type inference. + +```ts +import config from "./config.json"; +console.log(config.apiUrl); // fully typed +``` + +### `allowSyntheticDefaultImports` + +Allows `import x from 'y'` when the module has no `default` export (type checking only — no emitted helpers). Automatically enabled when `esModuleInterop` is `true`. + +### `esModuleInterop` + +Emits `__importDefault` and `__importStar` helpers for CommonJS/ESM interop. Enables `allowSyntheticDefaultImports`. + +```ts +// Without esModuleInterop: +import * as fs from "fs"; + +// With esModuleInterop: true +import fs from "fs"; // cleaner default import for CJS modules +``` + +### `moduleDetection` + +Controls how TypeScript determines if a file is a module or script. + +| Value | Behavior | +|-------|----------| +| `auto` (default) | Files with `import`/`export` are modules | +| `force` | All files treated as modules | +| `legacy` | TypeScript 4.x behavior | + +### `allowImportingTsExtensions` + +Allows imports with `.ts`, `.tsx`, `.mts` extensions. Requires `noEmit` or `emitDeclarationOnly`. + +```ts +import { foo } from "./foo.ts"; // for bundler environments like Vite +``` + +### `verbatimModuleSyntax` (TS 5.0+) + +Enforces that `import type` is used for type-only imports. Simplifies interop by ensuring type imports are always erased. + +```ts +import type { Foo } from "./foo"; // always erased — must use import type +import { type Bar, baz } from "./bar"; // Bar is erased, baz is kept +``` + +### `resolvePackageJsonExports` / `resolvePackageJsonImports` + +Controls whether TypeScript respects `package.json` `exports` and `imports` fields during resolution. Enabled by default when `moduleResolution` is `node16`, `nodenext`, or `bundler`. + +## Choosing Compiler Options + +- Reference material for [Choosing Compiler Options](https://www.typescriptlang.org/docs/handbook/modules/guides/choosing-compiler-options.html) + +### Node.js (CommonJS) + +For traditional Node.js applications using `require`: + +```json +{ + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": true + } +} +``` + +### Node.js (ESM, Node 16+) + +For Node.js applications using native ES modules: + +```json +{ + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16", + "esModuleInterop": true, + "resolveJsonModule": true + } +} +``` + +Important: `node16`/`nodenext` enforce strict ESM/CJS boundaries and require explicit file extensions in imports: + +```ts +// Required with nodenext +import { foo } from "./foo.js"; // .js extension required even for .ts source files +``` + +### Bundlers (Vite, esbuild, Webpack, Parcel) + +Recommended for most frontend or full-stack projects using a build tool: + +```json +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { "@/*": ["src/*"] } + } +} +``` + +The `bundler` mode (TS 5.0+) mirrors how bundlers actually resolve modules: +- Extensionless imports are allowed +- `package.json` `exports` field is respected +- Does not require `.js` extensions + +### Decision Guide + +| Environment | `module` | `moduleResolution` | +|---|---|---| +| Node.js CJS | `CommonJS` | `node` | +| Node.js ESM | `Node16` or `NodeNext` | `node16` or `nodenext` | +| Bundler (Vite, etc.) | `ESNext` | `bundler` | +| Library (dual CJS/ESM) | `NodeNext` | `nodenext` | + +Key notes: +- Avoid `"moduleResolution": "node"` with `"module": "ESNext"` — this is a common misconfiguration that does not reflect real runtime behavior +- Use `"verbatimModuleSyntax": true` in new projects to enforce correct type import syntax +- `"isolatedModules": true` is required when using esbuild, Babel, or SWC as the transpiler + +```json +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "isolatedModules": true + } +} +``` + +## ESM/CJS Interoperability + +- Reference material for [ESM/CJS Interoperability](https://www.typescriptlang.org/docs/handbook/modules/appendices/esm-cjs-interop.html) + +ESM and CommonJS have fundamentally different execution and export models. Understanding interop is essential when mixing module formats. + +### The Core Problem + +CommonJS exports a single value (`module.exports`). ESM has named exports and a default export. When ESM imports a CJS module, the entire `module.exports` object becomes the "namespace" — but what is the `default`? + +```ts +// CJS module (legacy.js) +module.exports = { foo: 1, bar: 2 }; + +// Importing from ESM — behavior differs by tool/flag +import legacy from "./legacy"; // legacy = { foo: 1, bar: 2 } (with esModuleInterop) +import * as legacy from "./legacy"; // namespace import +import { foo } from "./legacy"; // named import (may or may not work) +``` + +### `esModuleInterop` + +When enabled, TypeScript emits helper functions that make CJS default imports work correctly: + +```ts +// With esModuleInterop: true +import fs from "fs"; // OK — __importDefault helper wraps module.exports +import * as path from "path"; // OK — __importStar helper used + +// Without esModuleInterop (legacy approach) +import * as fs from "fs"; +const readFile = fs.readFile; // manual namespace import +``` + +The emitted helpers: +- `__importDefault(mod)` — wraps `module.exports` as `{ default: module.exports }` if not already ESM +- `__importStar(mod)` — creates a namespace object with `default` set to `module.exports` + +### Dynamic `import()` and CJS + +When using `"module": "NodeNext"`, dynamic import from a CJS file always returns a namespace object with a `default` property: + +```ts +// In a CJS file +const mod = await import("./esm-module.js"); +mod.default; // the default export +mod.namedExport; // a named export +``` + +### Named Exports from CJS + +Node.js (via static analysis) and bundlers can sometimes expose CJS object properties as named imports, but TypeScript types this conservatively: + +```ts +// someLib/index.js (CJS) +exports.foo = 1; +exports.bar = "hello"; + +// TypeScript with esModuleInterop +import { foo, bar } from "someLib"; // may work at runtime; TypeScript requires declaration +``` + +### `allowSyntheticDefaultImports` vs `esModuleInterop` + +| Option | Effect | +|---|---| +| `allowSyntheticDefaultImports` | Type-level only: suppresses errors for default imports from CJS modules | +| `esModuleInterop` | Emits runtime helpers + enables `allowSyntheticDefaultImports` | + +Use `esModuleInterop` for full correctness. Use `allowSyntheticDefaultImports` alone only when you know the runtime already handles interop (e.g., when using Babel or a bundler). + +### Interop in Node.js `node16`/`nodenext` + +With `"module": "NodeNext"`, TypeScript enforces strict boundaries: + +```ts +// .mts file (always ESM) — cannot use require() +import { readFile } from "fs/promises"; // OK + +// .cts file (always CJS) — cannot use top-level await +const { readFileSync } = require("fs"); // OK +``` + +```ts +// CJS importing ESM — NOT allowed in Node.js +// const mod = require("./esm-module.mjs"); // Error at runtime + +// ESM importing CJS — allowed +import cjsMod from "./cjs-module.cjs"; // OK +``` + +### Key Considerations + +- Always use `esModuleInterop: true` in new projects for correct CJS default import behavior +- When using `node16`/`nodenext`, be explicit with `.mts`/`.cts` extensions for files that must be ESM or CJS +- Bundlers (Vite, Webpack, esbuild) typically handle ESM/CJS interop transparently with `moduleResolution: "bundler"` +- Library authors targeting both CJS and ESM should use `"module": "NodeNext"` with dual package output diff --git a/skills/typescript-coder/references/typescript-quickstart.md b/skills/typescript-coder/references/typescript-quickstart.md new file mode 100644 index 000000000..9b7664c20 --- /dev/null +++ b/skills/typescript-coder/references/typescript-quickstart.md @@ -0,0 +1,727 @@ +# TypeScript Quick Start + +Quick start guides for TypeScript based on your background and experience. + +## JS to TS + +- Reference material for [JS to TS](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) + +TypeScript begins with the JavaScript you already know. If you write JavaScript, you already write TypeScript — TypeScript is a superset of JavaScript. + +### Step 1: Types by Inference + +TypeScript infers types automatically from your existing JavaScript patterns. No annotations needed to get started: + +```ts +// TypeScript infers: let helloWorld: string +let helloWorld = "Hello World"; + +// TypeScript infers: let count: number +let count = 42; + +// TypeScript infers: let flags: boolean[] +let flags = [true, false, true]; +``` + +### Step 2: Annotate Where Needed + +When TypeScript cannot infer a type — or when you want to be explicit about a contract — use type annotations: + +```ts +// Annotate function parameters and return types +function add(a: number, b: number): number { + return a + b; +} + +// Annotate variables explicitly +let userName: string = "Alice"; + +// Annotate objects with interfaces +interface User { + name: string; + id: number; +} + +const user: User = { + name: "Hayes", + id: 0, +}; +``` + +### Step 3: Composing Types with Unions and Generics + +**Union types** allow a value to be one of several types: + +```ts +type StringOrNumber = string | number; + +function formatId(id: string | number): string { + return `ID: ${id}`; +} + +// TypeScript narrows the type after a check +function printId(id: string | number) { + if (typeof id === "string") { + console.log(id.toUpperCase()); // TypeScript knows id is string + } else { + console.log(id.toFixed(0)); // TypeScript knows id is number + } +} +``` + +**Generics** make components reusable across types: + +```ts +// Generic function — works with any type T +function firstItem(arr: T[]): T | undefined { + return arr[0]; +} + +const first = firstItem([1, 2, 3]); // Type: number | undefined +const name = firstItem(["a", "b"]); // Type: string | undefined + +// Generic interface +interface ApiResponse { + data: T; + status: number; + message: string; +} +``` + +### Step 4: Structural Typing (Duck Typing) + +TypeScript checks **shapes**, not type names. If an object has all the required properties, it satisfies the type — no explicit declaration needed: + +```ts +interface Point { + x: number; + y: number; +} + +function logPoint(p: Point) { + console.log(`x=${p.x}, y=${p.y}`); +} + +// This plain object satisfies Point — no "implements" needed +const pt = { x: 12, y: 26 }; +logPoint(pt); // OK + +// Extra properties are fine +const pt3d = { x: 1, y: 2, z: 3 }; +logPoint(pt3d); // OK — only x and y are checked + +// Classes satisfy interfaces structurally too +class Coordinate { + constructor(public x: number, public y: number) {} +} +logPoint(new Coordinate(5, 10)); // OK +``` + +### Migrating from JavaScript + +When migrating an existing JavaScript project: + +1. Rename `.js` files to `.ts` one at a time +2. Add `tsconfig.json` with `"allowJs": true` to allow gradual migration +3. Fix type errors as you encounter them — or use `// @ts-ignore` temporarily +4. Remove `any` types progressively as you add proper type definitions + +```json +{ + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "strict": false, + "noImplicitAny": false + } +} +``` + +Then tighten settings over time as the codebase is migrated. + +--- + +## New to Programming + +- Reference material for [New to Programming](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html) + +TypeScript is a great first language for programming because it catches mistakes before you run your code. + +### What TypeScript Does + +TypeScript is a **static type checker** for JavaScript. It reads your code and identifies errors _before_ you run it, similar to how a spell checker finds mistakes in text. + +```ts +// TypeScript catches this mistake before the program runs +const user = { + firstName: "Angela", + lastName: "Davis", + role: "Professor", +}; + +// Typo: "lastNme" instead of "lastName" +console.log(user.lastNme); +// Error: Property 'lastNme' does not exist on type '{ firstName: string; lastName: string; role: string; }' +// Did you mean 'lastName'? +``` + +Without TypeScript, this error only shows up at runtime. With TypeScript, it is caught immediately. + +### TypeScript is JavaScript with Types + +All JavaScript code is valid TypeScript. TypeScript adds an optional layer of type annotations on top: + +```ts +// Plain JavaScript — also valid TypeScript +function greet(name) { + return "Hello, " + name + "!"; +} + +// TypeScript — with a type annotation +function greet(name: string): string { + return "Hello, " + name + "!"; +} +``` + +The `: string` annotations tell TypeScript what type a variable or parameter should be. + +### Types Are Removed at Runtime + +TypeScript's types only exist during development. When TypeScript compiles to JavaScript, all type information is erased. The JavaScript that runs in the browser or Node.js has no type information: + +```ts +// TypeScript source +function add(a: number, b: number): number { + return a + b; +} +``` + +```js +// Compiled JavaScript output (types erased) +function add(a, b) { + return a + b; +} +``` + +### The TypeScript Compiler + +Install TypeScript and use the `tsc` compiler: + +```bash +# Install TypeScript +npm install -g typescript + +# Compile a TypeScript file to JavaScript +tsc myfile.ts + +# Watch for changes and recompile automatically +tsc --watch myfile.ts + +# Initialize a project configuration +tsc --init +``` + +### Core Type Concepts for Beginners + +**Primitive types:** + +```ts +let age: number = 25; +let name: string = "Alice"; +let isActive: boolean = true; +``` + +**Arrays:** + +```ts +let scores: number[] = [100, 95, 87]; +let names: string[] = ["Alice", "Bob", "Charlie"]; +``` + +**Functions:** + +```ts +function multiply(x: number, y: number): number { + return x * y; +} + +// Arrow function +const square = (n: number): number => n * n; +``` + +**Objects:** + +```ts +// Inline object type +let person: { name: string; age: number } = { + name: "Alice", + age: 30, +}; + +// Reusable interface +interface Product { + id: number; + name: string; + price: number; + inStock?: boolean; // optional property +} +``` + +### Why Use TypeScript? + +- **Catch errors early** — find bugs before shipping code +- **Better editor support** — autocompletion, rename, go-to-definition +- **Self-documenting code** — types communicate intent to other developers +- **Safer refactoring** — TypeScript tells you what breaks when you change code + +--- + +## OOP to JS + +- Reference material for [OOP to JS](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-oop.html) + +If you are coming from Java, C#, or another class-oriented language, TypeScript's type system works differently than you might expect. + +### Types are Structural, Not Nominal + +In Java and C#, two classes with the same methods are still different types. In TypeScript, two objects with the same shape are the same type — regardless of their name or class. + +```ts +class Cat { + meow() { + console.log("Meow!"); + } +} + +class Dog { + meow() { + console.log("...Meow?"); + } +} + +// No error — both classes have the same shape +let animal: Cat = new Dog(); +``` + +This is called **structural typing** (or duck typing): "if it walks like a duck and quacks like a duck, it's a duck." + +### Types as Sets + +TypeScript types are best understood as **sets of values**. A type is not a class or a unique identity — it is a description of what values are allowed. + +```ts +// This interface describes a set of objects that have x and y +interface Pointlike { + x: number; + y: number; +} + +// Any object with x: number and y: number belongs to this "set" +const p1: Pointlike = { x: 1, y: 2 }; // OK +const p2: Pointlike = { x: 5, y: 10, z: 0 }; // OK — extra properties allowed +``` + +### No Runtime Type Information for Interfaces + +TypeScript interfaces and type aliases are **compile-time only**. They are completely erased when compiled to JavaScript. You cannot use `instanceof` with interfaces: + +```ts +interface Serializable { + serialize(): string; +} + +// This does NOT work — interfaces are erased at runtime +function save(obj: unknown) { + if (obj instanceof Serializable) { // Error: 'Serializable' only refers to a type + obj.serialize(); + } +} + +// Instead, use discriminated unions or type guards +function isSerializable(obj: unknown): obj is Serializable { + return typeof obj === "object" && obj !== null && typeof (obj as any).serialize === "function"; +} +``` + +### Classes Still Work as Expected + +TypeScript classes do work with `instanceof` because they exist at runtime as JavaScript constructor functions: + +```ts +class Animal { + constructor(public name: string) {} + speak() { return `${this.name} makes a sound`; } +} + +class Dog extends Animal { + speak() { return `${this.name} barks`; } +} + +const d = new Dog("Rex"); +console.log(d instanceof Dog); // true +console.log(d instanceof Animal); // true +``` + +### Key Differences from Java/C# + +| Java/C# | TypeScript | +|---------|------------| +| Nominal typing (names matter) | Structural typing (shapes matter) | +| Types exist at runtime | Types erased at compile time | +| All types are classes or primitives | Functions and object literals are common | +| Checked exceptions | No checked exceptions | +| Enums are full types | Enums exist but union types are often preferred | +| Generics are reified at runtime | Generics are erased at compile time | +| `implements` required | `implements` optional — compatibility is structural | + +### Recommended TypeScript Patterns for OOP Developers + +```ts +// Prefer interfaces for data shapes +interface UserDto { + id: string; + name: string; + email: string; +} + +// Use union types instead of Java-style enums +type Status = "pending" | "active" | "inactive"; + +// Use discriminated unions instead of inheritance hierarchies +type Result = + | { success: true; data: T } + | { success: false; error: string }; + +function getUser(id: string): Result { + if (id === "") { + return { success: false, error: "ID cannot be empty" }; + } + return { success: true, data: { id, name: "Alice", email: "alice@example.com" } }; +} + +const result = getUser("123"); +if (result.success) { + console.log(result.data.name); // TypeScript knows data exists +} else { + console.log(result.error); // TypeScript knows error exists +} +``` + +--- + +## Functional to JS + +- Reference material for [Functional to JS](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html) + +If you are coming from Haskell, Elm, PureScript, or other functional languages, TypeScript has many familiar concepts but with key differences. + +### TypeScript's Type System is Structural + +Like Haskell's structural approach, TypeScript uses **structural subtyping**. If a type has all the required properties, it is assignable to the expected type — no explicit declaration needed: + +```ts +type Named = { name: string }; + +function greet(x: Named) { + return "Hello, " + x.name; +} + +// Any object with name: string satisfies Named +greet({ name: "Alice" }); // OK +greet({ name: "Bob", age: 30 }); // OK — extra field is fine +``` + +### Discriminated Unions (Algebraic Data Types) + +TypeScript's discriminated unions are the equivalent of algebraic data types in functional languages: + +```ts +// Similar to a Haskell ADT: +// data Shape = Circle Double | Square Double | Triangle Double Double + +type Shape = + | { kind: "circle"; radius: number } + | { kind: "square"; x: number } + | { kind: "triangle"; x: number; y: number }; + +function area(s: Shape): number { + switch (s.kind) { + case "circle": return Math.PI * s.radius ** 2; + case "square": return s.x ** 2; + case "triangle": return (s.x * s.y) / 2; + } + // TypeScript ensures exhaustiveness +} +``` + +### Unit Types (Literal Types) + +Like Haskell's unit types, TypeScript supports literal types as specific value types: + +```ts +type Bit = 0 | 1; +type Direction = "north" | "south" | "east" | "west"; +type Bool = true | false; // same as boolean + +// Narrowing refines the type +function move(dir: Direction, steps: number) { + if (dir === "north" || dir === "south") { + // dir is: "north" | "south" + console.log(`Moving vertically ${steps} steps`); + } +} +``` + +### `never` and `unknown` — Bottom and Top Types + +TypeScript has the full lattice of types: + +```ts +// unknown = top type — any value is assignable to unknown +let anything: unknown = 42; +anything = "hello"; +anything = { x: 1 }; + +// You must narrow before using unknown +function process(val: unknown) { + if (typeof val === "string") { + console.log(val.toUpperCase()); // OK after narrowing + } +} + +// never = bottom type — a value of this type never exists +function fail(msg: string): never { + throw new Error(msg); +} + +// Exhaustiveness checking with never +function assertNever(x: never): never { + throw new Error("Unexpected value: " + x); +} +``` + +### Immutability with `readonly` and `as const` + +TypeScript supports immutability at the type level: + +```ts +// readonly properties +interface Config { + readonly host: string; + readonly port: number; +} + +// readonly arrays +function sum(nums: readonly number[]): number { + return nums.reduce((a, b) => a + b, 0); +} + +// as const — deeply immutable, all values become literal types +const DIRECTIONS = ["north", "south", "east", "west"] as const; +type Direction = typeof DIRECTIONS[number]; // "north" | "south" | "east" | "west" +``` + +### Higher-Order Functions and Generic Types + +TypeScript supports higher-order functions with precise generic types: + +```ts +// Map with generic types +function map(arr: T[], fn: (item: T) => U): U[] { + return arr.map(fn); +} + +const lengths = map(["hello", "world"], (s) => s.length); // number[] + +// Compose functions with types +type Fn = (a: A) => B; + +function compose(f: Fn, g: Fn): Fn { + return (a) => f(g(a)); +} + +const toUpperLength = compose( + (s: string) => s.length, + (s: string) => s.toUpperCase() +); +console.log(toUpperLength("hello")); // 5 +``` + +### Mapped and Conditional Types + +TypeScript has type-level programming features similar to type classes and type families: + +```ts +// Mapped types (similar to functor over record fields) +type Nullable = { [K in keyof T]: T[K] | null }; +type Readonly = { readonly [K in keyof T]: T[K] }; + +// Conditional types (type-level if-then-else) +type IsArray = T extends any[] ? true : false; +type A = IsArray; // true +type B = IsArray; // false + +// infer — type-level pattern matching +type ElementType = T extends (infer E)[] ? E : never; +type E = ElementType; // string +type N = ElementType; // never +``` + +--- + +## Installation + +- Reference material for [Installation](https://www.typescriptlang.org/download/) + +### Installing TypeScript via npm + +The recommended way to install TypeScript is as a local dev dependency in your project: + +```bash +# Install TypeScript as a project dev dependency (recommended) +npm install --save-dev typescript + +# Run the TypeScript compiler via npx +npx tsc +``` + +### Global Installation + +You can also install TypeScript globally for use across all projects: + +```bash +# Install globally +npm install -g typescript + +# Verify the installation +tsc --version +``` + +> Note: Global installation is convenient but local installation is preferred so each project can pin its own TypeScript version. + +### Using TypeScript in a Project + +**Initialize a new TypeScript project:** + +```bash +# Create a tsconfig.json with default settings +npx tsc --init +``` + +**Add TypeScript scripts to `package.json`:** + +```json +{ + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch", + "typecheck": "tsc --noEmit" + } +} +``` + +**Compile your project:** + +```bash +# Compile using tsconfig.json +npx tsc + +# Compile a single file (bypasses tsconfig.json) +npx tsc index.ts + +# Type-check without emitting output +npx tsc --noEmit +``` + +### Installing Type Definitions + +Many JavaScript libraries ship without TypeScript types. Install type definitions from the `@types` namespace: + +```bash +# Types for Node.js +npm install --save-dev @types/node + +# Types for common libraries +npm install --save-dev @types/express +npm install --save-dev @types/lodash +npm install --save-dev @types/jest + +# Search for available type definitions +npm search @types/[library-name] +``` + +### Minimal `tsconfig.json` + +A minimal configuration to get started: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +### TypeScript with Popular Frameworks + +**React (with Vite):** + +```bash +npm create vite@latest my-app -- --template react-ts +cd my-app && npm install +``` + +**Next.js:** + +```bash +npx create-next-app@latest my-app --typescript +``` + +**Node.js (Express):** + +```bash +mkdir my-api && cd my-api +npm init -y +npm install express +npm install --save-dev typescript @types/node @types/express ts-node +npx tsc --init +``` + +**Angular:** + +```bash +npm install -g @angular/cli +ng new my-app # TypeScript is the default +``` + +### TypeScript Versions + +TypeScript follows semver. To pin a specific version: + +```bash +# Install a specific version +npm install --save-dev typescript@5.3.3 + +# Install the latest stable +npm install --save-dev typescript@latest + +# Install the next/beta version +npm install --save-dev typescript@next +``` + +Check the currently installed version: + +```bash +npx tsc --version +# Output: Version 5.x.x +``` diff --git a/skills/typescript-coder/references/typescript-releases.md b/skills/typescript-coder/references/typescript-releases.md new file mode 100644 index 000000000..3229bc090 --- /dev/null +++ b/skills/typescript-coder/references/typescript-releases.md @@ -0,0 +1,269 @@ +# TypeScript Release Notes + +Reference material for TypeScript release notes and new features by version. + +--- + +## TypeScript 6.0 + +- Reference: [Announcing TypeScript 6.0 Beta](https://devblogs.microsoft.com/typescript/announcing-typescript-6-0-beta/) + +TypeScript 6.0 represents a major version milestone. Key areas of focus include stricter module semantics, improved interoperability, and performance improvements in the compiler and language service. See the announcement blog post for the full list of features and breaking changes. + +--- + +## TypeScript 5.x Release Notes + +> [!IMPORTANT] +> Always check for newer versions, fetching [latest TypeScript](https://github.com/microsoft/TypeScript/releases/latest). + +The following are reference links to the official release notes for each TypeScript version: + +- [TypeScript 5.0](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html) +- [TypeScript 5.1](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-1.html) +- [TypeScript 5.2](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html) +- [TypeScript 5.3](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-3.html) +- [TypeScript 5.4](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-4.html) +- [TypeScript 5.5](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html) +- [TypeScript 5.6](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-6.html) +- [TypeScript 5.7](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-7.html) +- [TypeScript 5.8](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-8.html) +- [TypeScript 5.9](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-9.html) + +--- + +## TypeScript 5.8 Highlights + +- Reference: [Announcing TypeScript 5.8](https://devblogs.microsoft.com/typescript/announcing-typescript-5-8/) + +### `require()` of ECMAScript Modules + +TypeScript 5.8 adds support for `require()`-ing ES modules when targeting Node.js environments that support it (Node.js 22+). Controlled via `--module nodenext` and `--moduleResolution nodenext`. + +### Granular Checks for Branches in Return Expressions + +TypeScript now performs finer-grained analysis on expressions inside `return` statements. Previously, TypeScript treated the entire `return` expression as a single unit; now it can drill into individual branches of ternary expressions and short-circuit operators. + +```typescript +// TypeScript 5.8 can now narrow within the returned expression +function getLabel(value: string | number): string { + return typeof value === "string" ? value.toUpperCase() : value.toFixed(2); +} +``` + +### `--erasableSyntaxOnly` Flag + +A new compiler flag that errors if the TypeScript file contains any syntax that cannot be erased to produce valid JavaScript. This is useful for tools (like Node.js's built-in TypeScript support) that strip types without transforming syntax. + +Syntax that is NOT erasable (and would error under this flag): +- `enum` declarations (non-`const`) +- `namespace` / `module` declarations +- Parameter properties in constructors (`constructor(private x: number)`) +- Legacy decorators with `emitDecoratorMetadata` + +```typescript +// Error under --erasableSyntaxOnly: enums require a transform +enum Direction { + Up, + Down, +} +``` + +### `--libReplacement` Flag + +Allows replacing standard library files (like `lib.dom.d.ts`) with custom equivalents, useful for projects that target non-browser environments or want to swap in a third-party DOM type library. + +--- + +## TypeScript 5.7 Highlights + +- Reference: [Announcing TypeScript 5.7](https://devblogs.microsoft.com/typescript/announcing-typescript-5-7/) + +### Checks for Never-Initialized Variables + +TypeScript 5.7 detects variables that are declared but provably never assigned before use, even across complex control flow paths. + +```typescript +let value: string; +console.log(value); // Error: Variable 'value' is used before being assigned. +``` + +### Path Rewriting for Relative Imports in Emit + +When using `--rewriteRelativeImportExtensions`, TypeScript rewrites relative `.ts` import extensions to `.js` in emitted output. This is especially useful for Node.js ESM workflows where file extensions must be explicit. + +```typescript +// Source +import { helper } from "./utils.ts"; + +// Emitted (with rewriting enabled) +import { helper } from "./utils.js"; +``` + +### `--target ES2024` and `--lib ES2024` + +TypeScript 5.7 adds ES2024 as a valid `target` and `lib` value, covering new built-in APIs such as `Promise.withResolvers()`, `Object.groupBy()`, and `Map.groupBy()`. + +### Support for `V8 Compile Caching` + +When running under Node.js, TypeScript 5.7 can take advantage of V8's compile cache API to speed up repeated executions of the TypeScript compiler. + +--- + +## TypeScript 5.5 Highlights + +- Reference: [Announcing TypeScript 5.5](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/) + +### Inferred Type Predicates + +TypeScript 5.5 can now infer type predicate return types from function bodies, without requiring an explicit `is` annotation. This means `Array.prototype.filter` and similar patterns now narrow types correctly. + +```typescript +// Before 5.5 — required explicit annotation: +function isString(x: unknown): x is string { + return typeof x === "string"; +} + +// With 5.5 — inferred automatically: +function isString(x: unknown) { + return typeof x === "string"; // inferred return type: x is string +} + +const mixed: (string | number)[] = ["hello", 42, "world", 1]; +const strings = mixed.filter(isString); // string[] — correctly narrowed +``` + +### Control Flow Narrowing for Constant Indexed Accesses + +TypeScript 5.5 narrows the type of indexed accesses when the index is a constant value. + +```typescript +function process(obj: Record, key: string) { + if (typeof obj[key] === "string") { + obj[key].toUpperCase(); // Now correctly narrowed to string + } +} +``` + +### JSDoc `@import` Tag + +Allows importing types in `.js` files using JSDoc without requiring `import type` statements. + +```js +/** @import { SomeType } from "some-module" */ + +/** @param {SomeType} value */ +function doSomething(value) {} +``` + +### Regular Expression Syntax Checking + +TypeScript 5.5 validates regex literal syntax at compile time, catching invalid patterns early. + +```typescript +const pattern = /(?\w+)/; // OK +const bad = /(?P\w+)/; // Error: invalid named capture group syntax +``` + +### `isolatedDeclarations` Compiler Option + +A new option that requires all exported declarations to have explicit type annotations, making it possible for other tools to generate `.d.ts` files without running the TypeScript compiler. + +```typescript +// Error under isolatedDeclarations — return type must be explicit +export function add(a: number, b: number) { + return a + b; +} + +// OK +export function add(a: number, b: number): number { + return a + b; +} +``` + +--- + +## TypeScript 5.0 Highlights + +- Reference: [Announcing TypeScript 5.0](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/) + +### Decorators (Standard) + +TypeScript 5.0 implements the TC39 Stage 3 decorators proposal. The new decorator syntax is incompatible with the legacy `experimentalDecorators` option. + +```typescript +function logged(fn: Function, ctx: ClassMethodDecoratorContext) { + return function (this: unknown, ...args: unknown[]) { + console.log(`Calling ${String(ctx.name)}`); + return fn.call(this, ...args); + }; +} + +class Greeter { + @logged + greet() { + return "Hello!"; + } +} +``` + +### `const` Type Parameters + +The `const` modifier on type parameters infers literal (narrowed) types rather than widened types. + +```typescript +function identity(value: T): T { + return value; +} + +const result = identity({ x: 10, y: 20 }); +// result: { x: 10, y: 20 } (not { x: number, y: number }) +``` + +### Multiple Config File `extends` + +`tsconfig.json` can now extend multiple base configurations. + +```json +{ + "extends": ["@tsconfig/strictest", "./base.json"], + "compilerOptions": { + "outDir": "./dist" + } +} +``` + +### `--moduleResolution bundler` + +A new `moduleResolution` strategy optimized for modern bundlers (Vite, esbuild, Parcel). It allows importing TypeScript files with `.ts` extensions and resolves `package.json` `exports` fields. + +### `--verbatimModuleSyntax` + +Replaces `importsNotUsedAsValues` and `preserveValueImports`. Forces explicit `import type` for type-only imports, ensuring the emitted output matches the source exactly. + +```typescript +// Error — must use 'import type' for type-only imports +import { SomeType } from "./types"; + +// OK +import type { SomeType } from "./types"; +``` + +### `export type *` Syntax + +```typescript +export type * from "./types"; +export type * as Types from "./types"; +``` + +### All `enum`s Are Union Enums + +Enum members now participate in union type narrowing more reliably. + +--- + +## Additional Release Notes + +For a full index of TypeScript release notes across all versions, see the [TypeScript Handbook Release Notes overview](https://www.typescriptlang.org/docs/handbook/release-notes/overview.html). + +Blog announcements for all releases are published at [devblogs.microsoft.com/typescript](https://devblogs.microsoft.com/typescript/). diff --git a/skills/typescript-coder/references/typescript-tools.md b/skills/typescript-coder/references/typescript-tools.md new file mode 100644 index 000000000..250263070 --- /dev/null +++ b/skills/typescript-coder/references/typescript-tools.md @@ -0,0 +1,241 @@ +# TypeScript Tools + +Reference material for TypeScript developer tools — the TypeScript Playground, TSConfig reference, and related tooling. + +--- + +## TypeScript Playground + +- Reference material for [TypeScript Playground](https://www.typescriptlang.org/play/) + +The TypeScript Playground is an interactive, browser-based coding environment. It allows developers to write, run, and experiment with TypeScript code directly in the browser — no installation required. It serves as a sandbox for learning TypeScript, testing code snippets, reproducing bugs, and sharing code examples with others. + +### Key Features + +- **Code Editor** — A Monaco-based editor with full syntax highlighting and IntelliSense, supporting multiple open files/tabs and real-time error highlighting. +- **Compiler Configuration Panel** — Toggle compiler options (e.g., `strict`, `noImplicitAny`) via a TS Config panel. Supports boolean flags, dropdown selectors, and TypeScript version switching (release, beta, nightly). +- **Sidebar Panels** — Includes tabs for Errors (compiler diagnostics), Logs/Console (runtime output), AST Explorer (Abstract Syntax Tree visualization), and community Plugins. +- **Shareable URLs** — Code is encoded into the URL, making it easy to share examples or bug reports. +- **Examples Library** — A built-in collection of code samples organized by topic. + +### How to Use It + +1. Navigate to `typescriptlang.org/play` +2. Type or paste TypeScript code in the left editor pane +3. View output (compiled JavaScript, errors, AST) in the right sidebar +4. Adjust compiler options via the *TS Config* dropdown in the toolbar +5. Share your code by copying the URL + +### Example Use Case + +```typescript +// Test strict null checks in the Playground +function greet(name: string | null) { + console.log("Hello, " + name.toUpperCase()); // Error if strictNullChecks is on +} +``` + +With `strictNullChecks` enabled in the Config panel, the Playground immediately highlights the potential null dereference — useful for learning and debugging TypeScript's type system interactively. + +--- + +## TSConfig Reference + +- Reference material for [TSConfig Reference](https://www.typescriptlang.org/tsconfig/) + +The `tsconfig.json` file controls how TypeScript compiles your project. Below are the compiler options grouped by category. + +### Type Checking + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `strict` | boolean | `false` | Enables all strict type-checking options | +| `strictNullChecks` | boolean | `false` | `null` and `undefined` are distinct types | +| `strictFunctionTypes` | boolean | `false` | Stricter checking of function parameter types (contravariance) | +| `strictBindCallApply` | boolean | `false` | Strict checking for `bind`, `call`, `apply` | +| `strictPropertyInitialization` | boolean | `false` | Class properties must be initialized in the constructor | +| `noImplicitAny` | boolean | `false` | Error on expressions with an implicit `any` type | +| `noImplicitThis` | boolean | `false` | Error on `this` with an implicit `any` type | +| `useUnknownInCatchVariables` | boolean | `false` | Catch clause variables typed as `unknown` instead of `any` | +| `alwaysStrict` | boolean | `false` | Parse in strict mode and emit `"use strict"` | +| `noUnusedLocals` | boolean | `false` | Error on unused local variables | +| `noUnusedParameters` | boolean | `false` | Error on unused function parameters | +| `exactOptionalPropertyTypes` | boolean | `false` | Disallows assigning `undefined` to optional properties | +| `noImplicitReturns` | boolean | `false` | Error when not all code paths return a value | +| `noFallthroughCasesInSwitch` | boolean | `false` | Error on fallthrough switch cases | +| `noUncheckedIndexedAccess` | boolean | `false` | Index access types include `undefined` | +| `noImplicitOverride` | boolean | `false` | Require `override` keyword on overridden methods | +| `noPropertyAccessFromIndexSignature` | boolean | `false` | Require bracket notation for index-signature properties | +| `allowUnusedLabels` | boolean | — | Allow unused labels | +| `allowUnreachableCode` | boolean | — | Allow unreachable code | + +### Modules + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `module` | string | varies | Module system: `commonjs`, `es2015`, `esnext`, `node16`, `nodenext`, etc. | +| `moduleResolution` | string | varies | Resolution strategy: `node`, `bundler`, `node16`, `nodenext` | +| `baseUrl` | string | — | Base directory for non-relative module names | +| `paths` | object | — | Path mapping entries for module aliases | +| `rootDirs` | string[] | — | Multiple root directories merged at runtime | +| `typeRoots` | string[] | — | Directories to include type definitions from | +| `types` | string[] | — | Only include listed `@types` packages globally | +| `allowSyntheticDefaultImports` | boolean | varies | Allow default imports from modules without a default export | +| `esModuleInterop` | boolean | `false` | Emit `__esModule` helpers for CommonJS/ES module interop | +| `allowUmdGlobalAccess` | boolean | `false` | Allow accessing UMD globals from modules | +| `resolveJsonModule` | boolean | `false` | Enable importing `.json` files | +| `noResolve` | boolean | `false` | Disable resolving imports/triple-slash references | +| `allowImportingTsExtensions` | boolean | `false` | Allow imports with `.ts`/`.tsx` extensions | +| `resolvePackageJsonExports` | boolean | varies | Use `exports` field in `package.json` for resolution | +| `resolvePackageJsonImports` | boolean | varies | Use `imports` field in `package.json` for resolution | +| `verbatimModuleSyntax` | boolean | `false` | Enforce that import/export style matches the emitted output | + +### Emit + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `target` | string | `ES3` | Compilation target: `ES5`, `ES6`/`ES2015` ... `ESNext` | +| `lib` | string[] | varies | Built-in API declaration sets to include (e.g. `DOM`, `ES2020`) | +| `outDir` | string | — | Output directory for compiled files | +| `outFile` | string | — | Bundle all output into a single file | +| `rootDir` | string | — | Root of the input source files | +| `declaration` | boolean | `false` | Generate `.d.ts` declaration files | +| `declarationDir` | string | — | Output directory for `.d.ts` files | +| `declarationMap` | boolean | `false` | Generate source maps for `.d.ts` files | +| `emitDeclarationOnly` | boolean | `false` | Only emit `.d.ts` files; no JavaScript output | +| `sourceMap` | boolean | `false` | Generate `.js.map` source map files | +| `inlineSourceMap` | boolean | `false` | Include source maps inline in the JS output | +| `inlineSources` | boolean | `false` | Include TypeScript source in source maps | +| `removeComments` | boolean | `false` | Strip all comments from output | +| `noEmit` | boolean | `false` | Do not emit any output files (type-check only) | +| `noEmitOnError` | boolean | `false` | Skip emit if there are type errors | +| `importHelpers` | boolean | `false` | Import helper functions from `tslib` | +| `downlevelIteration` | boolean | `false` | Correct (but verbose) iteration for older compilation targets | +| `preserveConstEnums` | boolean | `false` | Keep `const enum` declarations in emitted output | +| `stripInternal` | boolean | — | Remove declarations marked `@internal` | + +### JavaScript Support + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `allowJs` | boolean | `false` | Allow `.js` files to be included in the project | +| `checkJs` | boolean | `false` | Enable type checking in `.js` files | +| `maxNodeModuleJsDepth` | number | `0` | Max depth for type-checking JS inside `node_modules` | + +### Interop Constraints + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `isolatedModules` | boolean | `false` | Ensure each file can be safely transpiled in isolation | +| `forceConsistentCasingInFileNames` | boolean | `false` | Disallow inconsistently-cased imports | +| `isolatedDeclarations` | boolean | `false` | Require explicit types for public API surface | +| `esModuleInterop` | boolean | `false` | See Modules section | + +### Language and Environment + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `experimentalDecorators` | boolean | `false` | Enable legacy TC39 Stage 2 decorator support | +| `emitDecoratorMetadata` | boolean | `false` | Emit design-time type metadata for decorated declarations | +| `jsx` | string | — | JSX mode: `preserve`, `react`, `react-jsx`, `react-jsxdev`, `react-native` | +| `jsxFactory` | string | `React.createElement` | JSX factory function name | +| `jsxFragmentFactory` | string | `React.Fragment` | JSX fragment factory name | +| `jsxImportSource` | string | `react` | Module specifier to import JSX factory from | +| `moduleDetection` | string | `auto` | How TypeScript determines whether a file is a module | +| `noLib` | boolean | `false` | Exclude the default `lib.d.ts` | +| `useDefineForClassFields` | boolean | varies | Use ECMAScript-standard class field semantics | + +### Projects + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `incremental` | boolean | varies | Save build info to disk for faster incremental rebuilds | +| `composite` | boolean | `false` | Enable project references | +| `tsBuildInfoFile` | string | `.tsbuildinfo` | Path to the incremental build info file | +| `disableSourceOfProjectReferenceRedirect` | boolean | `false` | Use `.d.ts` instead of source files for project references | +| `disableSolutionSearching` | boolean | `false` | Opt out of multi-project reference discovery | +| `disableReferencedProjectLoad` | boolean | `false` | Reduce the number of loaded projects in editor | + +### Output Formatting + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `noErrorTruncation` | boolean | `false` | Show complete (non-truncated) error messages | +| `preserveWatchOutput` | boolean | `false` | Keep previous output on screen in watch mode | +| `pretty` | boolean | `true` | Colorize and format diagnostic output | + +### Completeness + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `skipDefaultLibCheck` | boolean | `false` | Skip type checking of default `.d.ts` library files | +| `skipLibCheck` | boolean | `false` | Skip type checking of all `.d.ts` declaration files | + +### Common tsconfig.json Example + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +### tsconfig.json for a React + Vite project + +```json +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} +``` + +### tsconfig.json for a Node.js library + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "node16", + "moduleResolution": "node16", + "strict": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} +``` diff --git a/skills/typescript-coder/references/typescript-tutorials.md b/skills/typescript-coder/references/typescript-tutorials.md new file mode 100644 index 000000000..c753eef81 --- /dev/null +++ b/skills/typescript-coder/references/typescript-tutorials.md @@ -0,0 +1,724 @@ +# TypeScript Tutorials + +Step-by-step tutorials for using TypeScript in various frameworks and build tools. + +--- + +## ASP.NET Core + +- Reference: [TypeScript with ASP.NET Core](https://www.typescriptlang.org/docs/handbook/asp-net-core.html) + +### Prerequisites + +- [.NET SDK](https://dotnet.microsoft.com/download) +- [Node.js and npm](https://nodejs.org/) + +### 1. Create a New ASP.NET Core Project + +```bash +dotnet new web -o MyTypescriptApp +cd MyTypescriptApp +``` + +### 2. Add TypeScript + +```bash +npm init -y +npm install --save-dev typescript +``` + +### 3. Configure TypeScript (`tsconfig.json`) + +```json +{ + "compilerOptions": { + "target": "ES5", + "module": "commonjs", + "sourceMap": true, + "outDir": "./wwwroot/js" + }, + "include": [ + "./src/**/*" + ] +} +``` + +### 4. Create TypeScript Source File + +```bash +mkdir src +``` + +`src/app.ts`: + +```typescript +function sayHello(name: string): string { + return `Hello, ${name}!`; +} + +const message = sayHello("ASP.NET Core"); +console.log(message); +``` + +### 5. Compile TypeScript + +```bash +npx tsc +``` + +This outputs compiled JavaScript to `wwwroot/js/app.js`. + +### 6. Reference in a Razor Page (`Pages/Index.cshtml`) + +```html +@page +@model IndexModel + +

    TypeScript + ASP.NET Core

    + +@section Scripts { + +} +``` + +### 7. Watch Mode for Development + +```bash +npx tsc --watch +``` + +### 8. Integrate with MSBuild + +Add a build target to your `.csproj` file to compile TypeScript automatically before each .NET build: + +```xml + + + net8.0 + + + + + + +``` + +| Step | Tool | +|------|------| +| Project scaffold | `dotnet new web` | +| TypeScript install | `npm install typescript` | +| Config | `tsconfig.json` | +| Compile | `npx tsc` | +| Output | `wwwroot/js/` | + +--- + +## Migrating from JavaScript + +- Reference: [Migrating from JavaScript](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html) + +### 1. Initial Setup + +```bash +npm install --save-dev typescript +npx tsc --init +``` + +### 2. Base `tsconfig.json` for Gradual Migration + +Start permissive and tighten over time: + +```json +{ + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "outDir": "./dist", + "strict": false, + "noImplicitAny": false + }, + "include": ["src/**/*"] +} +``` + +### 3. Gradual Migration Strategy + +**Phase 1 — Rename files one at a time:** + +``` +myFile.js → myFile.ts +``` + +**Phase 2 — Enable JS type checking:** + +```json +{ "checkJs": true } +``` + +**Phase 3 — Add type annotations incrementally:** + +```typescript +// Before (JavaScript) +function greet(name) { + return "Hello, " + name; +} + +// After (TypeScript) +function greet(name: string): string { + return "Hello, " + name; +} +``` + +### 4. Common Issues and Fixes + +**Implicit `any` errors:** + +```typescript +// Error: Parameter 'x' implicitly has an 'any' type +function add(x, y) { return x + y; } + +// Fix +function add(x: number, y: number): number { return x + y; } +``` + +**Missing type definitions for npm packages:** + +```bash +npm install --save-dev @types/lodash +npm install --save-dev @types/node +``` + +**Object shape errors:** + +```typescript +// Error: Property 'age' does not exist on type '{}' +const user = {}; +user.age = 25; + +// Fix: Define an interface +interface User { age: number; name: string; } +const user: User = { age: 25, name: "Alice" }; +``` + +**Module import issues:** + +```json +// tsconfig.json — add: +{ "esModuleInterop": true, "moduleResolution": "node" } +``` + +```typescript +// Then use default imports: +import express from 'express'; +``` + +### 5. Tightening `tsconfig.json` Over Time + +```json +{ + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noUnusedLocals": true, + "noUnusedParameters": true + } +} +``` + +### 6. Best Practices + +| Practice | Description | +|---|---| +| Use `unknown` over `any` | Forces a type check before use | +| Avoid type assertions (`as`) | Prefer type guards instead | +| Leverage type inference | Don't annotate what TypeScript can infer | +| Use `interface` for object shapes | Extensible and readable | +| Migrate leaf files first | Files with no local dependencies are easiest to start with | + +**Type guard example:** + +```typescript +// Avoid: +const val = someValue as string; + +// Prefer: +function isString(val: unknown): val is string { + return typeof val === "string"; +} + +if (isString(someValue)) { + someValue.toUpperCase(); // safely narrowed +} +``` + +--- + +## Working with the DOM + +- Reference: [TypeScript DOM Manipulation](https://www.typescriptlang.org/docs/handbook/dom-manipulation.html) + +### Enable DOM Type Definitions + +DOM types come from `lib.dom.d.ts`. Enable them in `tsconfig.json`: + +```json +{ + "compilerOptions": { + "lib": ["ES2020", "DOM", "DOM.Iterable"] + } +} +``` + +### `getElementById` + +Returns `HTMLElement | null` — the `null` case must be handled: + +```typescript +// Type: HTMLElement | null +const el = document.getElementById("myDiv"); + +// Null check required +if (el) { + el.textContent = "Hello"; +} + +// Non-null assertion — use only when certain the element exists +const el2 = document.getElementById("root")!; +``` + +### `querySelector` and `querySelectorAll` + +```typescript +// Returns Element | null +const div = document.querySelector("div"); + +// Generic overload for specific element types +const input = document.querySelector("#username"); +if (input) { + console.log(input.value); // .value available on HTMLInputElement +} + +// querySelectorAll returns NodeListOf +const items = document.querySelectorAll("li"); +items.forEach(item => { + console.log(item.textContent); +}); +``` + +### HTMLElement Subtypes + +| Interface | Corresponding Element | Notable Properties | +|---|---|---| +| `HTMLInputElement` | `` | `.value`, `.checked`, `.type` | +| `HTMLAnchorElement` | `` | `.href`, `.target` | +| `HTMLImageElement` | `` | `.src`, `.alt`, `.width` | +| `HTMLFormElement` | `
    ` | `.submit()`, `.reset()` | +| `HTMLButtonElement` | ` - -`); - -export class TodoView extends Backbone.View { - tagName = 'li' as const; - - events(): Backbone.EventsHash { - return { - 'click .toggle': 'onToggle', - 'click .destroy': 'onDestroy', - 'dblclick .title': 'onEdit', - }; - } - - initialize(): void { - this.listenTo(this.model, 'change', this.render); - this.listenTo(this.model, 'destroy', this.remove); - } - - render(): this { - this.$el.html(TEMPLATE(this.model.toJSON())); - this.$el.toggleClass('completed', !!this.model.get('completed')); - return this; - } - - private onToggle(): void { - this.model.toggle(); - } - - private onDestroy(): void { - this.model.destroy(); - } - - private onEdit(): void { - const newTitle = prompt('Edit todo:', this.model.get('title')); - if (newTitle !== null && newTitle.trim()) { - this.model.save({ title: newTitle.trim() }); - } - } -} -``` - -### `src/views/TodoListView.ts` - -```typescript -import Backbone from 'backbone'; -import type { TodoCollection } from '../collections/TodoCollection.js'; -import type { TodoModel } from '../models/TodoModel.js'; -import { TodoView } from './TodoView.js'; - -export class TodoListView extends Backbone.View { - declare collection: TodoCollection; - - events(): Backbone.EventsHash { - return { - 'keypress #new-todo': 'onKeyPress', - 'click #clear-completed': 'onClearCompleted', - }; - } - - initialize(): void { - this.listenTo(this.collection, 'add', this.addOne); - this.listenTo(this.collection, 'reset', this.addAll); - this.listenTo(this.collection, 'change remove', this.updateStatus); - this.collection.fetch(); - } - - render(): this { - this.$el.html(` -

    Todos

    - -
      -
      - `); - this.addAll(); - return this; - } - - private addOne(todo: TodoModel): void { - const view = new TodoView({ model: todo }); - this.$('#todo-list').append(view.render().el); - } - - private addAll(): void { - this.$('#todo-list').empty(); - this.collection.each((todo) => this.addOne(todo)); - this.updateStatus(); - } - - private updateStatus(): void { - const remaining = this.collection.remaining.length; - this.$('#footer').html( - `${remaining} item${remaining !== 1 ? 's' : ''} left - ` - ); - } - - private onKeyPress(e: JQuery.KeyPressEvent): void { - const input = this.$('#new-todo'); - const title = (input.val() as string).trim(); - if (e.which === 13 && title) { - this.collection.create({ title, completed: false, createdAt: new Date() }); - input.val(''); - } - } - - private onClearCompleted(): void { - this.collection.clearCompleted(); - } -} -``` - -### `src/router/AppRouter.ts` - -```typescript -import Backbone from 'backbone'; - -export class AppRouter extends Backbone.Router { - routes(): Backbone.RoutesHash { - return { - '': 'home', - 'todos/:id': 'showTodo', - '*path': 'notFound', - }; - } - - home(): void { - console.log('Route: home'); - } - - showTodo(id: string): void { - console.log(`Route: showTodo — id=${id}`); - } - - notFound(path: string): void { - console.warn(`Route not found: ${path}`); - } -} -``` - -### `src/app.ts` - -```typescript -import $ from 'jquery'; -import { TodoCollection } from './collections/TodoCollection.js'; -import { AppRouter } from './router/AppRouter.js'; -import { TodoListView } from './views/TodoListView.js'; - -$(() => { - const todos = new TodoCollection(); - - const appView = new TodoListView({ - collection: todos, - el: '#app', - }); - appView.render(); - - const router = new AppRouter(); - Backbone.history.start({ pushState: true }); -}); -``` - -## Getting Started - -```bash -# 1. Create project directory -mkdir my-backbone-app && cd my-backbone-app - -# 2. Copy project files (see structure above) - -# 3. Install dependencies -npm install - -# 4. Start the development server (http://localhost:8080) -npm start - -# 5. Build for production -npm run build -``` - -## Features - -- Strongly-typed Backbone Models, Collections, Views, and Router using `@types/backbone` -- Webpack 5 bundling with `ts-loader`, content-hash output filenames, and dev-server HMR -- Underscore templates compiled inline — no extra template loader needed -- Collection comparator, filtering helpers (`remaining`, `completed`), and `clearCompleted` -- View event delegation using the standard Backbone `events()` hash with TypeScript method references -- `Backbone.history` with `pushState` for clean URLs -- Strict TypeScript mode with source maps in both development and production diff --git a/skills/typescript-coder/assets/typescript-bitloops.md b/skills/typescript-coder/assets/typescript-bitloops.md deleted file mode 100644 index 9a8b2abe5..000000000 --- a/skills/typescript-coder/assets/typescript-bitloops.md +++ /dev/null @@ -1,547 +0,0 @@ -# TypeScript Bitloops DDD / Clean Architecture Template - -> A TypeScript project template based on the `generator-bitloops` Yeoman generator. Produces -> a Domain-Driven Design (DDD) application scaffold following clean/hexagonal architecture -> principles. Organises code into domain, application, and infrastructure layers with bounded -> contexts, strongly typed value objects, domain entities, aggregates, repositories, and -> use cases (application services). - -## License - -See the [generator-bitloops repository](https://github.com/bitloops/generator-bitloops) for -license terms. Bitloops open-source tooling is generally released under the MIT License. - -## Source - -- [generator-bitloops](https://github.com/bitloops/generator-bitloops) by Bitloops - -## Project Structure - -``` -my-bitloops-app/ -├── src/ -│ ├── bounded-contexts/ -│ │ └── iam/ # Identity & Access Management context -│ │ ├── domain/ -│ │ │ ├── entities/ -│ │ │ │ └── user.entity.ts -│ │ │ ├── value-objects/ -│ │ │ │ ├── email.value-object.ts -│ │ │ │ └── user-id.value-object.ts -│ │ │ ├── aggregates/ -│ │ │ │ └── user.aggregate.ts -│ │ │ ├── events/ -│ │ │ │ └── user-registered.event.ts -│ │ │ ├── errors/ -│ │ │ │ └── user.errors.ts -│ │ │ └── repositories/ -│ │ │ └── user.repository.ts # Port (interface) -│ │ ├── application/ -│ │ │ └── use-cases/ -│ │ │ └── register-user/ -│ │ │ ├── register-user.use-case.ts -│ │ │ ├── register-user.request.ts -│ │ │ └── register-user.response.ts -│ │ └── infrastructure/ -│ │ ├── persistence/ -│ │ │ └── mongo-user.repository.ts # Adapter -│ │ └── mappers/ -│ │ └── user.mapper.ts -│ ├── shared/ -│ │ ├── domain/ -│ │ │ ├── entity.base.ts -│ │ │ ├── aggregate-root.base.ts -│ │ │ ├── value-object.base.ts -│ │ │ ├── domain-event.base.ts -│ │ │ └── unique-entity-id.ts -│ │ └── result/ -│ │ └── result.ts -│ └── main.ts -├── tests/ -│ ├── unit/ -│ │ └── iam/ -│ │ └── user.entity.spec.ts -│ └── integration/ -│ └── iam/ -│ └── register-user.use-case.spec.ts -├── package.json -├── tsconfig.json -└── .env.example -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-bitloops-app", - "version": "1.0.0", - "description": "DDD / clean architecture TypeScript app via generator-bitloops", - "main": "dist/main.js", - "scripts": { - "build": "tsc", - "start": "node dist/main.js", - "dev": "ts-node-dev --respawn --transpile-only src/main.ts", - "test": "jest --coverage", - "test:unit": "jest tests/unit", - "test:integration": "jest tests/integration", - "lint": "eslint 'src/**/*.ts'", - "clean": "rimraf dist" - }, - "dependencies": { - "dotenv": "^16.3.1", - "uuid": "^9.0.0" - }, - "devDependencies": { - "@types/jest": "^29.5.11", - "@types/node": "^20.11.0", - "@types/uuid": "^9.0.7", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0", - "jest": "^29.7.0", - "rimraf": "^5.0.5", - "ts-jest": "^29.1.4", - "ts-node-dev": "^2.0.0", - "typescript": "^5.3.3" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "lib": ["ES2020"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "paths": { - "@shared/*": ["src/shared/*"], - "@iam/*": ["src/bounded-contexts/iam/*"] - } - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests"] -} -``` - -### `src/shared/result/result.ts` - -```typescript -export type Either = Left | Right; - -export class Left { - readonly value: L; - - constructor(value: L) { - this.value = value; - } - - isLeft(): this is Left { - return true; - } - - isRight(): this is Right { - return false; - } -} - -export class Right { - readonly value: R; - - constructor(value: R) { - this.value = value; - } - - isLeft(): this is Left { - return false; - } - - isRight(): this is Right { - return true; - } -} - -export const left = (value: L): Either => new Left(value); -export const right = (value: R): Either => new Right(value); -``` - -### `src/shared/domain/value-object.base.ts` - -```typescript -interface ValueObjectProps { - [index: string]: unknown; -} - -export abstract class ValueObject { - protected readonly props: T; - - constructor(props: T) { - this.props = Object.freeze(props); - } - - equals(other?: ValueObject): boolean { - if (other === null || other === undefined) return false; - if (other.props === undefined) return false; - return JSON.stringify(this.props) === JSON.stringify(other.props); - } -} -``` - -### `src/shared/domain/entity.base.ts` - -```typescript -import { UniqueEntityId } from './unique-entity-id'; - -export abstract class Entity { - protected readonly _id: UniqueEntityId; - protected readonly props: T; - - constructor(props: T, id?: UniqueEntityId) { - this._id = id ?? new UniqueEntityId(); - this.props = props; - } - - get id(): UniqueEntityId { - return this._id; - } - - equals(entity?: Entity): boolean { - if (entity === null || entity === undefined) return false; - if (!(entity instanceof Entity)) return false; - return this._id.equals(entity._id); - } -} -``` - -### `src/shared/domain/aggregate-root.base.ts` - -```typescript -import { Entity } from './entity.base'; -import { DomainEvent } from './domain-event.base'; -import { UniqueEntityId } from './unique-entity-id'; - -export abstract class AggregateRoot extends Entity { - private _domainEvents: DomainEvent[] = []; - - constructor(props: T, id?: UniqueEntityId) { - super(props, id); - } - - get domainEvents(): DomainEvent[] { - return this._domainEvents; - } - - protected addDomainEvent(event: DomainEvent): void { - this._domainEvents.push(event); - } - - clearEvents(): void { - this._domainEvents = []; - } -} -``` - -### `src/shared/domain/unique-entity-id.ts` - -```typescript -import { v4 as uuidv4 } from 'uuid'; -import { ValueObject } from './value-object.base'; - -interface UniqueEntityIdProps { - value: string; -} - -export class UniqueEntityId extends ValueObject { - constructor(id?: string) { - super({ value: id ?? uuidv4() }); - } - - get value(): string { - return this.props.value; - } - - toString(): string { - return this.props.value; - } -} -``` - -### `src/bounded-contexts/iam/domain/value-objects/email.value-object.ts` - -```typescript -import { Either, left, right } from '../../../../shared/result/result'; -import { ValueObject } from '../../../../shared/domain/value-object.base'; - -interface EmailProps { - value: string; -} - -type EmailError = { type: 'INVALID_EMAIL'; message: string }; - -export class Email extends ValueObject { - private static readonly EMAIL_REGEX = - /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; - - private constructor(props: EmailProps) { - super(props); - } - - get value(): string { - return this.props.value; - } - - static create(email: string): Either { - if (!email || !Email.EMAIL_REGEX.test(email)) { - return left({ - type: 'INVALID_EMAIL', - message: `"${email}" is not a valid email address`, - }); - } - return right(new Email({ value: email.toLowerCase() })); - } -} -``` - -### `src/bounded-contexts/iam/domain/aggregates/user.aggregate.ts` - -```typescript -import { AggregateRoot } from '../../../../shared/domain/aggregate-root.base'; -import { UniqueEntityId } from '../../../../shared/domain/unique-entity-id'; -import { Email } from '../value-objects/email.value-object'; -import { UserRegisteredEvent } from '../events/user-registered.event'; -import { Either, left, right } from '../../../../shared/result/result'; - -interface UserProps { - email: Email; - passwordHash: string; - name: string; - isActive: boolean; - createdAt: Date; -} - -type UserCreateError = { type: 'USER_ALREADY_EXISTS' | 'INVALID_EMAIL'; message: string }; - -export class User extends AggregateRoot { - private constructor(props: UserProps, id?: UniqueEntityId) { - super(props, id); - } - - get email(): Email { - return this.props.email; - } - - get name(): string { - return this.props.name; - } - - get isActive(): boolean { - return this.props.isActive; - } - - static create( - props: { email: string; passwordHash: string; name: string }, - id?: UniqueEntityId, - ): Either { - const emailOrError = Email.create(props.email); - if (emailOrError.isLeft()) { - return left({ type: 'INVALID_EMAIL', message: emailOrError.value.message }); - } - - const user = new User( - { - email: emailOrError.value, - passwordHash: props.passwordHash, - name: props.name, - isActive: true, - createdAt: new Date(), - }, - id, - ); - - user.addDomainEvent( - new UserRegisteredEvent({ userId: user.id.value, email: props.email }), - ); - - return right(user); - } -} -``` - -### `src/bounded-contexts/iam/domain/repositories/user.repository.ts` - -```typescript -import { User } from '../aggregates/user.aggregate'; - -// Port — implemented in infrastructure layer -export interface IUserRepository { - findById(id: string): Promise; - findByEmail(email: string): Promise; - save(user: User): Promise; - delete(id: string): Promise; -} -``` - -### `src/bounded-contexts/iam/application/use-cases/register-user/register-user.use-case.ts` - -```typescript -import { IUserRepository } from '../../../domain/repositories/user.repository'; -import { User } from '../../../domain/aggregates/user.aggregate'; -import { RegisterUserRequest } from './register-user.request'; -import { RegisterUserResponse } from './register-user.response'; -import { Either, left, right } from '../../../../../../shared/result/result'; - -type RegisterUserError = - | { type: 'USER_ALREADY_EXISTS'; message: string } - | { type: 'INVALID_EMAIL'; message: string } - | { type: 'UNEXPECTED_ERROR'; message: string }; - -export class RegisterUserUseCase { - constructor(private readonly userRepository: IUserRepository) {} - - async execute( - request: RegisterUserRequest, - ): Promise> { - try { - const existing = await this.userRepository.findByEmail(request.email); - if (existing) { - return left({ - type: 'USER_ALREADY_EXISTS', - message: `A user with email "${request.email}" already exists`, - }); - } - - // NOTE: in production, hash with bcrypt before creating the aggregate - const userOrError = User.create({ - email: request.email, - passwordHash: request.passwordHash, - name: request.name, - }); - - if (userOrError.isLeft()) { - return left({ type: 'INVALID_EMAIL', message: userOrError.value.message }); - } - - const user = userOrError.value; - await this.userRepository.save(user); - - return right({ userId: user.id.value, email: user.email.value }); - } catch (err) { - return left({ - type: 'UNEXPECTED_ERROR', - message: err instanceof Error ? err.message : 'Unexpected error', - }); - } - } -} -``` - -### `src/bounded-contexts/iam/application/use-cases/register-user/register-user.request.ts` - -```typescript -export interface RegisterUserRequest { - name: string; - email: string; - passwordHash: string; -} -``` - -### `src/bounded-contexts/iam/application/use-cases/register-user/register-user.response.ts` - -```typescript -export interface RegisterUserResponse { - userId: string; - email: string; -} -``` - -### `src/bounded-contexts/iam/domain/events/user-registered.event.ts` - -```typescript -import { DomainEvent } from '../../../../shared/domain/domain-event.base'; - -interface UserRegisteredProps { - userId: string; - email: string; -} - -export class UserRegisteredEvent extends DomainEvent { - readonly userId: string; - readonly email: string; - - constructor(props: UserRegisteredProps) { - super({ aggregateId: props.userId, eventName: 'UserRegistered' }); - this.userId = props.userId; - this.email = props.email; - } -} -``` - -### `src/shared/domain/domain-event.base.ts` - -```typescript -interface DomainEventProps { - aggregateId: string; - eventName: string; -} - -export abstract class DomainEvent { - readonly aggregateId: string; - readonly eventName: string; - readonly occurredOn: Date; - - constructor(props: DomainEventProps) { - this.aggregateId = props.aggregateId; - this.eventName = props.eventName; - this.occurredOn = new Date(); - } -} -``` - -## Getting Started - -```bash -# 1. Install dependencies -npm install - -# 2. Run the domain tests -npm run test:unit - -# 3. Run integration tests -npm run test:integration - -# 4. Build -npm run build - -# 5. Start -npm start -``` - -## Features - -- Domain-Driven Design with bounded-context folder layout -- Aggregate roots, domain entities, and immutable value objects -- `Either` monad for explicit, type-safe error handling without exceptions -- Domain events attached to aggregates and cleared after persistence -- Repository interfaces (ports) in the domain layer; adapters in infrastructure -- Use cases (application services) orchestrating domain logic -- `UniqueEntityId` value object wrapping UUIDs for identity management -- Strict TypeScript with no implicit `any` and unused-variable enforcement -- Jest unit and integration tests targeting individual layers independently diff --git a/skills/typescript-coder/assets/typescript-bscotch-template-modern.md b/skills/typescript-coder/assets/typescript-bscotch-template-modern.md deleted file mode 100644 index 8454d7867..000000000 --- a/skills/typescript-coder/assets/typescript-bscotch-template-modern.md +++ /dev/null @@ -1,455 +0,0 @@ - - -# TypeScript Template — Modernized (bscotch variation) - -> Based on [bscotch/typescript-template](https://github.com/bscotch/typescript-template) -> License: **MIT** -> This is a **modernized variation** — the original may be outdated. This version applies -> current best practices: ESM, Node.js 20+, TypeScript 5.x strict mode, and Vitest. - -## What Changed from the Original - -| Area | Original (bscotch) | This Modernized Variation | -|---|---|---| -| Module system | CommonJS or mixed | Pure ESM (`"type": "module"`) | -| Node.js target | Node 14/16 | Node.js 20+ | -| TypeScript | 4.x | 5.x strict mode | -| Test runner | Mocha or Jest | **Vitest** (ESM-native, fast) | -| tsconfig base | Permissive | `@tsconfig/node20` + strict overrides | -| Module resolution | `node` | `NodeNext` | - -## Project Structure - -``` -my-project/ -├── src/ -│ ├── index.ts # Main entry / public API -│ ├── lib/ -│ │ └── utils.ts # Internal utilities -│ └── types.ts # Shared type definitions -├── tests/ -│ ├── index.test.ts -│ └── utils.test.ts -├── dist/ # Compiled output (git-ignored) -├── .gitignore -├── package.json -├── tsconfig.json -├── tsconfig.build.json # Excludes test files for production build -├── vitest.config.ts -└── README.md -``` - -## `package.json` - -```json -{ - "name": "my-project", - "version": "1.0.0", - "description": "A TypeScript project", - "author": "Your Name ", - "license": "MIT", - "type": "module", - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" - } - }, - "files": [ - "dist", - "!dist/**/*.test.*", - "!dist/**/*.spec.*" - ], - "engines": { - "node": ">=20.0.0" - }, - "scripts": { - "build": "tsc -p tsconfig.build.json", - "build:watch": "tsc -p tsconfig.build.json --watch", - "clean": "rimraf dist coverage", - "prebuild": "npm run clean", - "dev": "node --watch --loader ts-node/esm src/index.ts", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", - "type-check": "tsc --noEmit", - "lint": "eslint src tests", - "lint:fix": "eslint src tests --fix", - "prepublishOnly": "npm run build && npm run type-check" - }, - "devDependencies": { - "@tsconfig/node20": "^20.1.4", - "@types/node": "^22.0.0", - "@vitest/coverage-v8": "^2.1.0", - "rimraf": "^6.0.0", - "typescript": "^5.7.0", - "vitest": "^2.1.0" - } -} -``` - -## `tsconfig.json` - -Used for editor support and type-checking (includes test files): - -```json -{ - "extends": "@tsconfig/node20/tsconfig.json", - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2023"], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "exactOptionalPropertyTypes": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true - }, - "include": ["src/**/*", "tests/**/*"], - "exclude": ["node_modules", "dist"] -} -``` - -## `tsconfig.build.json` - -Used only for the production build — excludes test files: - -```json -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "sourceMap": false, - "inlineSources": false - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests/**/*", "**/*.test.ts", "**/*.spec.ts"] -} -``` - -## `vitest.config.ts` - -```typescript -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - // Run tests in Node.js environment - environment: "node", - - // Glob patterns for test files - include: ["tests/**/*.test.ts", "src/**/*.test.ts"], - - // Coverage configuration - coverage: { - provider: "v8", - reporter: ["text", "json", "html"], - include: ["src/**/*.ts"], - exclude: [ - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/types.ts", - "node_modules/**", - ], - thresholds: { - lines: 80, - functions: 80, - branches: 70, - statements: 80, - }, - }, - - // Timeout per test in milliseconds - testTimeout: 10_000, - - // Reporter - reporter: "verbose", - }, -}); -``` - -## `src/types.ts` - -```typescript -/** - * Shared type definitions for the project. - * Export all public-facing types from here. - */ - -/** Generic result type — avoids throwing for expected error cases. */ -export type Result = - | { success: true; value: T } - | { success: false; error: E }; - -/** Creates a successful Result. */ -export function ok(value: T): Result { - return { success: true, value }; -} - -/** Creates a failed Result. */ -export function err(error: E): Result { - return { success: false, error }; -} - -/** A value that may be null or undefined. */ -export type Maybe = T | null | undefined; - -/** Deep readonly utility — makes all nested properties readonly. */ -export type DeepReadonly = { - readonly [P in keyof T]: T[P] extends object ? DeepReadonly : T[P]; -}; - -/** Unwrap a Promise type. */ -export type Awaited = T extends PromiseLike ? Awaited : T; -``` - -## `src/lib/utils.ts` - -```typescript -/** - * Internal utility functions. - */ - -import type { Maybe } from "../types.js"; - -/** - * Asserts that a value is non-null and non-undefined. - * Throws at runtime with a descriptive message if the assertion fails. - */ -export function assertDefined( - value: Maybe, - label = "value" -): asserts value is T { - if (value === null || value === undefined) { - throw new Error(`Expected ${label} to be defined, but got ${String(value)}`); - } -} - -/** - * Narrows an unknown value to string. - */ -export function isString(value: unknown): value is string { - return typeof value === "string"; -} - -/** - * Narrows an unknown value to number. - */ -export function isNumber(value: unknown): value is number { - return typeof value === "number" && !Number.isNaN(value); -} - -/** - * Returns the first defined value from a list of candidates. - */ -export function coalesce(...values: Array>): T | undefined { - return values.find((v) => v !== null && v !== undefined) as T | undefined; -} - -/** - * Groups an array of items by a key selector. - */ -export function groupBy( - items: readonly T[], - keySelector: (item: T) => K -): Record { - return items.reduce( - (acc, item) => { - const key = keySelector(item); - if (!acc[key]) { - acc[key] = []; - } - acc[key].push(item); - return acc; - }, - {} as Record - ); -} -``` - -## `src/index.ts` - -```typescript -/** - * Public API entry point. - * Re-export anything that should be part of the public interface. - */ - -export type { Result, Maybe, DeepReadonly } from "./types.js"; -export { ok, err } from "./types.js"; -export { assertDefined, isString, isNumber, coalesce, groupBy } from "./lib/utils.js"; - -// Example: application-specific logic -export interface AppOptions { - readonly name: string; - readonly version: string; - readonly logLevel?: "debug" | "info" | "warn" | "error"; -} - -export class App { - readonly #name: string; - readonly #version: string; - readonly #logLevel: NonNullable; - - constructor(options: AppOptions) { - this.#name = options.name; - this.#version = options.version; - this.#logLevel = options.logLevel ?? "info"; - } - - get name(): string { - return this.#name; - } - - get version(): string { - return this.#version; - } - - info(message: string): void { - if (this.#logLevel !== "error" && this.#logLevel !== "warn") { - console.log(`[${this.#name}] ${message}`); - } - } - - toString(): string { - return `${this.#name}@${this.#version}`; - } -} -``` - -## `tests/index.test.ts` - -```typescript -import { describe, expect, it } from "vitest"; -import { App, ok, err } from "../src/index.js"; - -describe("App", () => { - it("creates an app with the provided name and version", () => { - const app = new App({ name: "test-app", version: "1.0.0" }); - expect(app.name).toBe("test-app"); - expect(app.version).toBe("1.0.0"); - }); - - it("has a meaningful toString representation", () => { - const app = new App({ name: "my-lib", version: "2.0.0" }); - expect(app.toString()).toBe("my-lib@2.0.0"); - }); -}); - -describe("Result helpers", () => { - it("ok() creates a successful result", () => { - const result = ok(42); - expect(result.success).toBe(true); - if (result.success) { - expect(result.value).toBe(42); - } - }); - - it("err() creates a failed result", () => { - const result = err(new Error("something went wrong")); - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.message).toBe("something went wrong"); - } - }); -}); -``` - -## `tests/utils.test.ts` - -```typescript -import { describe, expect, it } from "vitest"; -import { assertDefined, coalesce, groupBy, isString, isNumber } from "../src/lib/utils.js"; - -describe("assertDefined", () => { - it("does not throw for a defined value", () => { - expect(() => assertDefined("hello", "greeting")).not.toThrow(); - }); - - it("throws for null", () => { - expect(() => assertDefined(null, "myVar")).toThrow( - "Expected myVar to be defined" - ); - }); - - it("throws for undefined", () => { - expect(() => assertDefined(undefined, "myVar")).toThrow( - "Expected myVar to be defined" - ); - }); -}); - -describe("coalesce", () => { - it("returns the first non-null/undefined value", () => { - expect(coalesce(null, undefined, 0, 1)).toBe(0); - }); - - it("returns undefined when all values are nullish", () => { - expect(coalesce(null, undefined)).toBeUndefined(); - }); -}); - -describe("groupBy", () => { - it("groups items by the key selector", () => { - const items = [ - { type: "fruit", name: "apple" }, - { type: "veggie", name: "carrot" }, - { type: "fruit", name: "banana" }, - ]; - const grouped = groupBy(items, (item) => item.type); - expect(grouped["fruit"]).toHaveLength(2); - expect(grouped["veggie"]).toHaveLength(1); - }); -}); - -describe("type guards", () => { - it("isString returns true for strings", () => { - expect(isString("hello")).toBe(true); - expect(isString(123)).toBe(false); - }); - - it("isNumber returns false for NaN", () => { - expect(isNumber(NaN)).toBe(false); - expect(isNumber(42)).toBe(true); - }); -}); -``` - -## Notable Modern TypeScript 5.x Features Used - -| Feature | Where | Notes | -|---|---|---| -| `exactOptionalPropertyTypes` | `tsconfig.json` | Prevents `undefined` being assigned to optional props accidentally | -| `noUncheckedIndexedAccess` | `tsconfig.json` | Index operations return `T \| undefined` for safety | -| `noImplicitOverride` | `tsconfig.json` | Subclass methods must use `override` keyword | -| Private class fields (`#`) | `src/index.ts` | True JS private, not just TypeScript-enforced | -| `satisfies` operator | Can be used in types.ts | Validates without widening the inferred type | -| `const` type parameters | Generic helpers | `function id<const T>(v: T): T` for narrower inference | - -## Vitest vs Jest — Why Vitest Here - -- **Native ESM support** — no `transform` config needed for ESM -- **Faster** — uses Vite's transform pipeline under the hood -- **`vitest.config.ts`** — single config file, TypeScript-first -- **Compatible API** — `describe/it/expect` are identical to Jest -- **Built-in coverage** — via `@vitest/coverage-v8`, no extra setup diff --git a/skills/typescript-coder/assets/typescript-ego.md b/skills/typescript-coder/assets/typescript-ego.md deleted file mode 100644 index d1bde94ae..000000000 --- a/skills/typescript-coder/assets/typescript-ego.md +++ /dev/null @@ -1,421 +0,0 @@ -# TypeScript Project Template (EgoDigital / generator-ego Style) - -> A TypeScript project starter based on patterns from EgoDigital's `generator-ego`. Produces an enterprise-grade Node.js/Express API application with structured middleware, logging, environment configuration, and modular route organisation. - -## License - -MIT License — See source repository for full license terms. - -## Source - -- [egodigital/generator-ego](https://github.com/egodigital/generator-ego) - -## Project Structure - -``` -my-ego-app/ -├── src/ -│ ├── controllers/ -│ │ ├── health.ts -│ │ └── users.ts -│ ├── middleware/ -│ │ ├── auth.ts -│ │ ├── errorHandler.ts -│ │ └── logger.ts -│ ├── models/ -│ │ └── user.ts -│ ├── routes/ -│ │ ├── index.ts -│ │ └── users.ts -│ ├── services/ -│ │ └── userService.ts -│ ├── types/ -│ │ └── index.ts -│ ├── utils/ -│ │ └── env.ts -│ ├── app.ts -│ └── index.ts -├── tests/ -│ ├── controllers/ -│ │ └── users.test.ts -│ └── services/ -│ └── userService.test.ts -├── .env -├── .env.example -├── .eslintrc.json -├── .gitignore -├── jest.config.ts -├── nodemon.json -├── package.json -└── tsconfig.json -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-ego-app", - "version": "1.0.0", - "description": "Enterprise TypeScript Node.js API", - "main": "dist/index.js", - "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "nodemon", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "lint": "eslint src --ext .ts", - "lint:fix": "eslint src --ext .ts --fix", - "clean": "rimraf dist" - }, - "dependencies": { - "compression": "^1.7.4", - "cors": "^2.8.5", - "dotenv": "^16.3.1", - "express": "^4.18.2", - "express-validator": "^7.0.1", - "helmet": "^7.0.0", - "morgan": "^1.10.0", - "winston": "^3.11.0" - }, - "devDependencies": { - "@types/compression": "^1.7.5", - "@types/cors": "^2.8.15", - "@types/express": "^4.17.20", - "@types/jest": "^29.5.7", - "@types/morgan": "^1.9.8", - "@types/node": "^20.8.10", - "@types/supertest": "^2.0.15", - "@typescript-eslint/eslint-plugin": "^6.9.1", - "@typescript-eslint/parser": "^6.9.1", - "eslint": "^8.52.0", - "jest": "^29.7.0", - "nodemon": "^3.0.1", - "rimraf": "^5.0.5", - "supertest": "^6.3.3", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.1", - "typescript": "^5.2.2" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "lib": ["ES2020"], - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "removeComments": false, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "baseUrl": ".", - "paths": { - "@controllers/*": ["src/controllers/*"], - "@middleware/*": ["src/middleware/*"], - "@models/*": ["src/models/*"], - "@routes/*": ["src/routes/*"], - "@services/*": ["src/services/*"], - "@types/*": ["src/types/*"], - "@utils/*": ["src/utils/*"] - } - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests"] -} -``` - -### `nodemon.json` - -```json -{ - "watch": ["src"], - "ext": "ts", - "exec": "ts-node -r tsconfig-paths/register src/index.ts", - "env": { - "NODE_ENV": "development" - } -} -``` - -### `src/index.ts` - -```typescript -import dotenv from "dotenv"; -dotenv.config(); - -import { createApp } from "./app"; -import { getEnv } from "./utils/env"; -import { createLogger } from "./middleware/logger"; - -const logger = createLogger("bootstrap"); - -async function bootstrap(): Promise { - const port = getEnv("PORT", "3000"); - const app = createApp(); - - app.listen(Number(port), () => { - logger.info(`Server running on port ${port} [${process.env.NODE_ENV ?? "development"}]`); - }); -} - -bootstrap().catch((err) => { - console.error("Failed to start server:", err); - process.exit(1); -}); -``` - -### `src/app.ts` - -```typescript -import express, { Application } from "express"; -import cors from "cors"; -import helmet from "helmet"; -import compression from "compression"; -import morgan from "morgan"; -import { router } from "./routes"; -import { errorHandler } from "./middleware/errorHandler"; - -export function createApp(): Application { - const app = express(); - - // Security middleware - app.use(helmet()); - app.use(cors()); - - // Request parsing - app.use(express.json({ limit: "10mb" })); - app.use(express.urlencoded({ extended: true })); - app.use(compression()); - - // Logging - app.use(morgan("combined")); - - // Routes - app.use("/api/v1", router); - - // Error handling (must be last) - app.use(errorHandler); - - return app; -} -``` - -### `src/utils/env.ts` - -```typescript -/** - * Retrieve a required environment variable, throwing if absent. - */ -export function requireEnv(key: string): string { - const value = process.env[key]; - if (value === undefined || value === "") { - throw new Error(`Missing required environment variable: ${key}`); - } - return value; -} - -/** - * Retrieve an optional environment variable with a default fallback. - */ -export function getEnv(key: string, defaultValue: string): string { - return process.env[key] ?? defaultValue; -} - -/** - * Retrieve a boolean environment variable. - */ -export function getBoolEnv(key: string, defaultValue = false): boolean { - const value = process.env[key]; - if (value === undefined) return defaultValue; - return ["true", "1", "yes"].includes(value.toLowerCase()); -} -``` - -### `src/middleware/logger.ts` - -```typescript -import winston from "winston"; - -const { combine, timestamp, printf, colorize, errors } = winston.format; - -const logFormat = printf(({ level, message, timestamp, context, stack }) => { - const ctx = context ? ` [${context}]` : ""; - return stack - ? `${timestamp} ${level}${ctx}: ${message}\n${stack}` - : `${timestamp} ${level}${ctx}: ${message}`; -}); - -export function createLogger(context?: string): winston.Logger { - return winston.createLogger({ - level: process.env.LOG_LEVEL ?? "info", - format: combine( - colorize(), - timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), - errors({ stack: true }), - logFormat - ), - defaultMeta: context ? { context } : {}, - transports: [ - new winston.transports.Console(), - new winston.transports.File({ - filename: "logs/error.log", - level: "error", - }), - ], - }); -} -``` - -### `src/middleware/errorHandler.ts` - -```typescript -import { Request, Response, NextFunction } from "express"; -import { createLogger } from "./logger"; - -const logger = createLogger("errorHandler"); - -export interface AppError extends Error { - statusCode?: number; - isOperational?: boolean; -} - -export function errorHandler( - err: AppError, - _req: Request, - res: Response, - _next: NextFunction -): void { - const statusCode = err.statusCode ?? 500; - const message = err.isOperational ? err.message : "Internal Server Error"; - - logger.error(err.message, { stack: err.stack }); - - res.status(statusCode).json({ - success: false, - error: { - message, - ...(process.env.NODE_ENV === "development" && { stack: err.stack }), - }, - }); -} -``` - -### `src/middleware/auth.ts` - -```typescript -import { Request, Response, NextFunction } from "express"; - -export interface AuthenticatedRequest extends Request { - user?: { - id: string; - email: string; - roles: string[]; - }; -} - -export function requireAuth( - req: AuthenticatedRequest, - res: Response, - next: NextFunction -): void { - const authHeader = req.headers.authorization; - - if (!authHeader || !authHeader.startsWith("Bearer ")) { - res.status(401).json({ success: false, error: { message: "Unauthorized" } }); - return; - } - - // TODO: Replace with real JWT verification - const token = authHeader.substring(7); - if (!token) { - res.status(401).json({ success: false, error: { message: "Invalid token" } }); - return; - } - - next(); -} -``` - -### `src/routes/index.ts` - -```typescript -import { Router } from "express"; -import { userRouter } from "./users"; - -export const router = Router(); - -router.get("/health", (_req, res) => { - res.json({ status: "ok", timestamp: new Date().toISOString() }); -}); - -router.use("/users", userRouter); -``` - -### `.env.example` - -``` -NODE_ENV=development -PORT=3000 -LOG_LEVEL=info -DATABASE_URL=postgres://user:password@localhost:5432/mydb -JWT_SECRET=change-me-in-production -``` - -## Getting Started - -1. Copy the template files into your project directory. -2. Install dependencies: - ```bash - npm install - ``` -3. Copy `.env.example` to `.env` and fill in real values: - ```bash - cp .env.example .env - ``` -4. Run in development mode with hot-reload: - ```bash - npm run dev - ``` -5. Build for production: - ```bash - npm run build - npm start - ``` -6. Run tests: - ```bash - npm test - ``` - -## Features - -- TypeScript 5.x with strict mode, decorators, and path aliases -- Express 4 with helmet, cors, compression, and morgan middleware -- Winston structured logging with per-module logger contexts -- Centralised error handler middleware with operational vs. programmer error distinction -- Environment variable utilities (`requireEnv`, `getEnv`, `getBoolEnv`) -- Auth middleware scaffold with Bearer token pattern -- Modular route and controller organisation -- Jest + ts-jest test setup with Supertest for HTTP integration tests -- nodemon-based development workflow with ts-node -- Path aliases (`@controllers/*`, `@services/*`, etc.) for clean imports diff --git a/skills/typescript-coder/assets/typescript-express-no-stress.md b/skills/typescript-coder/assets/typescript-express-no-stress.md deleted file mode 100644 index 8fa30f6fd..000000000 --- a/skills/typescript-coder/assets/typescript-express-no-stress.md +++ /dev/null @@ -1,584 +0,0 @@ -# Express No-Stress TypeScript API - -> A production-ready Express.js TypeScript API starter with OpenAPI 3.0 request/response validation, Swagger UI, structured logging, helmet security headers, and a clean controller-per-resource layout. Requests are validated automatically against your OpenAPI spec before reaching controller logic. - -## License - -MIT — See [source repository](https://github.com/cdimascio/generator-express-no-stress-typescript) for full license text. - -## Source - -- [cdimascio/generator-express-no-stress-typescript](https://github.com/cdimascio/generator-express-no-stress-typescript) - -## Project Structure - -``` -my-api/ -├── server/ -│ ├── api/ -│ │ ├── controllers/ -│ │ │ └── examples/ -│ │ │ ├── controller.ts -│ │ │ └── router.ts -│ │ ├── middlewares/ -│ │ │ └── error.handler.ts -│ │ └── services/ -│ │ └── examples.service.ts -│ ├── common/ -│ │ └── server.ts -│ ├── routes.ts -│ └── index.ts -├── openapi.yml -├── package.json -├── tsconfig.json -├── nodemon.json -├── .env -├── .gitignore -└── README.md -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-api", - "version": "1.0.0", - "description": "Production-ready Express TypeScript API", - "license": "MIT", - "private": true, - "scripts": { - "build": "tsc", - "build:watch": "tsc --watch", - "clean": "rimraf dist", - "dev": "nodemon", - "start": "node dist/server/index.js", - "lint": "eslint server --ext .ts", - "test": "jest --forceExit", - "test:watch": "jest --watch" - }, - "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.4.0", - "express": "^4.19.0", - "express-openapi-validator": "^5.3.0", - "helmet": "^7.1.0", - "morgan": "^1.10.0", - "swagger-ui-express": "^5.0.0", - "yaml": "^2.4.0" - }, - "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/morgan": "^1.9.9", - "@types/node": "^20.12.0", - "@types/swagger-ui-express": "^4.1.6", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.57.0", - "jest": "^29.7.0", - "nodemon": "^3.1.0", - "rimraf": "^5.0.0", - "supertest": "^6.3.0", - "ts-jest": "^29.1.0", - "typescript": "^5.4.0" - }, - "engines": { - "node": ">=20.0.0" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "Node", - "lib": ["ES2022"], - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "dist", - "rootDir": ".", - "resolveJsonModule": true, - "skipLibCheck": true - }, - "include": ["server"], - "exclude": ["node_modules", "dist"] -} -``` - -### `nodemon.json` - -```json -{ - "watch": ["server"], - "ext": "ts,yml,yaml,json", - "exec": "ts-node -r dotenv/config server/index.ts", - "env": { - "NODE_ENV": "development" - } -} -``` - -### `.env` - -``` -NODE_ENV=development -PORT=3000 -LOG_LEVEL=debug -REQUEST_LIMIT=100kb -OPENAPI_SPEC=/api/v1/spec -``` - -### `openapi.yml` - -```yaml -openapi: '3.0.3' -info: - title: My API - description: A production-ready Express TypeScript API - version: 1.0.0 -servers: - - url: /api/v1 - description: Local development server - -paths: - /examples: - get: - summary: List all examples - operationId: listExamples - tags: [examples] - parameters: - - in: query - name: limit - schema: - type: integer - minimum: 1 - maximum: 100 - default: 10 - responses: - '200': - description: A list of examples - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Example' - post: - summary: Create an example - operationId: createExample - tags: [examples] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateExampleRequest' - responses: - '201': - description: Created - content: - application/json: - schema: - $ref: '#/components/schemas/Example' - '422': - $ref: '#/components/responses/UnprocessableEntity' - - /examples/{id}: - get: - summary: Get an example by ID - operationId: getExample - tags: [examples] - parameters: - - $ref: '#/components/parameters/IdParam' - responses: - '200': - description: The example - content: - application/json: - schema: - $ref: '#/components/schemas/Example' - '404': - $ref: '#/components/responses/NotFound' - -components: - parameters: - IdParam: - name: id - in: path - required: true - schema: - type: string - - schemas: - Example: - type: object - required: [id, name, createdAt] - properties: - id: - type: string - name: - type: string - minLength: 1 - maxLength: 100 - description: - type: string - createdAt: - type: string - format: date-time - - CreateExampleRequest: - type: object - required: [name] - properties: - name: - type: string - minLength: 1 - maxLength: 100 - description: - type: string - - responses: - NotFound: - description: Resource not found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - UnprocessableEntity: - description: Validation error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - ErrorResponse: - type: object - required: [message] - properties: - message: - type: string - errors: - type: array - items: - type: object -``` - -### `server/common/server.ts` - -```typescript -import cors from 'cors'; -import * as dotenv from 'dotenv'; -import express, { Application } from 'express'; -import * as fs from 'fs'; -import helmet from 'helmet'; -import morgan from 'morgan'; -import * as OpenApiValidator from 'express-openapi-validator'; -import * as path from 'path'; -import swaggerUi from 'swagger-ui-express'; -import * as yaml from 'yaml'; -import { errorHandler } from '../api/middlewares/error.handler'; -import routes from '../routes'; - -dotenv.config(); - -export default class Server { - private readonly app: Application; - - constructor() { - this.app = express(); - this.middleware(); - this.routes(); - this.swagger(); - this.openApiValidator(); - this.errorHandler(); - } - - private middleware(): void { - this.app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev')); - this.app.use(express.json({ limit: process.env.REQUEST_LIMIT ?? '100kb' })); - this.app.use(express.urlencoded({ extended: true })); - this.app.use( - helmet({ - contentSecurityPolicy: process.env.NODE_ENV === 'production', - }) - ); - this.app.use(cors()); - } - - private routes(): void { - const apiBase = process.env.OPENAPI_SPEC ?? '/api/v1'; - this.app.use(apiBase, routes); - } - - private swagger(): void { - const specPath = path.resolve('openapi.yml'); - const specContent = yaml.parse(fs.readFileSync(specPath, 'utf8')) as object; - this.app.use('/api-explorer', swaggerUi.serve, swaggerUi.setup(specContent)); - this.app.get('/api/v1/spec', (_req, res) => { - res.sendFile(specPath); - }); - } - - private openApiValidator(): void { - const apiSpec = path.resolve('openapi.yml'); - this.app.use( - OpenApiValidator.middleware({ - apiSpec, - validateRequests: true, - validateResponses: process.env.NODE_ENV !== 'production', - operationHandlers: false, - }) - ); - } - - private errorHandler(): void { - this.app.use(errorHandler); - } - - listen(port: number): Application { - this.app.listen(port, () => { - console.log(`Server listening on port ${port}`); - console.log(`Swagger UI: http://localhost:${port}/api-explorer`); - }); - return this.app; - } -} -``` - -### `server/index.ts` - -```typescript -import Server from './common/server'; - -const port = parseInt(process.env.PORT ?? '3000', 10); -export default new Server().listen(port); -``` - -### `server/routes.ts` - -```typescript -import { Router } from 'express'; -import examplesRouter from './api/controllers/examples/router'; - -const router = Router(); - -router.use('/examples', examplesRouter); - -export default router; -``` - -### `server/api/controllers/examples/router.ts` - -```typescript -import { Router } from 'express'; -import controller from './controller'; - -const router = Router(); - -router.get('/', controller.list); -router.post('/', controller.create); -router.get('/:id', controller.get); -router.put('/:id', controller.update); -router.delete('/:id', controller.delete); - -export default router; -``` - -### `server/api/controllers/examples/controller.ts` - -```typescript -import { NextFunction, Request, Response } from 'express'; -import ExamplesService from '../../services/examples.service'; - -export class ExamplesController { - async list(req: Request, res: Response, next: NextFunction): Promise { - try { - const limit = Number(req.query['limit'] ?? 10); - const examples = await ExamplesService.list(limit); - res.json(examples); - } catch (err) { - next(err); - } - } - - async get(req: Request, res: Response, next: NextFunction): Promise { - try { - const example = await ExamplesService.get(req.params['id']!); - if (!example) { - res.status(404).json({ message: `Example ${req.params['id']} not found` }); - return; - } - res.json(example); - } catch (err) { - next(err); - } - } - - async create(req: Request, res: Response, next: NextFunction): Promise { - try { - const example = await ExamplesService.create(req.body); - res.status(201).json(example); - } catch (err) { - next(err); - } - } - - async update(req: Request, res: Response, next: NextFunction): Promise { - try { - const example = await ExamplesService.update(req.params['id']!, req.body); - res.json(example); - } catch (err) { - next(err); - } - } - - async delete(req: Request, res: Response, next: NextFunction): Promise { - try { - await ExamplesService.delete(req.params['id']!); - res.status(204).send(); - } catch (err) { - next(err); - } - } -} - -export default new ExamplesController(); -``` - -### `server/api/services/examples.service.ts` - -```typescript -import { randomUUID } from 'crypto'; - -export interface Example { - id: string; - name: string; - description?: string; - createdAt: string; -} - -interface CreateRequest { - name: string; - description?: string; -} - -// In-memory store (replace with a database in production) -const store = new Map(); - -const ExamplesService = { - async list(limit: number): Promise { - return Array.from(store.values()).slice(0, limit); - }, - - async get(id: string): Promise { - return store.get(id); - }, - - async create(data: CreateRequest): Promise { - const example: Example = { - id: randomUUID(), - name: data.name, - description: data.description, - createdAt: new Date().toISOString(), - }; - store.set(example.id, example); - return example; - }, - - async update(id: string, data: Partial): Promise { - const existing = store.get(id); - if (!existing) return undefined; - const updated = { ...existing, ...data }; - store.set(id, updated); - return updated; - }, - - async delete(id: string): Promise { - store.delete(id); - }, -}; - -export default ExamplesService; -``` - -### `server/api/middlewares/error.handler.ts` - -```typescript -import { ErrorRequestHandler, NextFunction, Request, Response } from 'express'; - -interface ApiError { - status?: number; - message: string; - errors?: unknown[]; -} - -export const errorHandler: ErrorRequestHandler = ( - err: ApiError, - _req: Request, - res: Response, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _next: NextFunction -): void => { - const status = err.status ?? 500; - const message = err.message ?? 'Internal Server Error'; - - if (process.env.NODE_ENV !== 'production') { - console.error(`[${status}] ${message}`, err.errors ?? ''); - } - - res.status(status).json({ - message, - ...(err.errors ? { errors: err.errors } : {}), - }); -}; -``` - -## Getting Started - -```bash -# 1. Create project directory and initialise -mkdir my-api && cd my-api -npm init -y - -# 2. Copy all project files (see structure above) - -# 3. Install dependencies -npm install - -# 4. Copy .env.example to .env and configure -cp .env .env.local - -# 5. Start in development mode (hot-reload via nodemon) -npm run dev - -# 6. Browse the Swagger UI -open http://localhost:3000/api-explorer - -# 7. Build for production -npm run build - -# 8. Start in production mode -npm start -``` - -## Features - -- OpenAPI 3.0 spec-first development — define once, validate automatically -- `express-openapi-validator` rejects invalid requests before they hit controller code -- Response validation in non-production environments catches API contract drift -- Swagger UI served at `/api-explorer` for interactive API exploration -- Helmet security headers enabled by default -- Morgan structured request logging (dev format locally, combined in production) -- Centralised error handler normalises all errors to a consistent JSON shape -- Nodemon with `ts-node` for zero-build development hot-reload -- Controller/Service/Router separation for clean, testable architecture diff --git a/skills/typescript-coder/assets/typescript-gulp-angular.md b/skills/typescript-coder/assets/typescript-gulp-angular.md deleted file mode 100644 index c5ccbb837..000000000 --- a/skills/typescript-coder/assets/typescript-gulp-angular.md +++ /dev/null @@ -1,373 +0,0 @@ -# TypeScript Gulp + Angular Project Template (Modernized) - -> A modernized TypeScript project starter inspired by `generator-gulp-angular`. The original generator targeted Angular 1.x (AngularJS) with Gulp 3; this template updates the approach for Angular 17+ and TypeScript 5.x while preserving the Gulp task-runner philosophy for builds, serving, and asset pipelines. - -## License - -MIT License — See source repository for full license terms. - -> Note: The original `swiip/generator-gulp-angular` generator is no longer actively maintained and targeted AngularJS (Angular 1.x). This template modernises its patterns for current Angular and TypeScript. - -## Source - -- [swiip/generator-gulp-angular](https://github.com/swiip/generator-gulp-angular) (original, AngularJS era) - -## Project Structure - -``` -my-gulp-angular-app/ -├── src/ -│ ├── app/ -│ │ ├── components/ -│ │ │ └── hero-card/ -│ │ │ ├── hero-card.component.ts -│ │ │ ├── hero-card.component.html -│ │ │ └── hero-card.component.css -│ │ ├── services/ -│ │ │ └── hero.service.ts -│ │ ├── models/ -│ │ │ └── hero.model.ts -│ │ ├── app.component.ts -│ │ ├── app.component.html -│ │ ├── app.module.ts -│ │ └── app-routing.module.ts -│ ├── assets/ -│ │ └── images/ -│ ├── environments/ -│ │ ├── environment.ts -│ │ └── environment.prod.ts -│ ├── styles/ -│ │ ├── _variables.css -│ │ └── main.css -│ ├── index.html -│ └── main.ts -├── tests/ -│ └── hero-card.component.spec.ts -├── dist/ ← Gulp build output -├── .eslintrc.json -├── .gitignore -├── gulpfile.ts -├── karma.conf.js -├── package.json -└── tsconfig.json -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-gulp-angular-app", - "version": "1.0.0", - "description": "Angular 17 + TypeScript application with Gulp build pipeline", - "scripts": { - "start": "gulp serve", - "build": "gulp build", - "build:prod": "gulp build --env=production", - "test": "gulp test", - "lint": "eslint src --ext .ts", - "clean": "gulp clean" - }, - "dependencies": { - "@angular/animations": "^17.0.0", - "@angular/common": "^17.0.0", - "@angular/compiler": "^17.0.0", - "@angular/core": "^17.0.0", - "@angular/forms": "^17.0.0", - "@angular/platform-browser": "^17.0.0", - "@angular/platform-browser-dynamic": "^17.0.0", - "@angular/router": "^17.0.0", - "rxjs": "^7.8.1", - "tslib": "^2.6.2", - "zone.js": "^0.14.2" - }, - "devDependencies": { - "@angular-devkit/build-angular": "^17.0.0", - "@angular/cli": "^17.0.0", - "@angular/compiler-cli": "^17.0.0", - "@types/jasmine": "^5.1.1", - "@types/node": "^20.8.10", - "@typescript-eslint/eslint-plugin": "^6.9.1", - "@typescript-eslint/parser": "^6.9.1", - "browser-sync": "^3.0.2", - "del": "^7.1.0", - "eslint": "^8.52.0", - "eslint-plugin-angular": "^4.1.0", - "fancy-log": "^2.0.0", - "gulp": "^5.0.0", - "gulp-clean-css": "^4.3.0", - "gulp-concat": "^2.6.0", - "gulp-htmlmin": "^5.0.1", - "gulp-if": "^3.0.0", - "gulp-rev": "^10.0.0", - "gulp-sourcemaps": "^3.0.0", - "gulp-typescript": "^6.0.0-alpha.1", - "gulp-uglify": "^3.0.2", - "jasmine-core": "^5.1.1", - "karma": "^6.4.2", - "karma-chrome-launcher": "^3.2.0", - "karma-jasmine": "^5.1.0", - "typescript": "^5.2.2" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "lib": ["ES2022", "DOM"], - "useDefineForClassFields": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "strict": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "sourceMap": true, - "declaration": false, - "outDir": "dist/app", - "baseUrl": "src" - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} -``` - -### `gulpfile.ts` - -```typescript -import gulp from "gulp"; -import ts from "gulp-typescript"; -import cleanCSS from "gulp-clean-css"; -import htmlmin from "gulp-htmlmin"; -import sourcemaps from "gulp-sourcemaps"; -import browserSync from "browser-sync"; -import { deleteAsync } from "del"; - -const bs = browserSync.create(); -const tsProject = ts.createProject("tsconfig.json"); - -const paths = { - ts: "src/**/*.ts", - html: ["src/**/*.html", "src/index.html"], - css: "src/**/*.css", - assets: "src/assets/**/*", - dist: "dist/", -}; - -// --- Clean --- -export async function clean(): Promise { - await deleteAsync([paths.dist]); -} - -// --- TypeScript --- -export function scripts(): NodeJS.ReadWriteStream { - return gulp - .src(paths.ts) - .pipe(sourcemaps.init()) - .pipe(tsProject()) - .js.pipe(sourcemaps.write(".")) - .pipe(gulp.dest(paths.dist)) - .pipe(bs.stream()); -} - -// --- CSS --- -export function styles(): NodeJS.ReadWriteStream { - return gulp - .src(paths.css) - .pipe(sourcemaps.init()) - .pipe(cleanCSS({ compatibility: "ie11" })) - .pipe(sourcemaps.write(".")) - .pipe(gulp.dest(paths.dist + "styles/")) - .pipe(bs.stream()); -} - -// --- HTML --- -export function templates(): NodeJS.ReadWriteStream { - return gulp - .src(paths.html) - .pipe(htmlmin({ collapseWhitespace: true, removeComments: true })) - .pipe(gulp.dest(paths.dist)) - .pipe(bs.stream()); -} - -// --- Assets --- -export function assets(): NodeJS.ReadWriteStream { - return gulp.src(paths.assets).pipe(gulp.dest(paths.dist + "assets/")); -} - -// --- Dev server --- -export function serve(done: () => void): void { - bs.init({ server: { baseDir: paths.dist }, port: 4200, open: false }); - done(); -} - -// --- Watch --- -export function watchFiles(): void { - gulp.watch(paths.ts, scripts); - gulp.watch(paths.css, styles); - gulp.watch(paths.html, templates); - gulp.watch(paths.assets, assets); -} - -// --- Composite tasks --- -export const build = gulp.series( - clean, - gulp.parallel(scripts, styles, templates, assets) -); - -export default gulp.series( - build, - serve, - watchFiles -); -``` - -### `src/app/models/hero.model.ts` - -```typescript -export interface Hero { - id: number; - name: string; - power: string; - alterEgo?: string; -} -``` - -### `src/app/services/hero.service.ts` - -```typescript -import { Injectable } from "@angular/core"; -import { Observable, of } from "rxjs"; -import { Hero } from "../models/hero.model"; - -@Injectable({ - providedIn: "root", -}) -export class HeroService { - private heroes: Hero[] = [ - { id: 1, name: "Windstorm", power: "Meteorology" }, - { id: 2, name: "Bombasto", power: "Super Strength", alterEgo: "Bob" }, - { id: 3, name: "Magneta", power: "Magnetism" }, - ]; - - getHeroes(): Observable { - return of(this.heroes); - } - - getHero(id: number): Observable { - return of(this.heroes.find((h) => h.id === id)); - } -} -``` - -### `src/app/components/hero-card/hero-card.component.ts` - -```typescript -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from "@angular/core"; -import { CommonModule } from "@angular/common"; -import { Hero } from "../../models/hero.model"; - -@Component({ - selector: "app-hero-card", - standalone: true, - imports: [CommonModule], - templateUrl: "./hero-card.component.html", - styleUrls: ["./hero-card.component.css"], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class HeroCardComponent { - @Input({ required: true }) hero!: Hero; - @Output() selected = new EventEmitter(); - - onSelect(): void { - this.selected.emit(this.hero); - } -} -``` - -### `src/app/components/hero-card/hero-card.component.html` - -```html -
      -

      {{ hero.name }}

      -

      Power: {{ hero.power }}

      -

      Alter Ego: {{ hero.alterEgo }}

      -
      -``` - -### `src/app/app.module.ts` - -```typescript -import { NgModule } from "@angular/core"; -import { BrowserModule } from "@angular/platform-browser"; -import { AppRoutingModule } from "./app-routing.module"; -import { AppComponent } from "./app.component"; - -@NgModule({ - declarations: [AppComponent], - imports: [BrowserModule, AppRoutingModule], - providers: [], - bootstrap: [AppComponent], -}) -export class AppModule {} -``` - -### `src/environments/environment.ts` - -```typescript -export const environment = { - production: false, - apiUrl: "http://localhost:3000/api", -}; -``` - -### `src/environments/environment.prod.ts` - -```typescript -export const environment = { - production: true, - apiUrl: "https://api.my-app.com", -}; -``` - -## Getting Started - -1. Install dependencies: - ```bash - npm install - ``` -2. Start the development server with Gulp (compiles TS, serves with BrowserSync, watches for changes): - ```bash - npm start - ``` - The app will be available at `http://localhost:4200`. -3. Build for production: - ```bash - npm run build:prod - ``` -4. Run tests with Karma/Jasmine: - ```bash - npm test - ``` - -## Features - -- Angular 17 with standalone components and `ChangeDetectionStrategy.OnPush` -- TypeScript 5.x with strict mode and Angular decorator support -- Gulp 5 build pipeline replacing the Angular CLI build tooling -- BrowserSync for live-reloading development server -- CSS minification via `gulp-clean-css` -- HTML minification via `gulp-htmlmin` -- TypeScript compilation via `gulp-typescript` -- Source maps in development for debuggable compiled output -- Environment-specific configuration files (`environment.ts` / `environment.prod.ts`) -- Modular component structure with standalone component pattern -- RxJS 7 Observable-based service layer diff --git a/skills/typescript-coder/assets/typescript-kodly-react.md b/skills/typescript-coder/assets/typescript-kodly-react.md deleted file mode 100644 index 12ed4d4ea..000000000 --- a/skills/typescript-coder/assets/typescript-kodly-react.md +++ /dev/null @@ -1,434 +0,0 @@ -# TypeScript React App (Kodly) - -> A TypeScript React application starter with React Router, a component-per-feature folder layout, CSS Modules styling, and Vitest for unit testing. Produces a Vite-powered SPA ready for modern React development. - -## License - -See [source repository](https://github.com/thepanther-io/kodly-react-yo-generator) for license terms. - -## Source - -- [thepanther-io/kodly-react-yo-generator](https://github.com/thepanther-io/kodly-react-yo-generator) - -## Project Structure - -``` -my-react-app/ -├── src/ -│ ├── app/ -│ │ ├── App.tsx -│ │ ├── App.module.css -│ │ └── router.tsx -│ ├── components/ -│ │ └── shared/ -│ │ ├── Button/ -│ │ │ ├── Button.tsx -│ │ │ ├── Button.module.css -│ │ │ └── Button.test.tsx -│ │ └── index.ts -│ ├── features/ -│ │ ├── home/ -│ │ │ ├── HomePage.tsx -│ │ │ └── HomePage.module.css -│ │ └── about/ -│ │ └── AboutPage.tsx -│ ├── hooks/ -│ │ └── useLocalStorage.ts -│ ├── types/ -│ │ └── global.d.ts -│ ├── main.tsx -│ └── vite-env.d.ts -├── public/ -│ └── favicon.svg -├── index.html -├── package.json -├── tsconfig.json -├── tsconfig.node.json -├── vite.config.ts -├── vitest.config.ts -├── .gitignore -└── README.md -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-react-app", - "version": "0.1.0", - "description": "TypeScript React application", - "license": "MIT", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "lint": "eslint src --ext .ts,.tsx", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", - "type-check": "tsc --noEmit" - }, - "dependencies": { - "react": "^18.3.0", - "react-dom": "^18.3.0", - "react-router-dom": "^6.23.0" - }, - "devDependencies": { - "@testing-library/jest-dom": "^6.4.0", - "@testing-library/react": "^15.0.0", - "@testing-library/user-event": "^14.5.0", - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "@vitejs/plugin-react": "^4.2.0", - "@vitest/coverage-v8": "^1.0.0", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.0", - "jsdom": "^24.0.0", - "typescript": "^5.4.0", - "vite": "^5.2.0", - "vitest": "^1.0.0" - }, - "engines": { - "node": ">=20.0.0" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "moduleResolution": "Bundler", - "jsx": "react-jsx", - "strict": true, - "exactOptionalPropertyTypes": true, - "noUncheckedIndexedAccess": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "skipLibCheck": true, - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - } - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} -``` - -### `tsconfig.node.json` - -```json -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "allowSyntheticDefaultImports": true, - "strict": true - }, - "include": ["vite.config.ts", "vitest.config.ts"] -} -``` - -### `vite.config.ts` - -```typescript -import react from '@vitejs/plugin-react'; -import path from 'path'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - server: { - port: 3000, - open: true, - }, - build: { - outDir: 'dist', - sourcemap: true, - rollupOptions: { - output: { - manualChunks: { - vendor: ['react', 'react-dom', 'react-router-dom'], - }, - }, - }, - }, -}); -``` - -### `vitest.config.ts` - -```typescript -import react from '@vitejs/plugin-react'; -import path from 'path'; -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - test: { - environment: 'jsdom', - setupFiles: ['./src/test-setup.ts'], - include: ['src/**/*.test.{ts,tsx}'], - coverage: { - provider: 'v8', - reporter: ['text', 'lcov', 'html'], - include: ['src/**/*.{ts,tsx}'], - exclude: ['src/**/*.test.*', 'src/main.tsx', 'src/vite-env.d.ts'], - }, - }, -}); -``` - -### `src/main.tsx` - -```tsx -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import { RouterProvider } from 'react-router-dom'; -import { router } from './app/router'; -import './index.css'; - -const root = document.getElementById('root'); -if (!root) throw new Error('Root element not found'); - -ReactDOM.createRoot(root).render( - - - -); -``` - -### `src/app/router.tsx` - -```tsx -import React from 'react'; -import { createBrowserRouter } from 'react-router-dom'; -import { App } from './App'; -import { AboutPage } from '@/features/about/AboutPage'; -import { HomePage } from '@/features/home/HomePage'; - -export const router = createBrowserRouter([ - { - path: '/', - element: , - children: [ - { index: true, element: }, - { path: 'about', element: }, - ], - }, -]); -``` - -### `src/app/App.tsx` - -```tsx -import React from 'react'; -import { Link, Outlet } from 'react-router-dom'; -import styles from './App.module.css'; - -export function App(): JSX.Element { - return ( -
      -
      - -
      -
      - -
      -
      -

      © {new Date().getFullYear()} My App

      -
      -
      - ); -} -``` - -### `src/features/home/HomePage.tsx` - -```tsx -import React from 'react'; -import { Button } from '@/components/shared'; -import styles from './HomePage.module.css'; - -export function HomePage(): JSX.Element { - const [count, setCount] = React.useState(0); - - return ( -
      -

      Welcome

      -

      Count: {count}

      - - -
      - ); -} -``` - -### `src/components/shared/Button/Button.tsx` - -```tsx -import React, { ButtonHTMLAttributes } from 'react'; -import styles from './Button.module.css'; - -export interface ButtonProps extends ButtonHTMLAttributes { - variant?: 'primary' | 'secondary' | 'danger'; -} - -export function Button({ variant = 'primary', className = '', children, ...rest }: ButtonProps): JSX.Element { - return ( - - ); -} -``` - -### `src/components/shared/Button/Button.test.tsx` - -```tsx -import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; -import { Button } from './Button'; - -describe('Button', () => { - it('renders with children', () => { - render(); - expect(screen.getByRole('button', { name: 'Click' })).toBeInTheDocument(); - }); - - it('calls onClick when clicked', async () => { - const handleClick = vi.fn(); - render(); - await userEvent.click(screen.getByRole('button')); - expect(handleClick).toHaveBeenCalledOnce(); - }); - - it('applies the secondary class when variant is secondary', () => { - render(); - expect(screen.getByRole('button')).toHaveClass('secondary'); - }); -}); -``` - -### `src/components/shared/index.ts` - -```typescript -export { Button } from './Button/Button'; -export type { ButtonProps } from './Button/Button'; -``` - -### `src/hooks/useLocalStorage.ts` - -```typescript -import { useCallback, useState } from 'react'; - -export function useLocalStorage( - key: string, - initialValue: T -): [T, (value: T | ((prev: T) => T)) => void] { - const [storedValue, setStoredValue] = useState(() => { - try { - const item = window.localStorage.getItem(key); - return item !== null ? (JSON.parse(item) as T) : initialValue; - } catch { - return initialValue; - } - }); - - const setValue = useCallback( - (value: T | ((prev: T) => T)) => { - setStoredValue((prev) => { - const next = value instanceof Function ? value(prev) : value; - try { - window.localStorage.setItem(key, JSON.stringify(next)); - } catch { - console.warn(`useLocalStorage: failed to persist key "${key}"`); - } - return next; - }); - }, - [key] - ); - - return [storedValue, setValue]; -} -``` - -## Getting Started - -```bash -# Prerequisites: Node.js 20+ - -# 1. Create project directory -mkdir my-react-app && cd my-react-app - -# 2. Copy project files (see structure above) - -# 3. Install dependencies -npm install - -# 4. Start the development server (http://localhost:3000) -npm run dev - -# 5. Run unit tests -npm test - -# 6. Build for production -npm run build - -# 7. Preview the production build locally -npm run preview -``` - -## Features - -- Vite for near-instant dev server startup and hot module replacement -- React Router v6 with `createBrowserRouter` and nested layouts -- CSS Modules for scoped, collision-free component styles -- Path alias `@/` mapped to `src/` for cleaner imports -- Vitest with `jsdom` environment for component testing (uses same Vite config) -- React Testing Library + `@testing-library/user-event` for user-interaction tests -- Vendor chunk splitting in production build for better caching -- Custom `useLocalStorage` hook as an example of composable, typed custom hooks -- Strict TypeScript with `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess` diff --git a/skills/typescript-coder/assets/typescript-lit-element.md b/skills/typescript-coder/assets/typescript-lit-element.md deleted file mode 100644 index 3728fdc62..000000000 --- a/skills/typescript-coder/assets/typescript-lit-element.md +++ /dev/null @@ -1,513 +0,0 @@ -# TypeScript Lit Web Component Template (lit-element-next) - -> A TypeScript web component project template based on `generator-lit-element-next` by -> motss. Produces a modern Lit 3 web component package with TypeScript decorators, Rollup -> bundling, Web Test Runner browser tests, custom element registration, and a publishable -> NPM package structure following open-wc conventions. - -## License - -MIT License. See [https://github.com/motss/generator-lit-element-next](https://github.com/motss/generator-lit-element-next) for full license terms. - -## Source - -- [generator-lit-element-next](https://github.com/motss/generator-lit-element-next) by motss - -## Project Structure - -``` -my-lit-component/ -├── src/ -│ ├── my-counter.ts # Main component implementation -│ ├── my-counter.styles.ts # Lit css`` tagged template styles -│ ├── types.ts # Shared TypeScript interfaces -│ └── index.ts # Package entry point / re-exports -├── test/ -│ ├── my-counter.test.ts # Web Test Runner specs -│ └── helpers.ts # Test helpers / fixtures -├── custom-elements.json # Custom Elements Manifest (CEM) -├── web-test-runner.config.mjs -├── rollup.config.mjs -├── package.json -├── tsconfig.json -└── .eslintrc.cjs -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-lit-component", - "version": "1.0.0", - "description": "A TypeScript Lit web component", - "type": "module", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" - } - }, - "customElements": "custom-elements.json", - "files": ["dist", "custom-elements.json"], - "scripts": { - "build": "tsc && rollup -c", - "build:watch": "tsc -w", - "test": "wtr", - "test:watch": "wtr --watch", - "lint": "eslint 'src/**/*.ts' 'test/**/*.ts'", - "format": "prettier --write 'src/**/*.ts'", - "analyze": "cem analyze --litelement", - "clean": "rimraf dist" - }, - "dependencies": { - "lit": "^3.1.2" - }, - "devDependencies": { - "@custom-elements-manifest/analyzer": "^0.9.3", - "@esm-bundle/chai": "^4.3.4-fix.0", - "@open-wc/testing": "^4.0.0", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.6", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "@web/test-runner": "^0.18.1", - "@web/test-runner-playwright": "^0.11.0", - "eslint": "^8.56.0", - "eslint-plugin-lit": "^1.11.0", - "eslint-plugin-wc": "^2.1.0", - "prettier": "^3.2.4", - "rimraf": "^5.0.5", - "rollup": "^4.9.5", - "tslib": "^2.6.2", - "typescript": "^5.3.3" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2021", - "module": "ES2020", - "moduleResolution": "bundler", - "lib": ["ES2021", "DOM", "DOM.Iterable"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "experimentalDecorators": true, - "useDefineForClassFields": false - }, - "include": ["src"], - "exclude": ["node_modules", "dist", "test"] -} -``` - -### `src/types.ts` - -```typescript -export interface CounterChangedDetail { - /** Current counter value after the change */ - value: number; - /** Direction of the last change */ - direction: 'increment' | 'decrement' | 'reset'; -} -``` - -### `src/my-counter.styles.ts` - -```typescript -import { css } from 'lit'; - -export const styles = css` - :host { - display: inline-block; - font-family: system-ui, sans-serif; - --my-counter-bg: #f0f4ff; - --my-counter-color: #1a1a2e; - --my-counter-accent: #4361ee; - --my-counter-radius: 8px; - } - - :host([hidden]) { - display: none; - } - - .container { - display: flex; - align-items: center; - gap: 12px; - padding: 12px 16px; - background: var(--my-counter-bg); - border-radius: var(--my-counter-radius); - color: var(--my-counter-color); - } - - button { - display: inline-flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: 2px solid var(--my-counter-accent); - border-radius: 50%; - background: transparent; - color: var(--my-counter-accent); - font-size: 1.25rem; - cursor: pointer; - transition: background 0.15s ease, color 0.15s ease; - } - - button:hover:not([disabled]) { - background: var(--my-counter-accent); - color: #fff; - } - - button[disabled] { - opacity: 0.4; - cursor: not-allowed; - } - - .value { - min-width: 3ch; - text-align: center; - font-size: 1.5rem; - font-weight: 600; - } - - .reset { - font-size: 0.75rem; - border-radius: 4px; - width: auto; - padding: 0 8px; - height: 28px; - } -`; -``` - -### `src/my-counter.ts` - -```typescript -import { LitElement, html } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import { styles } from './my-counter.styles'; -import { CounterChangedDetail } from './types'; - -/** - * An accessible counter web component built with Lit and TypeScript. - * - * @fires {CustomEvent} counter-changed - Fired whenever - * the counter value changes. - * - * @csspart container - The outer wrapper element. - * @csspart value - The element displaying the current count. - * - * @cssprop --my-counter-bg - Background colour (default: #f0f4ff). - * @cssprop --my-counter-color - Text colour (default: #1a1a2e). - * @cssprop --my-counter-accent - Accent colour for buttons (default: #4361ee). - * @cssprop --my-counter-radius - Border radius (default: 8px). - * - * @example - * ```html - * - * ``` - */ -@customElement('my-counter') -export class MyCounter extends LitElement { - static override styles = styles; - - /** Starting value for the counter */ - @property({ type: Number, attribute: 'initial-value' }) - initialValue = 0; - - /** Minimum allowed value (inclusive) */ - @property({ type: Number }) - min = -Infinity; - - /** Maximum allowed value (inclusive) */ - @property({ type: Number }) - max = Infinity; - - /** Step size for increment / decrement */ - @property({ type: Number }) - step = 1; - - @state() - private _value = 0; - - override connectedCallback(): void { - super.connectedCallback(); - this._value = this.initialValue; - } - - private _emit(direction: CounterChangedDetail['direction']): void { - this.dispatchEvent( - new CustomEvent('counter-changed', { - detail: { value: this._value, direction }, - bubbles: true, - composed: true, - }), - ); - } - - private _increment(): void { - if (this._value + this.step <= this.max) { - this._value += this.step; - this._emit('increment'); - } - } - - private _decrement(): void { - if (this._value - this.step >= this.min) { - this._value -= this.step; - this._emit('decrement'); - } - } - - private _reset(): void { - this._value = this.initialValue; - this._emit('reset'); - } - - override render() { - const atMin = this._value - this.step < this.min; - const atMax = this._value + this.step > this.max; - - return html` -
      - - - - ${this._value} - - - - - -
      - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - 'my-counter': MyCounter; - } -} -``` - -### `src/index.ts` - -```typescript -export { MyCounter } from './my-counter'; -export type { CounterChangedDetail } from './types'; -``` - -### `test/my-counter.test.ts` - -```typescript -import { html, fixture, expect } from '@open-wc/testing'; -import { MyCounter } from '../src/my-counter'; - -describe('MyCounter', () => { - it('renders with default value of 0', async () => { - const el = await fixture(html``); - const value = el.shadowRoot!.querySelector('.value'); - expect(value?.textContent?.trim()).to.equal('0'); - }); - - it('respects initial-value attribute', async () => { - const el = await fixture( - html``, - ); - const value = el.shadowRoot!.querySelector('.value'); - expect(value?.textContent?.trim()).to.equal('5'); - }); - - it('increments on + button click', async () => { - const el = await fixture(html``); - const incBtn = el.shadowRoot!.querySelectorAll('button')[1] as HTMLButtonElement; - incBtn.click(); - await el.updateComplete; - const value = el.shadowRoot!.querySelector('.value'); - expect(value?.textContent?.trim()).to.equal('1'); - }); - - it('decrements on - button click', async () => { - const el = await fixture(html``); - const decBtn = el.shadowRoot!.querySelector('button') as HTMLButtonElement; - decBtn.click(); - await el.updateComplete; - const value = el.shadowRoot!.querySelector('.value'); - expect(value?.textContent?.trim()).to.equal('2'); - }); - - it('fires counter-changed event on increment', async () => { - const el = await fixture(html``); - let eventDetail: { value: number; direction: string } | undefined; - el.addEventListener('counter-changed', (e) => { - eventDetail = (e as CustomEvent).detail; - }); - const incBtn = el.shadowRoot!.querySelectorAll('button')[1] as HTMLButtonElement; - incBtn.click(); - await el.updateComplete; - expect(eventDetail).to.deep.equal({ value: 1, direction: 'increment' }); - }); - - it('disables decrement button at min boundary', async () => { - const el = await fixture( - html``, - ); - const decBtn = el.shadowRoot!.querySelector('button') as HTMLButtonElement; - expect(decBtn.disabled).to.be.true; - }); - - it('resets to initial value', async () => { - const el = await fixture( - html``, - ); - const incBtn = el.shadowRoot!.querySelectorAll('button')[1] as HTMLButtonElement; - incBtn.click(); - await el.updateComplete; - const resetBtn = el.shadowRoot!.querySelectorAll('button')[2] as HTMLButtonElement; - resetBtn.click(); - await el.updateComplete; - const value = el.shadowRoot!.querySelector('.value'); - expect(value?.textContent?.trim()).to.equal('5'); - }); - - it('passes accessibility audit', async () => { - const el = await fixture(html``); - await expect(el).to.be.accessible(); - }); -}); -``` - -### `web-test-runner.config.mjs` - -```js -import { playwrightLauncher } from '@web/test-runner-playwright'; - -export default { - files: 'test/**/*.test.ts', - nodeResolve: true, - browsers: [ - playwrightLauncher({ product: 'chromium' }), - ], - plugins: [], - esbuildTarget: 'auto', -}; -``` - -### `rollup.config.mjs` - -```js -import resolve from '@rollup/plugin-node-resolve'; -import typescript from '@rollup/plugin-typescript'; - -export default { - input: 'src/index.ts', - output: { - dir: 'dist', - format: 'esm', - sourcemap: true, - preserveModules: true, - preserveModulesRoot: 'src', - }, - plugins: [ - resolve(), - typescript({ tsconfig: './tsconfig.json' }), - ], - external: ['lit', /^lit\//], -}; -``` - -### Usage in HTML - -```html - - - - - My Lit Counter Demo - - - - - - - - -``` - -## Getting Started - -```bash -# 1. Install dependencies -npm install - -# 2. Build the component -npm run build - -# 3. Run tests (requires Playwright — installed automatically) -npm test - -# 4. Run tests in watch mode during development -npm run test:watch - -# 5. Build in watch mode for live TypeScript compilation -npm run build:watch - -# 6. Generate the Custom Elements Manifest -npm run analyze - -# 7. Lint -npm run lint -``` - -## Features - -- Lit 3 with TypeScript class decorators (`@customElement`, `@property`, `@state`) -- `useDefineForClassFields: false` to ensure Lit decorators work correctly with TypeScript -- Scoped Shadow DOM styles using Lit's `css` tagged template literal -- CSS custom properties (CSS variables) for consumer-side theming -- CSS `part` attributes for structural styling from outside the shadow root -- Accessible ARIA attributes: `aria-live`, `aria-atomic`, descriptive `aria-label` on buttons -- Custom event (`counter-changed`) with typed `CustomEvent` detail -- `HTMLElementTagNameMap` augmentation for correct TypeScript types in consuming projects -- `@open-wc/testing` test utilities with accessibility audit via `axe-core` -- Web Test Runner + Playwright for real-browser testing -- Rollup ESM bundle with `preserveModules` for tree-shakeable output -- Custom Elements Manifest (`custom-elements.json`) for IDE tooling and documentation diff --git a/skills/typescript-coder/assets/typescript-nestjs-boilerplate.md b/skills/typescript-coder/assets/typescript-nestjs-boilerplate.md deleted file mode 100644 index fa5d3a706..000000000 --- a/skills/typescript-coder/assets/typescript-nestjs-boilerplate.md +++ /dev/null @@ -1,492 +0,0 @@ -# TypeScript NestJS Boilerplate Template - -> A production-ready NestJS boilerplate built with TypeScript. Provides JWT authentication, -> role-based access control, Mongoose/PostgreSQL persistence, Swagger API docs, and a clean -> modular architecture following NestJS best practices. Based on the Onix-Systems -> `nest-js-boilerplate` project. - -## License - -MIT License. See [https://github.com/Onix-Systems/nest-js-boilerplate](https://github.com/Onix-Systems/nest-js-boilerplate) for full license terms. - -## Source - -- [nest-js-boilerplate](https://github.com/Onix-Systems/nest-js-boilerplate) by Onix-Systems - -## Project Structure - -``` -nest-js-boilerplate/ -├── src/ -│ ├── auth/ -│ │ ├── auth.controller.ts -│ │ ├── auth.module.ts -│ │ ├── auth.service.ts -│ │ ├── dto/ -│ │ │ ├── sign-in.dto.ts -│ │ │ └── sign-up.dto.ts -│ │ └── strategies/ -│ │ ├── jwt.strategy.ts -│ │ └── local.strategy.ts -│ ├── users/ -│ │ ├── users.controller.ts -│ │ ├── users.module.ts -│ │ ├── users.service.ts -│ │ ├── schemas/ -│ │ │ └── users.schema.ts -│ │ └── dto/ -│ │ ├── create-user.dto.ts -│ │ └── update-user.dto.ts -│ ├── common/ -│ │ ├── decorators/ -│ │ │ └── roles.decorator.ts -│ │ ├── guards/ -│ │ │ ├── jwt-auth.guard.ts -│ │ │ └── roles.guard.ts -│ │ ├── interceptors/ -│ │ │ └── transform.interceptor.ts -│ │ └── filters/ -│ │ └── http-exception.filter.ts -│ ├── config/ -│ │ ├── configuration.ts -│ │ └── database.config.ts -│ ├── app.module.ts -│ └── main.ts -├── test/ -│ ├── app.e2e-spec.ts -│ └── jest-e2e.json -├── .env -├── .env.example -├── Dockerfile -├── docker-compose.yml -├── package.json -└── tsconfig.json -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "nest-js-boilerplate", - "version": "1.0.0", - "description": "NestJS TypeScript boilerplate with authentication and persistence", - "scripts": { - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" - }, - "dependencies": { - "@nestjs/common": "^10.0.0", - "@nestjs/core": "^10.0.0", - "@nestjs/jwt": "^10.1.1", - "@nestjs/mongoose": "^10.0.2", - "@nestjs/passport": "^10.0.2", - "@nestjs/platform-express": "^10.0.0", - "@nestjs/swagger": "^7.1.12", - "@nestjs/config": "^3.1.1", - "bcrypt": "^5.1.1", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.0", - "mongoose": "^8.0.1", - "passport": "^0.6.0", - "passport-jwt": "^4.0.1", - "passport-local": "^1.0.0", - "reflect-metadata": "^0.1.13", - "rxjs": "^7.8.1", - "swagger-ui-express": "^5.0.0" - }, - "devDependencies": { - "@nestjs/cli": "^10.0.0", - "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", - "@types/bcrypt": "^5.0.2", - "@types/express": "^4.17.21", - "@types/jest": "^29.5.11", - "@types/node": "^20.11.0", - "@types/passport-jwt": "^3.0.13", - "@types/passport-local": "^1.0.38", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.42.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", - "jest": "^29.7.0", - "prettier": "^3.0.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.4", - "ts-jest": "^29.1.1", - "ts-loader": "^9.4.3", - "ts-node": "^10.9.1", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2021", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false, - "paths": { - "@/*": ["src/*"] - } - } -} -``` - -### `src/main.ts` - -```typescript -import { NestFactory } from '@nestjs/core'; -import { ValidationPipe } from '@nestjs/common'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { AppModule } from './app.module'; -import { HttpExceptionFilter } from './common/filters/http-exception.filter'; -import { TransformInterceptor } from './common/interceptors/transform.interceptor'; - -async function bootstrap(): Promise { - const app = await NestFactory.create(AppModule); - - // Global validation pipe - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - transform: true, - forbidNonWhitelisted: true, - }), - ); - - // Global exception filter - app.useGlobalFilters(new HttpExceptionFilter()); - - // Global response transform interceptor - app.useGlobalInterceptors(new TransformInterceptor()); - - // CORS - app.enableCors(); - - // Swagger documentation - const config = new DocumentBuilder() - .setTitle('NestJS Boilerplate API') - .setDescription('REST API built with NestJS and TypeScript') - .setVersion('1.0') - .addBearerAuth( - { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }, - 'access-token', - ) - .build(); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api/docs', app, document); - - const port = process.env.PORT ?? 3000; - await app.listen(port); - console.log(`Application running on http://localhost:${port}`); - console.log(`Swagger docs at http://localhost:${port}/api/docs`); -} - -bootstrap(); -``` - -### `src/app.module.ts` - -```typescript -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { MongooseModule } from '@nestjs/mongoose'; -import { AuthModule } from './auth/auth.module'; -import { UsersModule } from './users/users.module'; -import configuration from './config/configuration'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - load: [configuration], - }), - MongooseModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (configService: ConfigService) => ({ - uri: configService.get('database.uri'), - }), - inject: [ConfigService], - }), - AuthModule, - UsersModule, - ], -}) -export class AppModule {} -``` - -### `src/auth/auth.controller.ts` - -```typescript -import { - Controller, - Post, - Body, - HttpCode, - HttpStatus, - UseGuards, - Get, - Request, -} from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; -import { AuthService } from './auth.service'; -import { SignInDto } from './dto/sign-in.dto'; -import { SignUpDto } from './dto/sign-up.dto'; -import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; - -@ApiTags('auth') -@Controller('auth') -export class AuthController { - constructor(private readonly authService: AuthService) {} - - @Post('sign-up') - @ApiOperation({ summary: 'Register a new user' }) - @ApiResponse({ status: 201, description: 'User created successfully.' }) - @ApiResponse({ status: 409, description: 'Email already in use.' }) - async signUp(@Body() signUpDto: SignUpDto) { - return this.authService.signUp(signUpDto); - } - - @Post('sign-in') - @HttpCode(HttpStatus.OK) - @ApiOperation({ summary: 'Authenticate a user and return a JWT' }) - @ApiResponse({ status: 200, description: 'Authentication successful.' }) - @ApiResponse({ status: 401, description: 'Invalid credentials.' }) - async signIn(@Body() signInDto: SignInDto) { - return this.authService.signIn(signInDto); - } - - @Get('profile') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth('access-token') - @ApiOperation({ summary: 'Get current user profile' }) - @ApiResponse({ status: 200, description: 'User profile returned.' }) - getProfile(@Request() req: any) { - return req.user; - } -} -``` - -### `src/auth/auth.service.ts` - -```typescript -import { - Injectable, - ConflictException, - UnauthorizedException, -} from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; -import * as bcrypt from 'bcrypt'; -import { UsersService } from '../users/users.service'; -import { SignUpDto } from './dto/sign-up.dto'; -import { SignInDto } from './dto/sign-in.dto'; - -@Injectable() -export class AuthService { - constructor( - private readonly usersService: UsersService, - private readonly jwtService: JwtService, - ) {} - - async signUp(signUpDto: SignUpDto) { - const existing = await this.usersService.findByEmail(signUpDto.email); - if (existing) { - throw new ConflictException('Email already registered'); - } - const hashedPassword = await bcrypt.hash(signUpDto.password, 10); - const user = await this.usersService.create({ - ...signUpDto, - password: hashedPassword, - }); - const { password: _pw, ...result } = user.toObject(); - return result; - } - - async signIn(signInDto: SignInDto) { - const user = await this.usersService.findByEmail(signInDto.email); - if (!user) { - throw new UnauthorizedException('Invalid credentials'); - } - const isMatch = await bcrypt.compare(signInDto.password, user.password); - if (!isMatch) { - throw new UnauthorizedException('Invalid credentials'); - } - const payload = { sub: user._id, email: user.email, roles: user.roles }; - return { - accessToken: this.jwtService.sign(payload), - }; - } -} -``` - -### `src/auth/dto/sign-up.dto.ts` - -```typescript -import { IsEmail, IsString, MinLength, IsOptional, IsArray } from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -export class SignUpDto { - @ApiProperty({ example: 'Jane Doe' }) - @IsString() - readonly name: string; - - @ApiProperty({ example: 'jane@example.com' }) - @IsEmail() - readonly email: string; - - @ApiProperty({ example: 'strongPassword123', minLength: 8 }) - @IsString() - @MinLength(8) - readonly password: string; - - @ApiPropertyOptional({ example: ['user'] }) - @IsOptional() - @IsArray() - @IsString({ each: true }) - readonly roles?: string[]; -} -``` - -### `src/users/schemas/users.schema.ts` - -```typescript -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document, HydratedDocument } from 'mongoose'; - -export type UserDocument = HydratedDocument; - -@Schema({ timestamps: true }) -export class User extends Document { - @Prop({ required: true }) - name: string; - - @Prop({ required: true, unique: true, lowercase: true }) - email: string; - - @Prop({ required: true }) - password: string; - - @Prop({ type: [String], default: ['user'] }) - roles: string[]; - - @Prop({ default: true }) - isActive: boolean; -} - -export const UserSchema = SchemaFactory.createForClass(User); -``` - -### `src/common/guards/jwt-auth.guard.ts` - -```typescript -import { Injectable } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; - -@Injectable() -export class JwtAuthGuard extends AuthGuard('jwt') {} -``` - -### `src/common/decorators/roles.decorator.ts` - -```typescript -import { SetMetadata } from '@nestjs/common'; - -export const ROLES_KEY = 'roles'; -export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); -``` - -### `src/config/configuration.ts` - -```typescript -export default () => ({ - port: parseInt(process.env.PORT ?? '3000', 10), - database: { - uri: process.env.MONGODB_URI ?? 'mongodb://localhost:27017/nestjs-boilerplate', - }, - jwt: { - secret: process.env.JWT_SECRET ?? 'super-secret-change-in-production', - expiresIn: process.env.JWT_EXPIRES_IN ?? '7d', - }, -}); -``` - -### `.env.example` - -``` -PORT=3000 -MONGODB_URI=mongodb://localhost:27017/nestjs-boilerplate -JWT_SECRET=change-this-to-a-long-random-secret -JWT_EXPIRES_IN=7d -``` - -## Getting Started - -```bash -# 1. Install dependencies -npm install - -# 2. Copy and configure environment variables -cp .env.example .env - -# 3. Start MongoDB (or use Docker) -docker-compose up -d mongo - -# 4. Run in development mode (with hot reload) -npm run start:dev - -# 5. Open Swagger docs -# http://localhost:3000/api/docs - -# 6. Run tests -npm test -npm run test:e2e - -# 7. Build for production -npm run build -npm run start:prod -``` - -## Features - -- Modular NestJS architecture with feature-based folder structure -- JWT authentication with Passport.js (local and JWT strategies) -- Role-based access control via custom guards and decorators -- MongoDB persistence with Mongoose and typed schemas -- Class-validator DTO validation with whitelist enforcement -- Swagger/OpenAPI documentation auto-generated from decorators -- Global HTTP exception filter with consistent error response shape -- Global response transform interceptor -- Docker Compose setup for local MongoDB -- Unit and end-to-end tests with Jest and Supertest -- Environment configuration via `@nestjs/config` with typed access diff --git a/skills/typescript-coder/assets/typescript-ngx-rocket.md b/skills/typescript-coder/assets/typescript-ngx-rocket.md deleted file mode 100644 index ec35a1b07..000000000 --- a/skills/typescript-coder/assets/typescript-ngx-rocket.md +++ /dev/null @@ -1,436 +0,0 @@ -# TypeScript Angular Enterprise Template (ngx-rocket) - -> An enterprise-grade Angular TypeScript application scaffold generated by -> `generator-ngx-rocket`. Includes authentication, internationalization (i18n), lazy-loaded -> feature modules, environment-based configuration, SCSS theming, and a comprehensive -> testing setup with Jest and Cypress. - -## License - -MIT License. See [https://github.com/ngx-rocket/generator-ngx-rocket](https://github.com/ngx-rocket/generator-ngx-rocket) for full license terms. - -## Source - -- [generator-ngx-rocket](https://github.com/ngx-rocket/generator-ngx-rocket) by ngx-rocket - -## Project Structure - -``` -my-ngx-app/ -├── src/ -│ ├── app/ -│ │ ├── core/ -│ │ │ ├── authentication/ -│ │ │ │ ├── authentication.service.ts -│ │ │ │ └── credentials.service.ts -│ │ │ ├── http/ -│ │ │ │ ├── api-prefix.interceptor.ts -│ │ │ │ └── error-handler.interceptor.ts -│ │ │ ├── i18n/ -│ │ │ │ └── i18n.module.ts -│ │ │ ├── shell/ -│ │ │ │ ├── shell.component.ts -│ │ │ │ ├── shell.component.html -│ │ │ │ └── shell-routing.module.ts -│ │ │ └── core.module.ts -│ │ ├── shared/ -│ │ │ ├── shared.module.ts -│ │ │ └── loader/ -│ │ │ ├── loader.component.ts -│ │ │ └── loader.component.html -│ │ ├── home/ -│ │ │ ├── home.component.ts -│ │ │ ├── home.component.html -│ │ │ ├── home.component.scss -│ │ │ ├── home.module.ts -│ │ │ └── home-routing.module.ts -│ │ ├── login/ -│ │ │ ├── login.component.ts -│ │ │ ├── login.component.html -│ │ │ ├── login.module.ts -│ │ │ └── login-routing.module.ts -│ │ ├── app.component.ts -│ │ ├── app.component.html -│ │ ├── app.module.ts -│ │ └── app-routing.module.ts -│ ├── assets/ -│ │ ├── i18n/ -│ │ │ ├── en-US.json -│ │ │ └── fr-FR.json -│ │ └── images/ -│ ├── environments/ -│ │ ├── environment.ts -│ │ └── environment.prod.ts -│ ├── styles.scss -│ ├── index.html -│ └── main.ts -├── e2e/ -│ └── src/ -│ └── app.e2e-spec.ts -├── angular.json -├── package.json -├── tsconfig.json -├── tsconfig.app.json -├── tsconfig.spec.json -└── jest.config.js -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-ngx-app", - "version": "0.0.1", - "scripts": { - "ng": "ng", - "start": "ng serve", - "build": "ng build", - "build:prod": "ng build --configuration production", - "test": "jest", - "test:watch": "jest --watch", - "test:ci": "jest --runInBand --no-coverage", - "e2e": "ng e2e", - "lint": "ng lint", - "extract-i18n": "ngx-translate-extract --input ./src --output ./src/assets/i18n --clean --sort --format namespaced-json", - "format": "prettier --write \"src/**/*.{ts,html,scss}\"" - }, - "dependencies": { - "@angular/animations": "^17.0.0", - "@angular/common": "^17.0.0", - "@angular/compiler": "^17.0.0", - "@angular/core": "^17.0.0", - "@angular/forms": "^17.0.0", - "@angular/platform-browser": "^17.0.0", - "@angular/platform-browser-dynamic": "^17.0.0", - "@angular/router": "^17.0.0", - "@ngx-translate/core": "^15.0.0", - "@ngx-translate/http-loader": "^8.0.0", - "rxjs": "~7.8.0", - "tslib": "^2.6.0", - "zone.js": "~0.14.2" - }, - "devDependencies": { - "@angular-builders/jest": "^17.0.0", - "@angular-devkit/build-angular": "^17.0.0", - "@angular/cli": "^17.0.0", - "@angular/compiler-cli": "^17.0.0", - "@types/jest": "^29.5.11", - "@types/node": "^20.11.0", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "prettier": "^3.1.1", - "ts-jest": "^29.1.4", - "typescript": "~5.2.2" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compileOnSave": false, - "compilerOptions": { - "baseUrl": "./", - "outDir": "./dist/out-tsc", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "sourceMap": true, - "declaration": false, - "downlevelIteration": true, - "experimentalDecorators": true, - "moduleResolution": "bundler", - "importHelpers": true, - "target": "ES2022", - "module": "ES2022", - "useDefineForClassFields": false, - "lib": ["ES2022", "dom"], - "paths": { - "@app/*": ["src/app/*"], - "@env/*": ["src/environments/*"] - } - }, - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true - } -} -``` - -### `tsconfig.app.json` - -```json -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/app", - "types": [] - }, - "files": [ - "src/main.ts" - ], - "include": [ - "src/**/*.d.ts" - ] -} -``` - -### `src/environments/environment.ts` - -```typescript -export const environment = { - production: false, - serverUrl: 'http://localhost:3000', - defaultLanguage: 'en-US', - supportedLanguages: ['en-US', 'fr-FR'], -}; -``` - -### `src/environments/environment.prod.ts` - -```typescript -export const environment = { - production: true, - serverUrl: '/api', - defaultLanguage: 'en-US', - supportedLanguages: ['en-US', 'fr-FR'], -}; -``` - -### `src/app/app-routing.module.ts` - -```typescript -import { NgModule } from '@angular/core'; -import { Routes, RouterModule, PreloadAllModules } from '@angular/router'; -import { ShellComponent } from './core/shell/shell.component'; -import { AuthGuard } from './core/authentication/auth.guard'; - -const routes: Routes = [ - { path: 'login', loadChildren: () => import('./login/login.module').then((m) => m.LoginModule) }, - { - path: '', - component: ShellComponent, - canActivate: [AuthGuard], - children: [ - { - path: 'home', - loadChildren: () => import('./home/home.module').then((m) => m.HomeModule), - data: { title: 'Home' }, - }, - { path: '', redirectTo: 'home', pathMatch: 'full' }, - ], - }, - { path: '**', redirectTo: '', pathMatch: 'full' }, -]; - -@NgModule({ - imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })], - exports: [RouterModule], - providers: [], -}) -export class AppRoutingModule {} -``` - -### `src/app/core/authentication/authentication.service.ts` - -```typescript -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable, of } from 'rxjs'; -import { map, catchError } from 'rxjs/operators'; -import { CredentialsService } from './credentials.service'; - -export interface LoginContext { - username: string; - password: string; - remember?: boolean; -} - -export interface LoginResponse { - token: string; - username: string; -} - -@Injectable({ providedIn: 'root' }) -export class AuthenticationService { - constructor( - private httpClient: HttpClient, - private credentialsService: CredentialsService, - ) {} - - login(context: LoginContext): Observable { - return this.httpClient.post('/auth/sign-in', context).pipe( - map((response) => { - this.credentialsService.setCredentials( - { username: response.username, token: response.token }, - context.remember, - ); - return response; - }), - ); - } - - logout(): Observable { - this.credentialsService.setCredentials(); - return of(true); - } -} -``` - -### `src/app/home/home.component.ts` - -```typescript -import { Component, OnInit } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { CredentialsService } from '../core/authentication/credentials.service'; - -export interface Quote { - text: string; - author: string; -} - -@Component({ - selector: 'app-home', - templateUrl: './home.component.html', - styleUrls: ['./home.component.scss'], -}) -export class HomeComponent implements OnInit { - quote: Quote | undefined; - isLoading = true; - currentUser: string | null | undefined; - - constructor( - private translate: TranslateService, - private credentialsService: CredentialsService, - ) {} - - ngOnInit(): void { - this.currentUser = this.credentialsService.credentials?.username; - this.loadQuote(); - } - - private loadQuote(): void { - // Simulate async data loading - setTimeout(() => { - this.quote = { - text: 'First, solve the problem. Then, write the code.', - author: 'John Johnson', - }; - this.isLoading = false; - }, 500); - } - - setLanguage(language: string): void { - this.translate.use(language); - } -} -``` - -### `src/app/core/http/api-prefix.interceptor.ts` - -```typescript -import { Injectable } from '@angular/core'; -import { - HttpRequest, - HttpHandler, - HttpEvent, - HttpInterceptor, -} from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { environment } from '@env/environment'; - -@Injectable() -export class ApiPrefixInterceptor implements HttpInterceptor { - intercept(request: HttpRequest, next: HttpHandler): Observable> { - // Do not prefix requests to external domains - if (!/^(http|https):/i.test(request.url)) { - request = request.clone({ url: environment.serverUrl + request.url }); - } - return next.handle(request); - } -} -``` - -### `src/assets/i18n/en-US.json` - -```json -{ - "APP_NAME": "My Angular App", - "HOME": { - "TITLE": "Home", - "WELCOME": "Welcome, {{username}}!", - "QUOTE_TITLE": "Quote of the Day" - }, - "LOGIN": { - "TITLE": "Sign In", - "USERNAME": "Username", - "PASSWORD": "Password", - "REMEMBER_ME": "Remember me", - "BUTTON": "Sign In", - "ERROR": "Incorrect username or password." - }, - "LOGOUT": "Sign Out" -} -``` - -### `jest.config.js` - -```js -module.exports = { - preset: 'jest-preset-angular', - setupFilesAfterFramework: ['/setup-jest.ts'], - globalSetup: 'jest-preset-angular/global-setup', - testPathIgnorePatterns: ['/node_modules/', '/dist/'], - coverageDirectory: 'coverage', - coverageReporters: ['html', 'text'], - collectCoverageFrom: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/environments/**'], -}; -``` - -## Getting Started - -```bash -# 1. Install the Angular CLI -npm install -g @angular/cli - -# 2. Install dependencies -npm install - -# 3. Start the development server -npm start -# Open http://localhost:4200 - -# 4. Run unit tests -npm test - -# 5. Run end-to-end tests -npm run e2e - -# 6. Build for production -npm run build:prod - -# 7. Extract i18n strings -npm run extract-i18n -``` - -## Features - -- Angular 17 with standalone components support and lazy-loaded feature modules -- JWT-based authentication with credential persistence (localStorage / sessionStorage) -- HTTP interceptors: API prefix injection and global error handling -- Internationalization via `@ngx-translate/core` with JSON translation files -- Path aliases: `@app/*` for application code, `@env/*` for environment files -- SCSS global theming with component-level style encapsulation -- Shell/layout pattern separating authenticated routes from public routes -- Route guards protecting authenticated areas -- Jest unit tests with `jest-preset-angular` -- Strict TypeScript and strict Angular template checking enabled -- Prettier code formatting diff --git a/skills/typescript-coder/assets/typescript-node-boilerplate.md b/skills/typescript-coder/assets/typescript-node-boilerplate.md deleted file mode 100644 index 241ded8e3..000000000 --- a/skills/typescript-coder/assets/typescript-node-boilerplate.md +++ /dev/null @@ -1,332 +0,0 @@ - - -# TypeScript Node.js Boilerplate - -> Based on [jsynowiec/node-typescript-boilerplate](https://github.com/jsynowiec/node-typescript-boilerplate) -> License: **Apache-2.0** -> A minimalistic, actively-maintained Node.js + TypeScript project template using ESM, Jest, and modern ESLint flat config. - -## Project Structure - -``` -node-typescript-boilerplate/ -├── src/ -│ ├── index.ts -│ └── __tests__/ -│ └── index.test.ts -├── dist/ # Compiled output (git-ignored) -├── .editorconfig -├── .gitignore -├── .nvmrc -├── eslint.config.mjs -├── jest.config.ts -├── package.json -├── tsconfig.json -└── README.md -``` - -## `package.json` - -```json -{ - "name": "node-typescript-boilerplate", - "version": "1.0.0", - "description": "Minimalistic project template to build a Node.js back-end application with TypeScript", - "author": "Your Name ", - "license": "Apache-2.0", - "type": "module", - "main": "./dist/index.js", - "exports": { - ".": "./dist/index.js" - }, - "typings": "./dist/index.d.ts", - "engines": { - "node": ">= 20" - }, - "scripts": { - "start": "node ./dist/index.js", - "build": "tsc -p tsconfig.json", - "clean": "rimraf ./dist ./coverage", - "prebuild": "npm run clean", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "lint": "eslint src", - "lint:fix": "eslint src --fix", - "type-check": "tsc --noEmit" - }, - "devDependencies": { - "@eslint/js": "^9.15.0", - "@types/jest": "^29.5.14", - "@types/node": "^22.9.0", - "eslint": "^9.15.0", - "globals": "^15.12.0", - "jest": "^29.7.0", - "rimraf": "^6.0.1", - "ts-jest": "^29.2.5", - "typescript": "^5.7.2", - "typescript-eslint": "^8.15.0" - } -} -``` - -## `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2023"], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "removeComments": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "coverage", "**/*.test.ts"] -} -``` - -## `jest.config.ts` - -```typescript -import type { Config } from "jest"; - -const config: Config = { - displayName: "node-typescript-boilerplate", - preset: "ts-jest/presets/default-esm", - testEnvironment: "node", - extensionsToTreatAsEsm: [".ts"], - moduleNameMapper: { - "^(\\.{1,2}/.*)\\.js$": "$1", - }, - transform: { - "^.+\\.tsx?$": [ - "ts-jest", - { - useESM: true, - }, - ], - }, - testMatch: ["**/src/__tests__/**/*.test.ts"], - coverageDirectory: "coverage", - collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts", "!src/__tests__/**"], -}; - -export default config; -``` - -## `eslint.config.mjs` - -```javascript -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; -import globals from "globals"; - -export default tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.recommended, - { - languageOptions: { - globals: { - ...globals.node, - ...globals.es2022, - }, - }, - }, - { - rules: { - "@typescript-eslint/no-unused-vars": [ - "error", - { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, - ], - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "no-console": "warn", - }, - }, - { - ignores: ["dist/**", "coverage/**", "node_modules/**"], - } -); -``` - -## `src/index.ts` - -```typescript -/** - * Main entry point for the application. - * Replace this with your actual application logic. - */ - -export interface AppConfig { - name: string; - version: string; - debug?: boolean; -} - -export function createApp(config: AppConfig): string { - const { name, version, debug = false } = config; - - if (debug) { - console.debug(`[DEBUG] Initializing ${name} v${version}`); - } - - return `${name} v${version} is running`; -} - -// Entry point — only runs when executed directly, not when imported -const config: AppConfig = { - name: "my-app", - version: "1.0.0", - debug: process.env.NODE_ENV === "development", -}; - -console.log(createApp(config)); -``` - -## `src/__tests__/index.test.ts` - -```typescript -import { describe, expect, it } from "@jest/globals"; -import { createApp } from "../index.js"; - -describe("createApp", () => { - it("returns a startup message with the app name and version", () => { - const result = createApp({ name: "test-app", version: "0.1.0" }); - expect(result).toBe("test-app v0.1.0 is running"); - }); - - it("includes the name and version in the returned string", () => { - const result = createApp({ name: "my-service", version: "2.3.1" }); - expect(result).toContain("my-service"); - expect(result).toContain("2.3.1"); - }); - - it("uses debug=false by default", () => { - // Should not throw even when debug is omitted - expect(() => - createApp({ name: "silent-app", version: "1.0.0" }) - ).not.toThrow(); - }); -}); -``` - -## `.editorconfig` - -```ini -root = true - -[*] -end_of_line = lf -insert_final_newline = true -charset = utf-8 -indent_style = space -indent_size = 2 -trim_trailing_whitespace = true - -[*.md] -trim_trailing_whitespace = false -``` - -## `.nvmrc` - -``` -20 -``` - -## `.gitignore` - -``` -# Compiled output -/dist -/coverage - -# Dependencies -/node_modules - -# Build artifacts -*.tsbuildinfo -.tsbuildinfo - -# Environment -.env -.env.* -!.env.example - -# OS artifacts -.DS_Store -Thumbs.db - -# IDE -.idea/ -.vscode/ -*.code-workspace -``` - -## Key Scripts Reference - -| Script | Command | Purpose | -|---|---|---| -| `build` | `tsc -p tsconfig.json` | Compile TypeScript to `dist/` | -| `clean` | `rimraf ./dist ./coverage` | Remove build artifacts | -| `start` | `node ./dist/index.js` | Run compiled output | -| `test` | `jest` | Run all tests | -| `test:coverage` | `jest --coverage` | Run tests with coverage report | -| `lint` | `eslint src` | Lint all TypeScript sources | -| `lint:fix` | `eslint src --fix` | Auto-fix lint issues | -| `type-check` | `tsc --noEmit` | Type-check without emitting files | - -## Quick Start - -```bash -# Clone or copy this template -git clone https://github.com/jsynowiec/node-typescript-boilerplate my-project -cd my-project - -# Install dependencies -npm install - -# Run type-checking -npm run type-check - -# Run tests -npm test - -# Build for production -npm run build - -# Start the app -npm start -``` - -## Notes on ESM + NodeNext - -This boilerplate uses `"module": "NodeNext"` and `"type": "module"` in `package.json`. - -- **Imports must use `.js` extensions** in TypeScript source, even though the files are `.ts`: - ```typescript - // Correct — TypeScript resolves this to index.ts at compile time - import { helper } from "./helper.js"; - - // Wrong — will fail at runtime - import { helper } from "./helper"; - ``` -- Jest is configured with `extensionsToTreatAsEsm` and a `moduleNameMapper` to handle `.js` → source file resolution during tests. -- If you prefer CommonJS, change `"module"` to `"CommonJS"` and remove `"type": "module"` from `package.json`. diff --git a/skills/typescript-coder/assets/typescript-node-module.md b/skills/typescript-coder/assets/typescript-node-module.md deleted file mode 100644 index 913827770..000000000 --- a/skills/typescript-coder/assets/typescript-node-module.md +++ /dev/null @@ -1,365 +0,0 @@ -# TypeScript Node.js Module / Library Template - -> A TypeScript Node.js module starter based on patterns from `generator-node-module-typescript`. Produces an npm-publishable library with CJS and ESM dual output via Rollup, Jest testing, and full TypeScript type declarations. - -## License - -MIT License — See source repository for full license terms. - -## Source - -- [codejamninja/generator-node-module-typescript](https://github.com/codejamninja/generator-node-module-typescript) - -## Project Structure - -``` -my-ts-module/ -├── src/ -│ ├── index.ts -│ ├── greet.ts -│ └── types.ts -├── tests/ -│ ├── greet.test.ts -│ └── index.test.ts -├── dist/ -│ ├── cjs/ ← CommonJS output -│ │ ├── index.js -│ │ └── index.d.ts -│ └── esm/ ← ES Module output -│ ├── index.js -│ └── index.d.ts -├── .eslintrc.json -├── .gitignore -├── .npmignore -├── babel.config.js -├── jest.config.ts -├── package.json -├── rollup.config.ts -├── tsconfig.json -└── tsconfig.cjs.json -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-ts-module", - "version": "1.0.0", - "description": "A TypeScript Node.js module", - "main": "./dist/cjs/index.js", - "module": "./dist/esm/index.js", - "types": "./dist/cjs/index.d.ts", - "exports": { - ".": { - "import": { - "types": "./dist/esm/index.d.ts", - "default": "./dist/esm/index.js" - }, - "require": { - "types": "./dist/cjs/index.d.ts", - "default": "./dist/cjs/index.js" - } - } - }, - "files": [ - "dist", - "src" - ], - "sideEffects": false, - "scripts": { - "build": "rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript", - "build:watch": "rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript --watch", - "clean": "rimraf dist", - "prebuild": "npm run clean", - "test": "jest", - "test:watch": "jest --watchAll", - "test:coverage": "jest --coverage", - "lint": "eslint src --ext .ts", - "lint:fix": "eslint src --ext .ts --fix", - "typecheck": "tsc --noEmit", - "prepublishOnly": "npm run build && npm run test" - }, - "keywords": ["typescript", "node", "module"], - "engines": { - "node": ">=18.0.0" - }, - "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.5", - "@types/jest": "^29.5.7", - "@types/node": "^20.8.10", - "@typescript-eslint/eslint-plugin": "^6.9.1", - "@typescript-eslint/parser": "^6.9.1", - "eslint": "^8.52.0", - "jest": "^29.7.0", - "rimraf": "^5.0.5", - "rollup": "^4.3.0", - "rollup-plugin-dts": "^6.1.0", - "ts-jest": "^29.1.1", - "tslib": "^2.6.2", - "typescript": "^5.2.2" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2020"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "strict": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "importHelpers": true - }, - "include": ["src"], - "exclude": ["node_modules", "dist", "tests"] -} -``` - -### `tsconfig.cjs.json` - -```json -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "node", - "outDir": "dist/cjs" - } -} -``` - -### `rollup.config.ts` - -```typescript -import { defineConfig } from "rollup"; -import resolve from "@rollup/plugin-node-resolve"; -import commonjs from "@rollup/plugin-commonjs"; -import typescript from "@rollup/plugin-typescript"; -import dts from "rollup-plugin-dts"; - -const external = (id: string) => - !id.startsWith(".") && !id.startsWith("/") && id !== "tslib"; - -export default defineConfig([ - // ESM build - { - input: "src/index.ts", - output: { - dir: "dist/esm", - format: "es", - sourcemap: true, - preserveModules: true, - preserveModulesRoot: "src", - }, - external, - plugins: [ - resolve(), - commonjs(), - typescript({ - tsconfig: "./tsconfig.json", - declarationDir: "dist/esm", - declaration: true, - declarationMap: true, - }), - ], - }, - // CJS build - { - input: "src/index.ts", - output: { - dir: "dist/cjs", - format: "cjs", - sourcemap: true, - exports: "named", - preserveModules: true, - preserveModulesRoot: "src", - }, - external, - plugins: [ - resolve(), - commonjs(), - typescript({ - tsconfig: "./tsconfig.cjs.json", - declarationDir: "dist/cjs", - declaration: true, - declarationMap: true, - }), - ], - }, - // Type declarations bundle (optional — for single-file .d.ts) - { - input: "src/index.ts", - output: { file: "dist/index.d.ts", format: "es" }, - external, - plugins: [dts()], - }, -]); -``` - -### `jest.config.ts` - -```typescript -import type { Config } from "jest"; - -const config: Config = { - preset: "ts-jest", - testEnvironment: "node", - roots: ["/tests"], - testMatch: ["**/*.test.ts"], - transform: { - "^.+\\.ts$": ["ts-jest", { tsconfig: "./tsconfig.json" }], - }, - collectCoverageFrom: ["src/**/*.ts", "!src/types.ts"], - coverageDirectory: "coverage", - coverageReporters: ["text", "lcov"], -}; - -export default config; -``` - -### `src/types.ts` - -```typescript -export interface GreetOptions { - /** The name to greet. */ - name: string; - /** Optional greeting prefix. Defaults to "Hello". */ - prefix?: string; - /** Whether to use formal capitalisation. */ - formal?: boolean; -} - -export type GreetResult = { - message: string; - timestamp: Date; -}; -``` - -### `src/greet.ts` - -```typescript -import { GreetOptions, GreetResult } from "./types"; - -/** - * Produce a greeting message. - * - * @example - * ```ts - * const result = greet({ name: "World" }); - * console.log(result.message); // "Hello, World!" - * ``` - */ -export function greet(options: GreetOptions): GreetResult { - const { name, prefix = "Hello", formal = false } = options; - - const displayName = formal - ? name.charAt(0).toUpperCase() + name.slice(1) - : name; - - return { - message: `${prefix}, ${displayName}!`, - timestamp: new Date(), - }; -} -``` - -### `src/index.ts` - -```typescript -export { greet } from "./greet"; -export type { GreetOptions, GreetResult } from "./types"; -``` - -### `tests/greet.test.ts` - -```typescript -import { greet } from "../src/greet"; - -describe("greet()", () => { - it("returns a message with the default prefix", () => { - const result = greet({ name: "World" }); - expect(result.message).toBe("Hello, World!"); - }); - - it("respects a custom prefix", () => { - const result = greet({ name: "Alice", prefix: "Hi" }); - expect(result.message).toBe("Hi, Alice!"); - }); - - it("capitalises the name when formal is true", () => { - const result = greet({ name: "alice", formal: true }); - expect(result.message).toBe("Hello, Alice!"); - }); - - it("includes a timestamp", () => { - const before = Date.now(); - const result = greet({ name: "Test" }); - const after = Date.now(); - expect(result.timestamp.getTime()).toBeGreaterThanOrEqual(before); - expect(result.timestamp.getTime()).toBeLessThanOrEqual(after); - }); -}); -``` - -### `.npmignore` - -``` -src/ -tests/ -*.config.ts -*.config.js -tsconfig*.json -.eslintrc.json -coverage/ -.github/ -``` - -## Getting Started - -1. Copy the template into your new module directory. -2. Update `name`, `description`, and `keywords` in `package.json`. -3. Install dependencies: - ```bash - npm install - ``` -4. Run tests to verify setup: - ```bash - npm test - ``` -5. Build dual CJS + ESM output: - ```bash - npm run build - ``` -6. Publish to npm: - ```bash - npm publish --access public - ``` - -## Features - -- TypeScript 5.x with strict mode and `importHelpers` (tslib) -- Dual CJS + ESM output via Rollup 4 with `preserveModules` for tree-shaking -- Separate `tsconfig.cjs.json` for the CommonJS build -- `exports` map in `package.json` with type-safe `import`/`require` conditions -- `rollup-plugin-dts` for bundling a single `.d.ts` entry-point declaration file -- Jest + ts-jest for native TypeScript test execution without pre-compilation -- `sideEffects: false` declared for optimal tree-shaking by bundlers -- `prepublishOnly` script enforces a passing build and test suite before publish -- `.npmignore` to keep the published package lean (only `dist/` and `src/`) diff --git a/skills/typescript-coder/assets/typescript-node-tsnext.md b/skills/typescript-coder/assets/typescript-node-tsnext.md deleted file mode 100644 index 495f6ceb9..000000000 --- a/skills/typescript-coder/assets/typescript-node-tsnext.md +++ /dev/null @@ -1,230 +0,0 @@ -# TypeScript Node.js Module (tsnext) - -> A modern TypeScript Node.js module starter with ESM-first output, strict compiler settings targeting the latest Node.js LTS, and Vitest for testing. Designed to produce dual-publishable packages (ESM + type declarations) using `NodeNext` module resolution. - -## License - -MIT — See [source repository](https://github.com/motss/generator-node-tsnext) for full license text. - -## Source - -- [motss/generator-node-tsnext](https://github.com/motss/generator-node-tsnext) - -## Project Structure - -``` -my-module/ -├── src/ -│ ├── index.ts -│ ├── lib/ -│ │ └── my-feature.ts -│ └── test/ -│ ├── index.test.ts -│ └── my-feature.test.ts -├── dist/ (generated — do not edit) -├── package.json -├── tsconfig.json -├── tsconfig.build.json -├── vitest.config.ts -├── .eslintrc.cjs -├── .gitignore -└── README.md -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-module", - "version": "0.1.0", - "description": "My TypeScript Node.js module", - "license": "MIT", - "author": "Your Name ", - "type": "module", - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - } - }, - "files": [ - "dist", - "!dist/**/*.test.*", - "!dist/**/*.spec.*" - ], - "scripts": { - "build": "tsc -p tsconfig.build.json", - "build:watch": "tsc -p tsconfig.build.json --watch", - "clean": "rimraf dist", - "lint": "eslint src --ext .ts", - "prebuild": "npm run clean", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", - "prepublishOnly": "npm run build" - }, - "devDependencies": { - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.0.0", - "eslint": "^8.0.0", - "rimraf": "^5.0.0", - "typescript": "^5.4.0", - "vitest": "^1.0.0" - }, - "engines": { - "node": ">=20.0.0" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022"], - "strict": true, - "exactOptionalPropertyTypes": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - "allowUnusedLabels": false, - "allowUnreachableCode": false, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "dist", - "rootDir": "src" - }, - "include": ["src"], - "exclude": ["node_modules", "dist", "src/test"] -} -``` - -### `tsconfig.build.json` - -```json -{ - "extends": "./tsconfig.json", - "exclude": ["node_modules", "dist", "src/test", "**/*.test.ts", "**/*.spec.ts"] -} -``` - -### `vitest.config.ts` - -```typescript -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - environment: 'node', - include: ['src/test/**/*.test.ts'], - coverage: { - provider: 'v8', - reporter: ['text', 'lcov', 'html'], - include: ['src/**/*.ts'], - exclude: ['src/test/**'], - }, - }, -}); -``` - -### `src/index.ts` - -```typescript -export { greet } from './lib/my-feature.js'; -export type { GreetOptions } from './lib/my-feature.js'; -``` - -### `src/lib/my-feature.ts` - -```typescript -export interface GreetOptions { - name: string; - greeting?: string; -} - -export function greet(options: GreetOptions): string { - const { name, greeting = 'Hello' } = options; - - if (!name.trim()) { - throw new TypeError('name must not be empty'); - } - - return `${greeting}, ${name}!`; -} -``` - -### `src/test/my-feature.test.ts` - -```typescript -import { describe, expect, it } from 'vitest'; -import { greet } from '../lib/my-feature.js'; - -describe('greet()', () => { - it('returns a greeting with the default prefix', () => { - expect(greet({ name: 'World' })).toBe('Hello, World!'); - }); - - it('returns a greeting with a custom prefix', () => { - expect(greet({ name: 'Alice', greeting: 'Hi' })).toBe('Hi, Alice!'); - }); - - it('throws when name is empty', () => { - expect(() => greet({ name: ' ' })).toThrow(TypeError); - }); -}); -``` - -### `.gitignore` - -``` -node_modules/ -dist/ -coverage/ -*.tsbuildinfo -.env -``` - -## Getting Started - -```bash -# 1. Initialise a new directory -mkdir my-module && cd my-module - -# 2. Copy / scaffold project files (see structure above) - -# 3. Install dependencies -npm install - -# 4. Run tests -npm test - -# 5. Build for publishing -npm run build - -# 6. Inspect the dist output -ls dist/ -``` - -## Features - -- ESM-first output with `"type": "module"` and `exports` map -- `NodeNext` module resolution for accurate Node.js ESM behaviour -- Strict TypeScript with `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess` -- Declaration files and source maps emitted alongside JS -- Vitest for fast, native-ESM unit testing with V8 coverage -- Separate `tsconfig.build.json` to exclude test files from the published build -- `engines` field enforces Node.js 20 LTS or later diff --git a/skills/typescript-coder/assets/typescript-package.md b/skills/typescript-coder/assets/typescript-package.md deleted file mode 100644 index 2cfab71c1..000000000 --- a/skills/typescript-coder/assets/typescript-package.md +++ /dev/null @@ -1,457 +0,0 @@ -# TypeScript npm Package Template - -> A TypeScript npm package starter based on patterns from `generator-typescript-package` by EricCrosson. Produces a modern, publishable npm package with a full `exports` map, npm provenance, semantic release, strict TypeScript, and GitHub Actions CI/CD. - -## License - -MIT License — See source repository for full license terms. - -## Source - -- [EricCrosson/generator-typescript-package](https://github.com/EricCrosson/generator-typescript-package) - -## Project Structure - -``` -my-ts-package/ -├── .github/ -│ └── workflows/ -│ ├── ci.yml ← Test and typecheck on every PR/push -│ └── release.yml ← Semantic release on merge to main -├── src/ -│ ├── index.ts ← Public API barrel -│ ├── core.ts ← Core implementation -│ └── types.ts ← Exported types -├── tests/ -│ └── core.test.ts -├── dist/ ← Build output (gitignored) -│ ├── cjs/ -│ └── esm/ -├── .eslintrc.json -├── .gitignore -├── .npmignore -├── .releaserc.json ← Semantic release config -├── package.json -├── tsconfig.json -├── tsconfig.cjs.json -└── tsup.config.ts ← tsup bundler config (replaces manual Rollup) -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "@yourscope/my-ts-package", - "version": "0.0.0", - "description": "A modern TypeScript npm package", - "license": "MIT", - "author": "Your Name ", - "repository": { - "type": "git", - "url": "https://github.com/yourname/my-ts-package.git" - }, - "keywords": ["typescript"], - "type": "module", - "main": "./dist/cjs/index.cjs", - "module": "./dist/esm/index.js", - "types": "./dist/esm/index.d.ts", - "exports": { - ".": { - "import": { - "types": "./dist/esm/index.d.ts", - "default": "./dist/esm/index.js" - }, - "require": { - "types": "./dist/cjs/index.d.cts", - "default": "./dist/cjs/index.cjs" - } - } - }, - "files": [ - "dist", - "src" - ], - "sideEffects": false, - "engines": { - "node": ">=18.0.0" - }, - "scripts": { - "build": "tsup", - "clean": "rimraf dist", - "prebuild": "npm run clean", - "test": "jest", - "test:watch": "jest --watchAll", - "test:coverage": "jest --coverage", - "typecheck": "tsc --noEmit", - "lint": "eslint src --ext .ts", - "lint:fix": "eslint src --ext .ts --fix", - "prepublishOnly": "npm run build && npm run test && npm run typecheck" - }, - "devDependencies": { - "@semantic-release/changelog": "^6.0.3", - "@semantic-release/git": "^10.0.1", - "@types/jest": "^29.5.7", - "@types/node": "^20.8.10", - "@typescript-eslint/eslint-plugin": "^6.9.1", - "@typescript-eslint/parser": "^6.9.1", - "eslint": "^8.52.0", - "jest": "^29.7.0", - "rimraf": "^5.0.5", - "semantic-release": "^22.0.8", - "ts-jest": "^29.1.1", - "tsup": "^8.0.0", - "typescript": "^5.2.2" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022"], - "strict": true, - "exactOptionalPropertyTypes": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] -} -``` - -### `tsup.config.ts` - -```typescript -import { defineConfig } from "tsup"; - -export default defineConfig([ - // ESM build - { - entry: ["src/index.ts"], - format: ["esm"], - outDir: "dist/esm", - dts: true, - sourcemap: true, - clean: false, - splitting: false, - treeshake: true, - }, - // CJS build - { - entry: ["src/index.ts"], - format: ["cjs"], - outDir: "dist/cjs", - dts: true, - sourcemap: true, - clean: false, - splitting: false, - }, -]); -``` - -### `src/types.ts` - -```typescript -export interface ParseOptions { - /** Trim whitespace from string values. Default: true. */ - trim?: boolean; - /** Throw on invalid input instead of returning undefined. Default: false. */ - strict?: boolean; -} - -export type ParseResult = - | { success: true; value: T } - | { success: false; error: string }; -``` - -### `src/core.ts` - -```typescript -import { ParseOptions, ParseResult } from "./types.js"; - -/** - * Parse a raw string value into a number. - * - * @example - * ```ts - * parseNumber(" 42 ") // => { success: true, value: 42 } - * parseNumber("abc") // => { success: false, error: "Not a number: abc" } - * ``` - */ -export function parseNumber( - raw: string, - options: ParseOptions = {} -): ParseResult { - const { trim = true, strict = false } = options; - const input = trim ? raw.trim() : raw; - const parsed = Number(input); - - if (Number.isNaN(parsed) || input === "") { - const error = `Not a number: ${raw}`; - if (strict) throw new TypeError(error); - return { success: false, error }; - } - - return { success: true, value: parsed }; -} - -/** - * Parse a raw string value into a boolean. - * Accepts "true"/"false" (case-insensitive) and "1"/"0". - */ -export function parseBoolean( - raw: string, - options: ParseOptions = {} -): ParseResult { - const { trim = true, strict = false } = options; - const input = (trim ? raw.trim() : raw).toLowerCase(); - - if (input === "true" || input === "1") return { success: true, value: true }; - if (input === "false" || input === "0") return { success: true, value: false }; - - const error = `Not a boolean: ${raw}`; - if (strict) throw new TypeError(error); - return { success: false, error }; -} -``` - -### `src/index.ts` - -```typescript -export { parseNumber, parseBoolean } from "./core.js"; -export type { ParseOptions, ParseResult } from "./types.js"; -``` - -### `tests/core.test.ts` - -```typescript -import { parseNumber, parseBoolean } from "../src/index.js"; - -describe("parseNumber()", () => { - it("parses a valid integer string", () => { - const result = parseNumber("42"); - expect(result).toEqual({ success: true, value: 42 }); - }); - - it("parses a float string", () => { - const result = parseNumber("3.14"); - expect(result).toEqual({ success: true, value: 3.14 }); - }); - - it("trims whitespace by default", () => { - const result = parseNumber(" 7 "); - expect(result).toEqual({ success: true, value: 7 }); - }); - - it("returns failure for non-numeric input", () => { - const result = parseNumber("abc"); - expect(result.success).toBe(false); - }); - - it("throws in strict mode for non-numeric input", () => { - expect(() => parseNumber("abc", { strict: true })).toThrow(TypeError); - }); -}); - -describe("parseBoolean()", () => { - it.each([["true", true], ["1", true], ["false", false], ["0", false]])( - 'parses "%s" as %s', - (input, expected) => { - const result = parseBoolean(input); - expect(result).toEqual({ success: true, value: expected }); - } - ); - - it("is case-insensitive", () => { - expect(parseBoolean("TRUE")).toEqual({ success: true, value: true }); - }); - - it("returns failure for unknown values", () => { - expect(parseBoolean("yes").success).toBe(false); - }); -}); -``` - -### `.releaserc.json` - -```json -{ - "branches": ["main"], - "plugins": [ - "@semantic-release/commit-analyzer", - "@semantic-release/release-notes-generator", - [ - "@semantic-release/changelog", - { "changelogFile": "CHANGELOG.md" } - ], - [ - "@semantic-release/npm", - { "npmPublish": true } - ], - [ - "@semantic-release/git", - { - "assets": ["CHANGELOG.md", "package.json"], - "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" - } - ], - "@semantic-release/github" - ] -} -``` - -### `.github/workflows/ci.yml` - -```yaml -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.x, 20.x, 22.x] - - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Typecheck - run: npm run typecheck - - - name: Lint - run: npm run lint - - - name: Test - run: npm run test:coverage - - - name: Upload coverage - uses: codecov/codecov-action@v3 - if: matrix.node-version == '20.x' -``` - -### `.github/workflows/release.yml` - -```yaml -name: Release - -on: - push: - branches: [main] - -permissions: - contents: write - issues: write - pull-requests: write - id-token: write # Required for npm provenance - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: false - - - uses: actions/setup-node@v4 - with: - node-version: 20.x - registry-url: https://registry.npmjs.org - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - NPM_CONFIG_PROVENANCE: true # Enables npm provenance attestation - run: npx semantic-release -``` - -### `.npmignore` - -``` -src/ -tests/ -*.config.ts -tsconfig*.json -.eslintrc.json -.releaserc.json -.github/ -coverage/ -CHANGELOG.md -``` - -## Getting Started - -1. Copy the template and update `name`, `description`, `author`, and `repository` in `package.json`. -2. Install dependencies: - ```bash - npm install - ``` -3. Implement your library in `src/core.ts` and update `src/index.ts` to export the public API. -4. Run tests: - ```bash - npm test - ``` -5. Typecheck: - ```bash - npm run typecheck - ``` -6. Build both CJS and ESM outputs: - ```bash - npm run build - ``` -7. To publish, add `NPM_TOKEN` as a GitHub Actions secret and push to `main`. Semantic release will version, tag, and publish automatically. - -## Features - -- TypeScript 5.x with the strictest practical settings: `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `noImplicitOverride` -- `"type": "module"` in `package.json` with `NodeNext` module resolution for correct ESM/CJS interop -- Dual CJS + ESM output via `tsup` with declaration files (`.d.ts` / `.d.cts`) per format -- Full `exports` map with `import`/`require` conditions and `types` subpaths -- `sideEffects: false` for optimal tree-shaking -- `npm provenance` attestation via `NPM_CONFIG_PROVENANCE: true` in the release workflow -- Semantic release with conventional commits for automatic versioning and changelog generation -- GitHub Actions CI matrix across Node.js 18/20/22 -- Codecov integration for coverage reporting -- `prepublishOnly` guard to prevent publishing a broken build -- `.npmignore` to keep the published tarball lean diff --git a/skills/typescript-coder/assets/typescript-project-template-modern.md b/skills/typescript-coder/assets/typescript-project-template-modern.md deleted file mode 100644 index 26bd46bba..000000000 --- a/skills/typescript-coder/assets/typescript-project-template-modern.md +++ /dev/null @@ -1,597 +0,0 @@ - - -# TypeScript Project Template — Modernized (NivaldoFarias variation) - -> Based on [NivaldoFarias/typescript-project-template](https://github.com/NivaldoFarias/typescript-project-template) -> License: **MIT** -> This is a **modernized variation** applying current best practices: -> TypeScript 5.x, optional Bun runtime, modern ESLint flat config, Prettier 3.x, -> Husky v9 + lint-staged, and Conventional Commits enforcement. - -## What Changed from the Original - -| Area | Original (NivaldoFarias) | This Modernized Variation | -|---|---|---| -| TypeScript | 4.x | **5.x** with all strict flags | -| ESLint config | `.eslintrc.json` / `.eslintrc.js` | **ESLint 9 flat config** (`eslint.config.ts`) | -| Husky | v4/v8 | **Husky v9** (`.husky/` scripts) | -| Runtime option | Node.js only | Node.js 20+ **or Bun 1.x** | -| Commit enforcement | Not present | **commitlint** + Conventional Commits | -| Formatting | Prettier 2.x | **Prettier 3.x** | -| Module system | CommonJS | **ESM** (`"type": "module"`) | - -## Project Structure - -``` -my-project/ -├── src/ -│ ├── index.ts # Application entry point -│ ├── config/ -│ │ └── index.ts # Environment configuration -│ ├── types/ -│ │ └── index.ts # Shared type definitions -│ └── utils/ -│ └── index.ts # Utility functions -├── tests/ -│ ├── unit/ -│ │ └── utils.test.ts -│ └── integration/ -│ └── app.test.ts -├── dist/ # Compiled output (git-ignored) -├── .husky/ -│ ├── pre-commit # Runs lint-staged -│ └── commit-msg # Runs commitlint -├── .gitignore -├── .nvmrc # Node version pin -├── .prettierrc # Prettier configuration -├── .prettierignore -├── commitlint.config.ts -├── eslint.config.ts -├── package.json -├── tsconfig.json -└── README.md -``` - -## `package.json` - -```json -{ - "name": "my-project", - "version": "0.1.0", - "description": "A TypeScript project", - "author": "Your Name ", - "license": "MIT", - "type": "module", - "main": "./dist/index.js", - "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" - } - }, - "engines": { - "node": ">=20.0.0" - }, - "scripts": { - "build": "tsc -p tsconfig.json", - "build:watch": "tsc -p tsconfig.json --watch", - "clean": "rimraf dist coverage", - "prebuild": "npm run clean", - "start": "node dist/index.js", - "dev": "tsx watch src/index.ts", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage", - "lint": "eslint .", - "lint:fix": "eslint . --fix", - "format": "prettier --write .", - "format:check": "prettier --check .", - "type-check": "tsc --noEmit", - "prepare": "husky", - "validate": "npm run type-check && npm run lint && npm run format:check && npm run test" - }, - "devDependencies": { - "@commitlint/cli": "^19.5.0", - "@commitlint/config-conventional": "^19.5.0", - "@eslint/js": "^9.15.0", - "@types/node": "^22.9.0", - "@vitest/coverage-v8": "^2.1.0", - "eslint": "^9.15.0", - "eslint-config-prettier": "^9.1.0", - "globals": "^15.12.0", - "husky": "^9.1.7", - "lint-staged": "^15.2.10", - "prettier": "^3.3.3", - "rimraf": "^6.0.1", - "tsx": "^4.19.2", - "typescript": "^5.7.2", - "typescript-eslint": "^8.15.0", - "vitest": "^2.1.0" - }, - "lint-staged": { - "*.{ts,tsx,mts,cts}": [ - "eslint --fix", - "prettier --write" - ], - "*.{json,md,yaml,yml}": [ - "prettier --write" - ] - } -} -``` - -## `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2023"], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "./dist", - "rootDir": "./src", - - "strict": true, - "exactOptionalPropertyTypes": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noFallthroughCasesInSwitch": true, - - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true, - "skipLibCheck": true, - "resolveJsonModule": true, - - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "incremental": true, - "tsBuildInfoFile": ".tsbuildinfo" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests/**/*"] -} -``` - -## `eslint.config.ts` - -Modern ESLint 9 flat config in TypeScript: - -```typescript -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; -import globals from "globals"; -import eslintConfigPrettier from "eslint-config-prettier"; - -export default tseslint.config( - // Ignore patterns (replaces .eslintignore) - { - ignores: ["dist/**", "coverage/**", "node_modules/**", "*.config.js"], - }, - - // Base JS rules - eslint.configs.recommended, - - // TypeScript rules - ...tseslint.configs.strictTypeChecked, - ...tseslint.configs.stylisticTypeChecked, - - // Type-aware linting requires parserOptions.project - { - languageOptions: { - globals: { - ...globals.node, - ...globals.es2022, - }, - parserOptions: { - project: true, - tsconfigRootDir: import.meta.dirname, - }, - }, - }, - - // Custom rule overrides - { - rules: { - // Allow unused vars prefixed with _ - "@typescript-eslint/no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - // Prefer const assertions over explicit type annotations where possible - "@typescript-eslint/prefer-as-const": "error", - // Require explicit return types on exported functions - "@typescript-eslint/explicit-module-boundary-types": "warn", - // Disallow floating promises - "@typescript-eslint/no-floating-promises": "error", - // Require await in async functions - "@typescript-eslint/require-await": "error", - // No console in production code (warn, not error) - "no-console": ["warn", { allow: ["warn", "error"] }], - }, - }, - - // Disable formatting rules that conflict with Prettier (must be last) - eslintConfigPrettier, -); -``` - -## `.prettierrc` - -```json -{ - "semi": true, - "singleQuote": false, - "quoteProps": "as-needed", - "trailingComma": "all", - "tabWidth": 2, - "useTabs": false, - "printWidth": 100, - "bracketSpacing": true, - "arrowParens": "always", - "endOfLine": "lf", - "overrides": [ - { - "files": "*.md", - "options": { - "printWidth": 80, - "proseWrap": "always" - } - } - ] -} -``` - -## `.prettierignore` - -``` -dist/ -coverage/ -node_modules/ -.tsbuildinfo -*.lock -``` - -## `commitlint.config.ts` - -```typescript -import type { UserConfig } from "@commitlint/types"; - -const config: UserConfig = { - extends: ["@commitlint/config-conventional"], - rules: { - // Type must be one of these - "type-enum": [ - 2, - "always", - [ - "build", // Changes that affect the build system or external dependencies - "chore", // Other changes that don't modify src or test files - "ci", // Changes to CI configuration files and scripts - "docs", // Documentation only changes - "feat", // A new feature - "fix", // A bug fix - "perf", // A code change that improves performance - "refactor",// A code change that neither fixes a bug nor adds a feature - "revert", // Reverts a previous commit - "style", // Changes that don't affect the meaning of the code - "test", // Adding missing tests or correcting existing tests - ], - ], - // Scope is optional but must be lowercase if provided - "scope-case": [2, "always", "lower-case"], - // Subject must not end with a period - "subject-full-stop": [2, "never", "."], - // Subject must start with lowercase - "subject-case": [2, "always", "lower-case"], - // Body must start after a blank line - "body-leading-blank": [2, "always"], - // Footer must start after a blank line - "footer-leading-blank": [2, "always"], - // Max header length - "header-max-length": [2, "always", 100], - }, -}; - -export default config; -``` - -## `.husky/pre-commit` - -```sh -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx lint-staged -``` - -## `.husky/commit-msg` - -```sh -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx --no -- commitlint --edit "$1" -``` - -## `src/config/index.ts` - -```typescript -/** - * Environment configuration — validated at startup. - * All environment variables are accessed through this module, never directly. - */ - -function requireEnv(key: string): string { - const value = process.env[key]; - if (!value) { - throw new Error( - `Missing required environment variable: ${key}. ` + - `Check your .env file or deployment configuration.` - ); - } - return value; -} - -function optionalEnv(key: string, defaultValue: string): string { - return process.env[key] ?? defaultValue; -} - -export const config = { - app: { - name: optionalEnv("APP_NAME", "my-project"), - version: optionalEnv("APP_VERSION", "0.1.0"), - port: Number(optionalEnv("PORT", "3000")), - nodeEnv: optionalEnv("NODE_ENV", "development") as - | "development" - | "production" - | "test", - }, - log: { - level: optionalEnv("LOG_LEVEL", "info") as - | "debug" - | "info" - | "warn" - | "error", - }, -} as const; - -export type AppConfig = typeof config; -``` - -## `src/types/index.ts` - -```typescript -/** - * Shared type definitions. Export all public types from here. - */ - -/** Discriminated union result type for error handling without exceptions. */ -export type Result = - | { readonly ok: true; readonly value: T } - | { readonly ok: false; readonly error: E }; - -export const Result = { - ok(value: T): Result { - return { ok: true, value }; - }, - err(error: E): Result { - return { ok: false, error }; - }, - isOk(result: Result): result is { ok: true; value: T } { - return result.ok; - }, -} as const; - -/** Represents a value that may not yet exist. */ -export type Option = { readonly some: true; readonly value: T } | { readonly some: false }; - -export const Option = { - some(value: T): Option { - return { some: true, value }; - }, - none(): Option { - return { some: false }; - }, - isSome(opt: Option): opt is { some: true; value: T } { - return opt.some; - }, -} as const; - -/** Pagination metadata for list responses. */ -export interface PaginationMeta { - readonly page: number; - readonly pageSize: number; - readonly total: number; - readonly totalPages: number; -} - -/** A paginated response wrapping a list of items. */ -export interface Paginated { - readonly items: readonly T[]; - readonly meta: PaginationMeta; -} -``` - -## `src/utils/index.ts` - -```typescript -/** - * General-purpose utility functions. - */ - -/** - * Sleeps for the given number of milliseconds. - */ -export function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** - * Chunks an array into sub-arrays of the specified size. - */ -export function chunk(array: readonly T[], size: number): T[][] { - if (size <= 0) throw new RangeError("Chunk size must be greater than 0"); - const result: T[][] = []; - for (let i = 0; i < array.length; i += size) { - result.push(array.slice(i, i + size) as T[]); - } - return result; -} - -/** - * Returns a new object with only the specified keys from the source. - */ -export function pick( - obj: T, - keys: readonly K[] -): Pick { - return keys.reduce( - (acc, key) => { - acc[key] = obj[key]; - return acc; - }, - {} as Pick - ); -} - -/** - * Returns a new object without the specified keys. - */ -export function omit( - obj: T, - keys: readonly K[] -): Omit { - const keysSet = new Set(keys); - return Object.fromEntries( - Object.entries(obj).filter(([k]) => !keysSet.has(k)) - ) as Omit; -} -``` - -## `src/index.ts` - -```typescript -/** - * Application entry point. - */ - -import { config } from "./config/index.js"; -import { Result } from "./types/index.js"; - -async function main(): Promise { - const { app, log } = config; - - if (log.level === "debug") { - console.warn( - `[DEBUG] Starting ${app.name} v${app.version} in ${app.nodeEnv} mode` - ); - } - - const result = await runApp(); - - if (!Result.isOk(result)) { - console.error("Fatal error:", result.error.message); - process.exit(1); - } - - console.warn(`${app.name} started successfully on port ${app.port}`); -} - -async function runApp(): Promise> { - try { - // Replace with your application logic - await Promise.resolve(); - return Result.ok(undefined); - } catch (error) { - return Result.err( - error instanceof Error ? error : new Error(String(error)) - ); - } -} - -main().catch((error: unknown) => { - console.error("Unhandled error:", error); - process.exit(1); -}); -``` - -## Bun Runtime Option - -To use **Bun** instead of Node.js: - -1. Install Bun: `curl -fsSL https://bun.sh/install | bash` -2. Replace the `dev` script in `package.json`: - ```json - "dev": "bun --watch src/index.ts" - ``` -3. Replace `tsx` with `bun` in the dev workflow — Bun runs TypeScript natively. -4. For testing, replace Vitest with Bun's built-in test runner: - ```json - "test": "bun test" - ``` - And rename test files to `*.test.ts` (Bun picks them up automatically). -5. The `tsconfig.json` target can be adjusted: `"target": "ESNext"` for Bun. - -> Note: Bun is not 100% compatible with all npm packages. Validate your dependencies before switching. - -## Conventional Commits Reference - -Valid commit message format: - -``` -(): - -[optional body] - -[optional footer(s)] -``` - -Examples: -``` -feat(auth): add JWT refresh token rotation - -fix(api): resolve race condition in user lookup - -chore: upgrade typescript to 5.7.2 - -docs(readme): add bun runtime setup instructions - -refactor(utils): extract pagination helper into separate module - -BREAKING CHANGE: rename Config interface to AppConfig -``` - -## Quick Setup Script - -```bash -# 1. Clone / scaffold -git init my-project && cd my-project - -# 2. Install dependencies -npm install - -# 3. Initialize Husky -npm run prepare - -# 4. Make Husky hooks executable (Linux/macOS) -chmod +x .husky/pre-commit .husky/commit-msg - -# 5. Verify everything works -npm run validate -``` diff --git a/skills/typescript-coder/assets/typescript-react-lib.md b/skills/typescript-coder/assets/typescript-react-lib.md deleted file mode 100644 index 3257418c9..000000000 --- a/skills/typescript-coder/assets/typescript-react-lib.md +++ /dev/null @@ -1,371 +0,0 @@ -# TypeScript React Component Library - -> A TypeScript React component library starter with Rollup bundling (CJS + ESM dual output), Jest and React Testing Library for tests, and proper `exports` map for tree-shaking. Produces a publishable npm package with type declarations. - -## License - -MIT — See [source repository](https://github.com/tanem/generator-typescript-react-lib) for full license text. - -## Source - -- [tanem/generator-typescript-react-lib](https://github.com/tanem/generator-typescript-react-lib) - -## Project Structure - -``` -my-react-lib/ -├── src/ -│ ├── index.ts (public API re-exports) -│ └── components/ -│ ├── Button/ -│ │ ├── Button.tsx -│ │ ├── Button.types.ts -│ │ └── __tests__/ -│ │ └── Button.test.tsx -│ └── index.ts (component barrel) -├── dist/ (generated — do not edit) -│ ├── cjs/ -│ │ ├── index.js -│ │ └── index.d.ts -│ └── esm/ -│ ├── index.js -│ └── index.d.ts -├── package.json -├── tsconfig.json -├── tsconfig.cjs.json -├── tsconfig.esm.json -├── rollup.config.js -├── jest.config.js -├── babel.config.js -├── .gitignore -└── README.md -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-react-lib", - "version": "0.1.0", - "description": "My TypeScript React component library", - "license": "MIT", - "author": "Your Name ", - "sideEffects": false, - "main": "./dist/cjs/index.js", - "module": "./dist/esm/index.js", - "types": "./dist/esm/index.d.ts", - "exports": { - ".": { - "import": { - "types": "./dist/esm/index.d.ts", - "default": "./dist/esm/index.js" - }, - "require": { - "types": "./dist/cjs/index.d.ts", - "default": "./dist/cjs/index.js" - } - } - }, - "files": ["dist"], - "scripts": { - "build": "npm run build:cjs && npm run build:esm", - "build:cjs": "rollup -c --environment BUILD:cjs", - "build:esm": "rollup -c --environment BUILD:esm", - "clean": "rimraf dist", - "lint": "eslint src --ext .ts,.tsx", - "prebuild": "npm run clean", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "prepublishOnly": "npm run build" - }, - "peerDependencies": { - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - }, - "devDependencies": { - "@babel/core": "^7.24.0", - "@babel/preset-env": "^7.24.0", - "@babel/preset-react": "^7.23.0", - "@babel/preset-typescript": "^7.23.0", - "@testing-library/jest-dom": "^6.4.0", - "@testing-library/react": "^15.0.0", - "@testing-library/user-event": "^14.5.0", - "@types/jest": "^29.5.0", - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "rimraf": "^5.0.0", - "rollup": "^4.13.0", - "@rollup/plugin-node-resolve": "^15.2.0", - "@rollup/plugin-commonjs": "^25.0.0", - "rollup-plugin-typescript2": "^0.36.0", - "typescript": "^5.4.0" - }, - "engines": { - "node": ">=18.0.0" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2018", - "module": "ESNext", - "moduleResolution": "Bundler", - "lib": ["ES2018", "DOM"], - "jsx": "react-jsx", - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "skipLibCheck": true - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] -} -``` - -### `tsconfig.cjs.json` - -```json -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "CommonJS", - "outDir": "dist/cjs" - }, - "exclude": ["node_modules", "dist", "**/__tests__/**", "**/*.test.*"] -} -``` - -### `tsconfig.esm.json` - -```json -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "module": "ESNext", - "outDir": "dist/esm" - }, - "exclude": ["node_modules", "dist", "**/__tests__/**", "**/*.test.*"] -} -``` - -### `rollup.config.js` - -```javascript -import commonjs from '@rollup/plugin-commonjs'; -import resolve from '@rollup/plugin-node-resolve'; -import typescript from 'rollup-plugin-typescript2'; - -const isEsm = process.env.BUILD === 'esm'; - -export default { - input: 'src/index.ts', - output: { - dir: isEsm ? 'dist/esm' : 'dist/cjs', - format: isEsm ? 'esm' : 'cjs', - preserveModules: true, - preserveModulesRoot: 'src', - sourcemap: true, - }, - external: ['react', 'react-dom', 'react/jsx-runtime'], - plugins: [ - resolve(), - commonjs(), - typescript({ - tsconfig: isEsm ? './tsconfig.esm.json' : './tsconfig.cjs.json', - useTsconfigDeclarationDir: true, - }), - ], -}; -``` - -### `jest.config.js` - -```javascript -/** @type {import('jest').Config} */ -export default { - testEnvironment: 'jsdom', - transform: { - '^.+\\.(ts|tsx)$': 'babel-jest', - }, - setupFilesAfterFramework: ['@testing-library/jest-dom'], - testMatch: ['**/__tests__/**/*.test.(ts|tsx)'], - collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/__tests__/**'], -}; -``` - -### `babel.config.js` - -```javascript -export default { - presets: [ - ['@babel/preset-env', { targets: { node: 'current' } }], - ['@babel/preset-react', { runtime: 'automatic' }], - '@babel/preset-typescript', - ], -}; -``` - -### `src/components/Button/Button.types.ts` - -```typescript -import type { ButtonHTMLAttributes, ReactNode } from 'react'; - -export type ButtonVariant = 'primary' | 'secondary' | 'ghost'; -export type ButtonSize = 'sm' | 'md' | 'lg'; - -export interface ButtonProps extends ButtonHTMLAttributes { - /** Visual style variant */ - variant?: ButtonVariant; - /** Size preset */ - size?: ButtonSize; - /** Whether the button is in a loading state */ - isLoading?: boolean; - /** Button content */ - children: ReactNode; -} -``` - -### `src/components/Button/Button.tsx` - -```tsx -import React from 'react'; -import type { ButtonProps } from './Button.types.js'; - -const sizeClasses: Record, string> = { - sm: 'btn--sm', - md: 'btn--md', - lg: 'btn--lg', -}; - -const variantClasses: Record, string> = { - primary: 'btn--primary', - secondary: 'btn--secondary', - ghost: 'btn--ghost', -}; - -export function Button({ - variant = 'primary', - size = 'md', - isLoading = false, - disabled, - children, - className = '', - ...rest -}: ButtonProps): JSX.Element { - const classes = [ - 'btn', - variantClasses[variant], - sizeClasses[size], - isLoading ? 'btn--loading' : '', - className, - ] - .filter(Boolean) - .join(' '); - - return ( - - ); -} -``` - -### `src/components/Button/__tests__/Button.test.tsx` - -```tsx -import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; -import { Button } from '../Button.js'; - -describe('Button', () => { - it('renders children', () => { - render(); - expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument(); - }); - - it('applies the primary variant class by default', () => { - render(); - expect(screen.getByRole('button')).toHaveClass('btn--primary'); - }); - - it('is disabled and aria-busy when isLoading is true', () => { - render(); - const btn = screen.getByRole('button'); - expect(btn).toBeDisabled(); - expect(btn).toHaveAttribute('aria-busy', 'true'); - }); - - it('calls onClick when clicked', async () => { - const handleClick = jest.fn(); - render(); - await userEvent.click(screen.getByRole('button')); - expect(handleClick).toHaveBeenCalledTimes(1); - }); -}); -``` - -### `src/components/index.ts` - -```typescript -export { Button } from './Button/Button.js'; -export type { ButtonProps, ButtonVariant, ButtonSize } from './Button/Button.types.js'; -``` - -### `src/index.ts` - -```typescript -export * from './components/index.js'; -``` - -## Getting Started - -```bash -# 1. Create project directory -mkdir my-react-lib && cd my-react-lib - -# 2. Initialise and copy project files (see structure above) -npm init -y - -# 3. Install dependencies -npm install - -# 4. Run tests -npm test - -# 5. Build library (produces dist/cjs and dist/esm) -npm run build - -# 6. Publish (runs build automatically via prepublishOnly) -npm publish -``` - -## Features - -- Dual CJS and ESM build output via Rollup -- TypeScript strict mode with full declaration file emission -- `exports` map for modern Node.js and bundler resolution -- `sideEffects: false` for optimal tree-shaking -- Jest + React Testing Library + jest-dom for component testing -- Babel transform for Jest (separate from the Rollup build pipeline) -- React listed as a peer dependency — consumers supply their own React -- `preserveModules` keeps one output file per source file for fine-grained tree-shaking diff --git a/skills/typescript-coder/assets/typescript-rznode.md b/skills/typescript-coder/assets/typescript-rznode.md deleted file mode 100644 index def63a87c..000000000 --- a/skills/typescript-coder/assets/typescript-rznode.md +++ /dev/null @@ -1,604 +0,0 @@ -# TypeScript Node.js REST API (rznode) - -> A TypeScript Node.js REST API starter built on Express.js with a clean route/controller separation, typed middleware patterns, environment-based configuration, and Jest for integration testing. Ready to extend with a database layer or additional resource routes. - -## License - -MIT — See [source repository](https://github.com/odedlevy02/rznode) for full license text. - -## Source - -- [odedlevy02/rznode](https://github.com/odedlevy02/rznode) - -## Project Structure - -``` -my-node-api/ -├── src/ -│ ├── controllers/ -│ │ └── items.controller.ts -│ ├── routes/ -│ │ ├── items.routes.ts -│ │ └── index.ts -│ ├── middleware/ -│ │ ├── auth.middleware.ts -│ │ ├── error.middleware.ts -│ │ ├── logger.middleware.ts -│ │ └── validate.middleware.ts -│ ├── models/ -│ │ └── item.model.ts -│ ├── services/ -│ │ └── items.service.ts -│ ├── config/ -│ │ └── env.ts -│ └── app.ts -├── tests/ -│ └── items.test.ts -├── package.json -├── tsconfig.json -├── nodemon.json -├── jest.config.ts -├── .env -├── .env.example -├── .gitignore -└── README.md -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-node-api", - "version": "1.0.0", - "description": "TypeScript Node.js REST API", - "license": "MIT", - "private": true, - "main": "dist/app.js", - "scripts": { - "build": "tsc", - "build:watch": "tsc --watch", - "clean": "rimraf dist", - "dev": "nodemon", - "start": "node dist/app.js", - "lint": "eslint src tests --ext .ts", - "test": "jest --forceExit", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage --forceExit" - }, - "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.4.0", - "express": "^4.19.0", - "helmet": "^7.1.0", - "morgan": "^1.10.0" - }, - "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/jest": "^29.5.0", - "@types/morgan": "^1.9.9", - "@types/node": "^20.12.0", - "@types/supertest": "^6.0.2", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.57.0", - "jest": "^29.7.0", - "nodemon": "^3.1.0", - "rimraf": "^5.0.0", - "supertest": "^6.3.0", - "ts-jest": "^29.1.0", - "ts-node": "^10.9.0", - "typescript": "^5.4.0" - }, - "engines": { - "node": ">=20.0.0" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "Node", - "lib": ["ES2022"], - "strict": true, - "noUncheckedIndexedAccess": true, - "noImplicitReturns": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "dist", - "rootDir": ".", - "skipLibCheck": true - }, - "include": ["src"], - "exclude": ["node_modules", "dist", "tests"] -} -``` - -### `nodemon.json` - -```json -{ - "watch": ["src"], - "ext": "ts,json", - "exec": "ts-node -r dotenv/config src/app.ts", - "env": { - "NODE_ENV": "development" - } -} -``` - -### `.env.example` - -``` -NODE_ENV=development -PORT=3000 -HOST=0.0.0.0 -LOG_LEVEL=dev -CORS_ORIGINS=http://localhost:3000,http://localhost:5173 -``` - -### `jest.config.ts` - -```typescript -import type { Config } from 'jest'; - -const config: Config = { - preset: 'ts-jest', - testEnvironment: 'node', - rootDir: '.', - testMatch: ['/tests/**/*.test.ts'], - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - }, - collectCoverageFrom: ['src/**/*.ts', '!src/app.ts'], - coverageDirectory: 'coverage', -}; - -export default config; -``` - -### `src/config/env.ts` - -```typescript -import * as dotenv from 'dotenv'; - -dotenv.config(); - -function requireEnv(key: string): string { - const val = process.env[key]; - if (!val) throw new Error(`Missing required environment variable: ${key}`); - return val; -} - -export const config = { - nodeEnv: (process.env['NODE_ENV'] ?? 'development') as 'development' | 'test' | 'production', - port: parseInt(process.env['PORT'] ?? '3000', 10), - host: process.env['HOST'] ?? '0.0.0.0', - corsOrigins: (process.env['CORS_ORIGINS'] ?? '').split(',').filter(Boolean), -} as const; -``` - -### `src/models/item.model.ts` - -```typescript -export interface Item { - id: string; - name: string; - description?: string; - tags: string[]; - createdAt: Date; - updatedAt: Date; -} - -export interface CreateItemDto { - name: string; - description?: string; - tags?: string[]; -} - -export interface UpdateItemDto { - name?: string; - description?: string; - tags?: string[]; -} - -export interface PaginatedResult { - data: T[]; - total: number; - page: number; - pageSize: number; -} -``` - -### `src/services/items.service.ts` - -```typescript -import { randomUUID } from 'crypto'; -import type { - CreateItemDto, - Item, - PaginatedResult, - UpdateItemDto, -} from '../models/item.model'; - -// In-memory store — swap for a real DB client in production -const items = new Map(); - -export const ItemsService = { - findAll(page = 1, pageSize = 10): PaginatedResult { - const all = Array.from(items.values()); - const start = (page - 1) * pageSize; - return { - data: all.slice(start, start + pageSize), - total: all.length, - page, - pageSize, - }; - }, - - findById(id: string): Item | undefined { - return items.get(id); - }, - - create(dto: CreateItemDto): Item { - const now = new Date(); - const item: Item = { - id: randomUUID(), - name: dto.name, - description: dto.description, - tags: dto.tags ?? [], - createdAt: now, - updatedAt: now, - }; - items.set(item.id, item); - return item; - }, - - update(id: string, dto: UpdateItemDto): Item | undefined { - const existing = items.get(id); - if (!existing) return undefined; - const updated: Item = { - ...existing, - ...dto, - updatedAt: new Date(), - }; - items.set(id, updated); - return updated; - }, - - delete(id: string): boolean { - return items.delete(id); - }, -}; -``` - -### `src/controllers/items.controller.ts` - -```typescript -import { NextFunction, Request, Response } from 'express'; -import type { CreateItemDto, UpdateItemDto } from '../models/item.model'; -import { ItemsService } from '../services/items.service'; - -export const ItemsController = { - getAll(req: Request, res: Response, next: NextFunction): void { - try { - const page = parseInt(String(req.query['page'] ?? '1'), 10); - const pageSize = parseInt(String(req.query['pageSize'] ?? '10'), 10); - res.json(ItemsService.findAll(page, pageSize)); - } catch (err) { - next(err); - } - }, - - getById(req: Request, res: Response, next: NextFunction): void { - try { - const item = ItemsService.findById(req.params['id']!); - if (!item) { - res.status(404).json({ message: `Item '${req.params['id']}' not found` }); - return; - } - res.json(item); - } catch (err) { - next(err); - } - }, - - create(req: Request, res: Response, next: NextFunction): void { - try { - const dto = req.body as CreateItemDto; - if (!dto.name?.trim()) { - res.status(400).json({ message: 'name is required' }); - return; - } - res.status(201).json(ItemsService.create(dto)); - } catch (err) { - next(err); - } - }, - - update(req: Request, res: Response, next: NextFunction): void { - try { - const item = ItemsService.update(req.params['id']!, req.body as UpdateItemDto); - if (!item) { - res.status(404).json({ message: `Item '${req.params['id']}' not found` }); - return; - } - res.json(item); - } catch (err) { - next(err); - } - }, - - delete(req: Request, res: Response, next: NextFunction): void { - try { - const deleted = ItemsService.delete(req.params['id']!); - if (!deleted) { - res.status(404).json({ message: `Item '${req.params['id']}' not found` }); - return; - } - res.status(204).send(); - } catch (err) { - next(err); - } - }, -}; -``` - -### `src/routes/items.routes.ts` - -```typescript -import { Router } from 'express'; -import { ItemsController } from '../controllers/items.controller'; - -const router = Router(); - -router.get('/', ItemsController.getAll); -router.post('/', ItemsController.create); -router.get('/:id', ItemsController.getById); -router.patch('/:id', ItemsController.update); -router.delete('/:id', ItemsController.delete); - -export default router; -``` - -### `src/routes/index.ts` - -```typescript -import { Router } from 'express'; -import itemsRouter from './items.routes'; - -const router = Router(); - -router.use('/items', itemsRouter); - -// Health-check -router.get('/health', (_req, res) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }); -}); - -export default router; -``` - -### `src/middleware/error.middleware.ts` - -```typescript -import { ErrorRequestHandler } from 'express'; - -export interface AppError extends Error { - statusCode?: number; - errors?: unknown[]; -} - -export const errorMiddleware: ErrorRequestHandler = ( - err: AppError, - _req, - res, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _next -) => { - const statusCode = err.statusCode ?? 500; - const message = err.message || 'Internal Server Error'; - - if (statusCode >= 500) { - console.error('[ERROR]', err); - } - - res.status(statusCode).json({ - message, - ...(err.errors ? { errors: err.errors } : {}), - ...(process.env['NODE_ENV'] !== 'production' ? { stack: err.stack } : {}), - }); -}; -``` - -### `src/middleware/logger.middleware.ts` - -```typescript -import morgan, { StreamOptions } from 'morgan'; - -const stream: StreamOptions = { - write: (message: string) => console.info(message.trim()), -}; - -export const loggerMiddleware = morgan( - process.env['NODE_ENV'] === 'production' ? 'combined' : 'dev', - { stream } -); -``` - -### `src/middleware/auth.middleware.ts` - -```typescript -import { NextFunction, Request, Response } from 'express'; - -// Extend the Express Request type with an authenticated user field -declare global { - namespace Express { - interface Request { - user?: { id: string; roles: string[] }; - } - } -} - -export function requireAuth(req: Request, res: Response, next: NextFunction): void { - const authHeader = req.headers['authorization']; - if (!authHeader?.startsWith('Bearer ')) { - res.status(401).json({ message: 'Missing or invalid Authorization header' }); - return; - } - - // Decode/verify your JWT here (e.g. using jsonwebtoken) - const token = authHeader.slice(7); - if (!token) { - res.status(401).json({ message: 'Empty token' }); - return; - } - - // Attach the decoded user to the request - req.user = { id: 'placeholder-id', roles: ['user'] }; - next(); -} -``` - -### `src/app.ts` - -```typescript -import cors from 'cors'; -import * as dotenv from 'dotenv'; -import express from 'express'; -import helmet from 'helmet'; -import { config } from './config/env'; -import { errorMiddleware } from './middleware/error.middleware'; -import { loggerMiddleware } from './middleware/logger.middleware'; -import routes from './routes'; - -dotenv.config(); - -const app = express(); - -// Security & parsing -app.use(helmet()); -app.use(cors({ origin: config.corsOrigins.length ? config.corsOrigins : '*' })); -app.use(express.json()); -app.use(express.urlencoded({ extended: false })); - -// Logging -app.use(loggerMiddleware); - -// Routes -app.use('/api/v1', routes); - -// Error handling (must be last) -app.use(errorMiddleware); - -if (require.main === module) { - app.listen(config.port, config.host, () => { - console.log(`API running at http://${config.host}:${config.port}/api/v1`); - console.log(`Health check: http://${config.host}:${config.port}/api/v1/health`); - }); -} - -export default app; -``` - -### `tests/items.test.ts` - -```typescript -import request from 'supertest'; -import app from '../src/app'; - -describe('Items API', () => { - describe('GET /api/v1/items', () => { - it('returns 200 with a paginated result', async () => { - const res = await request(app).get('/api/v1/items'); - expect(res.status).toBe(200); - expect(res.body).toMatchObject({ data: expect.any(Array), total: expect.any(Number) }); - }); - }); - - describe('POST /api/v1/items', () => { - it('creates an item and returns 201', async () => { - const res = await request(app) - .post('/api/v1/items') - .send({ name: 'Widget', description: 'A test widget', tags: ['test'] }); - expect(res.status).toBe(201); - expect(res.body).toMatchObject({ name: 'Widget', tags: ['test'] }); - expect(res.body.id).toBeTruthy(); - }); - - it('returns 400 when name is missing', async () => { - const res = await request(app).post('/api/v1/items').send({ description: 'no name' }); - expect(res.status).toBe(400); - }); - }); - - describe('GET /api/v1/items/:id', () => { - it('returns 404 for unknown id', async () => { - const res = await request(app).get('/api/v1/items/does-not-exist'); - expect(res.status).toBe(404); - }); - }); - - describe('GET /api/v1/health', () => { - it('returns status ok', async () => { - const res = await request(app).get('/api/v1/health'); - expect(res.status).toBe(200); - expect(res.body.status).toBe('ok'); - }); - }); -}); -``` - -## Getting Started - -```bash -# 1. Create project directory -mkdir my-node-api && cd my-node-api - -# 2. Copy project files (see structure above) - -# 3. Install dependencies -npm install - -# 4. Configure environment -cp .env.example .env - -# 5. Start in development mode (hot-reload) -npm run dev - -# 6. Verify the health endpoint -curl http://localhost:3000/api/v1/health - -# 7. Run integration tests -npm test - -# 8. Build for production -npm run build - -# 9. Start production server -npm start -``` - -## Features - -- Express 4 with full TypeScript types via `@types/express` -- Clean Controller/Service/Route separation — controllers handle HTTP, services handle logic -- Typed middleware: error handler, Morgan logger, CORS, Helmet, and a bearer-token auth stub -- `declare global` augmentation of `Express.Request` for attaching an authenticated user -- `ts-node` + Nodemon for zero-build development hot-reload -- Supertest integration tests running directly against the Express app instance -- `ts-jest` preset for native TypeScript Jest execution without a separate build step -- Environment config loaded via `dotenv` with a helper that throws on missing required variables -- `app.ts` is importable for testing AND runnable as the server entry point via `require.main === module` -- Health-check endpoint at `/api/v1/health` for container liveness probes diff --git a/skills/typescript-coder/assets/typescript-tsx-adobe.md b/skills/typescript-coder/assets/typescript-tsx-adobe.md deleted file mode 100644 index 5717aa878..000000000 --- a/skills/typescript-coder/assets/typescript-tsx-adobe.md +++ /dev/null @@ -1,454 +0,0 @@ -# TypeScript React/TSX Project Template (Adobe Style) - -> A TypeScript + React project starter based on patterns from Adobe's `generator-tsx`. Produces a component-library-ready React application using TSX, with Webpack bundling, Jest testing, and Storybook documentation support. - -## License - -Apache License 2.0 — See source repository for full license terms. - -## Source - -- [adobe/generator-tsx](https://github.com/adobe/generator-tsx) - -## Project Structure - -``` -my-tsx-app/ -├── .storybook/ -│ ├── main.ts -│ └── preview.ts -├── src/ -│ ├── components/ -│ │ ├── Button/ -│ │ │ ├── Button.tsx -│ │ │ ├── Button.test.tsx -│ │ │ ├── Button.stories.tsx -│ │ │ └── index.ts -│ │ └── index.ts -│ ├── hooks/ -│ │ └── useTheme.ts -│ ├── types/ -│ │ └── index.ts -│ ├── index.ts -│ └── setupTests.ts -├── public/ -│ └── index.html -├── .eslintrc.json -├── .prettierrc -├── babel.config.js -├── jest.config.ts -├── package.json -├── tsconfig.json -├── tsconfig.build.json -└── webpack.config.ts -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-tsx-app", - "version": "1.0.0", - "description": "TypeScript React component library", - "main": "dist/cjs/index.js", - "module": "dist/esm/index.js", - "types": "dist/types/index.d.ts", - "files": [ - "dist" - ], - "scripts": { - "build": "webpack --config webpack.config.ts", - "build:types": "tsc --project tsconfig.build.json --emitDeclarationOnly", - "start": "webpack serve --config webpack.config.ts --mode development", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "lint": "eslint src --ext .ts,.tsx", - "lint:fix": "eslint src --ext .ts,.tsx --fix", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - }, - "devDependencies": { - "@babel/core": "^7.23.0", - "@babel/preset-env": "^7.23.0", - "@babel/preset-react": "^7.22.15", - "@babel/preset-typescript": "^7.23.0", - "@storybook/addon-essentials": "^7.5.0", - "@storybook/react": "^7.5.0", - "@storybook/react-webpack5": "^7.5.0", - "@testing-library/jest-dom": "^6.1.4", - "@testing-library/react": "^14.1.0", - "@testing-library/user-event": "^14.5.1", - "@types/jest": "^29.5.7", - "@types/react": "^18.2.33", - "@types/react-dom": "^18.2.14", - "@typescript-eslint/eslint-plugin": "^6.9.1", - "@typescript-eslint/parser": "^6.9.1", - "babel-loader": "^9.1.3", - "css-loader": "^6.8.1", - "eslint": "^8.52.0", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "prettier": "^3.0.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "style-loader": "^3.3.3", - "ts-node": "^10.9.1", - "typescript": "^5.2.2", - "webpack": "^5.89.0", - "webpack-cli": "^5.1.4", - "webpack-dev-server": "^4.15.1" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2020", - "lib": ["DOM", "DOM.Iterable", "ES2020"], - "module": "ESNext", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "strict": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - } - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] -} -``` - -### `tsconfig.build.json` - -```json -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "declaration": true, - "declarationDir": "dist/types", - "outDir": "dist/esm", - "rootDir": "src" - }, - "exclude": ["node_modules", "dist", "**/*.test.tsx", "**/*.stories.tsx"] -} -``` - -### `webpack.config.ts` - -```typescript -import path from "path"; -import { Configuration } from "webpack"; -import "webpack-dev-server"; - -const config: Configuration = { - entry: "./src/index.ts", - output: { - path: path.resolve(__dirname, "dist"), - filename: "bundle.js", - library: { - name: "MyTsxApp", - type: "umd", - }, - globalObject: "this", - clean: true, - }, - resolve: { - extensions: [".tsx", ".ts", ".js"], - alias: { - "@": path.resolve(__dirname, "src"), - }, - }, - module: { - rules: [ - { - test: /\.(ts|tsx)$/, - use: "babel-loader", - exclude: /node_modules/, - }, - { - test: /\.css$/, - use: ["style-loader", "css-loader"], - }, - ], - }, - devServer: { - static: "./public", - port: 3000, - hot: true, - }, -}; - -export default config; -``` - -### `babel.config.js` - -```javascript -module.exports = { - presets: [ - ["@babel/preset-env", { targets: { node: "current" } }], - ["@babel/preset-react", { runtime: "automatic" }], - "@babel/preset-typescript", - ], -}; -``` - -### `jest.config.ts` - -```typescript -import type { Config } from "jest"; - -const config: Config = { - preset: "ts-jest", - testEnvironment: "jsdom", - setupFilesAfterFramework: ["/src/setupTests.ts"], - moduleNameMapper: { - "^@/(.*)$": "/src/$1", - "\\.(css|less|scss)$": "identity-obj-proxy", - }, - transform: { - "^.+\\.(ts|tsx)$": "babel-jest", - }, - collectCoverageFrom: [ - "src/**/*.{ts,tsx}", - "!src/**/*.stories.{ts,tsx}", - "!src/index.ts", - ], -}; - -export default config; -``` - -### `src/components/Button/Button.tsx` - -```tsx -import React, { ButtonHTMLAttributes, forwardRef } from "react"; - -export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger"; -export type ButtonSize = "sm" | "md" | "lg"; - -export interface ButtonProps extends ButtonHTMLAttributes { - variant?: ButtonVariant; - size?: ButtonSize; - isLoading?: boolean; - leftIcon?: React.ReactNode; - rightIcon?: React.ReactNode; -} - -export const Button = forwardRef( - ( - { - variant = "primary", - size = "md", - isLoading = false, - leftIcon, - rightIcon, - children, - disabled, - className = "", - ...rest - }, - ref - ) => { - const baseClasses = "btn"; - const variantClass = `btn--${variant}`; - const sizeClass = `btn--${size}`; - const loadingClass = isLoading ? "btn--loading" : ""; - - return ( - - ); - } -); - -Button.displayName = "Button"; - -export default Button; -``` - -### `src/components/Button/Button.test.tsx` - -```tsx -import React from "react"; -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { Button } from "./Button"; - -describe("Button", () => { - it("renders with default props", () => { - render(); - expect(screen.getByRole("button", { name: /click me/i })).toBeInTheDocument(); - }); - - it("calls onClick when clicked", async () => { - const handleClick = jest.fn(); - render(); - await userEvent.click(screen.getByRole("button")); - expect(handleClick).toHaveBeenCalledTimes(1); - }); - - it("is disabled when isLoading is true", () => { - render(); - expect(screen.getByRole("button")).toBeDisabled(); - expect(screen.getByRole("button")).toHaveAttribute("aria-busy", "true"); - }); - - it("applies variant class", () => { - render(); - expect(screen.getByRole("button")).toHaveClass("btn--secondary"); - }); -}); -``` - -### `src/components/Button/Button.stories.tsx` - -```tsx -import type { Meta, StoryObj } from "@storybook/react"; -import { Button } from "./Button"; - -const meta: Meta = { - title: "Components/Button", - component: Button, - tags: ["autodocs"], - argTypes: { - variant: { - control: { type: "select" }, - options: ["primary", "secondary", "ghost", "danger"], - }, - size: { - control: { type: "select" }, - options: ["sm", "md", "lg"], - }, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Primary: Story = { - args: { - variant: "primary", - children: "Button", - }, -}; - -export const Secondary: Story = { - args: { - variant: "secondary", - children: "Button", - }, -}; - -export const Loading: Story = { - args: { - isLoading: true, - children: "Loading...", - }, -}; -``` - -### `src/setupTests.ts` - -```typescript -import "@testing-library/jest-dom"; -``` - -### `.storybook/main.ts` - -```typescript -import type { StorybookConfig } from "@storybook/react-webpack5"; - -const config: StorybookConfig = { - stories: ["../src/**/*.stories.@(ts|tsx|mdx)"], - addons: [ - "@storybook/addon-essentials", - "@storybook/addon-interactions", - ], - framework: { - name: "@storybook/react-webpack5", - options: {}, - }, - docs: { - autodocs: "tag", - }, - typescript: { - check: true, - }, -}; - -export default config; -``` - -## Getting Started - -1. Copy this template or clone from the source generator. -2. Install dependencies: - ```bash - npm install - ``` -3. Start the development server: - ```bash - npm start - ``` -4. Run tests: - ```bash - npm test - ``` -5. Start Storybook for component documentation: - ```bash - npm run storybook - ``` -6. Build the library for distribution: - ```bash - npm run build && npm run build:types - ``` - -## Features - -- TypeScript 5.x with strict mode enabled -- React 18 with JSX transform (`react-jsx`) — no need to import React in every file -- Webpack 5 bundling with hot module replacement in development -- Babel transpilation supporting modern JS/TS/TSX -- Jest + React Testing Library for unit and component testing -- Storybook 7 for interactive component documentation -- ESLint + Prettier for code quality and formatting -- `forwardRef` pattern on components for ref forwarding -- Path alias `@/` mapped to `src/` for clean imports -- UMD output for broad compatibility as a distributable library -- CSS Modules support via css-loader diff --git a/skills/typescript-coder/assets/typescript-tsx-docgen.md b/skills/typescript-coder/assets/typescript-tsx-docgen.md deleted file mode 100644 index 917590ee7..000000000 --- a/skills/typescript-coder/assets/typescript-tsx-docgen.md +++ /dev/null @@ -1,502 +0,0 @@ -# TypeScript TSX Component Library with Documentation (tsx-docgen) - -> A TypeScript React component library template based on `generator-tsx-docgen`. Produces a -> publishable NPM component library with Rollup bundling, TypeDoc API documentation -> generation, Storybook interactive component explorer, Jest + React Testing Library tests, -> and strict TypeScript throughout. - -## License - -See the [generator-tsx-docgen npm package](https://www.npmjs.com/package/generator-tsx-docgen) -for license terms. - -## Source - -- [generator-tsx-docgen on npm](https://www.npmjs.com/package/generator-tsx-docgen) - -## Project Structure - -``` -my-component-lib/ -├── src/ -│ ├── components/ -│ │ ├── Button/ -│ │ │ ├── Button.tsx -│ │ │ ├── Button.types.ts -│ │ │ ├── Button.module.css -│ │ │ ├── Button.stories.tsx -│ │ │ ├── Button.test.tsx -│ │ │ └── index.ts -│ │ ├── Input/ -│ │ │ ├── Input.tsx -│ │ │ ├── Input.types.ts -│ │ │ ├── Input.test.tsx -│ │ │ └── index.ts -│ │ └── index.ts # Barrel — re-exports all components -│ ├── hooks/ -│ │ └── useDebounce.ts -│ ├── types/ -│ │ └── common.types.ts -│ └── index.ts # Library entry point -├── .storybook/ -│ ├── main.ts -│ └── preview.ts -├── docs/ # Generated TypeDoc output (gitignored) -├── dist/ # Rollup build output (gitignored) -├── jest.config.ts -├── package.json -├── rollup.config.mjs -├── tsconfig.json -├── tsconfig.build.json -└── typedoc.json -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-component-lib", - "version": "1.0.0", - "description": "TypeScript React component library with documentation generation", - "main": "dist/cjs/index.js", - "module": "dist/esm/index.js", - "types": "dist/types/index.d.ts", - "exports": { - ".": { - "import": "./dist/esm/index.js", - "require": "./dist/cjs/index.js", - "types": "./dist/types/index.d.ts" - } - }, - "files": ["dist"], - "sideEffects": false, - "scripts": { - "build": "rollup -c", - "build:types": "tsc --project tsconfig.build.json --emitDeclarationOnly", - "dev": "rollup -c --watch", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build", - "docs": "typedoc", - "lint": "eslint 'src/**/*.{ts,tsx}'", - "lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix", - "format": "prettier --write 'src/**/*.{ts,tsx,css}'" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - }, - "devDependencies": { - "@storybook/addon-actions": "^7.6.7", - "@storybook/addon-docs": "^7.6.7", - "@storybook/addon-essentials": "^7.6.7", - "@storybook/react": "^7.6.7", - "@storybook/react-vite": "^7.6.7", - "@testing-library/jest-dom": "^6.2.0", - "@testing-library/react": "^14.1.2", - "@testing-library/user-event": "^14.5.2", - "@types/jest": "^29.5.11", - "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "prettier": "^3.2.4", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "rollup": "^4.9.5", - "rollup-plugin-dts": "^6.1.0", - "rollup-plugin-peer-deps-external": "^2.2.4", - "rollup-plugin-postcss": "^4.0.2", - "storybook": "^7.6.7", - "ts-jest": "^29.1.4", - "typedoc": "^0.25.7", - "typescript": "^5.3.3", - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.6" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "jsx": "react-jsx", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "bundler", - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "outDir": "./dist" - }, - "include": ["src"], - "exclude": ["node_modules", "dist", "**/*.stories.tsx", "**/*.test.tsx"] -} -``` - -### `tsconfig.build.json` - -```json -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./dist/types", - "declaration": true, - "emitDeclarationOnly": true - }, - "exclude": ["node_modules", "dist", "**/*.stories.tsx", "**/*.test.tsx", "**/*.spec.tsx"] -} -``` - -### `rollup.config.mjs` - -```js -import resolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import typescript from '@rollup/plugin-typescript'; -import postcss from 'rollup-plugin-postcss'; -import peerDepsExternal from 'rollup-plugin-peer-deps-external'; -import dts from 'rollup-plugin-dts'; -import { readFileSync } from 'fs'; - -const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')); - -export default [ - // Main bundle: ESM + CJS - { - input: 'src/index.ts', - output: [ - { - file: pkg.module, - format: 'esm', - sourcemap: true, - exports: 'named', - }, - { - file: pkg.main, - format: 'cjs', - sourcemap: true, - exports: 'named', - }, - ], - plugins: [ - peerDepsExternal(), - resolve(), - commonjs(), - typescript({ tsconfig: './tsconfig.json', exclude: ['**/*.test.*', '**/*.stories.*'] }), - postcss({ modules: true, extract: false }), - ], - external: ['react', 'react-dom'], - }, - // Type declarations - { - input: 'dist/types/index.d.ts', - output: [{ file: 'dist/types/index.d.ts', format: 'esm' }], - plugins: [dts()], - external: [/\.css$/], - }, -]; -``` - -### `typedoc.json` - -```json -{ - "entryPoints": ["./src/index.ts"], - "out": "./docs", - "tsconfig": "./tsconfig.json", - "name": "My Component Library", - "readme": "./README.md", - "plugin": [], - "excludePrivate": true, - "excludeProtected": false, - "excludeExternals": true, - "categorizeByGroup": true, - "categoryOrder": ["Components", "Hooks", "Types", "*"], - "sort": ["alphabetical"] -} -``` - -### `src/components/Button/Button.types.ts` - -```typescript -import { ButtonHTMLAttributes, ReactNode } from 'react'; - -export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost'; -export type ButtonSize = 'sm' | 'md' | 'lg'; - -/** - * Props for the Button component. - * @category Components - */ -export interface ButtonProps extends ButtonHTMLAttributes { - /** Visual style variant */ - variant?: ButtonVariant; - /** Size preset */ - size?: ButtonSize; - /** Show a loading spinner and disable interaction */ - isLoading?: boolean; - /** Render button as full width */ - fullWidth?: boolean; - /** Left-aligned icon node */ - leftIcon?: ReactNode; - /** Right-aligned icon node */ - rightIcon?: ReactNode; - /** Button label */ - children: ReactNode; -} -``` - -### `src/components/Button/Button.tsx` - -```typescript -import React, { forwardRef } from 'react'; -import styles from './Button.module.css'; -import { ButtonProps } from './Button.types'; - -/** - * Primary UI element for triggering actions. - * - * @example - * ```tsx - * - * ``` - * @category Components - */ -export const Button = forwardRef( - ( - { - variant = 'primary', - size = 'md', - isLoading = false, - fullWidth = false, - leftIcon, - rightIcon, - children, - disabled, - className, - ...rest - }, - ref, - ) => { - const classes = [ - styles.button, - styles[variant], - styles[size], - fullWidth ? styles.fullWidth : '', - isLoading ? styles.loading : '', - className ?? '', - ] - .filter(Boolean) - .join(' '); - - return ( - - ); - }, -); - -Button.displayName = 'Button'; -``` - -### `src/components/Button/Button.test.tsx` - -```typescript -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import '@testing-library/jest-dom'; -import { Button } from './Button'; - -describe('Button', () => { - it('renders children', () => { - render(); - expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument(); - }); - - it('is disabled when isLoading is true', () => { - render(); - expect(screen.getByRole('button')).toBeDisabled(); - expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true'); - }); - - it('calls onClick when clicked', async () => { - const handleClick = jest.fn(); - render(); - await userEvent.click(screen.getByRole('button')); - expect(handleClick).toHaveBeenCalledTimes(1); - }); - - it('does not call onClick when disabled', async () => { - const handleClick = jest.fn(); - render(); - await userEvent.click(screen.getByRole('button')); - expect(handleClick).not.toHaveBeenCalled(); - }); -}); -``` - -### `src/components/Button/Button.stories.tsx` - -```typescript -import React from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; -import { Button } from './Button'; - -const meta: Meta = { - title: 'Components/Button', - component: Button, - parameters: { - layout: 'centered', - }, - tags: ['autodocs'], - argTypes: { - variant: { - control: 'select', - options: ['primary', 'secondary', 'danger', 'ghost'], - }, - size: { - control: 'select', - options: ['sm', 'md', 'lg'], - }, - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Primary: Story = { - args: { variant: 'primary', children: 'Button' }, -}; - -export const Secondary: Story = { - args: { variant: 'secondary', children: 'Button' }, -}; - -export const Loading: Story = { - args: { isLoading: true, children: 'Loading…' }, -}; - -export const FullWidth: Story = { - args: { fullWidth: true, children: 'Full Width Button' }, -}; -``` - -### `src/index.ts` - -```typescript -// Components -export { Button } from './components/Button'; -export type { ButtonProps, ButtonVariant, ButtonSize } from './components/Button/Button.types'; - -export { Input } from './components/Input'; -export type { InputProps } from './components/Input/Input.types'; - -// Hooks -export { useDebounce } from './hooks/useDebounce'; - -// Types -export type { Size, Variant } from './types/common.types'; -``` - -### `jest.config.ts` - -```typescript -import type { Config } from 'jest'; - -const config: Config = { - testEnvironment: 'jsdom', - setupFilesAfterFramework: ['/jest.setup.ts'], - transform: { - '^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: 'tsconfig.json' }], - }, - moduleNameMapper: { - '\\.module\\.css$': 'identity-obj-proxy', - '\\.css$': '/__mocks__/styleMock.js', - }, - collectCoverageFrom: [ - 'src/**/*.{ts,tsx}', - '!src/**/*.stories.tsx', - '!src/**/index.ts', - '!src/types/**', - ], -}; - -export default config; -``` - -## Getting Started - -```bash -# 1. Install dependencies -npm install - -# 2. Start Storybook for component development -npm run storybook -# Open http://localhost:6006 - -# 3. Run tests -npm test - -# 4. Generate TypeDoc API documentation -npm run docs -# Open docs/index.html - -# 5. Build the library for distribution -npm run build - -# 6. Build static Storybook site -npm run build-storybook -``` - -## Features - -- Rollup bundling producing both ESM and CJS outputs with source maps -- TypeDoc API documentation generated directly from TSDoc comments and TypeScript types -- Storybook 7 with `autodocs` tag for zero-config component documentation pages -- CSS Modules for scoped component styles, processed by `rollup-plugin-postcss` -- `forwardRef` pattern for all components to support ref forwarding -- Jest + React Testing Library + `@testing-library/user-event` for accessible test queries -- Strict TypeScript with `noUnusedLocals` and `noUnusedParameters` -- Barrel exports from `src/index.ts` for a clean public API surface -- Peer dependency configuration so consumers supply their own React version -- Separate `tsconfig.build.json` for declaration-only emission diff --git a/skills/typescript-coder/assets/typescript-xes-bdf.md b/skills/typescript-coder/assets/typescript-xes-bdf.md deleted file mode 100644 index 68f4f2547..000000000 --- a/skills/typescript-coder/assets/typescript-xes-bdf.md +++ /dev/null @@ -1,455 +0,0 @@ -# TypeScript XES BDF Project Template - -> A TypeScript project template based on the `generator-xes-bdf` Yeoman generator. Produces -> a structured TypeScript application scaffold with Express-based REST API, dependency -> injection, service/controller layering, and a testing setup. The generator targets teams -> seeking an opinionated, batteries-included TypeScript backend starter. - -## License - -See the [generator-xes-bdf npm package](https://www.npmjs.com/package/generator-xes-bdf) and -its linked repository for license terms. - -## Source - -- [generator-xes-bdf on npm](https://www.npmjs.com/package/generator-xes-bdf) - -## Project Structure - -``` -my-xes-bdf-app/ -├── src/ -│ ├── controllers/ -│ │ └── item.controller.ts -│ ├── services/ -│ │ └── item.service.ts -│ ├── models/ -│ │ └── item.model.ts -│ ├── middleware/ -│ │ ├── auth.middleware.ts -│ │ └── error.middleware.ts -│ ├── config/ -│ │ └── app.config.ts -│ ├── types/ -│ │ └── index.d.ts -│ ├── app.ts -│ └── server.ts -├── tests/ -│ ├── unit/ -│ │ └── item.service.spec.ts -│ └── integration/ -│ └── item.controller.spec.ts -├── .env -├── .env.example -├── .eslintrc.json -├── .prettierrc -├── package.json -├── tsconfig.json -└── README.md -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "my-xes-bdf-app", - "version": "1.0.0", - "description": "TypeScript application scaffolded with generator-xes-bdf", - "main": "dist/server.js", - "scripts": { - "build": "tsc", - "start": "node dist/server.js", - "dev": "ts-node-dev --respawn --transpile-only src/server.ts", - "lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'", - "lint:fix": "eslint 'src/**/*.ts' 'tests/**/*.ts' --fix", - "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'", - "test": "jest --coverage", - "test:watch": "jest --watch", - "test:unit": "jest tests/unit", - "test:integration": "jest tests/integration", - "clean": "rimraf dist" - }, - "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.3.1", - "express": "^4.18.2", - "express-validator": "^7.0.1", - "http-status-codes": "^2.3.0" - }, - "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/jest": "^29.5.11", - "@types/node": "^20.11.0", - "@types/supertest": "^6.0.2", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.1.3", - "jest": "^29.7.0", - "prettier": "^3.2.4", - "rimraf": "^5.0.5", - "supertest": "^6.3.4", - "ts-jest": "^29.1.4", - "ts-node-dev": "^2.0.0", - "typescript": "^5.3.3" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "lib": ["ES2020"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "tests"] -} -``` - -### `src/app.ts` - -```typescript -import express, { Application } from 'express'; -import cors from 'cors'; -import { json, urlencoded } from 'express'; -import { errorMiddleware } from './middleware/error.middleware'; -import { ItemController } from './controllers/item.controller'; - -export function createApp(): Application { - const app: Application = express(); - - // Body parsing - app.use(json()); - app.use(urlencoded({ extended: true })); - - // CORS - app.use(cors()); - - // Health check - app.get('/health', (_req, res) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }); - }); - - // Routes - const itemController = new ItemController(); - app.use('/api/items', itemController.router); - - // Global error handler (must be last) - app.use(errorMiddleware); - - return app; -} -``` - -### `src/server.ts` - -```typescript -import * as dotenv from 'dotenv'; -dotenv.config(); - -import { createApp } from './app'; -import { AppConfig } from './config/app.config'; - -const app = createApp(); -const config = new AppConfig(); - -app.listen(config.port, () => { - console.log(`Server running on http://localhost:${config.port}`); - console.log(`Environment: ${config.nodeEnv}`); -}); -``` - -### `src/config/app.config.ts` - -```typescript -export class AppConfig { - readonly port: number; - readonly nodeEnv: string; - readonly apiPrefix: string; - - constructor() { - this.port = parseInt(process.env.PORT ?? '3000', 10); - this.nodeEnv = process.env.NODE_ENV ?? 'development'; - this.apiPrefix = process.env.API_PREFIX ?? '/api'; - } - - get isDevelopment(): boolean { - return this.nodeEnv === 'development'; - } - - get isProduction(): boolean { - return this.nodeEnv === 'production'; - } -} -``` - -### `src/models/item.model.ts` - -```typescript -export interface Item { - id: string; - name: string; - description: string; - price: number; - quantity: number; - createdAt: Date; - updatedAt: Date; -} - -export interface CreateItemDto { - name: string; - description: string; - price: number; - quantity: number; -} - -export interface UpdateItemDto { - name?: string; - description?: string; - price?: number; - quantity?: number; -} -``` - -### `src/services/item.service.ts` - -```typescript -import { v4 as uuidv4 } from 'uuid'; -import { Item, CreateItemDto, UpdateItemDto } from '../models/item.model'; - -export class ItemService { - private items: Map = new Map(); - - findAll(): Item[] { - return Array.from(this.items.values()); - } - - findById(id: string): Item | undefined { - return this.items.get(id); - } - - create(dto: CreateItemDto): Item { - const now = new Date(); - const item: Item = { - id: uuidv4(), - ...dto, - createdAt: now, - updatedAt: now, - }; - this.items.set(item.id, item); - return item; - } - - update(id: string, dto: UpdateItemDto): Item | null { - const existing = this.items.get(id); - if (!existing) return null; - const updated: Item = { ...existing, ...dto, updatedAt: new Date() }; - this.items.set(id, updated); - return updated; - } - - delete(id: string): boolean { - return this.items.delete(id); - } -} -``` - -### `src/controllers/item.controller.ts` - -```typescript -import { Router, Request, Response, NextFunction } from 'express'; -import { StatusCodes } from 'http-status-codes'; -import { ItemService } from '../services/item.service'; -import { CreateItemDto, UpdateItemDto } from '../models/item.model'; - -export class ItemController { - readonly router: Router; - private readonly service: ItemService; - - constructor() { - this.router = Router(); - this.service = new ItemService(); - this.initRoutes(); - } - - private initRoutes(): void { - this.router.get('/', this.getAll.bind(this)); - this.router.get('/:id', this.getById.bind(this)); - this.router.post('/', this.create.bind(this)); - this.router.patch('/:id', this.update.bind(this)); - this.router.delete('/:id', this.remove.bind(this)); - } - - private getAll(_req: Request, res: Response): void { - res.json(this.service.findAll()); - } - - private getById(req: Request, res: Response): void { - const item = this.service.findById(req.params.id); - if (!item) { - res.status(StatusCodes.NOT_FOUND).json({ message: 'Item not found' }); - return; - } - res.json(item); - } - - private create(req: Request, res: Response): void { - const dto = req.body as CreateItemDto; - const item = this.service.create(dto); - res.status(StatusCodes.CREATED).json(item); - } - - private update(req: Request, res: Response): void { - const dto = req.body as UpdateItemDto; - const item = this.service.update(req.params.id, dto); - if (!item) { - res.status(StatusCodes.NOT_FOUND).json({ message: 'Item not found' }); - return; - } - res.json(item); - } - - private remove(req: Request, res: Response): void { - const deleted = this.service.delete(req.params.id); - if (!deleted) { - res.status(StatusCodes.NOT_FOUND).json({ message: 'Item not found' }); - return; - } - res.status(StatusCodes.NO_CONTENT).send(); - } -} -``` - -### `src/middleware/error.middleware.ts` - -```typescript -import { Request, Response, NextFunction } from 'express'; -import { StatusCodes } from 'http-status-codes'; - -export interface AppError extends Error { - statusCode?: number; -} - -export function errorMiddleware( - err: AppError, - _req: Request, - res: Response, - _next: NextFunction, -): void { - const statusCode = err.statusCode ?? StatusCodes.INTERNAL_SERVER_ERROR; - res.status(statusCode).json({ - message: err.message ?? 'Internal Server Error', - ...(process.env.NODE_ENV === 'development' && { stack: err.stack }), - }); -} -``` - -### `tests/unit/item.service.spec.ts` - -```typescript -import { ItemService } from '../../src/services/item.service'; - -describe('ItemService', () => { - let service: ItemService; - - beforeEach(() => { - service = new ItemService(); - }); - - it('should return an empty list initially', () => { - expect(service.findAll()).toHaveLength(0); - }); - - it('should create a new item', () => { - const dto = { name: 'Test', description: 'Desc', price: 9.99, quantity: 5 }; - const item = service.create(dto); - expect(item.id).toBeDefined(); - expect(item.name).toBe('Test'); - expect(service.findAll()).toHaveLength(1); - }); - - it('should return undefined for a missing item', () => { - expect(service.findById('nonexistent')).toBeUndefined(); - }); - - it('should update an item', () => { - const item = service.create({ name: 'Old', description: '', price: 1, quantity: 1 }); - const updated = service.update(item.id, { name: 'New' }); - expect(updated?.name).toBe('New'); - }); - - it('should delete an item', () => { - const item = service.create({ name: 'Del', description: '', price: 1, quantity: 1 }); - expect(service.delete(item.id)).toBe(true); - expect(service.findAll()).toHaveLength(0); - }); -}); -``` - -### `.env.example` - -``` -PORT=3000 -NODE_ENV=development -API_PREFIX=/api -``` - -## Getting Started - -```bash -# 1. Install dependencies -npm install - -# 2. Copy and configure environment variables -cp .env.example .env - -# 3. Start in development mode (hot reload) -npm run dev - -# 4. Run tests -npm test - -# 5. Build for production -npm run build - -# 6. Start production server -npm start -``` - -## Features - -- Express 4 REST API with typed request/response handlers -- Controller/Service/Model layered architecture -- UUID-based entity identity generation -- Global error middleware with environment-aware stack trace output -- Health check endpoint -- CORS and body parsing pre-configured -- `http-status-codes` for readable HTTP status references -- ESLint + Prettier enforced code style -- Jest unit and integration tests with Supertest -- `ts-node-dev` for fast development reloading -- Strict TypeScript compilation with unused-variable enforcement diff --git a/skills/typescript-coder/assets/typescript-zotero-plugin.md b/skills/typescript-coder/assets/typescript-zotero-plugin.md deleted file mode 100644 index bc60c14c1..000000000 --- a/skills/typescript-coder/assets/typescript-zotero-plugin.md +++ /dev/null @@ -1,397 +0,0 @@ -# TypeScript Zotero Plugin Template - -> A TypeScript project starter for building Zotero 7 plugins. Produces a Zotero-compatible plugin using the bootstrap (non-overlay) plugin API, TypeScript compilation, and a structured manifest. Inspired by patterns from the Zotero plugin ecosystem, including projects hosted at retorque.re (Better BibTeX for Zotero by Emiliano Heyns). - -## License - -AGPL-3.0 — The Better BibTeX for Zotero project (retorque.re) is licensed under AGPL-3.0. Review licensing carefully before distributing a plugin based on its patterns. For a permissive alternative, consider MIT; consult the source project's license file. - -## Source - -- [retorque.re (Better BibTeX for Zotero)](https://retorque.re/) -- [Better BibTeX GitHub](https://github.com/retorquere/zotero-better-bibtex) - -## Project Structure - -``` -zotero-my-plugin/ -├── src/ -│ ├── bootstrap.ts ← Plugin lifecycle entry point -│ ├── plugin.ts ← Main plugin class -│ ├── prefs.ts ← Preferences/settings management -│ ├── ui.ts ← Menu and UI registration -│ └── types/ -│ └── zotero.d.ts ← Zotero global type declarations -├── addon/ -│ ├── manifest.json ← Plugin manifest (Zotero 7 / WebExtension-style) -│ ├── prefs/ -│ │ └── prefs.xhtml ← Preferences pane (XUL) -│ └── locale/ -│ └── en-US/ -│ └── addon.ftl ← Fluent localisation strings -├── scripts/ -│ └── build.mjs ← Build/zip script -├── .gitignore -├── package.json -├── tsconfig.json -└── README.md -``` - -## Key Files - -### `package.json` - -```json -{ - "name": "zotero-my-plugin", - "version": "1.0.0", - "description": "A Zotero 7 plugin built with TypeScript", - "scripts": { - "build": "tsc && node scripts/build.mjs", - "watch": "tsc --watch", - "package": "node scripts/build.mjs --zip", - "typecheck": "tsc --noEmit", - "clean": "rimraf build dist" - }, - "devDependencies": { - "@types/node": "^20.8.10", - "rimraf": "^5.0.5", - "typescript": "^5.2.2", - "zotero-types": "^1.3.22" - } -} -``` - -### `tsconfig.json` - -```json -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2020", "DOM"], - "outDir": "build", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": false, - "sourceMap": false, - "noUnusedLocals": true, - "noUnusedParameters": true - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "build", "dist"] -} -``` - -### `addon/manifest.json` - -```json -{ - "manifest_version": 2, - "name": "My Zotero Plugin", - "version": "1.0.0", - "description": "A sample Zotero 7 plugin", - "homepage_url": "https://github.com/yourname/zotero-my-plugin", - "author": "Your Name ", - "applications": { - "zotero": { - "id": "my-plugin@example.com", - "update_url": "https://raw.githubusercontent.com/yourname/zotero-my-plugin/main/update.json", - "strict_min_version": "7.0.0" - } - }, - "icons": { - "32": "icons/icon32.png", - "48": "icons/icon48.png" - }, - "permissions": [], - "browser_specific_settings": { - "zotero": { - "id": "my-plugin@example.com" - } - } -} -``` - -### `src/bootstrap.ts` - -```typescript -/** - * Zotero 7 Bootstrap Plugin Entry Point. - * - * Zotero 7 uses a WebExtension-like bootstrap model. The plugin must export - * these lifecycle functions, which Zotero calls at the appropriate times. - */ - -import { MyPlugin } from "./plugin"; - -let plugin: MyPlugin | null = null; - -/** - * Called when the plugin is first installed. - */ -export function install(_data: { version: string; reason: string }): void { - Zotero.log("my-plugin: install"); -} - -/** - * Called each time Zotero starts (or the plugin is enabled). - * The plugin should register its UI elements and listeners here. - */ -export async function startup(data: { - id: string; - version: string; - rootURI: string; -}): Promise { - Zotero.log(`my-plugin: startup v${data.version}`); - - // Wait for Zotero to be fully initialised before registering UI - await Zotero.initializationPromise; - - plugin = new MyPlugin(data.rootURI); - await plugin.init(); -} - -/** - * Called when Zotero quits or the plugin is disabled. - */ -export function shutdown(_data: { id: string; version: string; reason: string }): void { - Zotero.log("my-plugin: shutdown"); - plugin?.unload(); - plugin = null; -} - -/** - * Called when the plugin is uninstalled. - */ -export function uninstall(_data: { version: string; reason: string }): void { - Zotero.log("my-plugin: uninstall"); -} -``` - -### `src/plugin.ts` - -```typescript -import { registerPrefs } from "./prefs"; -import { registerUI, unregisterUI } from "./ui"; - -export class MyPlugin { - private rootURI: string; - private registeredWindows: Set = new Set(); - - constructor(rootURI: string) { - this.rootURI = rootURI; - } - - async init(): Promise { - Zotero.log("my-plugin: initialising"); - - // Register preferences defaults - registerPrefs(); - - // Register UI elements in all open windows - for (const win of Zotero.getMainWindows()) { - this.addToWindow(win); - } - - // Listen for new windows opening - Zotero.uiReadyPromise.then(() => { - Services.wm.addListener(this.windowListener); - }); - } - - addToWindow(win: Window): void { - if (this.registeredWindows.has(win)) return; - this.registeredWindows.add(win); - registerUI(win, this.rootURI); - } - - removeFromWindow(win: Window): void { - unregisterUI(win); - this.registeredWindows.delete(win); - } - - unload(): void { - Services.wm.removeListener(this.windowListener); - for (const win of this.registeredWindows) { - this.removeFromWindow(win); - } - this.registeredWindows.clear(); - } - - private windowListener = { - onOpenWindow: (xulWindow: Zotero.XULWindow) => { - const win = xulWindow - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow) as Window; - win.addEventListener("load", () => this.addToWindow(win), { once: true }); - }, - onCloseWindow: (xulWindow: Zotero.XULWindow) => { - const win = xulWindow - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow) as Window; - this.removeFromWindow(win); - }, - }; -} -``` - -### `src/prefs.ts` - -```typescript -const PREF_PREFIX = "extensions.my-plugin"; - -export function registerPrefs(): void { - const defaults = Services.prefs.getDefaultBranch(""); - defaults.setBoolPref(`${PREF_PREFIX}.enabled`, true); - defaults.setCharPref(`${PREF_PREFIX}.apiKey`, ""); - defaults.setIntPref(`${PREF_PREFIX}.maxResults`, 100); -} - -export function getPref(key: string): T { - const branch = Services.prefs.getBranch(`${PREF_PREFIX}.`); - const type = branch.getPrefType(key); - - if (type === branch.PREF_BOOL) return branch.getBoolPref(key) as T; - if (type === branch.PREF_INT) return branch.getIntPref(key) as T; - if (type === branch.PREF_STRING) return branch.getCharPref(key) as T; - - throw new Error(`Unknown pref type for key: ${key}`); -} - -export function setPref(key: string, value: T): void { - const branch = Services.prefs.getBranch(`${PREF_PREFIX}.`); - if (typeof value === "boolean") branch.setBoolPref(key, value); - else if (typeof value === "number") branch.setIntPref(key, value); - else branch.setCharPref(key, value); -} -``` - -### `src/ui.ts` - -```typescript -export function registerUI(win: Window, rootURI: string): void { - const doc = win.document; - - // Add a menu item to the Tools menu - const toolsMenu = doc.getElementById("menu_ToolsPopup"); - if (!toolsMenu) return; - - const menuItem = doc.createXULElement("menuitem"); - menuItem.id = "my-plugin-menu-item"; - menuItem.setAttribute("label", "My Plugin"); - menuItem.setAttribute("oncommand", ""); - menuItem.addEventListener("command", () => { - openPluginDialog(win, rootURI); - }); - - toolsMenu.appendChild(menuItem); -} - -export function unregisterUI(win: Window): void { - const doc = win.document; - doc.getElementById("my-plugin-menu-item")?.remove(); -} - -function openPluginDialog(win: Window, rootURI: string): void { - win.openDialog( - `${rootURI}content/dialog.xhtml`, - "my-plugin-dialog", - "chrome,centerscreen,resizable=yes" - ); -} -``` - -### `src/types/zotero.d.ts` - -```typescript -/** - * Minimal ambient declarations for Zotero globals. - * For comprehensive typings, install the `zotero-types` package. - */ - -declare const Zotero: { - log: (msg: string) => void; - initializationPromise: Promise; - uiReadyPromise: Promise; - getMainWindows: () => Window[]; - XULWindow: unknown; -}; - -declare const Services: { - prefs: { - getDefaultBranch: (root: string) => mozIBranch; - getBranch: (root: string) => mozIBranch; - }; - wm: { - addListener: (listener: unknown) => void; - removeListener: (listener: unknown) => void; - }; -}; - -declare const Ci: { - nsIInterfaceRequestor: unknown; - nsIDOMWindow: unknown; -}; - -interface mozIBranch { - PREF_BOOL: number; - PREF_INT: number; - PREF_STRING: number; - getPrefType: (key: string) => number; - getBoolPref: (key: string) => boolean; - getIntPref: (key: string) => number; - getCharPref: (key: string) => string; - setBoolPref: (key: string, value: boolean) => void; - setIntPref: (key: string, value: number) => void; - setCharPref: (key: string, value: string) => void; -} -``` - -### `addon/locale/en-US/addon.ftl` - -``` -# Fluent localisation file - -my-plugin-menu-label = My Plugin -my-plugin-prefs-title = My Plugin Preferences -my-plugin-prefs-enabled = Enable My Plugin -my-plugin-prefs-api-key = API Key: -``` - -## Getting Started - -1. Install dependencies: - ```bash - npm install - ``` -2. Install the `zotero-types` package for comprehensive Zotero API typings: - ```bash - npm install --save-dev zotero-types - ``` -3. Compile TypeScript: - ```bash - npm run build - ``` -4. Package into a `.xpi` file for installation: - ```bash - npm run package - ``` -5. In Zotero 7, install via **Tools > Add-ons > Install Add-on From File** and select the `.xpi`. - -## Features - -- Zotero 7 bootstrap (non-overlay) plugin architecture — no legacy XUL overlay required -- TypeScript 5.x compilation to ES2020 for Zotero's SpiderMonkey runtime -- Plugin lifecycle hooks: `install`, `startup`, `shutdown`, `uninstall` -- Window listener pattern to register UI in all existing and future main windows -- XUL menu item registration and cleanup via `registerUI` / `unregisterUI` -- Preferences management via `Services.prefs` with typed `getPref` / `setPref` helpers -- Fluent (`.ftl`) localisation file for translatable strings -- WebExtension-style `manifest.json` for Zotero 7 plugin metadata -- Ambient type declarations for Zotero globals (supplemented by `zotero-types`) diff --git a/skills/typescript-coder/references/basics.md b/skills/typescript-coder/references/basics.md index 7e3d7b45b..90138f8f0 100644 --- a/skills/typescript-coder/references/basics.md +++ b/skills/typescript-coder/references/basics.md @@ -36,8 +36,8 @@ npx tsc ``` ```bash -Version X.Y.Z -tsc: The TypeScript Compiler - Version X.Y.Z +Version 4.5.5 +tsc: The TypeScript Compiler - Version 4.5.5 ``` ### Configuring compiler diff --git a/skills/typescript-coder/references/typescript-cheatsheet.md b/skills/typescript-coder/references/cheatsheet.md similarity index 100% rename from skills/typescript-coder/references/typescript-cheatsheet.md rename to skills/typescript-coder/references/cheatsheet.md diff --git a/skills/typescript-coder/references/elements.md b/skills/typescript-coder/references/elements.md index 8a1c0b30b..5653298bf 100644 --- a/skills/typescript-coder/references/elements.md +++ b/skills/typescript-coder/references/elements.md @@ -55,14 +55,14 @@ let ourTuple: [number, boolean, string]; ourTuple = [false, 'Coding God was mistaken', 5]; ``` -### Tuple mutation and push behavior +### Readonly Tuple ```ts // define our tuple let ourTuple: [number, boolean, string]; // initialize correctly ourTuple = [5, false, 'Coding God was here']; -// We can push more elements, but indices beyond 2 are typed as a union of the element types +// We have no type safety in our tuple for indexes 3+ ourTuple.push('Something new and wrong'); console.log(ourTuple); ``` @@ -415,8 +415,6 @@ console.log((x).length); ```ts let x = 'hello'; - -// Force-cast x to number so the compiler lets us call number methods -console.log(((x as unknown) as number).toFixed(2)); -// Runtime: TypeError, because x is still the string "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/typescript-essentials.md b/skills/typescript-coder/references/essentials.md similarity index 97% rename from skills/typescript-coder/references/typescript-essentials.md rename to skills/typescript-coder/references/essentials.md index 5a48cd185..87c625ec1 100644 --- a/skills/typescript-coder/references/typescript-essentials.md +++ b/skills/typescript-coder/references/essentials.md @@ -372,7 +372,6 @@ class Range implements Iterable { ``` For older compile targets, enable full iterator support: - ```ts // tsconfig.json // { "compilerOptions": { "target": "ES5", "downlevelIteration": true } } @@ -382,26 +381,24 @@ For older compile targets, enable full iterator support: - Reference material for [JSX](https://www.typescriptlang.org/docs/handbook/jsx.html) -To use JSX: name files `.tsx` and set the `jsx` compiler option. +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": "react-jsx", "jsxImportSource": "react" } } +// { "compilerOptions": { "jsx": "preserve" } } // or "react-jsx", etc. ``` JSX emission modes: -| Mode | Input | Output | File | -|------|-------|--------|------| +| Mode | Input | Output | File Extension | +|------|-------|--------|----------------| | `preserve` | `
      ` | `
      ` | `.jsx` | | `react` | `
      ` | `React.createElement("div")` | `.js` | -| `react-native` | `
      ` | `
      ` | `.js` | | `react-jsx` | `
      ` | `_jsx("div", {})` | `.js` | -| `react-jsxdev` | `
      ` | `_jsxDEV("div", {})` | `.js` | ```tsx // Intrinsic elements — lowercase, mapped via JSX.IntrinsicElements -const a =
      ; +const element =
      ; // Value-based elements — capitalized components function MyButton(props: { label: string }) { @@ -418,20 +415,14 @@ interface ButtonProps { function Button({ label, onClick, disabled }: ButtonProps) { return ; } -// -
      - ); - } - - return this.props.children; - } -} - -// Usage -function App() { - return ( - Oops! Something broke.
      }> - - - ); -} -``` - ### Best Practices - Don't Swallow Errors ```ts diff --git a/skills/typescript-coder/references/projects.md b/skills/typescript-coder/references/projects.md deleted file mode 100644 index 6b1cac9df..000000000 --- a/skills/typescript-coder/references/projects.md +++ /dev/null @@ -1,821 +0,0 @@ -# TypeScript Projects - -## TypeScript Configuration - -- Reference material for [Configuration](https://www.w3schools.com/typescript/typescript_config.php) -- See [What is a tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) for additional information -- See [TSConfig Reference](https://www.typescriptlang.org/tsconfig/) for additional information -- See [Integrating with Build Tools](https://www.typescriptlang.org/docs/handbook/integrating-with-build-tools.html) for additional information - -### Basic Configuration - -```json -{ - "compilerOptions": { - "target": "es6", - "module": "commonjs" - }, - "include": ["src/**/*"] -} -``` - -### Advanced Configuration - -```json -{ - "compilerOptions": { - "target": "es2020", - "module": "esnext", - "strict": true, - "baseUrl": ".", - "paths": { - "@app/*": ["src/app/*"] - }, - "outDir": "dist", - "esModuleInterop": true - }, - "include": ["src"], - "exclude": ["node_modules", "dist"] -} -``` - -### Initialize Configuration - -```bash -tsc --init -``` - -## TypeScript Node.js - -- Reference material for [Node.js](https://www.w3schools.com/typescript/typescript_nodejs.php) -- See [TypeScript Tooling in 5 minutes](https://www.typescriptlang.org/docs/handbook/typescript-tooling-in-5-minutes.html) for additional information -- See [Modules](https://www.typescriptlang.org/docs/handbook/2/modules.html) for additional information - -### Setting Up Node.js Project - -```bash -mkdir my-ts-node-app -cd my-ts-node-app -npm init -y -npm install typescript @types/node --save-dev -npx tsc --init -``` - -### Create Project Structure - -```bash -mkdir src -# later add files like: src/server.ts, src/middleware/auth.ts -``` - -### TypeScript Configuration - -```json -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules"] -} -``` - -### Install Dependencies - -```bash -npm install express body-parser -npm install --save-dev ts-node nodemon @types/express -``` - -### Project Structure - -``` -my-ts-node-app/ - src/ - server.ts - middleware/ - auth.ts - entity/ - User.ts - config/ - database.ts - dist/ - node_modules/ - package.json - tsconfig.json -``` - -### Basic Express Server Example - -```ts -import express, { Request, Response, NextFunction } from 'express'; -import { json } from 'body-parser'; - -interface User { - id: number; - username: string; - email: string; -} - -// Initialize Express app -const app = express(); -const PORT = process.env.PORT || 3000; - -// Middleware -app.use(json()); - -// In-memory database -const users: User[] = [ - { id: 1, username: 'user1', email: 'user1@example.com' }, - { id: 2, username: 'user2', email: 'user2@example.com' } -]; - -// Routes -app.get('/api/users', (req: Request, res: Response) => { - res.json(users); -}); - -app.get('/api/users/:id', (req: Request, res: Response) => { - const user = users.find(u => u.id === parseInt(req.params.id)); - if (!user) return res.status(404).json({ message: 'User not found' }); - res.json(user); -}); - -app.post('/api/users', (req: Request, res: Response) => { - const { username, email } = req.body; - - if (!username || !email) { - return res.status(400).json({ message: 'Username and email are required' }); - } - - const newUser: User = { - id: users.length + 1, - username, - email - }; - - users.push(newUser); - res.status(201).json(newUser); -}); - -// Error handling middleware -app.use((err: Error, req: Request, res: Response, next: NextFunction) => { - console.error(err.stack); - res.status(500).json({ message: 'Something went wrong!' }); -}); - -// Start server -app.listen(PORT, () => { - console.log(`Server is running on http://localhost:${PORT}`); -}); -``` - -### Express Middleware with Authentication - -```ts -import { Request, Response, NextFunction } from 'express'; - -// Extend the Express Request type to include custom properties -declare global { - namespace Express { - interface Request { - user?: { id: number; role: string }; - } - } -} - -export const authenticate = (req: Request, res: Response, next: NextFunction) => { - const token = req.header('Authorization')?.replace('Bearer ', ''); - - if (!token) { - return res.status(401).json({ message: 'No token provided' }); - } - - try { - /********************************************************************************************* - IMPORTANT - An attacker can exploit this by sending any arbitrary token to gain access to protected - routes that rely on authenticate/authorize, resulting in a complete authentication and - authorization bypass. Replace the mock decoded assignment with real JWT verification - (including signature, expiry, and claims checks) and ensure that invalid or missing tokens - never populate req.user or reach privileged handlers. - **********************************************************************************************/ - // In a real app, verify the JWT token here - const decoded = { id: 1, role: 'admin' }; // Mock decoded token - req.user = decoded; - next(); - } catch (error) { - res.status(401).json({ message: 'Invalid token' }); - } -}; - -export const authorize = (roles: string[]) => { - return (req: Request, res: Response, next: NextFunction) => { - if (!req.user) { - return res.status(401).json({ message: 'Not authenticated' }); - } - - if (!roles.includes(req.user.role)) { - return res.status(403).json({ message: 'Not authorized' }); - } - - next(); - }; -}; -``` - -### Using Middleware in Routes - -```ts -// src/server.ts -import { authenticate, authorize } from './middleware/auth'; - -app.get('/api/admin', authenticate, authorize(['admin']), (req, res) => { - res.json({ message: `Hello admin ${req.user?.id}` }); -}); -``` - -### Database Integration with TypeORM - Entity - -```ts -import { Entity, PrimaryGeneratedColumn, Column, - CreateDateColumn, UpdateDateColumn } from 'typeorm'; - -@Entity('users') -export class User { - @PrimaryGeneratedColumn() - id: number; - - @Column({ unique: true }) - username: string; - - @Column({ unique: true }) - email: string; - - @Column({ select: false }) - password: string; - - @Column({ default: 'user' }) - role: string; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} -``` - -### Database Configuration - -```ts -import 'reflect-metadata'; -import { DataSource } from 'typeorm'; -import { User } from '../entity/User'; - -export const AppDataSource = new DataSource({ - type: 'postgres', - host: process.env.DB_HOST || 'localhost', - port: parseInt(process.env.DB_PORT || '5432'), - username: process.env.DB_USERNAME || 'postgres', - password: process.env.DB_PASSWORD || 'postgres', - database: process.env.DB_NAME || 'mydb', - synchronize: process.env.NODE_ENV !== 'production', - logging: false, - entities: [User], - migrations: [], - subscribers: [], -}); -``` - -### Initialize Database - -```ts -// src/server.ts -import { AppDataSource } from './config/database'; - -AppDataSource.initialize() - .then(() => { - app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); - }) - .catch((err) => { - console.error('DB init error', err); - process.exit(1); - }); -``` - -### Package Scripts - -```json -{ - "scripts": { - "build": "tsc", - "start": "node dist/server.js", - "dev": "nodemon --exec ts-node src/server.ts", - "watch": "tsc -w", - "test": "jest --config jest.config.js" - } -} -``` - -### Development Mode - -```bash -npm run dev -``` - -### Production Build - -```bash -npm run build -npm start -``` - -### Run with Source Maps - -```bash -node --enable-source-maps dist/server.js -``` - -## TypeScript React - -- Reference material for [React](https://www.w3schools.com/typescript/typescript_react.php) -- See [JSX](https://www.typescriptlang.org/docs/handbook/jsx.html) for additional information -- See [React & Webpack](https://webpack.js.org/guides/typescript/) for additional information - -### Getting Started - -```bash -npm create vite@latest my-app -- --template react-ts -cd my-app -npm install -npm run dev -``` - -### TypeScript Configuration for React - -```json -{ - "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "moduleResolution": "Node", - "jsx": "react-jsx", - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src"] -} -``` - -### Component Typing - -```tsx -// Greeting.tsx -type GreetingProps = { - name: string; - age?: number; -}; - -export function Greeting({ name, age }: GreetingProps) { - return ( -
      -

      Hello, {name}!

      - {age !== undefined &&

      You are {age} years old

      } -
      - ); -} -``` - -### Event Handlers - -```tsx -// Input change -function NameInput() { - function handleChange(e: React.ChangeEvent) { - console.log(e.target.value); - } - return ; -} - -// Button click -function SaveButton() { - function handleClick(e: React.MouseEvent) { - e.preventDefault(); - } - return ; -} -``` - -### useState Hook - -```tsx -const [count, setCount] = React.useState(0); -const [status, setStatus] = React.useState<'idle' | 'loading' | 'error'>('idle'); - -type User = { id: string; name: string }; -const [user, setUser] = React.useState(null); -``` - -### useRef Hook - -```tsx -function FocusInput() { - const inputRef = React.useRef(null); - return inputRef.current?.select()} />; -} -``` - -### Children Props - -```tsx -type CardProps = { title: string; children?: React.ReactNode }; -function Card({ title, children }: CardProps) { - return ( -
      -

      {title}

      - {children} -
      - ); -} -``` - -### Generic Fetch Function - -```tsx -async function fetchJson(url: string): Promise { - const res = await fetch(url); - if (!res.ok) throw new Error('Network error'); - return res.json() as Promise; -} - -// Usage inside an async function/component effect -async function loadPosts() { - type Post = { id: number; title: string }; - const posts = await fetchJson("/api/posts"); - console.log(posts); -} -``` - -### Context API with TypeScript - -```tsx -type Theme = 'light' | 'dark'; -const ThemeContext = - React.createContext<{ theme: Theme; toggle(): void } | null>(null); - -function ThemeProvider({ children }: { children: React.ReactNode }) { - const [theme, setTheme] = React.useState('light'); - const value = - { theme, toggle: () => setTheme(t => (t === 'light' ? 'dark' : 'light')) }; - return {children}; -} - -function useTheme() { - const ctx = React.useContext(ThemeContext); - if (!ctx) throw new Error('useTheme must be used within ThemeProvider'); - return ctx; -} -``` - -### Vite Environment Types - -```ts -// src/vite-env.d.ts -/// -``` - -### TypeScript Config for Vite Types - -```json -{ - "compilerOptions": { - "types": ["vite/client"] - } -} -``` - -### Path Aliases - -```json -// tsconfig.json -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["src/*"], - "@components/*": ["src/components/*"], - "@utils/*": ["src/utils/*"] - } - } -} -``` - -```ts -// Usage in your code -import { Button } from '@/components/Button'; -import { formatDate } from '@utils/date'; -``` - -## TypeScript Tooling - -- Reference material for [Tooling](https://www.w3schools.com/typescript/typescript_tooling.php) -- See [Integrating with Build Tools](https://www.typescriptlang.org/docs/handbook/integrating-with-build-tools.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 - -### Install ESLint - -```bash -# Install ESLint with TypeScript support -npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -``` - -### ESLint Configuration - -```json -// .eslintrc.json -{ - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking" - ], - "parserOptions": { - "project": "./tsconfig.json", - "ecmaVersion": 2020, - "sourceType": "module" - }, - "rules": { - "@typescript-eslint/explicit-function-return-type": "warn", - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] - } -} -``` - -### ESLint Scripts - -```json -// package.json -{ - "scripts": { - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "eslint . --ext .ts,.tsx --fix", - "type-check": "tsc --noEmit" - } -} -``` - -### Install Prettier - -```bash -# Install Prettier and related packages -npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier -``` - -### Prettier Configuration - -```json -// .prettierrc -{ - "semi": true, - "singleQuote": true, - "tabWidth": 2, - "printWidth": 100, - "trailingComma": "es5", - "bracketSpacing": true, - "arrowParens": "avoid" -} -``` - -### Prettier Ignore File - -``` -// .prettierignore -node_modules -build -dist -.next -.vscode -``` - -### Integrate Prettier with ESLint - -```json -// .eslintrc.json -{ - "extends": [ - // ... other configs - "plugin:prettier/recommended" // Must be last in the array - ] -} -``` - -### Setup with Vite - -```bash -# Create a new project with React + TypeScript -npm create vite@latest my-app -- --template react-ts - -# Navigate to project directory -cd my-app - -# Install dependencies -npm install - -# Start development server -npm run dev -``` - -### Webpack Configuration - -```js -// webpack.config.js -const path = require('path'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); - -module.exports = { - entry: './src/index.tsx', - module: { - rules: [ - { - test: /\.(ts|tsx)$/, - use: 'ts-loader', - exclude: /node_modules/, - }, - { - test: /\.css$/, - use: ['style-loader', 'css-loader'], - }, - ], - }, - resolve: { - extensions: ['.tsx', '.ts', '.js'], - }, - output: { - filename: 'bundle.js', - path: path.resolve(__dirname, 'dist'), - }, - plugins: [ - new HtmlWebpackPlugin({ - template: './public/index.html', - }), - ], - devServer: { - static: path.join(__dirname, 'dist'), - compress: true, - port: 3000, - hot: true, - }, -}; -``` - -### TypeScript Configuration for Build Tools - -```json -// tsconfig.json -{ - "compilerOptions": { - "target": "es2020", - "module": "esnext", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - } - }, - "include": ["src"], - "exclude": ["node_modules"] -} -``` - -### VS Code Settings - -```json -// .vscode/settings.json -{ - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, - "source.organizeImports": true - }, - "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], - "typescript.tsdk": "node_modules/typescript/lib", - "typescript.preferences.importModuleSpecifier": "non-relative" -} -``` - -### VS Code Launch Configuration - -```json -// .vscode/launch.json -{ - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Launch Chrome against localhost", - "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}", - "sourceMaps": true, - "sourceMapPathOverrides": { - "webpack:///./~/*": "${workspaceFolder}/node_modules/*", - "webpack:///./*": "${workspaceFolder}/src/*" - } - }, - { - "type": "node", - "request": "launch", - "name": "Debug Tests", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/jest", - "args": ["--runInBand", "--watchAll=false"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "sourceMaps": true - } - ] -} -``` - -### Install Testing Dependencies - -```bash -# Install testing dependencies -npm install --save-dev jest @types/jest ts-jest @testing-library/react @testing-library/jest-dom @testing-library/user-event -``` - -### Jest Configuration - -```js -// jest.config.js -module.exports = { - preset: 'ts-jest', - testEnvironment: 'jsdom', - setupFilesAfterEnv: ['@testing-library/jest-dom'], - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - '\\\.(css|less|scss|sass)$': 'identity-obj-proxy', - }, - transform: { - '^.+\\\\.tsx?$': 'ts-jest', - }, - testMatch: ['**/__tests__/**/*.test.(ts|tsx)'], -}; -``` - -### Example Test File - -```tsx -// src/__tests__/Button.test.tsx -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import Button from '../components/Button'; - -describe('Button', () => { - it('renders button with correct text', () => { - render(); - expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument(); - }); - - it('calls onClick when clicked', () => { - const handleClick = jest.fn(); - render(); - - fireEvent.click(screen.getByRole('button')); - expect(handleClick).toHaveBeenCalledTimes(1); - }); -}); -``` diff --git a/skills/typescript-coder/references/types.md b/skills/typescript-coder/references/types.md index e1dbb828a..59405da3b 100644 --- a/skills/typescript-coder/references/types.md +++ b/skills/typescript-coder/references/types.md @@ -1425,42 +1425,41 @@ tsc --outFile sample.js main.ts ### Namespace Augmentation ```ts -// Original namespace -declare namespace Express { +// Original namespace in a library +declare namespace App { interface Request { - user?: { id: number; name: string }; + id: string; + timestamp: number; } interface Response { - json(data: any): void; + send(data: any): void; } } // Later in your application (e.g., in a .d.ts file) -declare namespace Express { +declare namespace App { // Augment the Request interface interface Request { // Add custom properties - requestTime?: number; + userId?: number; // Add methods log(message: string): void; } // Add new types - interface UserSession { - userId: number; + interface Session { + token: string; expires: Date; } } // Usage in your application -const app = express(); - -app.use((req: Express.Request, res: Express.Response, next) => { +function handleRequest(req: App.Request, res: App.Response) { // Augmented properties and methods are available - req.requestTime = Date.now(); - req.log('Request started'); - next(); -}); + req.userId = 123; + req.log('Request received'); + res.send({ success: true }); +} ``` ### Generic Namespaces diff --git a/skills/typescript-coder/references/typescript-configuration.md b/skills/typescript-coder/references/typescript-configuration.md deleted file mode 100644 index f85754c78..000000000 --- a/skills/typescript-coder/references/typescript-configuration.md +++ /dev/null @@ -1,1054 +0,0 @@ -# TypeScript Project Configuration - -Reference material for configuring TypeScript projects, the TypeScript compiler, and build tool integration. - -## What is a tsconfig.json - -- Reference material for [What is a tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) - -The presence of a `tsconfig.json` file in a directory indicates that the directory is the root of a TypeScript project. The file specifies the root files and the compiler options required to compile the project. - -**How TypeScript locates a `tsconfig.json`:** - -- When `tsc` is invoked without input files, the compiler searches for `tsconfig.json` starting in the current directory, then each parent directory -- When `tsc` is invoked with input files directly, `tsconfig.json` is ignored -- Use `tsc --project` (or `tsc -p`) to specify an explicit path to a config file - -**Top-level `tsconfig.json` fields:** - -```json -{ - "compilerOptions": { }, - "files": ["src/main.ts", "src/utils.ts"], - "include": ["src/**/*"], - "exclude": ["node_modules", "**/*.spec.ts"], - "extends": "./tsconfig.base.json", - "references": [{ "path": "../shared" }] -} -``` - -**`files`** — an explicit list of files to include. If any file cannot be found, an error occurs: - -```json -{ - "compilerOptions": { "outDir": "dist" }, - "files": [ - "core.ts", - "app.ts" - ] -} -``` - -**`include`** — glob patterns for files to include (supports `*`, `**`, `?`): - -```json -{ - "include": ["src/**/*", "tests/**/*"] -} -``` - -- `*` matches any file segment (excludes directory separators) -- `**` matches any directory nesting depth -- `?` matches any single character - -Supported extensions automatically: `.ts`, `.tsx`, `.d.ts`. With `allowJs`: `.js`, `.jsx`. - -**`exclude`** — glob patterns for files to exclude. Defaults to `node_modules`, `bower_components`, `jspm_packages`, and the `outDir` if set: - -```json -{ - "exclude": ["node_modules", "**/*.test.ts", "dist"] -} -``` - -Note: `exclude` only prevents files from being *included* automatically — a file referenced via `import` or a triple-slash directive will still be included. - -**`extends`** — inherit configuration from a base file: - -```json -{ - "extends": "@tsconfig/node18/tsconfig.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src"] -} -``` - -All relative paths in the base file are resolved relative to the base file. Fields from the inheriting config override the base. Arrays (`files`, `include`, `exclude`) are not merged — they replace the base values entirely. - -**`jsconfig.json`** — TypeScript recognizes `jsconfig.json` files as well; they are equivalent to a `tsconfig.json` with `"allowJs": true` set by default. - -## Compiler Options in MSBuild - -- Reference material for [Compiler Options in MSBuild](https://www.typescriptlang.org/docs/handbook/compiler-options-in-msbuild.html) - -TypeScript can be integrated into MSBuild projects (Visual Studio, `.csproj`, `.vbproj`) via the `Microsoft.TypeScript.MSBuild` NuGet package. - -**Install the NuGet package:** - -```xml - -``` - -**TypeScript compiler options in an MSBuild `.csproj` file** are set inside a ``: - -```xml - - - ES2020 - ES2020 - true - true - wwwroot/js - true - false - true - - -``` - -**Common MSBuild TypeScript properties:** - -| MSBuild Property | tsconfig Equivalent | Description | -|---|---|---| -| `TypeScriptTarget` | `target` | ECMAScript output version | -| `TypeScriptModuleKind` | `module` | Module system | -| `TypeScriptOutDir` | `outDir` | Output directory | -| `TypeScriptSourceMap` | `sourceMap` | Emit source maps | -| `TypeScriptStrict` | `strict` | Enable all strict checks | -| `TypeScriptNoImplicitAny` | `noImplicitAny` | Error on implicit `any` | -| `TypeScriptRemoveComments` | `removeComments` | Strip comments | -| `TypeScriptNoEmitOnError` | `noEmitOnError` | Don't emit on errors | -| `TypeScriptDeclaration` | `declaration` | Emit `.d.ts` files | -| `TypeScriptExperimentalDecorators` | `experimentalDecorators` | Enable decorators | -| `TypeScriptjsx` | `jsx` | JSX compilation mode | -| `TypeScriptNoResolve` | `noResolve` | Disable module resolution | -| `TypeScriptPreserveConstEnums` | `preserveConstEnums` | Keep const enum declarations | -| `TypeScriptSuppressImplicitAnyIndexErrors` | `suppressImplicitAnyIndexErrors` | Suppress index implicit any | - -**Using a `tsconfig.json` with MSBuild** (preferred over individual properties): - -```xml - - true - - - - -``` - -**Compile TypeScript as part of the build** automatically by including `.ts` files: - -```xml - - - -``` - -## TSConfig Reference - -- Reference material for [TSConfig Reference](https://www.typescriptlang.org/tsconfig/) - -Comprehensive reference for all TypeScript compiler options available in `tsconfig.json`. - -### Type Checking Options - -```json -{ - "compilerOptions": { - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "strictFunctionTypes": true, - "strictBindCallApply": true, - "strictPropertyInitialization": true, - "noImplicitThis": true, - "useUnknownInCatchVariables": true, - "alwaysStrict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "exactOptionalPropertyTypes": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "allowUnusedLabels": false, - "allowUnreachableCode": false - } -} -``` - -| Option | Default | Description | -|---|---|---| -| `strict` | `false` | Enable all strict type-checking options | -| `noImplicitAny` | `false` | Error on expressions with an implied `any` type | -| `strictNullChecks` | `false` | `null`/`undefined` not assignable to other types | -| `strictFunctionTypes` | `false` | Stricter checking of function parameter types (contravariant) | -| `strictBindCallApply` | `false` | Strict checking of `bind`, `call`, and `apply` methods | -| `strictPropertyInitialization` | `false` | Properties must be initialized in the constructor | -| `noImplicitThis` | `false` | Error on `this` expressions with an implied `any` type | -| `useUnknownInCatchVariables` | `false` | Catch clause variables are `unknown` instead of `any` | -| `alwaysStrict` | `false` | Parse in strict mode; emit `"use strict"` in output | -| `noUnusedLocals` | `false` | Report errors on unused local variables | -| `noUnusedParameters` | `false` | Report errors on unused function parameters | -| `exactOptionalPropertyTypes` | `false` | Distinguish between `undefined` value and missing key | -| `noImplicitReturns` | `false` | Report error when not all code paths return a value | -| `noFallthroughCasesInSwitch` | `false` | Report errors for fallthrough cases in `switch` | -| `noUncheckedIndexedAccess` | `false` | Include `undefined` in index signature return type | -| `noImplicitOverride` | `false` | Require `override` keyword for overridden methods | - -### Module Options - -```json -{ - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "baseUrl": "./", - "paths": { "@app/*": ["src/*"] }, - "rootDirs": ["src", "generated"], - "typeRoots": ["./typings", "./node_modules/@types"], - "types": ["node", "jest"], - "allowUmdGlobalAccess": false, - "moduleSuffixes": [".ios", ".native", ""], - "allowImportingTsExtensions": true, - "resolvePackageJsonExports": true, - "resolvePackageJsonImports": true, - "customConditions": ["my-condition"], - "resolveJsonModule": true, - "allowArbitraryExtensions": true, - "noResolve": false - } -} -``` - -| Option | Description | -|---|---| -| `module` | Module system: `CommonJS`, `ES2015`–`ES2022`, `ESNext`, `Node16`, `NodeNext`, `Preserve` | -| `moduleResolution` | Resolution strategy: `classic`, `node`, `node16`, `nodenext`, `bundler` | -| `baseUrl` | Base directory for non-relative module names | -| `paths` | Re-map module names to different locations | -| `rootDirs` | Virtual merged directory for module resolution | -| `typeRoots` | Directories to include type definitions from | -| `types` | Only include these `@types` packages globally | -| `resolveJsonModule` | Allow importing `.json` files | -| `esModuleInterop` | Emit compatibility helpers for CommonJS/ES module interop | -| `allowSyntheticDefaultImports` | Allow `import x from 'module'` even without a default export | - -### Emit Options - -```json -{ - "compilerOptions": { - "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": false, - "sourceMap": true, - "inlineSourceMap": false, - "outFile": "./output.js", - "outDir": "./dist", - "removeComments": false, - "noEmit": false, - "importHelpers": true, - "importsNotUsedAsValues": "remove", - "downlevelIteration": false, - "sourceRoot": "", - "mapRoot": "", - "inlineSources": false, - "emitBOM": false, - "newLine": "lf", - "stripInternal": false, - "noEmitHelpers": false, - "noEmitOnError": true, - "preserveConstEnums": false, - "declarationDir": "./types", - "preserveValueImports": false - } -} -``` - -| Option | Default | Description | -|---|---|---| -| `declaration` | `false` | Generate `.d.ts` files from TS/JS files | -| `declarationMap` | `false` | Generate source maps for `.d.ts` files | -| `emitDeclarationOnly` | `false` | Only output `.d.ts` files, no JS | -| `sourceMap` | `false` | Generate `.js.map` source map files | -| `outDir` | — | Redirect output structure to the directory | -| `outFile` | — | Concatenate and emit output to a single file | -| `removeComments` | `false` | Strip all comments from TypeScript files | -| `noEmit` | `false` | Do not emit compiler output files | -| `noEmitOnError` | `false` | Do not emit if any type checking errors were reported | -| `importHelpers` | `false` | Import helper functions from `tslib` | -| `downlevelIteration` | `false` | Emit more compliant, but verbose JS for iterables | -| `declarationDir` | — | Output directory for generated declaration files | - -### JavaScript Support Options - -```json -{ - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "maxNodeModuleJsDepth": 0 - } -} -``` - -| Option | Default | Description | -|---|---|---| -| `allowJs` | `false` | Allow JavaScript files to be imported in the project | -| `checkJs` | `false` | Enable error reporting in JS files (requires `allowJs`) | -| `maxNodeModuleJsDepth` | `0` | Max depth to search `node_modules` for JS files | - -### Language and Environment Options - -```json -{ - "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "jsx": "react-jsx", - "jsxFactory": "React.createElement", - "jsxFragmentFactory": "React.Fragment", - "jsxImportSource": "react", - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "useDefineForClassFields": true, - "moduleDetection": "force" - } -} -``` - -| Option | Default | Description | -|---|---|---| -| `target` | `ES3` | Set the JS language version: `ES3`, `ES5`, `ES2015`–`ES2022`, `ESNext` | -| `lib` | (based on `target`) | Bundled library declaration files to include | -| `jsx` | — | JSX code generation: `preserve`, `react`, `react-jsx`, `react-jsxdev`, `react-native` | -| `experimentalDecorators` | `false` | Enable experimental support for decorators | -| `emitDecoratorMetadata` | `false` | Emit design-type metadata for decorated declarations | -| `useDefineForClassFields` | `true` (ES2022+) | Use `Object.defineProperty` for class fields | -| `moduleDetection` | `auto` | Strategy for detecting if a file is a script or module | - -### Interop Constraints - -```json -{ - "compilerOptions": { - "isolatedModules": true, - "verbatimModuleSyntax": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "preserveSymlinks": false, - "forceConsistentCasingInFileNames": true - } -} -``` - -| Option | Default | Description | -|---|---|---| -| `isolatedModules` | `false` | Ensure each file can be safely transpiled in isolation | -| `verbatimModuleSyntax` | `false` | Do not transform/elide any imports or exports not marked `type` | -| `esModuleInterop` | `false` | Emit additional JS to ease support for CJS modules | -| `allowSyntheticDefaultImports` | `false` | Allow default imports from non-default-exporting modules | -| `forceConsistentCasingInFileNames` | `false` | Ensure imports have consistent casing | - -### Completeness / Performance Options - -```json -{ - "compilerOptions": { - "skipDefaultLibCheck": false, - "skipLibCheck": true - } -} -``` - -| Option | Default | Description | -|---|---|---| -| `skipLibCheck` | `false` | Skip type checking of all declaration files (`.d.ts`) | -| `skipDefaultLibCheck` | `false` | Skip type checking of default TypeScript lib files | - -### Projects / Incremental Compilation - -```json -{ - "compilerOptions": { - "incremental": true, - "composite": true, - "tsBuildInfoFile": "./.tsbuildinfo", - "disableSourceOfProjectReferenceRedirect": false, - "disableSolutionSearching": false, - "disableReferencedProjectLoad": false - } -} -``` - -| Option | Default | Description | -|---|---|---| -| `incremental` | `false` | Save information about last compilation to speed up future builds | -| `composite` | `false` | Required for project references; enforces constraints for project structure | -| `tsBuildInfoFile` | `.tsbuildinfo` | File path to store incremental compilation information | - -### Output Formatting / Diagnostics - -```json -{ - "compilerOptions": { - "noErrorTruncation": false, - "diagnostics": false, - "extendedDiagnostics": false, - "listFiles": false, - "listEmittedFiles": false, - "traceResolution": false, - "explainFiles": false - } -} -``` - -## tsc CLI Options - -- Reference material for [tsc CLI Options](https://www.typescriptlang.org/docs/handbook/compiler-options.html) - -The TypeScript compiler `tsc` can be invoked from the command line to compile TypeScript files. Most `tsconfig.json` options can also be passed as CLI flags. - -**Basic usage:** - -```bash -# Compile a single file -tsc app.ts - -# Use a specific tsconfig -tsc --project tsconfig.json -tsc -p tsconfig.json - -# Watch mode -tsc --watch -tsc -w - -# Build mode (project references) -tsc --build -tsc -b - -# Type check only (no emit) -tsc --noEmit - -# Show version -tsc --version -tsc -v - -# Show help -tsc --help -tsc -h - -# Print diagnostic information -tsc --diagnostics -``` - -**Override tsconfig from CLI:** - -```bash -# Compile to ES2020, CommonJS modules, strict mode -tsc --target ES2020 --module commonjs --strict - -# Enable source maps and output to dist/ -tsc --sourceMap --outDir dist - -# Check JS files -tsc --allowJs --checkJs -``` - -**Important CLI-only flags (not valid in tsconfig):** - -| Flag | Description | -|---|---| -| `--build` / `-b` | Build project references in the correct dependency order | -| `--clean` | Delete output files from a `--build` invocation | -| `--dry` | Show what `--build` would do without actually building | -| `--force` | Force a rebuild in `--build` mode | -| `--watch` / `-w` | Watch input files for changes | -| `--project` / `-p` | Compile the project in the given directory or tsconfig | -| `--version` / `-v` | Print the compiler's version | -| `--help` / `-h` | Print help message | -| `--listFilesOnly` | Print names of files that would be part of compilation | -| `--generateTrace` | Generate an event trace and types list | - -**Key compiler flags (also available in tsconfig):** - -```bash -tsc --target ES2020 -tsc --module NodeNext -tsc --moduleResolution NodeNext -tsc --strict -tsc --noImplicitAny -tsc --strictNullChecks -tsc --noEmit -tsc --noEmitOnError -tsc --declaration -tsc --sourceMap -tsc --outDir ./dist -tsc --rootDir ./src -tsc --allowJs -tsc --checkJs -tsc --esModuleInterop -tsc --resolveJsonModule -tsc --skipLibCheck -tsc --lib ES2020,DOM -tsc --jsx react-jsx -tsc --incremental -tsc --composite -``` - -**Build a project using `--build`:** - -```bash -# Build with project references -tsc --build tsconfig.json - -# Clean build artifacts -tsc --build --clean - -# Rebuild everything (ignore incremental cache) -tsc --build --force - -# Dry run to see what would be built -tsc --build --dry --verbose -``` - -## Project References - -- Reference material for [Project References](https://www.typescriptlang.org/docs/handbook/project-references.html) - -Project References (introduced in TypeScript 3.0) allow TypeScript programs to be structured into smaller pieces. This improves build times, enforces logical separation between components, and enables code to be organized into new and better ways. - -**Basic project reference configuration:** - -```json -// tsconfig.json (in the root or a consuming project) -{ - "compilerOptions": { - "declaration": true - }, - "references": [ - { "path": "../shared" }, - { "path": "../utils", "prepend": true } - ] -} -``` - -**Referenced project requirements (`composite: true` is required):** - -```json -// shared/tsconfig.json -{ - "compilerOptions": { - "composite": true, - "declaration": true, - "declarationMap": true, - "rootDir": "src", - "outDir": "dist" - }, - "include": ["src"] -} -``` - -**What `composite: true` enforces:** - -- `rootDir` must be set (defaults to directory containing `tsconfig.json`) -- All implementation files must be matched by an `include` pattern or listed in `files` -- `declaration` must be `true` -- `.tsbuildinfo` files are generated for incremental builds - -**Building with project references:** - -```bash -# Build all projects in dependency order -tsc --build - -# Build a specific project -tsc --build src/tsconfig.json - -# Clean all project reference build artifacts -tsc --build --clean - -# Force full rebuild -tsc --build --force - -# Verbose output -tsc --build --verbose -``` - -**Monorepo `tsconfig.json` layout example:** - -``` -project/ - tsconfig.json ← solution-level config - shared/ - tsconfig.json ← composite: true - src/ - index.ts - server/ - tsconfig.json ← references shared - src/ - main.ts - client/ - tsconfig.json ← references shared - src/ - app.ts -``` - -**Solution-level config (no `include`, only `references`):** - -```json -// tsconfig.json -{ - "files": [], - "references": [ - { "path": "shared" }, - { "path": "server" }, - { "path": "client" } - ] -} -``` - -**`prepend` option** — concatenate a project's output before the current project's output (for `outFile` bundling only): - -```json -{ - "references": [ - { "path": "../utils", "prepend": true } - ] -} -``` - -**`disableSourceOfProjectReferenceRedirect`** — for very large projects, use `.d.ts` instead of source files when following project references (faster but less accurate error reporting): - -```json -{ - "compilerOptions": { - "disableSourceOfProjectReferenceRedirect": true - } -} -``` - -**Key benefits of project references:** - -- Faster incremental builds — only rebuild changed projects -- Strong logical boundaries between packages -- Better editor responsiveness in large monorepos -- Supports `--build` mode with dependency ordering -- `declarationMap` enables "go to source" navigation across project boundaries - -## Integrating with Build Tools - -- Reference material for [Integrating with Build Tools](https://www.typescriptlang.org/docs/handbook/integrating-with-build-tools.html) - -TypeScript can be integrated with various JavaScript build tools and bundlers. - -### Babel - -Use `@babel/preset-typescript` to strip TypeScript types using Babel (no type checking — use `tsc --noEmit` separately): - -```bash -npm install --save-dev @babel/preset-typescript @babel/core @babel/cli -``` - -```json -// babel.config.json -{ - "presets": ["@babel/preset-typescript"] -} -``` - -Compile TypeScript with Babel: - -```bash -babel --extensions '.ts,.tsx' src --out-dir dist -``` - -Note: Babel does not type-check. Run `tsc --noEmit` separately for type checking. - -### Webpack - -Using `ts-loader`: - -```bash -npm install --save-dev ts-loader webpack webpack-cli -``` - -```js -// webpack.config.js -module.exports = { - entry: "./src/index.ts", - module: { - rules: [ - { - test: /\.tsx?$/, - use: "ts-loader", - exclude: /node_modules/, - }, - ], - }, - resolve: { - extensions: [".tsx", ".ts", ".js"], - }, - output: { - filename: "bundle.js", - path: __dirname + "/dist", - }, -}; -``` - -Using `babel-loader` with `@babel/preset-typescript` (type checking separate): - -```bash -npm install --save-dev babel-loader @babel/core @babel/preset-env @babel/preset-typescript -``` - -```js -// webpack.config.js -module.exports = { - module: { - rules: [ - { - test: /\.(ts|tsx)$/, - use: { - loader: "babel-loader", - options: { - presets: ["@babel/preset-env", "@babel/preset-typescript"], - }, - }, - }, - ], - }, -}; -``` - -### Vite - -Vite uses `esbuild` for TypeScript transpilation (no type checking during build — use `tsc --noEmit` separately): - -```bash -npm create vite@latest my-app -- --template vanilla-ts -``` - -```json -// tsconfig.json (Vite-recommended settings) -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "strict": true - } -} -``` - -### Rollup - -Using `@rollup/plugin-typescript`: - -```bash -npm install --save-dev @rollup/plugin-typescript tslib -``` - -```js -// rollup.config.js -import typescript from "@rollup/plugin-typescript"; - -export default { - input: "src/index.ts", - output: { - file: "dist/bundle.js", - format: "cjs", - }, - plugins: [typescript()], -}; -``` - -### Gulp - -Using `gulp-typescript`: - -```bash -npm install --save-dev gulp-typescript -``` - -```js -const gulp = require("gulp"); -const ts = require("gulp-typescript"); - -const tsProject = ts.createProject("tsconfig.json"); - -gulp.task("typescript", function () { - return tsProject.src().pipe(tsProject()).js.pipe(gulp.dest("dist")); -}); -``` - -### Grunt - -Using `grunt-ts`: - -```bash -npm install --save-dev grunt-ts -``` - -```js -// Gruntfile.js -module.exports = function (grunt) { - grunt.initConfig({ - ts: { - default: { - tsconfig: "./tsconfig.json", - }, - }, - }); - grunt.loadNpmTasks("grunt-ts"); - grunt.registerTask("default", ["ts"]); -}; -``` - -### Browserify - -Using `tsify`: - -```bash -npm install --save-dev tsify browserify -``` - -```bash -browserify main.ts -p tsify --debug > bundle.js -``` - -### Jest - -Using `ts-jest` for running TypeScript tests: - -```bash -npm install --save-dev ts-jest @types/jest -``` - -```json -// jest.config.json -{ - "preset": "ts-jest", - "testEnvironment": "node" -} -``` - -Using `@swc/jest` for faster transforms: - -```bash -npm install --save-dev @swc/jest @swc/core -``` - -```json -// jest.config.json -{ - "transform": { - "^.+\\.(t|j)sx?$": "@swc/jest" - } -} -``` - -### MSBuild - -See the [Compiler Options in MSBuild](#compiler-options-in-msbuild) section above. - -## Configuring Watch - -- Reference material for [Configuring Watch](https://www.typescriptlang.org/docs/handbook/configuring-watch.html) - -TypeScript 3.8+ supports `watchOptions` in `tsconfig.json` to configure how the compiler watches files and directories. - -**`watchOptions` configuration:** - -```json -{ - "watchOptions": { - "watchFile": "useFsEvents", - "watchDirectory": "useFsEvents", - "fallbackPolling": "dynamicPriority", - "synchronousWatchDirectory": true, - "excludeDirectories": ["**/node_modules", "_build"], - "excludeFiles": ["build/fileWhichChangesOften.ts"] - } -} -``` - -**`watchFile` strategies** — controls how individual files are watched: - -| Strategy | Description | -|---|---| -| `fixedPollingInterval` | Check files for changes at a fixed interval | -| `priorityPollingInterval` | Check files at different intervals based on heuristics | -| `dynamicPriorityPolling` | Polling interval adjusts dynamically based on change frequency | -| `useFsEvents` (default) | Use OS file system events (`inotify`, `FSEvents`, `ReadDirectoryChangesW`) | -| `useFsEventsOnParentDirectory` | Watch the parent directory instead of individual files | - -**`watchDirectory` strategies** — controls how directory trees are watched: - -| Strategy | Description | -|---|---| -| `fixedPollingInterval` | Check directory for changes at a fixed interval | -| `dynamicPriorityPolling` | Polling interval adjusts dynamically | -| `useFsEvents` (default) | Use OS file system events for directory watching | - -**`fallbackPolling`** — polling strategy to use when OS file system events are unavailable: - -| Value | Description | -|---|---| -| `fixedPollingInterval` | Fixed interval polling | -| `priorityPollingInterval` | Priority-based polling | -| `dynamicPriorityPolling` | Dynamic interval polling (default) | - -**`synchronousWatchDirectory`** — disable deferred watching on directories (useful on systems that don't support recursive watching natively): - -```json -{ - "watchOptions": { - "synchronousWatchDirectory": true - } -} -``` - -**`excludeDirectories`** — reduce the number of directories watched (avoids watching large directories like `node_modules`): - -```json -{ - "watchOptions": { - "excludeDirectories": ["**/node_modules", "dist", ".git"] - } -} -``` - -**`excludeFiles`** — exclude specific files from being watched: - -```json -{ - "watchOptions": { - "excludeFiles": ["src/generated/**/*.ts"] - } -} -``` - -**Environment variable configuration** — TypeScript watch behavior can also be influenced by the `TSC_WATCHFILE` and `TSC_WATCHDIRECTORY` environment variables: - -```bash -# Force polling for file watching -TSC_WATCHFILE=DynamicPriorityPolling tsc --watch - -# Force polling for directory watching -TSC_WATCHDIRECTORY=FixedPollingInterval tsc --watch -``` - -**Recommended watch config for large projects:** - -```json -{ - "watchOptions": { - "watchFile": "useFsEvents", - "watchDirectory": "useFsEvents", - "fallbackPolling": "dynamicPriority", - "excludeDirectories": ["**/node_modules", "dist", "build", ".git"] - } -} -``` - -## Nightly Builds - -- Reference material for [Nightly Builds](https://www.typescriptlang.org/docs/handbook/nightly-builds.html) - -TypeScript publishes nightly builds to npm as the `typescript@next` package, allowing developers to test upcoming features and report bugs before official releases. - -**Install the TypeScript nightly build:** - -```bash -# Install globally -npm install -g typescript@next - -# Install locally in a project -npm install --save-dev typescript@next -``` - -**Verify the installed version:** - -```bash -tsc --version -# Example output: Version 5.x.0-dev.20240115 -``` - -**Using nightly builds in Visual Studio Code:** - -1. Install `typescript@next` locally in the project: - ```bash - npm install --save-dev typescript@next - ``` - -2. Open the VS Code Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) - -3. Run **"TypeScript: Select TypeScript Version..."** - -4. Choose **"Use Workspace Version"** - -This makes VS Code use the project's local TypeScript (the nightly build) instead of the bundled version. - -**Setting workspace TypeScript version via `.vscode/settings.json`:** - -```json -{ - "typescript.tsdk": "node_modules/typescript/lib" -} -``` - -**Nightly builds in CI pipelines:** - -```json -// package.json -{ - "devDependencies": { - "typescript": "next" - }, - "scripts": { - "typecheck": "tsc --noEmit" - } -} -``` - -```yaml -# GitHub Actions example -- name: Install nightly TypeScript - run: npm install typescript@next -- name: Type check - run: npx tsc --noEmit -``` - -**Switching back to a stable release:** - -```bash -# Install specific version -npm install --save-dev typescript@5.3.3 - -# Install latest stable -npm install --save-dev typescript@latest -``` - -**`@next` vs `@rc`:** - -| Tag | Description | -|---|---| -| `typescript@next` | Nightly build — latest development snapshot | -| `typescript@rc` | Release Candidate — nearly stable, pre-release | -| `typescript@latest` | Stable release | -| `typescript@beta` | Beta release — significant features, may have bugs | - -**Checking available versions:** - -```bash -npm dist-tag ls typescript -``` diff --git a/skills/typescript-coder/references/typescript-d.ts-templates.md b/skills/typescript-coder/references/typescript-d.ts-templates.md deleted file mode 100644 index aa9f9959a..000000000 --- a/skills/typescript-coder/references/typescript-d.ts-templates.md +++ /dev/null @@ -1,403 +0,0 @@ -# TypeScript d.ts Templates - -Template patterns for TypeScript declaration files (.d.ts) from the official TypeScript documentation. - -## Modules .d.ts - -- Reference material for [Modules .d.ts](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-d-ts.html) - -Use this template when writing a declaration file for a module that is consumed via `import` or `require`. This is the most common template for npm packages. - -### Determining What Kind of Module - -Before writing a module declaration, look at the JavaScript source for clues: - -- Uses `module.exports = ...` or `exports.foo = ...` — use `export =` syntax -- Uses `export default` or named `export` statements — use ES module syntax -- Both accessible as a global and via `require` — use `export as namespace` (UMD) - -### Module Template - -```ts -// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] -// Project: [~THE PROJECT NAME~] -// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> - -/*~ This is the module template file. You should rename it to index.d.ts - *~ and place it in a folder with the same name as the module. - *~ For example, if you were writing a file for "super-greeter", this - *~ file should be 'super-greeter/index.d.ts' - */ - -/*~ If this module is a UMD module that exposes a global variable 'myLib' - *~ when loaded outside a module loader environment, declare that global here. - *~ Remove this section if your library is not UMD. - */ -export as namespace myLib; - -/*~ If this module exports functions, declare them like so. - */ -export function myFunction(a: string): string; -export function myOtherFunction(a: number): number; - -/*~ You can declare types that are available via importing the module. - */ -export interface someType { - name: string; - length: number; - extras?: string[]; -} - -/*~ You can declare properties of the module using const, let, or var. - */ -export const myField: number; - -/*~ If there are types, properties, or methods inside dotted names in your - *~ module, declare them inside a 'namespace'. - */ -export namespace subProp { - /*~ For example, given this definition, someone could write: - *~ import { subProp } from 'yourModule'; - *~ subProp.foo(); - *~ or - *~ import * as yourModule from 'yourModule'; - *~ yourModule.subProp.foo(); - */ - export function foo(): void; -} -``` - -### Exporting a Class and Namespace Together - -When the module exports a class as well as related types: - -```ts -export = MyClass; - -declare class MyClass { - constructor(someParam?: string); - someProperty: string[]; - myMethod(opts: MyClass.MyClassMethodOptions): number; -} - -declare namespace MyClass { - export interface MyClassMethodOptions { - width?: number; - height?: number; - } -} -``` - -## Module: Plugin - -- Reference material for [Module: Plugin](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-plugin-d-ts.html) - -Use this template when writing a declaration file for a module that augments another module. A plugin imports a base module and extends its types. - -### When to Use - -- Your library is imported alongside another library and adds capabilities to it -- You call `require("super-greeter")` and then your plugin augments its behavior -- Example: a charting plugin that adds methods to a base chart library - -### Plugin Template - -```ts -// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] -// Project: [~THE PROJECT NAME~] -// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> - -/*~ This is the module plugin template file. You should rename it to index.d.ts - *~ and place it in a folder with the same name as the module. - *~ For example, if you were writing a file for "super-greeter", this - *~ file should be 'super-greeter/index.d.ts' - */ - -/*~ On this line, import the module which this module adds to */ -import { greeter } from "super-greeter"; - -/*~ Here, declare the same module as the one you imported above, - *~ then expand the existing declaration of the greeter function. - */ -declare module "super-greeter" { - /*~ Here, declare the things that are added by your plugin. - *~ You can add new members to existing types. - */ - interface Greeter { - printHello(): void; - } -} -``` - -### Key Points - -- The `import` at the top is required to make this file a module (not a script), which enables `declare module` augmentation. -- The `declare module "super-greeter"` block must exactly match the original module's specifier string. -- Only exported members can be augmented; you cannot add new exports to an existing module. - -## Module: Class - -- Reference material for [Module: Class](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-class-d-ts.html) - -Use this template when the module's primary export is a class (constructor function). The library is used like `new Greeter("hello")`. - -### Usage Example (JavaScript) - -```js -const Greeter = require("super-greeter"); -const greeter = new Greeter("Hello, world"); -greeter.sayHello(); -``` - -### Module: Class Template - -```ts -// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] -// Project: [~THE PROJECT NAME~] -// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> - -/*~ This is the module-class template file. You should rename it to index.d.ts - *~ and place it in a folder with the same name as the module. - *~ For example, if you were writing a file for "super-greeter", this - *~ file should be 'super-greeter/index.d.ts' - */ - -// Note that ES6 modules cannot directly export class objects. -// This file should be imported using the CommonJS-style: -// import x = require('[~THE MODULE~]'); -// -// Alternatively, if --allowSyntheticDefaultImports or -// --esModuleInterop is turned on, this file can also be -// imported as a default import: -// import x from '[~THE MODULE~]'; -// -// Refer to the TypeScript documentation at -// https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require -// to understand common workarounds for this limitation of ES6 modules. - -/*~ This declaration specifies that the class constructor function - *~ is the exported object from the file. - */ -export = MyClass; - -/*~ Write your module's methods and properties in this class */ -declare class MyClass { - constructor(someParam?: string); - someProperty: string[]; - myMethod(opts: MyClass.MyClassMethodOptions): number; -} - -/*~ If you want to expose types from your module as well, you can - *~ place them in this block. Note that if you include this namespace, - *~ the module can be temporarily used as a namespace, although this - *~ isn't a recommended use. - */ -declare namespace MyClass { - /** Documentation comment */ - export interface MyClassMethodOptions { - width?: number; - height?: number; - } -} -``` - -### Key Points - -- `export =` is used because the library uses `module.exports = MyClass` (CommonJS). -- To use this with `import MyClass from "..."` syntax, enable `esModuleInterop` in `tsconfig.json`. -- The `declare namespace MyClass` block lets you export nested types that are accessible as `MyClass.MyClassMethodOptions`. - -## Module: Function - -- Reference material for [Module: Function](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-function-d-ts.html) - -Use this template when the module's primary export is a callable function. The library is used like `myFn(42)` after requiring it. - -### Usage Example (JavaScript) - -```js -const x = require("super-greeter"); -const y = x(42); -const z = x("hello"); -``` - -### Module: Function Template - -```ts -// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] -// Project: [~THE PROJECT NAME~] -// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> - -/*~ This is the module-function template file. You should rename it to index.d.ts - *~ and place it in a folder with the same name as the module. - *~ For example, if you were writing a file for "super-greeter", this - *~ file should be 'super-greeter/index.d.ts' - */ - -/*~ This declaration specifies that the function - *~ is the exported object from the file. - */ -export = MyFunction; - -/*~ This example shows how to have multiple overloads of your function */ -declare function MyFunction(name: string): MyFunction.NamespaceName; -declare function MyFunction(name: string, greeting: string): MyFunction.NamespaceName; - -/*~ If you want to expose types from your module as well, you can - *~ place them in this block. Often you will want to describe the - *~ shape of the return type of the function; that type should - *~ be declared in here as shown. - */ -declare namespace MyFunction { - export interface NamespaceName { - firstName: string; - lastName: string; - } -} -``` - -### Key Points - -- `export =` is required when the module uses `module.exports = myFunction`. -- Overloads are declared as separate `declare function` statements before the namespace. -- The `declare namespace MyFunction` block is merged with the function declaration, letting the function have properties. - -## Global .d.ts - -- Reference material for [Global .d.ts](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-d-ts.html) - -Use this template for libraries loaded via a `` as the installation method -- Usage examples with no `import`/`require` statement -- References to a global like `$` or `_` with no import - -### Global Template - -```ts -// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] -// Project: [~THE PROJECT NAME~] -// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> - -/*~ If this library is callable (e.g. can be invoked as myLib(3)), - *~ include those call signatures here. - *~ Otherwise, delete this section. - */ -declare function myLib(a: string): string; -declare function myLib(a: number): number; - -/*~ If you want the name of this library to be a valid type name, - *~ you can do so here. - *~ - *~ For example, this allows us to write 'var x: myLib'. - *~ Be sure this actually makes sense! If it doesn't, just - *~ delete this declaration and add types inside the namespace below. - */ -interface myLib { - name: string; - length: number; - extras?: string[]; -} - -/*~ If your library has properties exposed on a global variable, - *~ place them here. - *~ You should also place types (interfaces and type aliases) here. - */ -declare namespace myLib { - //~ We can write 'myLib.timeout = 50' - let timeout: number; - //~ We can access 'myLib.version', but not change it - const version: string; - //~ There's some class we can create via 'let c = new myLib.Cat(42)' - //~ Or reference, e.g. 'function f(c: myLib.Cat) { ... }' - class Cat { - constructor(n: number); - //~ We can read 'c.age' from a 'Cat' instance - readonly age: number; - //~ We can invoke 'c.purr()' from a 'Cat' instance - purr(): void; - } - //~ We can declare a variable as - //~ 'var s: myLib.CatSettings = { weight: 5, name: "Maru" };' - interface CatSettings { - weight: number; - name: string; - tailLength?: number; - } - //~ We can write 'myLib.VetID = 42' or 'myLib.VetID = "bob"' - type VetID = string | number; - //~ We can invoke 'myLib.checkCat(c)' or 'myLib.checkCat(c, v)' - function checkCat(c: Cat, s?: VetID): boolean; -} -``` - -### Key Points - -- Do not include `export` or `import` statements — that would make this a module file, not a global script file. -- Top-level `declare` statements describe what exists in the global scope. -- `declare namespace myLib` groups all properties and types accessible under the `myLib` global. -- An `interface myLib` at the top level (merged with the namespace) allows `myLib` to be used as a type directly. - -## Global: Modifying Module - -- Reference material for [Global: Modifying Module](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/global-modifying-module-d-ts.html) - -Use this template when a library is imported as a module but has the side effect of modifying the global scope (e.g., adding methods to built-in prototypes like `String.prototype`). - -### When to Use - -- The library is loaded with `require("my-lib")` or `import "my-lib"` for its side effects -- The import adds new methods to existing global types (e.g., `String`, `Array`, `Promise`) -- Usage example: `require("moment"); moment().format("YYYY")` combined with `String.prototype.toDate()` - -### Global-Modifying Module Template - -```ts -// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~] -// Project: [~THE PROJECT NAME~] -// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]> - -/*~ This is the global-modifying module template file. You should rename it - *~ to index.d.ts and place it in a folder with the same name as the module. - *~ For example, if you were writing a file for "super-greeter", this - *~ file should be 'super-greeter/index.d.ts' - */ - -/*~ Note: If your global-modifying module is callable or constructable, you'll - *~ need to combine the patterns here with those in the module-class or - *~ module-function template files. - */ -declare global { - /*~ Here, declare things in the normal global namespace */ - interface String { - fancyFormat(opts: StringFormatOptions): string; - } -} - -/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */ -export {}; - -/*~ Mark the types that are referenced inside the 'declare global' block here */ -export interface StringFormatOptions { - fancinessLevel: number; -} -``` - -### Key Points - -- The file must be a **module** (contain at least one `export` or `import`) for `declare global` to work. The `export {}` statement at the bottom satisfies this requirement without exporting anything. -- `declare global { }` is the mechanism for adding to the global scope from within a module file. -- If the module also has callable or constructable exports, combine this template with the module-function or module-class template. -- Consumers install the type augmentation simply by importing the module: - -```ts -import "super-greeter"; - -const s = "hello"; -s.fancyFormat({ fancinessLevel: 3 }); // OK — added by the module -``` diff --git a/skills/typescript-coder/references/typescript-declaration-files.md b/skills/typescript-coder/references/typescript-declaration-files.md deleted file mode 100644 index 5a7a87e07..000000000 --- a/skills/typescript-coder/references/typescript-declaration-files.md +++ /dev/null @@ -1,651 +0,0 @@ -# TypeScript Declaration Files - -Reference material for writing and consuming TypeScript declaration files (.d.ts) from the official TypeScript documentation. - -## Introduction - -- Reference material for [Introduction](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) - -Declaration files (`.d.ts`) describe the shape of JavaScript code to TypeScript. They allow the TypeScript compiler to type-check usage of JavaScript libraries that were not written in TypeScript. - -There are two primary sources of declaration files: - -- **Bundled with a package** — the library author includes `.d.ts` files in the npm package alongside the JavaScript output. -- **DefinitelyTyped (`@types`)** — a community-maintained repository of declaration files for libraries that don't ship their own. Install via `npm install --save-dev @types/`. - -TypeScript resolves declaration files automatically when using `import` or `require`, looking first at the package's `types` or `typings` field in `package.json`, then for an `index.d.ts` at the package root. - -The declaration file guide covers: - -1. Writing declarations by example (common patterns) -2. Identifying the correct library structure (global, module, UMD) -3. Do's and Don'ts for avoiding common mistakes -4. A deep dive into how declarations work internally -5. How to publish and consume declaration files - -## Declaration Reference - -- Reference material for [Declaration Reference](https://www.typescriptlang.org/docs/handbook/declaration-files/by-example.html) - -Common patterns for writing declaration files, organized by the kind of thing being declared. - -### Global Variables - -```ts -/** The number of widgets present */ -declare var foo: number; -``` - -### Global Functions - -```ts -declare function greet(greeting: string): void; -``` - -Functions can have overloads: - -```ts -declare function getWidget(n: number): Widget; -declare function getWidget(s: string): Widget[]; -``` - -### Objects with Properties - -Use `declare namespace` to describe objects accessed via dotted notation: - -```ts -declare namespace myLib { - function makeGreeting(s: string): string; - let numberOfGreetings: number; -} -``` - -Usage: `myLib.makeGreeting("hello")` and `myLib.numberOfGreetings`. - -### Reusable Types — Interfaces - -```ts -interface GreetingSettings { - greeting: string; - duration?: number; - color?: string; -} - -declare function greet(setting: GreetingSettings): void; -``` - -### Reusable Types — Type Aliases - -```ts -type GreetingLike = string | (() => string) | MyGreeter; - -declare function greet(g: GreetingLike): void; -``` - -### Organizing Types with Nested Namespaces - -```ts -declare namespace GreetingLib { - interface LogOptions { - verbose?: boolean; - } - interface AlertOptions { - modal: boolean; - title?: string; - color?: string; - } -} -``` - -These are referenced as `GreetingLib.LogOptions` and `GreetingLib.AlertOptions`. - -Namespaces can be nested: - -```ts -declare namespace GreetingLib.Options { - interface Log { - verbose?: boolean; - } - interface Alert { - modal: boolean; - title?: string; - color?: string; - } -} -``` - -### Classes - -```ts -declare class Greeter { - constructor(greeting: string); - greeting: string; - showGreeting(): void; -} -``` - -### Enums - -```ts -declare enum Shading { - None, - Streamline, - Matte, - Glossy, -} -``` - -### Modules (CommonJS / ESM exports) - -Use `export =` for CommonJS-style modules (when the library uses `module.exports = ...`): - -```ts -export = MyModule; - -declare function MyModule(): void; -declare namespace MyModule { - let version: string; -} -``` - -Use named exports for ESM-style: - -```ts -export function myFunction(a: string): string; -export const myField: number; -export interface SomeType { - name: string; - length: number; -} -``` - -### UMD Modules - -Libraries usable both as globals and as modules use `export as namespace`: - -```ts -export as namespace myLib; -export function makeGreeting(s: string): string; -export let numberOfGreetings: number; -``` - -## Library Structures - -- Reference material for [Library Structures](https://www.typescriptlang.org/docs/handbook/declaration-files/library-structures.html) - -Choosing the correct declaration file structure depends on how the library is consumed. - -### Global Libraries - -A global library is accessible from the global scope — no `import` or `require` needed. Examples: older jQuery (`$()`), Underscore.js (old style). - -Identifying characteristics in JavaScript source: - -- Top-level `var` statements or `function` declarations -- `window.myLib = ...` assignments -- References to `document` or `window` - -Use the `global.d.ts` template. - -```ts -declare function myLib(a: string): string; -declare namespace myLib { - let timeout: number; -} -``` - -### Module Libraries - -Libraries consumed via `require()` or `import`. Most modern npm packages are module libraries. - -Identifying characteristics: - -- `const x = require("foo")` or `import x from "foo"` in usage examples -- `exports.myFn = ...` or `module.exports = ...` in the source -- `define(...)` (AMD) in the source - -Use the `module.d.ts` template. - -### UMD Libraries - -Libraries that can be used as a global (via a ` - - -``` - -### TypeScript-aware Editors - -TypeScript's language service powers rich editor tooling in: - -- **Visual Studio Code** — built-in TypeScript support -- **WebStorm / IntelliJ IDEA** — built-in TypeScript support -- **Vim / Neovim** — via tsserver language server -- **Emacs** — via tide or lsp-mode - -All major editors support: - -- Inline error reporting -- Autocompletion (IntelliSense) -- Go-to-definition and find-all-references -- Rename refactoring -- Hover documentation diff --git a/skills/typescript-coder/references/typescript-handbook.md b/skills/typescript-coder/references/typescript-handbook.md deleted file mode 100644 index dfb269c37..000000000 --- a/skills/typescript-coder/references/typescript-handbook.md +++ /dev/null @@ -1,499 +0,0 @@ -# TypeScript Handbook - -Comprehensive TypeScript reference based on the official TypeScript Handbook. This document covers core concepts and patterns. - -## Reference Sources - -- [The TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) -- [The Basics](https://www.typescriptlang.org/docs/handbook/2/basic-types.html) -- [Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html) -- [Narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) -- [More on Functions](https://www.typescriptlang.org/docs/handbook/2/functions.html) -- [Object Types](https://www.typescriptlang.org/docs/handbook/2/objects.html) -- [Classes](https://www.typescriptlang.org/docs/handbook/2/classes.html) -- [Modules](https://www.typescriptlang.org/docs/handbook/2/modules.html) - -## Getting Started - -TypeScript is a strongly typed programming language that builds on JavaScript. It adds optional static typing to JavaScript, enabling better tooling, error detection, and code quality. - -### Key Benefits - -- **Type Safety**: Catch errors at compile time instead of runtime -- **Better IDE Support**: Enhanced autocomplete, refactoring, and navigation -- **Code Documentation**: Types serve as inline documentation -- **Modern JavaScript Features**: Use latest ECMAScript features with backward compatibility -- **Gradual Adoption**: Add TypeScript incrementally to existing projects - -## The Basics - -### Static Type Checking - -TypeScript analyzes your code to find errors before execution: - -```typescript -// TypeScript catches this error at compile time -const message = "hello"; -message(); // Error: This expression is not callable -``` - -### Non-Exception Failures - -TypeScript catches common mistakes: - -```typescript -const user = { name: "Alice", age: 30 }; - -// Typos -user.location; // Error: Property 'location' does not exist - -// Uncalled functions -if (user.age.toFixed) // Error: Did you mean to call this? - -// Logical errors -const value = Math.random() < 0.5 ? "a" : "b"; -if (value !== "a") { - // ... -} else if (value === "b") { // Error: This comparison is always false -``` - -## Everyday Types - -### Primitives - -```typescript -let name: string = "Alice"; -let age: number = 30; -let isActive: boolean = true; -``` - -### Arrays - -```typescript -let numbers: number[] = [1, 2, 3]; -let strings: Array = ["a", "b", "c"]; -``` - -### Functions - -```typescript -// Parameter type annotations -function greet(name: string): string { - return `Hello, ${name}!`; -} - -// Optional parameters -function buildName(first: string, last?: string): string { - return last ? `${first} ${last}` : first; -} - -// Default parameters -function multiply(a: number, b: number = 1): number { - return a * b; -} - -// Rest parameters -function sum(...numbers: number[]): number { - return numbers.reduce((acc, n) => acc + n, 0); -} -``` - -### Object Types - -```typescript -// Anonymous object type -function printCoord(pt: { x: number; y: number }) { - console.log(pt.x, pt.y); -} - -// Optional properties -function printName(obj: { first: string; last?: string }) { - // ... -} - -// Readonly properties -interface ReadonlyPerson { - readonly name: string; - readonly age: number; -} -``` - -### Union Types - -```typescript -function printId(id: number | string) { - if (typeof id === "string") { - console.log(id.toUpperCase()); - } else { - console.log(id); - } -} -``` - -### Type Aliases - -```typescript -type Point = { - x: number; - y: number; -}; - -type ID = number | string; -``` - -### Interfaces - -```typescript -interface Point { - x: number; - y: number; -} - -// Extending interfaces -interface ColoredPoint extends Point { - color: string; -} -``` - -## Narrowing - -### typeof Guards - -```typescript -function padLeft(padding: number | string, input: string) { - if (typeof padding === "number") { - return " ".repeat(padding) + input; - } - return padding + input; -} -``` - -### Truthiness Narrowing - -```typescript -function printAll(strs: string | string[] | null) { - if (strs && typeof strs === "object") { - for (const s of strs) { - console.log(s); - } - } else if (typeof strs === "string") { - console.log(strs); - } -} -``` - -### Equality Narrowing - -```typescript -function example(x: string | number, y: string | boolean) { - if (x === y) { - x.toUpperCase(); // x is string - y.toUpperCase(); // y is string - } -} -``` - -### in Operator Narrowing - -```typescript -type Fish = { swim: () => void }; -type Bird = { fly: () => void }; - -function move(animal: Fish | Bird) { - if ("swim" in animal) { - return animal.swim(); - } - return animal.fly(); -} -``` - -### instanceof Narrowing - -```typescript -function logValue(x: Date | string) { - if (x instanceof Date) { - console.log(x.toUTCString()); - } else { - console.log(x.toUpperCase()); - } -} -``` - -### Discriminated Unions - -```typescript -interface Circle { - kind: "circle"; - radius: number; -} - -interface Square { - kind: "square"; - sideLength: number; -} - -type Shape = Circle | Square; - -function getArea(shape: Shape) { - switch (shape.kind) { - case "circle": - return Math.PI * shape.radius ** 2; - case "square": - return shape.sideLength ** 2; - } -} -``` - -## Functions - -### Function Type Expressions - -```typescript -type GreetFunction = (name: string) => void; - -function greeter(fn: GreetFunction) { - fn("World"); -} -``` - -### Call Signatures - -```typescript -type DescribableFunction = { - description: string; - (someArg: number): boolean; -}; -``` - -### Generic Functions - -```typescript -function firstElement(arr: T[]): T | undefined { - return arr[0]; -} - -// Inference -const s = firstElement(["a", "b", "c"]); // string -const n = firstElement([1, 2, 3]); // number -``` - -### Constraints - -```typescript -function longest(a: T, b: T) { - if (a.length >= b.length) { - return a; - } - return b; -} -``` - -### Function Overloads - -```typescript -function makeDate(timestamp: number): Date; -function makeDate(m: number, d: number, y: number): Date; -function makeDate(mOrTimestamp: number, d?: number, y?: number): Date { - if (d !== undefined && y !== undefined) { - return new Date(y, mOrTimestamp, d); - } - return new Date(mOrTimestamp); -} -``` - -## Object Types - -### Index Signatures - -```typescript -interface StringArray { - [index: number]: string; -} - -interface NumberDictionary { - [key: string]: number; - length: number; -} -``` - -### Extending Types - -```typescript -interface BasicAddress { - name?: string; - street: string; - city: string; -} - -interface AddressWithUnit extends BasicAddress { - unit: string; -} -``` - -### Intersection Types - -```typescript -interface Colorful { - color: string; -} - -interface Circle { - radius: number; -} - -type ColorfulCircle = Colorful & Circle; -``` - -## Generics - -### Generic Types - -```typescript -function identity(arg: T): T { - return arg; -} - -let myIdentity: (arg: T) => T = identity; -``` - -### Generic Classes - -```typescript -class GenericNumber { - zeroValue: T; - add: (x: T, y: T) => T; -} -``` - -### Generic Constraints - -```typescript -interface Lengthwise { - length: number; -} - -function loggingIdentity(arg: T): T { - console.log(arg.length); - return arg; -} -``` - -## Manipulation Types - -### Mapped Types - -```typescript -type OptionsFlags = { - [Property in keyof T]: boolean; -}; -``` - -### Conditional Types - -```typescript -type NameOrId = T extends number - ? IdLabel - : NameLabel; -``` - -### Template Literal Types - -```typescript -type World = "world"; -type Greeting = `hello ${World}`; -``` - -## Classes - -### Class Members - -```typescript -class Point { - x: number; - y: number; - - constructor(x: number, y: number) { - this.x = x; - this.y = y; - } - - scale(n: number): void { - this.x *= n; - this.y *= n; - } -} -``` - -### Inheritance - -```typescript -class Animal { - move() { - console.log("Moving along!"); - } -} - -class Dog extends Animal { - bark() { - console.log("Woof!"); - } -} -``` - -### Member Visibility - -```typescript -class Base { - public x = 0; - protected y = 0; - private z = 0; -} -``` - -### Abstract Classes - -```typescript -abstract class Base { - abstract getName(): string; - - printName() { - console.log("Hello, " + this.getName()); - } -} -``` - -## Modules - -### Exporting - -```typescript -// Named exports -export function add(x: number, y: number): number { - return x + y; -} - -export interface Point { - x: number; - y: number; -} - -// Default export -export default class Calculator { - add(x: number, y: number): number { - return x + y; - } -} -``` - -### Importing - -```typescript -import { add, Point } from "./math"; -import Calculator from "./Calculator"; -import * as math from "./math"; -``` - ---- - -> [!NOTE] -> This handbook covers the core concepts from the official TypeScript documentation. For the most up-to-date information, visit [typescriptlang.org/docs/handbook](https://www.typescriptlang.org/docs/handbook/intro.html) diff --git a/skills/typescript-coder/references/typescript-module-references.md b/skills/typescript-coder/references/typescript-module-references.md deleted file mode 100644 index c138c4af4..000000000 --- a/skills/typescript-coder/references/typescript-module-references.md +++ /dev/null @@ -1,483 +0,0 @@ -# TypeScript Modules Reference - -Reference material for TypeScript module systems, theory, and configuration options. - -## Introduction - -- Reference material for [Introduction](https://www.typescriptlang.org/docs/handbook/modules/introduction.html) - -A file is a **module** in TypeScript if it contains at least one top-level `import` or `export`. Files without these are treated as **scripts** whose declarations exist in the global scope. - -```ts -// module.ts — IS a module (has export); isolated scope -export const name = "TypeScript"; -export function greet(n: string) { return `Hello, ${n}`; } - -// script.ts — NOT a module (no import/export); global scope -const name = "TypeScript"; - -// Force a file to be a module with no exports -export {}; -``` - -TypeScript uses the same ES Module syntax as JavaScript for imports and exports: - -```ts -// Named exports and imports -export function add(a: number, b: number): number { return a + b; } -import { add } from "./math"; - -// Default export and import -export default class User { constructor(public name: string) {} } -import User from "./user"; - -// Re-export -export { add as sum } from "./math"; -export * from "./utils"; - -// Type-only imports — fully erased at runtime (no runtime cost) -import type { User } from "./types"; -import { type Config, loadConfig } from "./config"; -``` - -Key distinctions: - -| Concept | Description | -|---|---| -| Module | File with `import`/`export`; isolated scope | -| Script | File without them; shares global scope | -| `moduleResolution` | Controls how TypeScript finds imported modules | -| `module` compiler option | Controls the output format of the emitted JS | - -The `module` compiler option and `moduleResolution` option are distinct settings that work together: -- `module` determines the output format (CommonJS, ESNext, NodeNext, etc.) -- `moduleResolution` determines how import paths are resolved to files - -## Theory - -- Reference material for [Theory](https://www.typescriptlang.org/docs/handbook/modules/theory.html) - -### The Host - -TypeScript code always runs in a **host environment** that determines what global APIs are available and what module system is in use. Common hosts: Node.js, browsers, Deno, bundlers (Webpack, esbuild, Vite, Rollup). - -### Module Systems - -| System | Syntax | Used By | -|---|---|---| -| ESM (ECMAScript Modules) | `import` / `export` | Browsers, modern Node.js | -| CommonJS | `require()` / `module.exports` | Node.js (default) | -| AMD | `define()` | RequireJS | -| UMD | CJS + AMD fallback | Libraries | -| SystemJS | `System.register` | Legacy bundlers | - -### How TypeScript Determines Module Format - -The `module` compiler option is the primary signal. For Node.js, the `package.json` `"type"` field and file extension also matter: - -```json -// package.json -{ "type": "module" } // .js files treated as ESM -{ "type": "commonjs" } // .js files treated as CJS (default) -``` - -File extension overrides `"type"` field: -- `.mjs` / `.mts` — always ESM -- `.cjs` / `.cts` — always CJS -- `.js` / `.ts` — determined by `"type"` field - -### Module Resolution - -Controls how TypeScript resolves module specifiers to files: - -```ts -// With moduleResolution: "nodenext" — extension required -import { foo } from "./foo.js"; - -// With moduleResolution: "bundler" — extension optional -import { foo } from "./foo"; -``` - -### Module Output - -TypeScript transpiles module syntax based on the `module` setting: - -```ts -// Input (TypeScript) -import { x } from "./mod"; -export const y = x + 1; -``` - -```js -// Output with module: "commonjs" -"use strict"; -const mod_1 = require("./mod"); -exports.y = mod_1.x + 1; -``` - -```js -// Output with module: "esnext" -import { x } from "./mod"; -export const y = x + 1; -``` - -### Important Notes - -```ts -// isolatedModules: true — each file must be independently transpilable (required by esbuild, Babel) -// verbatimModuleSyntax — TS 5.0+: import type is always erased; inline type imports allowed -import type { Foo } from "./foo"; // always erased -import { type Bar, baz } from "./bar"; // Bar erased, baz kept - -// A file with no imports/exports is a script — pollutes global scope -const x = 1; // global - -// Make it a module explicitly -export {}; // now isolated scope -``` - -### Structural Module Resolution - -TypeScript uses a two-pass approach: -1. **Syntactic analysis** — determine if a file is a module or script -2. **Semantic analysis** — resolve imports and type-check - -The module format affects how TypeScript handles: -- Top-level `await` (ESM only) -- `import.meta` (ESM only) -- `require()` calls (CJS only) -- Dynamic `import()` (available in both) - -## Reference - -- Reference material for [Reference](https://www.typescriptlang.org/docs/handbook/modules/reference.html) - -### `module` - -Specifies the module code generation format for emitted JavaScript. - -```json -{ - "compilerOptions": { - "module": "NodeNext" - } -} -``` - -| Value | Use Case | -|-------|----------| -| `CommonJS` | Traditional Node.js | -| `ESNext` / `ES2020` / `ES2022` | Modern bundlers | -| `Node16` / `NodeNext` | Modern Node.js with ESM support | -| `Preserve` | Pass-through (TS 5.4+); keeps input module syntax | -| `None` | No module system | -| `AMD` | AMD / RequireJS | -| `UMD` | Universal module (CJS + AMD) | -| `System` | SystemJS | - -### `moduleResolution` - -Controls how TypeScript resolves module imports. - -```json -{ "compilerOptions": { "moduleResolution": "bundler" } } -``` - -| Value | Description | -|-------|-------------| -| `node` | Mimics Node.js CommonJS resolution (legacy) | -| `node16` / `nodenext` | Node.js ESM + CJS hybrid resolution | -| `bundler` | For bundler tools (Vite, esbuild); extensionless imports allowed | -| `classic` | Legacy TypeScript behavior (not recommended) | - -### `baseUrl` - -Sets the base directory for resolving non-relative module names. - -```json -{ "compilerOptions": { "baseUrl": "./src" } } -``` - -```ts -// With baseUrl: "./src" -import { utils } from "utils/helpers"; // resolves to ./src/utils/helpers -``` - -### `paths` - -Maps module names to file locations. Requires `baseUrl`. - -```json -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@app/*": ["src/app/*"], - "@utils/*": ["src/utils/*"] - } - } -} -``` - -```ts -import { Component } from "@app/components"; // → src/app/components -import { debounce } from "@utils/debounce"; // → src/utils/debounce -``` - -Note: `paths` affects type resolution only. Bundlers need their own alias configuration. - -### `rootDirs` - -Treats multiple directories as a single virtual root for module resolution. - -```json -{ "compilerOptions": { "rootDirs": ["src", "generated"] } } -``` - -```ts -// src/views/main.ts can import from generated/views/ as if same folder -import { template } from "./templates"; // resolves to generated/views/templates.ts -``` - -### `resolveJsonModule` - -Allows importing `.json` files with full type inference. - -```ts -import config from "./config.json"; -console.log(config.apiUrl); // fully typed -``` - -### `allowSyntheticDefaultImports` - -Allows `import x from 'y'` when the module has no `default` export (type checking only — no emitted helpers). Automatically enabled when `esModuleInterop` is `true`. - -### `esModuleInterop` - -Emits `__importDefault` and `__importStar` helpers for CommonJS/ESM interop. Enables `allowSyntheticDefaultImports`. - -```ts -// Without esModuleInterop: -import * as fs from "fs"; - -// With esModuleInterop: true -import fs from "fs"; // cleaner default import for CJS modules -``` - -### `moduleDetection` - -Controls how TypeScript determines if a file is a module or script. - -| Value | Behavior | -|-------|----------| -| `auto` (default) | Files with `import`/`export` are modules | -| `force` | All files treated as modules | -| `legacy` | TypeScript 4.x behavior | - -### `allowImportingTsExtensions` - -Allows imports with `.ts`, `.tsx`, `.mts` extensions. Requires `noEmit` or `emitDeclarationOnly`. - -```ts -import { foo } from "./foo.ts"; // for bundler environments like Vite -``` - -### `verbatimModuleSyntax` (TS 5.0+) - -Enforces that `import type` is used for type-only imports. Simplifies interop by ensuring type imports are always erased. - -```ts -import type { Foo } from "./foo"; // always erased — must use import type -import { type Bar, baz } from "./bar"; // Bar is erased, baz is kept -``` - -### `resolvePackageJsonExports` / `resolvePackageJsonImports` - -Controls whether TypeScript respects `package.json` `exports` and `imports` fields during resolution. Enabled by default when `moduleResolution` is `node16`, `nodenext`, or `bundler`. - -## Choosing Compiler Options - -- Reference material for [Choosing Compiler Options](https://www.typescriptlang.org/docs/handbook/modules/guides/choosing-compiler-options.html) - -### Node.js (CommonJS) - -For traditional Node.js applications using `require`: - -```json -{ - "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "node", - "esModuleInterop": true, - "resolveJsonModule": true - } -} -``` - -### Node.js (ESM, Node 16+) - -For Node.js applications using native ES modules: - -```json -{ - "compilerOptions": { - "module": "Node16", - "moduleResolution": "Node16", - "esModuleInterop": true, - "resolveJsonModule": true - } -} -``` - -Important: `node16`/`nodenext` enforce strict ESM/CJS boundaries and require explicit file extensions in imports: - -```ts -// Required with nodenext -import { foo } from "./foo.js"; // .js extension required even for .ts source files -``` - -### Bundlers (Vite, esbuild, Webpack, Parcel) - -Recommended for most frontend or full-stack projects using a build tool: - -```json -{ - "compilerOptions": { - "module": "ESNext", - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "baseUrl": ".", - "paths": { "@/*": ["src/*"] } - } -} -``` - -The `bundler` mode (TS 5.0+) mirrors how bundlers actually resolve modules: -- Extensionless imports are allowed -- `package.json` `exports` field is respected -- Does not require `.js` extensions - -### Decision Guide - -| Environment | `module` | `moduleResolution` | -|---|---|---| -| Node.js CJS | `CommonJS` | `node` | -| Node.js ESM | `Node16` or `NodeNext` | `node16` or `nodenext` | -| Bundler (Vite, etc.) | `ESNext` | `bundler` | -| Library (dual CJS/ESM) | `NodeNext` | `nodenext` | - -Key notes: -- Avoid `"moduleResolution": "node"` with `"module": "ESNext"` — this is a common misconfiguration that does not reflect real runtime behavior -- Use `"verbatimModuleSyntax": true` in new projects to enforce correct type import syntax -- `"isolatedModules": true` is required when using esbuild, Babel, or SWC as the transpiler - -```json -{ - "compilerOptions": { - "module": "ESNext", - "moduleResolution": "bundler", - "verbatimModuleSyntax": true, - "isolatedModules": true - } -} -``` - -## ESM/CJS Interoperability - -- Reference material for [ESM/CJS Interoperability](https://www.typescriptlang.org/docs/handbook/modules/appendices/esm-cjs-interop.html) - -ESM and CommonJS have fundamentally different execution and export models. Understanding interop is essential when mixing module formats. - -### The Core Problem - -CommonJS exports a single value (`module.exports`). ESM has named exports and a default export. When ESM imports a CJS module, the entire `module.exports` object becomes the "namespace" — but what is the `default`? - -```ts -// CJS module (legacy.js) -module.exports = { foo: 1, bar: 2 }; - -// Importing from ESM — behavior differs by tool/flag -import legacy from "./legacy"; // legacy = { foo: 1, bar: 2 } (with esModuleInterop) -import * as legacy from "./legacy"; // namespace import -import { foo } from "./legacy"; // named import (may or may not work) -``` - -### `esModuleInterop` - -When enabled, TypeScript emits helper functions that make CJS default imports work correctly: - -```ts -// With esModuleInterop: true -import fs from "fs"; // OK — __importDefault helper wraps module.exports -import * as path from "path"; // OK — __importStar helper used - -// Without esModuleInterop (legacy approach) -import * as fs from "fs"; -const readFile = fs.readFile; // manual namespace import -``` - -The emitted helpers: -- `__importDefault(mod)` — wraps `module.exports` as `{ default: module.exports }` if not already ESM -- `__importStar(mod)` — creates a namespace object with `default` set to `module.exports` - -### Dynamic `import()` and CJS - -When using `"module": "NodeNext"`, dynamic import from a CJS file always returns a namespace object with a `default` property: - -```ts -// In a CJS file -const mod = await import("./esm-module.js"); -mod.default; // the default export -mod.namedExport; // a named export -``` - -### Named Exports from CJS - -Node.js (via static analysis) and bundlers can sometimes expose CJS object properties as named imports, but TypeScript types this conservatively: - -```ts -// someLib/index.js (CJS) -exports.foo = 1; -exports.bar = "hello"; - -// TypeScript with esModuleInterop -import { foo, bar } from "someLib"; // may work at runtime; TypeScript requires declaration -``` - -### `allowSyntheticDefaultImports` vs `esModuleInterop` - -| Option | Effect | -|---|---| -| `allowSyntheticDefaultImports` | Type-level only: suppresses errors for default imports from CJS modules | -| `esModuleInterop` | Emits runtime helpers + enables `allowSyntheticDefaultImports` | - -Use `esModuleInterop` for full correctness. Use `allowSyntheticDefaultImports` alone only when you know the runtime already handles interop (e.g., when using Babel or a bundler). - -### Interop in Node.js `node16`/`nodenext` - -With `"module": "NodeNext"`, TypeScript enforces strict boundaries: - -```ts -// .mts file (always ESM) — cannot use require() -import { readFile } from "fs/promises"; // OK - -// .cts file (always CJS) — cannot use top-level await -const { readFileSync } = require("fs"); // OK -``` - -```ts -// CJS importing ESM — NOT allowed in Node.js -// const mod = require("./esm-module.mjs"); // Error at runtime - -// ESM importing CJS — allowed -import cjsMod from "./cjs-module.cjs"; // OK -``` - -### Key Considerations - -- Always use `esModuleInterop: true` in new projects for correct CJS default import behavior -- When using `node16`/`nodenext`, be explicit with `.mts`/`.cts` extensions for files that must be ESM or CJS -- Bundlers (Vite, Webpack, esbuild) typically handle ESM/CJS interop transparently with `moduleResolution: "bundler"` -- Library authors targeting both CJS and ESM should use `"module": "NodeNext"` with dual package output diff --git a/skills/typescript-coder/references/typescript-quickstart.md b/skills/typescript-coder/references/typescript-quickstart.md deleted file mode 100644 index 9b7664c20..000000000 --- a/skills/typescript-coder/references/typescript-quickstart.md +++ /dev/null @@ -1,727 +0,0 @@ -# TypeScript Quick Start - -Quick start guides for TypeScript based on your background and experience. - -## JS to TS - -- Reference material for [JS to TS](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) - -TypeScript begins with the JavaScript you already know. If you write JavaScript, you already write TypeScript — TypeScript is a superset of JavaScript. - -### Step 1: Types by Inference - -TypeScript infers types automatically from your existing JavaScript patterns. No annotations needed to get started: - -```ts -// TypeScript infers: let helloWorld: string -let helloWorld = "Hello World"; - -// TypeScript infers: let count: number -let count = 42; - -// TypeScript infers: let flags: boolean[] -let flags = [true, false, true]; -``` - -### Step 2: Annotate Where Needed - -When TypeScript cannot infer a type — or when you want to be explicit about a contract — use type annotations: - -```ts -// Annotate function parameters and return types -function add(a: number, b: number): number { - return a + b; -} - -// Annotate variables explicitly -let userName: string = "Alice"; - -// Annotate objects with interfaces -interface User { - name: string; - id: number; -} - -const user: User = { - name: "Hayes", - id: 0, -}; -``` - -### Step 3: Composing Types with Unions and Generics - -**Union types** allow a value to be one of several types: - -```ts -type StringOrNumber = string | number; - -function formatId(id: string | number): string { - return `ID: ${id}`; -} - -// TypeScript narrows the type after a check -function printId(id: string | number) { - if (typeof id === "string") { - console.log(id.toUpperCase()); // TypeScript knows id is string - } else { - console.log(id.toFixed(0)); // TypeScript knows id is number - } -} -``` - -**Generics** make components reusable across types: - -```ts -// Generic function — works with any type T -function firstItem(arr: T[]): T | undefined { - return arr[0]; -} - -const first = firstItem([1, 2, 3]); // Type: number | undefined -const name = firstItem(["a", "b"]); // Type: string | undefined - -// Generic interface -interface ApiResponse { - data: T; - status: number; - message: string; -} -``` - -### Step 4: Structural Typing (Duck Typing) - -TypeScript checks **shapes**, not type names. If an object has all the required properties, it satisfies the type — no explicit declaration needed: - -```ts -interface Point { - x: number; - y: number; -} - -function logPoint(p: Point) { - console.log(`x=${p.x}, y=${p.y}`); -} - -// This plain object satisfies Point — no "implements" needed -const pt = { x: 12, y: 26 }; -logPoint(pt); // OK - -// Extra properties are fine -const pt3d = { x: 1, y: 2, z: 3 }; -logPoint(pt3d); // OK — only x and y are checked - -// Classes satisfy interfaces structurally too -class Coordinate { - constructor(public x: number, public y: number) {} -} -logPoint(new Coordinate(5, 10)); // OK -``` - -### Migrating from JavaScript - -When migrating an existing JavaScript project: - -1. Rename `.js` files to `.ts` one at a time -2. Add `tsconfig.json` with `"allowJs": true` to allow gradual migration -3. Fix type errors as you encounter them — or use `// @ts-ignore` temporarily -4. Remove `any` types progressively as you add proper type definitions - -```json -{ - "compilerOptions": { - "allowJs": true, - "checkJs": false, - "strict": false, - "noImplicitAny": false - } -} -``` - -Then tighten settings over time as the codebase is migrated. - ---- - -## New to Programming - -- Reference material for [New to Programming](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html) - -TypeScript is a great first language for programming because it catches mistakes before you run your code. - -### What TypeScript Does - -TypeScript is a **static type checker** for JavaScript. It reads your code and identifies errors _before_ you run it, similar to how a spell checker finds mistakes in text. - -```ts -// TypeScript catches this mistake before the program runs -const user = { - firstName: "Angela", - lastName: "Davis", - role: "Professor", -}; - -// Typo: "lastNme" instead of "lastName" -console.log(user.lastNme); -// Error: Property 'lastNme' does not exist on type '{ firstName: string; lastName: string; role: string; }' -// Did you mean 'lastName'? -``` - -Without TypeScript, this error only shows up at runtime. With TypeScript, it is caught immediately. - -### TypeScript is JavaScript with Types - -All JavaScript code is valid TypeScript. TypeScript adds an optional layer of type annotations on top: - -```ts -// Plain JavaScript — also valid TypeScript -function greet(name) { - return "Hello, " + name + "!"; -} - -// TypeScript — with a type annotation -function greet(name: string): string { - return "Hello, " + name + "!"; -} -``` - -The `: string` annotations tell TypeScript what type a variable or parameter should be. - -### Types Are Removed at Runtime - -TypeScript's types only exist during development. When TypeScript compiles to JavaScript, all type information is erased. The JavaScript that runs in the browser or Node.js has no type information: - -```ts -// TypeScript source -function add(a: number, b: number): number { - return a + b; -} -``` - -```js -// Compiled JavaScript output (types erased) -function add(a, b) { - return a + b; -} -``` - -### The TypeScript Compiler - -Install TypeScript and use the `tsc` compiler: - -```bash -# Install TypeScript -npm install -g typescript - -# Compile a TypeScript file to JavaScript -tsc myfile.ts - -# Watch for changes and recompile automatically -tsc --watch myfile.ts - -# Initialize a project configuration -tsc --init -``` - -### Core Type Concepts for Beginners - -**Primitive types:** - -```ts -let age: number = 25; -let name: string = "Alice"; -let isActive: boolean = true; -``` - -**Arrays:** - -```ts -let scores: number[] = [100, 95, 87]; -let names: string[] = ["Alice", "Bob", "Charlie"]; -``` - -**Functions:** - -```ts -function multiply(x: number, y: number): number { - return x * y; -} - -// Arrow function -const square = (n: number): number => n * n; -``` - -**Objects:** - -```ts -// Inline object type -let person: { name: string; age: number } = { - name: "Alice", - age: 30, -}; - -// Reusable interface -interface Product { - id: number; - name: string; - price: number; - inStock?: boolean; // optional property -} -``` - -### Why Use TypeScript? - -- **Catch errors early** — find bugs before shipping code -- **Better editor support** — autocompletion, rename, go-to-definition -- **Self-documenting code** — types communicate intent to other developers -- **Safer refactoring** — TypeScript tells you what breaks when you change code - ---- - -## OOP to JS - -- Reference material for [OOP to JS](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-oop.html) - -If you are coming from Java, C#, or another class-oriented language, TypeScript's type system works differently than you might expect. - -### Types are Structural, Not Nominal - -In Java and C#, two classes with the same methods are still different types. In TypeScript, two objects with the same shape are the same type — regardless of their name or class. - -```ts -class Cat { - meow() { - console.log("Meow!"); - } -} - -class Dog { - meow() { - console.log("...Meow?"); - } -} - -// No error — both classes have the same shape -let animal: Cat = new Dog(); -``` - -This is called **structural typing** (or duck typing): "if it walks like a duck and quacks like a duck, it's a duck." - -### Types as Sets - -TypeScript types are best understood as **sets of values**. A type is not a class or a unique identity — it is a description of what values are allowed. - -```ts -// This interface describes a set of objects that have x and y -interface Pointlike { - x: number; - y: number; -} - -// Any object with x: number and y: number belongs to this "set" -const p1: Pointlike = { x: 1, y: 2 }; // OK -const p2: Pointlike = { x: 5, y: 10, z: 0 }; // OK — extra properties allowed -``` - -### No Runtime Type Information for Interfaces - -TypeScript interfaces and type aliases are **compile-time only**. They are completely erased when compiled to JavaScript. You cannot use `instanceof` with interfaces: - -```ts -interface Serializable { - serialize(): string; -} - -// This does NOT work — interfaces are erased at runtime -function save(obj: unknown) { - if (obj instanceof Serializable) { // Error: 'Serializable' only refers to a type - obj.serialize(); - } -} - -// Instead, use discriminated unions or type guards -function isSerializable(obj: unknown): obj is Serializable { - return typeof obj === "object" && obj !== null && typeof (obj as any).serialize === "function"; -} -``` - -### Classes Still Work as Expected - -TypeScript classes do work with `instanceof` because they exist at runtime as JavaScript constructor functions: - -```ts -class Animal { - constructor(public name: string) {} - speak() { return `${this.name} makes a sound`; } -} - -class Dog extends Animal { - speak() { return `${this.name} barks`; } -} - -const d = new Dog("Rex"); -console.log(d instanceof Dog); // true -console.log(d instanceof Animal); // true -``` - -### Key Differences from Java/C# - -| Java/C# | TypeScript | -|---------|------------| -| Nominal typing (names matter) | Structural typing (shapes matter) | -| Types exist at runtime | Types erased at compile time | -| All types are classes or primitives | Functions and object literals are common | -| Checked exceptions | No checked exceptions | -| Enums are full types | Enums exist but union types are often preferred | -| Generics are reified at runtime | Generics are erased at compile time | -| `implements` required | `implements` optional — compatibility is structural | - -### Recommended TypeScript Patterns for OOP Developers - -```ts -// Prefer interfaces for data shapes -interface UserDto { - id: string; - name: string; - email: string; -} - -// Use union types instead of Java-style enums -type Status = "pending" | "active" | "inactive"; - -// Use discriminated unions instead of inheritance hierarchies -type Result = - | { success: true; data: T } - | { success: false; error: string }; - -function getUser(id: string): Result { - if (id === "") { - return { success: false, error: "ID cannot be empty" }; - } - return { success: true, data: { id, name: "Alice", email: "alice@example.com" } }; -} - -const result = getUser("123"); -if (result.success) { - console.log(result.data.name); // TypeScript knows data exists -} else { - console.log(result.error); // TypeScript knows error exists -} -``` - ---- - -## Functional to JS - -- Reference material for [Functional to JS](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html) - -If you are coming from Haskell, Elm, PureScript, or other functional languages, TypeScript has many familiar concepts but with key differences. - -### TypeScript's Type System is Structural - -Like Haskell's structural approach, TypeScript uses **structural subtyping**. If a type has all the required properties, it is assignable to the expected type — no explicit declaration needed: - -```ts -type Named = { name: string }; - -function greet(x: Named) { - return "Hello, " + x.name; -} - -// Any object with name: string satisfies Named -greet({ name: "Alice" }); // OK -greet({ name: "Bob", age: 30 }); // OK — extra field is fine -``` - -### Discriminated Unions (Algebraic Data Types) - -TypeScript's discriminated unions are the equivalent of algebraic data types in functional languages: - -```ts -// Similar to a Haskell ADT: -// data Shape = Circle Double | Square Double | Triangle Double Double - -type Shape = - | { kind: "circle"; radius: number } - | { kind: "square"; x: number } - | { kind: "triangle"; x: number; y: number }; - -function area(s: Shape): number { - switch (s.kind) { - case "circle": return Math.PI * s.radius ** 2; - case "square": return s.x ** 2; - case "triangle": return (s.x * s.y) / 2; - } - // TypeScript ensures exhaustiveness -} -``` - -### Unit Types (Literal Types) - -Like Haskell's unit types, TypeScript supports literal types as specific value types: - -```ts -type Bit = 0 | 1; -type Direction = "north" | "south" | "east" | "west"; -type Bool = true | false; // same as boolean - -// Narrowing refines the type -function move(dir: Direction, steps: number) { - if (dir === "north" || dir === "south") { - // dir is: "north" | "south" - console.log(`Moving vertically ${steps} steps`); - } -} -``` - -### `never` and `unknown` — Bottom and Top Types - -TypeScript has the full lattice of types: - -```ts -// unknown = top type — any value is assignable to unknown -let anything: unknown = 42; -anything = "hello"; -anything = { x: 1 }; - -// You must narrow before using unknown -function process(val: unknown) { - if (typeof val === "string") { - console.log(val.toUpperCase()); // OK after narrowing - } -} - -// never = bottom type — a value of this type never exists -function fail(msg: string): never { - throw new Error(msg); -} - -// Exhaustiveness checking with never -function assertNever(x: never): never { - throw new Error("Unexpected value: " + x); -} -``` - -### Immutability with `readonly` and `as const` - -TypeScript supports immutability at the type level: - -```ts -// readonly properties -interface Config { - readonly host: string; - readonly port: number; -} - -// readonly arrays -function sum(nums: readonly number[]): number { - return nums.reduce((a, b) => a + b, 0); -} - -// as const — deeply immutable, all values become literal types -const DIRECTIONS = ["north", "south", "east", "west"] as const; -type Direction = typeof DIRECTIONS[number]; // "north" | "south" | "east" | "west" -``` - -### Higher-Order Functions and Generic Types - -TypeScript supports higher-order functions with precise generic types: - -```ts -// Map with generic types -function map(arr: T[], fn: (item: T) => U): U[] { - return arr.map(fn); -} - -const lengths = map(["hello", "world"], (s) => s.length); // number[] - -// Compose functions with types -type Fn = (a: A) => B; - -function compose(f: Fn, g: Fn): Fn { - return (a) => f(g(a)); -} - -const toUpperLength = compose( - (s: string) => s.length, - (s: string) => s.toUpperCase() -); -console.log(toUpperLength("hello")); // 5 -``` - -### Mapped and Conditional Types - -TypeScript has type-level programming features similar to type classes and type families: - -```ts -// Mapped types (similar to functor over record fields) -type Nullable = { [K in keyof T]: T[K] | null }; -type Readonly = { readonly [K in keyof T]: T[K] }; - -// Conditional types (type-level if-then-else) -type IsArray = T extends any[] ? true : false; -type A = IsArray; // true -type B = IsArray; // false - -// infer — type-level pattern matching -type ElementType = T extends (infer E)[] ? E : never; -type E = ElementType; // string -type N = ElementType; // never -``` - ---- - -## Installation - -- Reference material for [Installation](https://www.typescriptlang.org/download/) - -### Installing TypeScript via npm - -The recommended way to install TypeScript is as a local dev dependency in your project: - -```bash -# Install TypeScript as a project dev dependency (recommended) -npm install --save-dev typescript - -# Run the TypeScript compiler via npx -npx tsc -``` - -### Global Installation - -You can also install TypeScript globally for use across all projects: - -```bash -# Install globally -npm install -g typescript - -# Verify the installation -tsc --version -``` - -> Note: Global installation is convenient but local installation is preferred so each project can pin its own TypeScript version. - -### Using TypeScript in a Project - -**Initialize a new TypeScript project:** - -```bash -# Create a tsconfig.json with default settings -npx tsc --init -``` - -**Add TypeScript scripts to `package.json`:** - -```json -{ - "scripts": { - "build": "tsc", - "build:watch": "tsc --watch", - "typecheck": "tsc --noEmit" - } -} -``` - -**Compile your project:** - -```bash -# Compile using tsconfig.json -npx tsc - -# Compile a single file (bypasses tsconfig.json) -npx tsc index.ts - -# Type-check without emitting output -npx tsc --noEmit -``` - -### Installing Type Definitions - -Many JavaScript libraries ship without TypeScript types. Install type definitions from the `@types` namespace: - -```bash -# Types for Node.js -npm install --save-dev @types/node - -# Types for common libraries -npm install --save-dev @types/express -npm install --save-dev @types/lodash -npm install --save-dev @types/jest - -# Search for available type definitions -npm search @types/[library-name] -``` - -### Minimal `tsconfig.json` - -A minimal configuration to get started: - -```json -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "lib": ["ES2020"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} -``` - -### TypeScript with Popular Frameworks - -**React (with Vite):** - -```bash -npm create vite@latest my-app -- --template react-ts -cd my-app && npm install -``` - -**Next.js:** - -```bash -npx create-next-app@latest my-app --typescript -``` - -**Node.js (Express):** - -```bash -mkdir my-api && cd my-api -npm init -y -npm install express -npm install --save-dev typescript @types/node @types/express ts-node -npx tsc --init -``` - -**Angular:** - -```bash -npm install -g @angular/cli -ng new my-app # TypeScript is the default -``` - -### TypeScript Versions - -TypeScript follows semver. To pin a specific version: - -```bash -# Install a specific version -npm install --save-dev typescript@5.3.3 - -# Install the latest stable -npm install --save-dev typescript@latest - -# Install the next/beta version -npm install --save-dev typescript@next -``` - -Check the currently installed version: - -```bash -npx tsc --version -# Output: Version 5.x.x -``` diff --git a/skills/typescript-coder/references/typescript-releases.md b/skills/typescript-coder/references/typescript-releases.md deleted file mode 100644 index 3229bc090..000000000 --- a/skills/typescript-coder/references/typescript-releases.md +++ /dev/null @@ -1,269 +0,0 @@ -# TypeScript Release Notes - -Reference material for TypeScript release notes and new features by version. - ---- - -## TypeScript 6.0 - -- Reference: [Announcing TypeScript 6.0 Beta](https://devblogs.microsoft.com/typescript/announcing-typescript-6-0-beta/) - -TypeScript 6.0 represents a major version milestone. Key areas of focus include stricter module semantics, improved interoperability, and performance improvements in the compiler and language service. See the announcement blog post for the full list of features and breaking changes. - ---- - -## TypeScript 5.x Release Notes - -> [!IMPORTANT] -> Always check for newer versions, fetching [latest TypeScript](https://github.com/microsoft/TypeScript/releases/latest). - -The following are reference links to the official release notes for each TypeScript version: - -- [TypeScript 5.0](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html) -- [TypeScript 5.1](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-1.html) -- [TypeScript 5.2](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html) -- [TypeScript 5.3](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-3.html) -- [TypeScript 5.4](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-4.html) -- [TypeScript 5.5](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html) -- [TypeScript 5.6](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-6.html) -- [TypeScript 5.7](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-7.html) -- [TypeScript 5.8](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-8.html) -- [TypeScript 5.9](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-9.html) - ---- - -## TypeScript 5.8 Highlights - -- Reference: [Announcing TypeScript 5.8](https://devblogs.microsoft.com/typescript/announcing-typescript-5-8/) - -### `require()` of ECMAScript Modules - -TypeScript 5.8 adds support for `require()`-ing ES modules when targeting Node.js environments that support it (Node.js 22+). Controlled via `--module nodenext` and `--moduleResolution nodenext`. - -### Granular Checks for Branches in Return Expressions - -TypeScript now performs finer-grained analysis on expressions inside `return` statements. Previously, TypeScript treated the entire `return` expression as a single unit; now it can drill into individual branches of ternary expressions and short-circuit operators. - -```typescript -// TypeScript 5.8 can now narrow within the returned expression -function getLabel(value: string | number): string { - return typeof value === "string" ? value.toUpperCase() : value.toFixed(2); -} -``` - -### `--erasableSyntaxOnly` Flag - -A new compiler flag that errors if the TypeScript file contains any syntax that cannot be erased to produce valid JavaScript. This is useful for tools (like Node.js's built-in TypeScript support) that strip types without transforming syntax. - -Syntax that is NOT erasable (and would error under this flag): -- `enum` declarations (non-`const`) -- `namespace` / `module` declarations -- Parameter properties in constructors (`constructor(private x: number)`) -- Legacy decorators with `emitDecoratorMetadata` - -```typescript -// Error under --erasableSyntaxOnly: enums require a transform -enum Direction { - Up, - Down, -} -``` - -### `--libReplacement` Flag - -Allows replacing standard library files (like `lib.dom.d.ts`) with custom equivalents, useful for projects that target non-browser environments or want to swap in a third-party DOM type library. - ---- - -## TypeScript 5.7 Highlights - -- Reference: [Announcing TypeScript 5.7](https://devblogs.microsoft.com/typescript/announcing-typescript-5-7/) - -### Checks for Never-Initialized Variables - -TypeScript 5.7 detects variables that are declared but provably never assigned before use, even across complex control flow paths. - -```typescript -let value: string; -console.log(value); // Error: Variable 'value' is used before being assigned. -``` - -### Path Rewriting for Relative Imports in Emit - -When using `--rewriteRelativeImportExtensions`, TypeScript rewrites relative `.ts` import extensions to `.js` in emitted output. This is especially useful for Node.js ESM workflows where file extensions must be explicit. - -```typescript -// Source -import { helper } from "./utils.ts"; - -// Emitted (with rewriting enabled) -import { helper } from "./utils.js"; -``` - -### `--target ES2024` and `--lib ES2024` - -TypeScript 5.7 adds ES2024 as a valid `target` and `lib` value, covering new built-in APIs such as `Promise.withResolvers()`, `Object.groupBy()`, and `Map.groupBy()`. - -### Support for `V8 Compile Caching` - -When running under Node.js, TypeScript 5.7 can take advantage of V8's compile cache API to speed up repeated executions of the TypeScript compiler. - ---- - -## TypeScript 5.5 Highlights - -- Reference: [Announcing TypeScript 5.5](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/) - -### Inferred Type Predicates - -TypeScript 5.5 can now infer type predicate return types from function bodies, without requiring an explicit `is` annotation. This means `Array.prototype.filter` and similar patterns now narrow types correctly. - -```typescript -// Before 5.5 — required explicit annotation: -function isString(x: unknown): x is string { - return typeof x === "string"; -} - -// With 5.5 — inferred automatically: -function isString(x: unknown) { - return typeof x === "string"; // inferred return type: x is string -} - -const mixed: (string | number)[] = ["hello", 42, "world", 1]; -const strings = mixed.filter(isString); // string[] — correctly narrowed -``` - -### Control Flow Narrowing for Constant Indexed Accesses - -TypeScript 5.5 narrows the type of indexed accesses when the index is a constant value. - -```typescript -function process(obj: Record, key: string) { - if (typeof obj[key] === "string") { - obj[key].toUpperCase(); // Now correctly narrowed to string - } -} -``` - -### JSDoc `@import` Tag - -Allows importing types in `.js` files using JSDoc without requiring `import type` statements. - -```js -/** @import { SomeType } from "some-module" */ - -/** @param {SomeType} value */ -function doSomething(value) {} -``` - -### Regular Expression Syntax Checking - -TypeScript 5.5 validates regex literal syntax at compile time, catching invalid patterns early. - -```typescript -const pattern = /(?\w+)/; // OK -const bad = /(?P\w+)/; // Error: invalid named capture group syntax -``` - -### `isolatedDeclarations` Compiler Option - -A new option that requires all exported declarations to have explicit type annotations, making it possible for other tools to generate `.d.ts` files without running the TypeScript compiler. - -```typescript -// Error under isolatedDeclarations — return type must be explicit -export function add(a: number, b: number) { - return a + b; -} - -// OK -export function add(a: number, b: number): number { - return a + b; -} -``` - ---- - -## TypeScript 5.0 Highlights - -- Reference: [Announcing TypeScript 5.0](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/) - -### Decorators (Standard) - -TypeScript 5.0 implements the TC39 Stage 3 decorators proposal. The new decorator syntax is incompatible with the legacy `experimentalDecorators` option. - -```typescript -function logged(fn: Function, ctx: ClassMethodDecoratorContext) { - return function (this: unknown, ...args: unknown[]) { - console.log(`Calling ${String(ctx.name)}`); - return fn.call(this, ...args); - }; -} - -class Greeter { - @logged - greet() { - return "Hello!"; - } -} -``` - -### `const` Type Parameters - -The `const` modifier on type parameters infers literal (narrowed) types rather than widened types. - -```typescript -function identity(value: T): T { - return value; -} - -const result = identity({ x: 10, y: 20 }); -// result: { x: 10, y: 20 } (not { x: number, y: number }) -``` - -### Multiple Config File `extends` - -`tsconfig.json` can now extend multiple base configurations. - -```json -{ - "extends": ["@tsconfig/strictest", "./base.json"], - "compilerOptions": { - "outDir": "./dist" - } -} -``` - -### `--moduleResolution bundler` - -A new `moduleResolution` strategy optimized for modern bundlers (Vite, esbuild, Parcel). It allows importing TypeScript files with `.ts` extensions and resolves `package.json` `exports` fields. - -### `--verbatimModuleSyntax` - -Replaces `importsNotUsedAsValues` and `preserveValueImports`. Forces explicit `import type` for type-only imports, ensuring the emitted output matches the source exactly. - -```typescript -// Error — must use 'import type' for type-only imports -import { SomeType } from "./types"; - -// OK -import type { SomeType } from "./types"; -``` - -### `export type *` Syntax - -```typescript -export type * from "./types"; -export type * as Types from "./types"; -``` - -### All `enum`s Are Union Enums - -Enum members now participate in union type narrowing more reliably. - ---- - -## Additional Release Notes - -For a full index of TypeScript release notes across all versions, see the [TypeScript Handbook Release Notes overview](https://www.typescriptlang.org/docs/handbook/release-notes/overview.html). - -Blog announcements for all releases are published at [devblogs.microsoft.com/typescript](https://devblogs.microsoft.com/typescript/). diff --git a/skills/typescript-coder/references/typescript-tools.md b/skills/typescript-coder/references/typescript-tools.md deleted file mode 100644 index 250263070..000000000 --- a/skills/typescript-coder/references/typescript-tools.md +++ /dev/null @@ -1,241 +0,0 @@ -# TypeScript Tools - -Reference material for TypeScript developer tools — the TypeScript Playground, TSConfig reference, and related tooling. - ---- - -## TypeScript Playground - -- Reference material for [TypeScript Playground](https://www.typescriptlang.org/play/) - -The TypeScript Playground is an interactive, browser-based coding environment. It allows developers to write, run, and experiment with TypeScript code directly in the browser — no installation required. It serves as a sandbox for learning TypeScript, testing code snippets, reproducing bugs, and sharing code examples with others. - -### Key Features - -- **Code Editor** — A Monaco-based editor with full syntax highlighting and IntelliSense, supporting multiple open files/tabs and real-time error highlighting. -- **Compiler Configuration Panel** — Toggle compiler options (e.g., `strict`, `noImplicitAny`) via a TS Config panel. Supports boolean flags, dropdown selectors, and TypeScript version switching (release, beta, nightly). -- **Sidebar Panels** — Includes tabs for Errors (compiler diagnostics), Logs/Console (runtime output), AST Explorer (Abstract Syntax Tree visualization), and community Plugins. -- **Shareable URLs** — Code is encoded into the URL, making it easy to share examples or bug reports. -- **Examples Library** — A built-in collection of code samples organized by topic. - -### How to Use It - -1. Navigate to `typescriptlang.org/play` -2. Type or paste TypeScript code in the left editor pane -3. View output (compiled JavaScript, errors, AST) in the right sidebar -4. Adjust compiler options via the *TS Config* dropdown in the toolbar -5. Share your code by copying the URL - -### Example Use Case - -```typescript -// Test strict null checks in the Playground -function greet(name: string | null) { - console.log("Hello, " + name.toUpperCase()); // Error if strictNullChecks is on -} -``` - -With `strictNullChecks` enabled in the Config panel, the Playground immediately highlights the potential null dereference — useful for learning and debugging TypeScript's type system interactively. - ---- - -## TSConfig Reference - -- Reference material for [TSConfig Reference](https://www.typescriptlang.org/tsconfig/) - -The `tsconfig.json` file controls how TypeScript compiles your project. Below are the compiler options grouped by category. - -### Type Checking - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `strict` | boolean | `false` | Enables all strict type-checking options | -| `strictNullChecks` | boolean | `false` | `null` and `undefined` are distinct types | -| `strictFunctionTypes` | boolean | `false` | Stricter checking of function parameter types (contravariance) | -| `strictBindCallApply` | boolean | `false` | Strict checking for `bind`, `call`, `apply` | -| `strictPropertyInitialization` | boolean | `false` | Class properties must be initialized in the constructor | -| `noImplicitAny` | boolean | `false` | Error on expressions with an implicit `any` type | -| `noImplicitThis` | boolean | `false` | Error on `this` with an implicit `any` type | -| `useUnknownInCatchVariables` | boolean | `false` | Catch clause variables typed as `unknown` instead of `any` | -| `alwaysStrict` | boolean | `false` | Parse in strict mode and emit `"use strict"` | -| `noUnusedLocals` | boolean | `false` | Error on unused local variables | -| `noUnusedParameters` | boolean | `false` | Error on unused function parameters | -| `exactOptionalPropertyTypes` | boolean | `false` | Disallows assigning `undefined` to optional properties | -| `noImplicitReturns` | boolean | `false` | Error when not all code paths return a value | -| `noFallthroughCasesInSwitch` | boolean | `false` | Error on fallthrough switch cases | -| `noUncheckedIndexedAccess` | boolean | `false` | Index access types include `undefined` | -| `noImplicitOverride` | boolean | `false` | Require `override` keyword on overridden methods | -| `noPropertyAccessFromIndexSignature` | boolean | `false` | Require bracket notation for index-signature properties | -| `allowUnusedLabels` | boolean | — | Allow unused labels | -| `allowUnreachableCode` | boolean | — | Allow unreachable code | - -### Modules - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `module` | string | varies | Module system: `commonjs`, `es2015`, `esnext`, `node16`, `nodenext`, etc. | -| `moduleResolution` | string | varies | Resolution strategy: `node`, `bundler`, `node16`, `nodenext` | -| `baseUrl` | string | — | Base directory for non-relative module names | -| `paths` | object | — | Path mapping entries for module aliases | -| `rootDirs` | string[] | — | Multiple root directories merged at runtime | -| `typeRoots` | string[] | — | Directories to include type definitions from | -| `types` | string[] | — | Only include listed `@types` packages globally | -| `allowSyntheticDefaultImports` | boolean | varies | Allow default imports from modules without a default export | -| `esModuleInterop` | boolean | `false` | Emit `__esModule` helpers for CommonJS/ES module interop | -| `allowUmdGlobalAccess` | boolean | `false` | Allow accessing UMD globals from modules | -| `resolveJsonModule` | boolean | `false` | Enable importing `.json` files | -| `noResolve` | boolean | `false` | Disable resolving imports/triple-slash references | -| `allowImportingTsExtensions` | boolean | `false` | Allow imports with `.ts`/`.tsx` extensions | -| `resolvePackageJsonExports` | boolean | varies | Use `exports` field in `package.json` for resolution | -| `resolvePackageJsonImports` | boolean | varies | Use `imports` field in `package.json` for resolution | -| `verbatimModuleSyntax` | boolean | `false` | Enforce that import/export style matches the emitted output | - -### Emit - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `target` | string | `ES3` | Compilation target: `ES5`, `ES6`/`ES2015` ... `ESNext` | -| `lib` | string[] | varies | Built-in API declaration sets to include (e.g. `DOM`, `ES2020`) | -| `outDir` | string | — | Output directory for compiled files | -| `outFile` | string | — | Bundle all output into a single file | -| `rootDir` | string | — | Root of the input source files | -| `declaration` | boolean | `false` | Generate `.d.ts` declaration files | -| `declarationDir` | string | — | Output directory for `.d.ts` files | -| `declarationMap` | boolean | `false` | Generate source maps for `.d.ts` files | -| `emitDeclarationOnly` | boolean | `false` | Only emit `.d.ts` files; no JavaScript output | -| `sourceMap` | boolean | `false` | Generate `.js.map` source map files | -| `inlineSourceMap` | boolean | `false` | Include source maps inline in the JS output | -| `inlineSources` | boolean | `false` | Include TypeScript source in source maps | -| `removeComments` | boolean | `false` | Strip all comments from output | -| `noEmit` | boolean | `false` | Do not emit any output files (type-check only) | -| `noEmitOnError` | boolean | `false` | Skip emit if there are type errors | -| `importHelpers` | boolean | `false` | Import helper functions from `tslib` | -| `downlevelIteration` | boolean | `false` | Correct (but verbose) iteration for older compilation targets | -| `preserveConstEnums` | boolean | `false` | Keep `const enum` declarations in emitted output | -| `stripInternal` | boolean | — | Remove declarations marked `@internal` | - -### JavaScript Support - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `allowJs` | boolean | `false` | Allow `.js` files to be included in the project | -| `checkJs` | boolean | `false` | Enable type checking in `.js` files | -| `maxNodeModuleJsDepth` | number | `0` | Max depth for type-checking JS inside `node_modules` | - -### Interop Constraints - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `isolatedModules` | boolean | `false` | Ensure each file can be safely transpiled in isolation | -| `forceConsistentCasingInFileNames` | boolean | `false` | Disallow inconsistently-cased imports | -| `isolatedDeclarations` | boolean | `false` | Require explicit types for public API surface | -| `esModuleInterop` | boolean | `false` | See Modules section | - -### Language and Environment - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `experimentalDecorators` | boolean | `false` | Enable legacy TC39 Stage 2 decorator support | -| `emitDecoratorMetadata` | boolean | `false` | Emit design-time type metadata for decorated declarations | -| `jsx` | string | — | JSX mode: `preserve`, `react`, `react-jsx`, `react-jsxdev`, `react-native` | -| `jsxFactory` | string | `React.createElement` | JSX factory function name | -| `jsxFragmentFactory` | string | `React.Fragment` | JSX fragment factory name | -| `jsxImportSource` | string | `react` | Module specifier to import JSX factory from | -| `moduleDetection` | string | `auto` | How TypeScript determines whether a file is a module | -| `noLib` | boolean | `false` | Exclude the default `lib.d.ts` | -| `useDefineForClassFields` | boolean | varies | Use ECMAScript-standard class field semantics | - -### Projects - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `incremental` | boolean | varies | Save build info to disk for faster incremental rebuilds | -| `composite` | boolean | `false` | Enable project references | -| `tsBuildInfoFile` | string | `.tsbuildinfo` | Path to the incremental build info file | -| `disableSourceOfProjectReferenceRedirect` | boolean | `false` | Use `.d.ts` instead of source files for project references | -| `disableSolutionSearching` | boolean | `false` | Opt out of multi-project reference discovery | -| `disableReferencedProjectLoad` | boolean | `false` | Reduce the number of loaded projects in editor | - -### Output Formatting - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `noErrorTruncation` | boolean | `false` | Show complete (non-truncated) error messages | -| `preserveWatchOutput` | boolean | `false` | Keep previous output on screen in watch mode | -| `pretty` | boolean | `true` | Colorize and format diagnostic output | - -### Completeness - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `skipDefaultLibCheck` | boolean | `false` | Skip type checking of default `.d.ts` library files | -| `skipLibCheck` | boolean | `false` | Skip type checking of all `.d.ts` declaration files | - -### Common tsconfig.json Example - -```json -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "moduleResolution": "node", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} -``` - -### tsconfig.json for a React + Vite project - -```json -{ - "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} -``` - -### tsconfig.json for a Node.js library - -```json -{ - "compilerOptions": { - "target": "ES2022", - "module": "node16", - "moduleResolution": "node16", - "strict": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "./dist", - "rootDir": "./src", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] -} -``` diff --git a/skills/typescript-coder/references/typescript-tutorials.md b/skills/typescript-coder/references/typescript-tutorials.md deleted file mode 100644 index c753eef81..000000000 --- a/skills/typescript-coder/references/typescript-tutorials.md +++ /dev/null @@ -1,724 +0,0 @@ -# TypeScript Tutorials - -Step-by-step tutorials for using TypeScript in various frameworks and build tools. - ---- - -## ASP.NET Core - -- Reference: [TypeScript with ASP.NET Core](https://www.typescriptlang.org/docs/handbook/asp-net-core.html) - -### Prerequisites - -- [.NET SDK](https://dotnet.microsoft.com/download) -- [Node.js and npm](https://nodejs.org/) - -### 1. Create a New ASP.NET Core Project - -```bash -dotnet new web -o MyTypescriptApp -cd MyTypescriptApp -``` - -### 2. Add TypeScript - -```bash -npm init -y -npm install --save-dev typescript -``` - -### 3. Configure TypeScript (`tsconfig.json`) - -```json -{ - "compilerOptions": { - "target": "ES5", - "module": "commonjs", - "sourceMap": true, - "outDir": "./wwwroot/js" - }, - "include": [ - "./src/**/*" - ] -} -``` - -### 4. Create TypeScript Source File - -```bash -mkdir src -``` - -`src/app.ts`: - -```typescript -function sayHello(name: string): string { - return `Hello, ${name}!`; -} - -const message = sayHello("ASP.NET Core"); -console.log(message); -``` - -### 5. Compile TypeScript - -```bash -npx tsc -``` - -This outputs compiled JavaScript to `wwwroot/js/app.js`. - -### 6. Reference in a Razor Page (`Pages/Index.cshtml`) - -```html -@page -@model IndexModel - -

      TypeScript + ASP.NET Core

      - -@section Scripts { - -} -``` - -### 7. Watch Mode for Development - -```bash -npx tsc --watch -``` - -### 8. Integrate with MSBuild - -Add a build target to your `.csproj` file to compile TypeScript automatically before each .NET build: - -```xml - - - net8.0 - - - - - - -``` - -| Step | Tool | -|------|------| -| Project scaffold | `dotnet new web` | -| TypeScript install | `npm install typescript` | -| Config | `tsconfig.json` | -| Compile | `npx tsc` | -| Output | `wwwroot/js/` | - ---- - -## Migrating from JavaScript - -- Reference: [Migrating from JavaScript](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html) - -### 1. Initial Setup - -```bash -npm install --save-dev typescript -npx tsc --init -``` - -### 2. Base `tsconfig.json` for Gradual Migration - -Start permissive and tighten over time: - -```json -{ - "compilerOptions": { - "allowJs": true, - "checkJs": false, - "outDir": "./dist", - "strict": false, - "noImplicitAny": false - }, - "include": ["src/**/*"] -} -``` - -### 3. Gradual Migration Strategy - -**Phase 1 — Rename files one at a time:** - -``` -myFile.js → myFile.ts -``` - -**Phase 2 — Enable JS type checking:** - -```json -{ "checkJs": true } -``` - -**Phase 3 — Add type annotations incrementally:** - -```typescript -// Before (JavaScript) -function greet(name) { - return "Hello, " + name; -} - -// After (TypeScript) -function greet(name: string): string { - return "Hello, " + name; -} -``` - -### 4. Common Issues and Fixes - -**Implicit `any` errors:** - -```typescript -// Error: Parameter 'x' implicitly has an 'any' type -function add(x, y) { return x + y; } - -// Fix -function add(x: number, y: number): number { return x + y; } -``` - -**Missing type definitions for npm packages:** - -```bash -npm install --save-dev @types/lodash -npm install --save-dev @types/node -``` - -**Object shape errors:** - -```typescript -// Error: Property 'age' does not exist on type '{}' -const user = {}; -user.age = 25; - -// Fix: Define an interface -interface User { age: number; name: string; } -const user: User = { age: 25, name: "Alice" }; -``` - -**Module import issues:** - -```json -// tsconfig.json — add: -{ "esModuleInterop": true, "moduleResolution": "node" } -``` - -```typescript -// Then use default imports: -import express from 'express'; -``` - -### 5. Tightening `tsconfig.json` Over Time - -```json -{ - "compilerOptions": { - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "strictFunctionTypes": true, - "noUnusedLocals": true, - "noUnusedParameters": true - } -} -``` - -### 6. Best Practices - -| Practice | Description | -|---|---| -| Use `unknown` over `any` | Forces a type check before use | -| Avoid type assertions (`as`) | Prefer type guards instead | -| Leverage type inference | Don't annotate what TypeScript can infer | -| Use `interface` for object shapes | Extensible and readable | -| Migrate leaf files first | Files with no local dependencies are easiest to start with | - -**Type guard example:** - -```typescript -// Avoid: -const val = someValue as string; - -// Prefer: -function isString(val: unknown): val is string { - return typeof val === "string"; -} - -if (isString(someValue)) { - someValue.toUpperCase(); // safely narrowed -} -``` - ---- - -## Working with the DOM - -- Reference: [TypeScript DOM Manipulation](https://www.typescriptlang.org/docs/handbook/dom-manipulation.html) - -### Enable DOM Type Definitions - -DOM types come from `lib.dom.d.ts`. Enable them in `tsconfig.json`: - -```json -{ - "compilerOptions": { - "lib": ["ES2020", "DOM", "DOM.Iterable"] - } -} -``` - -### `getElementById` - -Returns `HTMLElement | null` — the `null` case must be handled: - -```typescript -// Type: HTMLElement | null -const el = document.getElementById("myDiv"); - -// Null check required -if (el) { - el.textContent = "Hello"; -} - -// Non-null assertion — use only when certain the element exists -const el2 = document.getElementById("root")!; -``` - -### `querySelector` and `querySelectorAll` - -```typescript -// Returns Element | null -const div = document.querySelector("div"); - -// Generic overload for specific element types -const input = document.querySelector("#username"); -if (input) { - console.log(input.value); // .value available on HTMLInputElement -} - -// querySelectorAll returns NodeListOf -const items = document.querySelectorAll("li"); -items.forEach(item => { - console.log(item.textContent); -}); -``` - -### HTMLElement Subtypes - -| Interface | Corresponding Element | Notable Properties | -|---|---|---| -| `HTMLInputElement` | `` | `.value`, `.checked`, `.type` | -| `HTMLAnchorElement` | `` | `.href`, `.target` | -| `HTMLImageElement` | `` | `.src`, `.alt`, `.width` | -| `HTMLFormElement` | `` | `.submit()`, `.reset()` | -| `HTMLButtonElement` | `