feat: initial commit — global Claude Code config
Tracked: CLAUDE.md, agents, skills, settings, memory. Ephemeral data (sessions, history, telemetry, tasks) excluded via .gitignore. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
|||||||
|
# Secrets
|
||||||
|
.credentials.json
|
||||||
|
|
||||||
|
# All session/ephemeral/runtime data
|
||||||
|
history.jsonl
|
||||||
|
session-env/
|
||||||
|
sessions/
|
||||||
|
debug/
|
||||||
|
cache/
|
||||||
|
downloads/
|
||||||
|
file-history/
|
||||||
|
paste-cache/
|
||||||
|
plans/
|
||||||
|
plugins/
|
||||||
|
backups/
|
||||||
|
agent-memory/
|
||||||
|
shell-snapshots/
|
||||||
|
tasks/
|
||||||
|
telemetry/
|
||||||
|
todos/
|
||||||
|
stats-cache.json
|
||||||
|
|
||||||
|
# Projects — only track memory subdirs
|
||||||
|
projects/*
|
||||||
|
!projects/-home-zuf-dev-code/
|
||||||
|
projects/-home-zuf-dev-code/*
|
||||||
|
!projects/-home-zuf-dev-code/memory/
|
||||||
|
|
||||||
|
# Settings local (may contain MCP tokens)
|
||||||
|
settings.local.json
|
||||||
|
|
||||||
|
# Skills build artifacts
|
||||||
|
skills/**/node_modules/
|
||||||
|
skills/**/__pycache__/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# Global Instructions
|
||||||
|
|
||||||
|
## AITM Task Manager (localhost:3333)
|
||||||
|
|
||||||
|
AITM (AI Task Manager) je centralni spravce tasku pro vsechny projekty. Bezi na `http://localhost:3333`.
|
||||||
|
|
||||||
|
Kdyz uzivatel chce vytvorit task, zadat praci, naplánovat zmenu, nebo rekne "vytvor task" / "pridej task" / "zarad do fronty" — pouzij AITM REST API.
|
||||||
|
|
||||||
|
### Vytvoreni tasku
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:3333/api/tasks \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data-binary @- <<'ENDJSON'
|
||||||
|
{
|
||||||
|
"title": "<kratky nazev anglicky, max 60 znaku>",
|
||||||
|
"prompt": "<detailni zadani — viz Jak psat prompt>",
|
||||||
|
"tabId": "<tabId>",
|
||||||
|
"projectId": "<projectId>",
|
||||||
|
"priority": "medium",
|
||||||
|
"useWorktree": true,
|
||||||
|
"usePipeline": true,
|
||||||
|
"autoMergeDev": true
|
||||||
|
}
|
||||||
|
ENDJSON
|
||||||
|
```
|
||||||
|
|
||||||
|
### Povinne parametry
|
||||||
|
|
||||||
|
| Parametr | Typ | Popis |
|
||||||
|
|----------|-----|-------|
|
||||||
|
| `title` | string | Kratky nazev (anglicky, 1 radek, max 60 znaku) |
|
||||||
|
| `prompt` | string | Detailni zadani pro AI agenta (anglicky) |
|
||||||
|
| `tabId` | string | Tab v AITM — urcuje projekt |
|
||||||
|
| `projectId` | string | Konkretni projekt v tabu |
|
||||||
|
| `priority` | string | `low` / `medium` / `high` |
|
||||||
|
| `useWorktree` | bool | `true` = vlastni branch + worktree (VZDY true) |
|
||||||
|
| `usePipeline` | bool | **`true`** = spusti 8-krokovou pipeline (branch→architect→code→review→fix→test→e2e→docs). **VZDY true** pokud uzivatel nerekne jinak |
|
||||||
|
| `autoMergeDev` | bool | `true` = po uspesne pipeline auto-merge do dev branch |
|
||||||
|
|
||||||
|
### Jak psat prompt (DULEZITE)
|
||||||
|
|
||||||
|
Prompt je to nejdulezitejsi — AI agent ho dostane jako jediny kontext. Musi byt:
|
||||||
|
|
||||||
|
1. **Anglicky** — vsechny prompty pis anglicky
|
||||||
|
2. **Zacni s "READ CLAUDE.md first"** — kazdy prompt musi zacinat timto, aby agent nacetl projektove konvence
|
||||||
|
3. **Konkretni soubory** — uved presne cesty k souborum ktere se maji vytvorit/zmenit
|
||||||
|
4. **Ocekavane chovani** — co ma kod delat, ne jak to implementovat (nech agenta rozhodnout)
|
||||||
|
5. **Verify prikazy** — na konci uved build/test prikazy pro overeni
|
||||||
|
6. **Self-contained** — prompt musi obsahovat VSECHNY potrebne detaily (barvy, texty, rozmery, API tvary). Agent NEMA pristup k externim dokumentum mimo projekt
|
||||||
|
|
||||||
|
#### Sablona promptu:
|
||||||
|
```
|
||||||
|
READ CLAUDE.md first for project conventions and build commands.
|
||||||
|
|
||||||
|
## What to do
|
||||||
|
<1-3 vety co je cilem>
|
||||||
|
|
||||||
|
## Files to create/modify
|
||||||
|
### lib/path/to/file.dart
|
||||||
|
<presny popis co soubor obsahuje — tridy, metody, typy>
|
||||||
|
|
||||||
|
### lib/path/to/another.dart
|
||||||
|
<...>
|
||||||
|
|
||||||
|
## Details
|
||||||
|
<specificke hodnoty: barvy, texty, rozmery, API endpointy, enumy>
|
||||||
|
|
||||||
|
## Delete .gitkeep from: <cesty>
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
<build/test prikazy>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Chyby ktere NEDELAT v promptu:
|
||||||
|
- Neodkazovat na soubory v JINEM repu/projektu (agent je nevidi)
|
||||||
|
- Nepsat cesky (agent pouziva anglicke instrukce)
|
||||||
|
- Nedavat vague zadani ("udelej to hezke") — bud konkretni
|
||||||
|
- Nezapominat na verify krok (flutter analyze, dotnet build, npm run check...)
|
||||||
|
- Nepouzivat JSON s newlines v `-d` parametru curlu — VZDY pouzij heredoc `--data-binary @- <<'ENDJSON'`
|
||||||
|
|
||||||
|
### Zjisteni stavu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vsechny tasky
|
||||||
|
curl -s http://localhost:3333/api/tasks | jq .
|
||||||
|
|
||||||
|
# Stav tabu
|
||||||
|
curl -s http://localhost:3333/api/status | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Taby a projekty
|
||||||
|
|
||||||
|
| tabId | projectId | Projekt | Stack | Default branch |
|
||||||
|
|-------|-----------|---------|-------|----------------|
|
||||||
|
| `cryptorobot` | `cr-fe` | RobotFE | Blazor | dev |
|
||||||
|
| `cryptorobot` | `cr-be` | RobotCBBE | .NET API | dev |
|
||||||
|
| `cryptorobot` | `cr-market` | MarketData | .NET Service | dev |
|
||||||
|
| `csai` | `csai-fe` | CSAI.Web | Blazor | dev |
|
||||||
|
| `csai` | `csai-be` | CSAI.Api | .NET API | dev |
|
||||||
|
| `centralstore` | `cs-web` | CS Web | Blazor | dev |
|
||||||
|
| `centralstore` | `cs-gateway` | CS Gateway | .NET API | dev |
|
||||||
|
| `centralstore` | `cs-platform` | CS Platform | Aspire | dev |
|
||||||
|
| `ctm` | `ctm-dashboard` | AITM Dashboard | Node/TS | dev |
|
||||||
|
| `tmm` | `tmm-app` | NudgeMobile | Flutter | dev |
|
||||||
|
| `ctm-web` | `ctm-web-app` | CTM-WEB | Vue.js | dev |
|
||||||
|
| `kittie` | `km-app` | NuggeMobile | Flutter | dev |
|
||||||
|
|
||||||
|
### Urceni projektu (DULEZITE)
|
||||||
|
|
||||||
|
"vytvor task" (bez diakritiky i s ni) = vytvor task do **stavajiciho projektu** ve kterem uzivatel prave pracuje.
|
||||||
|
|
||||||
|
Jak urcit `tabId` a `projectId`:
|
||||||
|
1. **Podivej se na CWD** (aktualni pracovni adresar) a porovnej s tabulkou nize
|
||||||
|
2. Pokud CWD odpovida nejakemu `root` nebo `root/path` z tabulky → pouzij ten tab+projekt
|
||||||
|
3. Pokud CWD neodpovida zadnemu projektu nebo je nejednoznacny → **ZEPTEJ SE** uzivatele do ktereho projektu task patri
|
||||||
|
|
||||||
|
| CWD obsahuje | tabId | projectId |
|
||||||
|
|-------------|-------|-----------|
|
||||||
|
| `/OTCryptoRobot/RobotFE` nebo `/OTCryptoRobot` (bez podslozky) | `cryptorobot` | `cr-fe` |
|
||||||
|
| `/OTCryptoRobot/RobotCBBE` | `cryptorobot` | `cr-be` |
|
||||||
|
| `/OTCryptoRobot/MarketDataService` | `cryptorobot` | `cr-market` |
|
||||||
|
| `/CSAI/CSAI.Web` | `csai` | `csai-fe` |
|
||||||
|
| `/CSAI/CSAI.Api` nebo `/CSAI` (bez podslozky) | `csai` | `csai-be` |
|
||||||
|
| `/CentralStore` | `centralstore` | `cs-web` |
|
||||||
|
| `/AITM` nebo `/ClaudeTaskManager` | `ctm` | `ctm-dashboard` |
|
||||||
|
| `/TaskManagerMobile` | `tmm` | `tmm-app` |
|
||||||
|
| `/CTM-WEB` | `ctm-web` | `ctm-web-app` |
|
||||||
|
| `/NuggeMobile` | `kittie` | `km-app` |
|
||||||
|
|
||||||
|
Pokud tab ma vice projektu a z CWD neni jasne ktery (napr. root `/OTCryptoRobot` bez konkretni podslozky), zeptej se: "Do ktereho projektu? RobotFE, RobotCBBE, nebo MarketData?"
|
||||||
|
|
||||||
|
### Pravidla
|
||||||
|
|
||||||
|
- `useWorktree: true` — VZDY
|
||||||
|
- `usePipeline: true` — VZDY (pokud uzivatel explicitne nerekne "bez pipeline")
|
||||||
|
- `autoMergeDev: true` — VZDY (pokud uzivatel nerekne "bez merge")
|
||||||
|
- Pokud uzivatel nespecifikuje prioritu, pouzij `medium`
|
||||||
|
- Nikdy nerikej "Claude" — vzdy "AI"
|
||||||
|
- Pro JSON body VZDY pouzij heredoc format (`--data-binary @- <<'ENDJSON'`) — nikoliv `-d` s inline JSON (problemy s escapovanim)
|
||||||
|
|
||||||
|
### Priklad: jednoduchy task
|
||||||
|
|
||||||
|
Uzivatel v projektu CryptoRobot/RobotFE rekne: "Pridej cancellation token do vsech API callu"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:3333/api/tasks \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data-binary @- <<'ENDJSON'
|
||||||
|
{"title":"Add cancellation token to all API calls","prompt":"READ CLAUDE.md first.\n\nAdd CancellationToken parameter to all API call methods in RobotFE.\n\n## What to do\nFind all HttpClient calls and ensure they pass CancellationToken. Update service interfaces and implementations.\n\n## Files to modify\n- All files in Services/ that use HttpClient\n- Corresponding interfaces in Interfaces/\n\n## Verify\ndotnet build && dotnet test","tabId":"cryptorobot","projectId":"cr-fe","priority":"medium","useWorktree":true,"usePipeline":true,"autoMergeDev":true}
|
||||||
|
ENDJSON
|
||||||
|
```
|
||||||
|
|
||||||
|
### Priklad: vice tasku najednou
|
||||||
|
|
||||||
|
Pokud uzivatel chce rozdelit praci na vice tasku, vytvor je postupne curlem. AITM je zaradi do fronty a spusti sekvencne (1 task na tab v dany cas). Kazdy task musi byt self-contained — nesmi zaviset na tom ze predchozi task jeste nebyl mergnut.
|
||||||
|
|
||||||
|
### Pipeline kroky (8-step)
|
||||||
|
|
||||||
|
Kdyz `usePipeline: true`, task projde automaticky:
|
||||||
|
1. **branch** — vytvoreni worktree + branch z dev
|
||||||
|
2. **architect** — navrh implementace (plan)
|
||||||
|
3. **code** — implementace dle planu
|
||||||
|
4. **review** — code review
|
||||||
|
5. **fix** — oprava nalezených problemu
|
||||||
|
6. **test** — unit testy (paralelne s e2e)
|
||||||
|
7. **e2e** — end-to-end testy (paralelne s test)
|
||||||
|
8. **docs** — dokumentace
|
||||||
|
|
||||||
|
Po uspesnem dokonceni: auto-merge do dev + auto-archive.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: agent-pr
|
||||||
|
description: Handles pull requests - creation, review comments response, and code updates based on PR feedback
|
||||||
|
model: sonnet
|
||||||
|
color: green
|
||||||
|
---
|
||||||
|
|
||||||
|
# AgentPR - Pull Request Handler
|
||||||
|
|
||||||
|
You are a PR specialist for the OTCryptoRobot trading bot project (.NET 10, Blazor Server, Radzen UI, PostgreSQL, gRPC, SignalR).
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
|
||||||
|
1. **Create Pull Requests**
|
||||||
|
- Analyze all changes in the current branch vs base branch
|
||||||
|
- Write clear PR title (max 70 chars) and description
|
||||||
|
- Include summary of changes, affected microservices, and test plan
|
||||||
|
- Use `gh pr create` with proper formatting
|
||||||
|
|
||||||
|
2. **Respond to Review Comments**
|
||||||
|
- Read all PR review comments using `gh pr view` and `gh api`
|
||||||
|
- Understand the reviewer's concern
|
||||||
|
- Make requested code changes
|
||||||
|
- Reply to comments explaining what was changed
|
||||||
|
- Push updated commits
|
||||||
|
|
||||||
|
3. **PR Maintenance**
|
||||||
|
- Keep PR up to date with base branch (rebase if needed)
|
||||||
|
- Resolve merge conflicts
|
||||||
|
- Update PR description if scope changes
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Read `CLAUDE.md` for project context
|
||||||
|
2. Use `gh pr list` to find relevant PRs
|
||||||
|
3. Use `gh pr view <number>` for PR details
|
||||||
|
4. Use `gh api repos/{owner}/{repo}/pulls/{number}/comments` for review comments
|
||||||
|
5. Make changes, commit, push
|
||||||
|
6. Reply to comments using `gh api`
|
||||||
|
|
||||||
|
## Quality Standards
|
||||||
|
- PR descriptions must explain WHY, not just WHAT
|
||||||
|
- Identify which microservice(s) are affected (MarketDataService, TradingService, FE)
|
||||||
|
- Always run `dotnet build` before pushing
|
||||||
|
- Never force-push without checking with the user first
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: agent-review
|
||||||
|
description: Reviews code changes for quality, patterns, security, and best practices
|
||||||
|
model: sonnet
|
||||||
|
color: blue
|
||||||
|
---
|
||||||
|
|
||||||
|
# AgentReview - Code Review Specialist
|
||||||
|
|
||||||
|
You are a code review expert for the OTCryptoRobot trading bot (.NET 10, microservices, gRPC, SignalR).
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
|
||||||
|
1. **Code Quality Review**
|
||||||
|
- Check adherence to generic `<C, F>` currency patterns
|
||||||
|
- Verify Repository Pattern with DbContextFactory
|
||||||
|
- Check Orchestrator Pattern usage
|
||||||
|
- Verify DI registration in RegisterService.cs
|
||||||
|
- Look for code duplication
|
||||||
|
|
||||||
|
2. **Security Review**
|
||||||
|
- Check for credential leaks (Coinbase API keys)
|
||||||
|
- Verify input validation on API endpoints
|
||||||
|
- Check WebSocket security
|
||||||
|
- Review authentication on controllers
|
||||||
|
|
||||||
|
3. **Performance Review**
|
||||||
|
- Check for N+1 query problems in EF Core
|
||||||
|
- Verify async/await usage (critical for trading bot)
|
||||||
|
- Check CancellationToken propagation
|
||||||
|
- Review SignalR message sizes
|
||||||
|
- Check gRPC streaming patterns
|
||||||
|
|
||||||
|
4. **Architecture Review**
|
||||||
|
- Verify microservice boundaries (MarketDataService vs TradingService)
|
||||||
|
- Check inter-service communication patterns
|
||||||
|
- Verify no cross-service database access
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
For each issue found:
|
||||||
|
```
|
||||||
|
**[SEVERITY] file.cs:line - Brief description**
|
||||||
|
Problem: What's wrong
|
||||||
|
Suggestion: How to fix it
|
||||||
|
```
|
||||||
|
|
||||||
|
Severity levels:
|
||||||
|
- CRITICAL - Must fix before merge
|
||||||
|
- WARNING - Should fix
|
||||||
|
- SUGGESTION - Nice to have
|
||||||
|
- INFO - FYI
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: agent-test
|
||||||
|
description: Handles testing - runs tests, writes unit/integration tests, analyzes test failures
|
||||||
|
model: sonnet
|
||||||
|
color: orange
|
||||||
|
---
|
||||||
|
|
||||||
|
# AgentTest - Testing Specialist
|
||||||
|
|
||||||
|
You are a testing expert for the OTCryptoRobot trading bot (.NET 10, xUnit, Playwright).
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
|
||||||
|
1. **Run Tests**
|
||||||
|
- Execute unit tests: `dotnet test TestOnlyCoinBase/TestOnlyCoinBase.csproj`
|
||||||
|
- Execute E2E tests: `dotnet test PlaywrightTest/PlaywrightTest.csproj`
|
||||||
|
- Analyze test results and failures
|
||||||
|
|
||||||
|
2. **Write Unit Tests**
|
||||||
|
- Create tests in `TestOnlyCoinBase` project
|
||||||
|
- Use xUnit framework
|
||||||
|
- Mock external dependencies (Coinbase API, database)
|
||||||
|
- Test trading strategies, order management, wallet operations
|
||||||
|
|
||||||
|
3. **Trading-Specific Testing**
|
||||||
|
- Test order placement and tracking
|
||||||
|
- Test strategy calculations (Sharp, Iteration, Cup)
|
||||||
|
- Test WebSocket message handling
|
||||||
|
- Test gRPC client/server streaming
|
||||||
|
- Test SignalR hub methods
|
||||||
|
|
||||||
|
4. **Analyze Test Failures**
|
||||||
|
- Read test output and stack traces
|
||||||
|
- Check logs in `RobotCBBE/Logs/` and `MarketDataService/Logs/`
|
||||||
|
- Identify root cause of failures
|
||||||
|
|
||||||
|
## Quality Standards
|
||||||
|
- Test names follow: MethodName_Scenario_ExpectedResult
|
||||||
|
- Always test edge cases for financial calculations
|
||||||
|
- Mock Coinbase API calls (never hit real API in tests)
|
||||||
|
- Use InMemory database provider for repository tests
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
name: junior-file-organizer
|
||||||
|
description: Use this agent when you need to perform simple file and folder organization tasks such as renaming files, reorganizing directory structures, moving files between folders, or creating new organizational hierarchies. This agent is specifically designed for straightforward structural changes and should NOT be used for code refactoring, code modifications, or complex architectural changes. Examples of when to use:\n\n<example>\nContext: User wants to reorganize project files into a clearer structure.\nuser: "I have all my components in one folder. Can you organize them into feature-based subfolders?"\nassistant: "I'll use the junior-file-organizer agent to help reorganize your component structure into feature-based folders."\n<commentary>The user needs simple file organization without code changes, perfect for junior-file-organizer.</commentary>\n</example>\n\n<example>\nContext: User needs to rename multiple files to follow a naming convention.\nuser: "Can you rename all my test files to use the .test.js extension instead of .spec.js?"\nassistant: "Let me use the junior-file-organizer agent to rename your test files to the new convention."\n<commentary>Simple renaming task without code modification - ideal for junior-file-organizer.</commentary>\n</example>\n\n<example>\nContext: User wants to create a new folder structure for documentation.\nuser: "I need to set up a docs folder with subfolders for api, guides, and examples"\nassistant: "I'll use the junior-file-organizer agent to create that documentation folder structure for you."\n<commentary>Creating simple directory structure - straightforward task for junior-file-organizer.</commentary>\n</example>
|
||||||
|
model: haiku
|
||||||
|
color: yellow
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a Junior File Organization Specialist, an AI agent focused exclusively on simple file and folder management tasks. Your expertise lies in performing straightforward organizational changes while maintaining a cautious, methodical approach.
|
||||||
|
|
||||||
|
**Your Core Responsibilities:**
|
||||||
|
- Rename files and folders according to specified conventions
|
||||||
|
- Reorganize directory structures based on clear specifications
|
||||||
|
- Move files between folders to improve organization
|
||||||
|
- Create new folder hierarchies for better project structure
|
||||||
|
- Ensure file paths and references remain valid after changes
|
||||||
|
|
||||||
|
**Critical Boundaries - What You DO NOT Do:**
|
||||||
|
- You do NOT modify code content or logic
|
||||||
|
- You do NOT perform refactoring of any kind
|
||||||
|
- You do NOT make architectural decisions
|
||||||
|
- You do NOT handle complex multi-step transformations
|
||||||
|
- You do NOT make assumptions about code dependencies
|
||||||
|
|
||||||
|
**Your Working Methodology:**
|
||||||
|
|
||||||
|
1. **Understand First**: Before making any changes, clearly understand:
|
||||||
|
- What files/folders need to be affected
|
||||||
|
- What the desired end state looks like
|
||||||
|
- Whether any naming conventions should be followed
|
||||||
|
- If there are any constraints or requirements
|
||||||
|
|
||||||
|
2. **Ask When Uncertain**: If the request is ambiguous or could be interpreted multiple ways:
|
||||||
|
- Present 2-3 concrete options for how to proceed
|
||||||
|
- Explain the differences between the options
|
||||||
|
- Ask the user to choose or clarify their preference
|
||||||
|
- Never guess or assume what the user wants
|
||||||
|
|
||||||
|
3. **Plan Before Acting**: Before executing changes:
|
||||||
|
- List all files/folders that will be affected
|
||||||
|
- Describe the specific changes you'll make
|
||||||
|
- Identify any potential issues (like name conflicts)
|
||||||
|
- Get confirmation if the changes are significant
|
||||||
|
|
||||||
|
4. **Execute Carefully**: When performing changes:
|
||||||
|
- Make one logical change at a time
|
||||||
|
- Verify each change was successful
|
||||||
|
- Keep track of what you've done
|
||||||
|
- Report any errors or unexpected results immediately
|
||||||
|
|
||||||
|
5. **Verify Results**: After completing changes:
|
||||||
|
- Confirm all intended changes were made
|
||||||
|
- Check that no unintended side effects occurred
|
||||||
|
- Provide a summary of what was accomplished
|
||||||
|
|
||||||
|
**When to Ask for Clarification:**
|
||||||
|
- The request involves more than simple file/folder operations
|
||||||
|
- Multiple valid interpretations exist for the request
|
||||||
|
- The scope seems too large or complex for your capabilities
|
||||||
|
- You're unsure if a change might break something
|
||||||
|
- The request involves code modifications
|
||||||
|
|
||||||
|
**Example Scenarios You Handle Well:**
|
||||||
|
- "Rename all .jsx files to .tsx"
|
||||||
|
- "Move all test files into a __tests__ folder"
|
||||||
|
- "Create a folder structure: src/components, src/utils, src/hooks"
|
||||||
|
- "Organize images into subfolders by type (icons, photos, logos)"
|
||||||
|
|
||||||
|
**Example Scenarios You Should Decline:**
|
||||||
|
- "Refactor this component to use hooks" (code modification)
|
||||||
|
- "Reorganize the entire codebase architecture" (too complex)
|
||||||
|
- "Split this large file into smaller modules" (code refactoring)
|
||||||
|
- "Update all imports after moving files" (code modification)
|
||||||
|
|
||||||
|
**Your Communication Style:**
|
||||||
|
- Be clear and direct about what you can and cannot do
|
||||||
|
- When presenting options, make them concrete and actionable
|
||||||
|
- Explain your reasoning in simple terms
|
||||||
|
- Admit when something is beyond your scope
|
||||||
|
- Always confirm understanding before making changes
|
||||||
|
|
||||||
|
**Quality Assurance:**
|
||||||
|
- Double-check file paths before moving or renaming
|
||||||
|
- Ensure no naming conflicts will occur
|
||||||
|
- Verify you have the necessary permissions
|
||||||
|
- Test with a small subset first if dealing with many files
|
||||||
|
- Keep a mental log of changes for rollback if needed
|
||||||
|
|
||||||
|
Remember: You are a junior specialist focused on simple, safe organizational tasks. Your value comes from being reliable, careful, and knowing your limits. When in doubt, ask - it's always better to clarify than to make incorrect assumptions.
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
---
|
||||||
|
name: project-mapper
|
||||||
|
description: "Use this agent when you need to create, update, or regenerate a CLAUDE.md or similar project documentation file that maps the entire codebase into clearly defined sections and components. Use it when onboarding a new project, after significant structural changes, or when the existing documentation doesn't adequately describe where code lives. Also use it when the user asks to 'map the project', 'create documentation for Claude', 'update CLAUDE.md', or mentions wanting Claude to understand the project structure better.\\n\\nExamples:\\n\\n- user: \"Zmapuj projekt a vytvoř CLAUDE.md\"\\n assistant: \"I'm going to use the Agent tool to launch the project-mapper agent to analyze the codebase and create a comprehensive CLAUDE.md file.\"\\n\\n- user: \"Přidal jsem nový modul pro platby, aktualizuj dokumentaci\"\\n assistant: \"Let me use the project-mapper agent to scan the new payment module and update the project documentation with the new section.\"\\n\\n- user: \"Claude se nevyzná v projektu, potřebuju lepší readme\"\\n assistant: \"I'll use the project-mapper agent to analyze the entire project structure and create a well-organized CLAUDE.md that maps every component.\"\\n\\n- user: \"BE - Task\" (after project-mapper has run)\\n assistant: \"Based on the project map, BE - Task refers to the backend task management system located in src/server.ts, specifically the task queue, task execution pipeline, and task persistence in tasks.json. Key socket events: task:create, task:update, task:delete, task:run.\"\\n\\n- user: \"Vypiš sekce projektu\"\\n assistant: \"I'll use the project-mapper agent to list all mapped sections of the current project.\""
|
||||||
|
model: sonnet
|
||||||
|
color: yellow
|
||||||
|
memory: user
|
||||||
|
---
|
||||||
|
|
||||||
|
You are an elite project documentation architect specializing in creating concise, token-efficient project maps for AI assistants (especially Claude). Your expertise is in analyzing codebases and producing structured CLAUDE.md files that allow any AI to instantly understand where every piece of functionality lives.
|
||||||
|
|
||||||
|
## Your Core Mission
|
||||||
|
|
||||||
|
Analyze the project's source code, directory structure, configuration files, and existing documentation to produce a **section-based project map** in Markdown format. The map must be:
|
||||||
|
|
||||||
|
1. **Concise** — No verbose explanations. Short, precise descriptions.
|
||||||
|
2. **Section-based** — Every logical part of the project gets its own labeled section with a short code (e.g., `BE-TASK`, `FE-DASH`).
|
||||||
|
3. **Location-aware** — Every section specifies exact file paths and line ranges where relevant.
|
||||||
|
4. **Token-efficient** — The document should minimize tokens while maximizing Claude's understanding.
|
||||||
|
|
||||||
|
## Section Format
|
||||||
|
|
||||||
|
Each section should follow this template:
|
||||||
|
|
||||||
|
```
|
||||||
|
## [CODE] Section Name
|
||||||
|
- **What**: One-line description of what this component does
|
||||||
|
- **Where**: File paths (and key functions/classes if applicable)
|
||||||
|
- **Key files**:
|
||||||
|
- `path/to/file.ts` — brief purpose
|
||||||
|
- `path/to/other.ts` — brief purpose
|
||||||
|
- **Depends on**: [OTHER-CODE], [ANOTHER-CODE]
|
||||||
|
- **Socket/API events**: (if applicable) event names
|
||||||
|
- **Notes**: Any critical gotchas or patterns (optional, only if important)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Section Code Convention
|
||||||
|
|
||||||
|
Use short, memorable codes:
|
||||||
|
- `BE-*` for backend (e.g., `BE-TASK`, `BE-PIPE`, `BE-GIT`, `BE-TEST`, `BE-SOCK`)
|
||||||
|
- `FE-*` for frontend (e.g., `FE-DASH`, `FE-TERM`, `FE-MODAL`, `FE-SETTINGS`)
|
||||||
|
- `CFG-*` for configuration (e.g., `CFG-MAIN`, `CFG-BUILD`)
|
||||||
|
- `AGENT-*` for agent definitions
|
||||||
|
- `TYPE-*` for type definitions
|
||||||
|
- `DOC-*` for documentation
|
||||||
|
- `INFRA-*` for infrastructure (systemd, deploy, etc.)
|
||||||
|
|
||||||
|
## How to Analyze a Project
|
||||||
|
|
||||||
|
1. **Read the directory tree** — Use file listing to understand the full structure.
|
||||||
|
2. **Read existing documentation** — Check for CLAUDE.md, README.md, package.json, config files.
|
||||||
|
3. **Scan source files** — Read key source files to understand what each module does. Focus on:
|
||||||
|
- Entry points (main/index files)
|
||||||
|
- Route definitions / API endpoints
|
||||||
|
- Socket event handlers
|
||||||
|
- Component structure (frontend)
|
||||||
|
- Type definitions (understand the domain model)
|
||||||
|
- Configuration files
|
||||||
|
4. **Identify logical sections** — Group related files into sections.
|
||||||
|
5. **Map dependencies** — Note which sections depend on which.
|
||||||
|
6. **Generate the document** — Write the CLAUDE.md with all sections.
|
||||||
|
|
||||||
|
## Output Document Structure
|
||||||
|
|
||||||
|
The generated CLAUDE.md should have:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Project Name — Project Map
|
||||||
|
|
||||||
|
## Quick Reference (Section Codes)
|
||||||
|
| Code | Name | Key Path |
|
||||||
|
|------|------|----------|
|
||||||
|
| BE-TASK | Backend Task System | src/server.ts |
|
||||||
|
| FE-DASH | Frontend Dashboard | src/frontend/render.ts |
|
||||||
|
| ... | ... | ... |
|
||||||
|
|
||||||
|
## [Section details as described above]
|
||||||
|
|
||||||
|
## Shortcuts
|
||||||
|
When user says → They mean:
|
||||||
|
- "BE-TASK" → Backend task queue, execution, persistence
|
||||||
|
- "FE-DASH" → Frontend dashboard rendering
|
||||||
|
- ...
|
||||||
|
```
|
||||||
|
|
||||||
|
The **Quick Reference table** at the top is critical — it allows Claude to immediately print all sections on startup and lets the user reference any section by its short code.
|
||||||
|
|
||||||
|
## Startup Behavior
|
||||||
|
|
||||||
|
When this document is loaded, Claude should be able to:
|
||||||
|
1. List all section codes and their one-line descriptions
|
||||||
|
2. When user mentions a code like `BE-TASK`, immediately know the exact files, functions, and purpose
|
||||||
|
3. Navigate directly to relevant code without scanning the whole project
|
||||||
|
|
||||||
|
## Best Practices from Top Project Documentation Approaches
|
||||||
|
|
||||||
|
- **Anthropic's recommended CLAUDE.md pattern**: Project overview, architecture, key commands, coding conventions — all in one file
|
||||||
|
- **Cursor/Windsurf rules pattern**: Section-based rules with clear triggers
|
||||||
|
- **Aider conventions pattern**: Repository map with file-to-purpose mapping
|
||||||
|
- **Key insight**: The best project docs are NOT comprehensive wikis — they are **lookup tables** that map concepts to locations
|
||||||
|
|
||||||
|
## Quality Checks
|
||||||
|
|
||||||
|
Before finalizing, verify:
|
||||||
|
- [ ] Every source directory has at least one section covering it
|
||||||
|
- [ ] No section is longer than 15 lines
|
||||||
|
- [ ] Every section has exact file paths
|
||||||
|
- [ ] The Quick Reference table is complete
|
||||||
|
- [ ] Section codes are consistent and memorable
|
||||||
|
- [ ] The document would save tokens compared to Claude scanning files itself
|
||||||
|
- [ ] Shortcuts section maps common user phrases to sections
|
||||||
|
|
||||||
|
## Important Rules
|
||||||
|
|
||||||
|
- **Never write long paragraphs.** Use bullet points and tables.
|
||||||
|
- **Never describe what code does in detail.** Just say what it IS and where it LIVES.
|
||||||
|
- **Always include file paths.** A section without paths is useless.
|
||||||
|
- **Keep the whole document under 300 lines** if possible. Brevity is the goal.
|
||||||
|
- **Use the project's own terminology** — don't invent new names for things.
|
||||||
|
- **If the project already has a CLAUDE.md**, preserve its useful content and enhance it with section mapping. Don't delete existing valuable instructions (like build commands, git setup, etc.).
|
||||||
|
|
||||||
|
**Update your agent memory** as you discover codebase structure, module boundaries, naming conventions, architectural patterns, and key file locations. This builds up institutional knowledge across conversations. Write concise notes about what you found and where.
|
||||||
|
|
||||||
|
Examples of what to record:
|
||||||
|
- New modules or components discovered and their section codes
|
||||||
|
- File reorganizations or renames
|
||||||
|
- Dependency relationships between sections
|
||||||
|
- Common patterns used across the codebase (e.g., Socket.IO event naming, component structure)
|
||||||
|
- Key entry points and their purposes
|
||||||
|
|
||||||
|
# Persistent Agent Memory
|
||||||
|
|
||||||
|
You have a persistent Persistent Agent Memory directory at `/home/zuf/.claude/agent-memory/project-mapper/`. Its contents persist across conversations.
|
||||||
|
|
||||||
|
As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned.
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise
|
||||||
|
- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md
|
||||||
|
- Update or remove memories that turn out to be wrong or outdated
|
||||||
|
- Organize memory semantically by topic, not chronologically
|
||||||
|
- Use the Write and Edit tools to update your memory files
|
||||||
|
|
||||||
|
What to save:
|
||||||
|
- Stable patterns and conventions confirmed across multiple interactions
|
||||||
|
- Key architectural decisions, important file paths, and project structure
|
||||||
|
- User preferences for workflow, tools, and communication style
|
||||||
|
- Solutions to recurring problems and debugging insights
|
||||||
|
|
||||||
|
What NOT to save:
|
||||||
|
- Session-specific context (current task details, in-progress work, temporary state)
|
||||||
|
- Information that might be incomplete — verify against project docs before writing
|
||||||
|
- Anything that duplicates or contradicts existing CLAUDE.md instructions
|
||||||
|
- Speculative or unverified conclusions from reading a single file
|
||||||
|
|
||||||
|
Explicit user requests:
|
||||||
|
- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions
|
||||||
|
- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files
|
||||||
|
- Since this memory is user-scope, keep learnings general since they apply across all projects
|
||||||
|
|
||||||
|
## MEMORY.md
|
||||||
|
|
||||||
|
Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: senior-architect-coder
|
||||||
|
description: Use this agent when you need to implement code that must strictly adhere to project architecture and coding standards defined in .md files (especially CLAUDE.md). This agent is ideal for:\n\n<example>\nContext: User needs to implement a new API endpoint following project standards.\nuser: "I need to add a POST endpoint for creating user profiles"\nassistant: "I'm going to use the senior-architect-coder agent to implement this endpoint according to our project's architecture standards."\n<Task tool call to senior-architect-coder>\n</example>\n\n<example>\nContext: User wants to refactor existing code to match project patterns.\nuser: "Can you refactor this authentication logic to follow our patterns?"\nassistant: "Let me use the senior-architect-coder agent to refactor this code according to our established architecture."\n<Task tool call to senior-architect-coder>\n</example>\n\n<example>\nContext: User is implementing a new feature module.\nuser: "I need to create a payment processing module"\nassistant: "I'll use the senior-architect-coder agent to design and implement this module following our project's architectural guidelines."\n<Task tool call to senior-architect-coder>\n</example>
|
||||||
|
model: sonnet
|
||||||
|
color: purple
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a Senior Software Architect and Developer with deep expertise in maintaining code quality and architectural consistency across complex projects.
|
||||||
|
|
||||||
|
Your primary responsibility is to write production-ready code that strictly adheres to the project's established architecture, patterns, and coding standards as defined in markdown documentation files (especially CLAUDE.md and other architectural documentation).
|
||||||
|
|
||||||
|
## Core Workflow:
|
||||||
|
|
||||||
|
1. **Always Read Architecture First**: Before writing any code, you MUST:
|
||||||
|
- Locate and read CLAUDE.md and any other relevant .md files in the project
|
||||||
|
- Understand the project structure, coding standards, naming conventions, and architectural patterns
|
||||||
|
- Identify any specific requirements, constraints, or preferences outlined in the documentation
|
||||||
|
- Note any technology stack requirements, design patterns, or best practices specified
|
||||||
|
|
||||||
|
2. **Analyze Requirements**: When given a task:
|
||||||
|
- Clarify the exact requirements if anything is ambiguous
|
||||||
|
- Ask specific, focused questions to gather missing information
|
||||||
|
- If you have a better approach or see potential issues, present options clearly
|
||||||
|
- Never assume - always verify when uncertain
|
||||||
|
|
||||||
|
3. **Code Implementation**: When writing code:
|
||||||
|
- Follow the exact patterns, conventions, and standards from the project documentation
|
||||||
|
- Write clean, maintainable, production-quality code
|
||||||
|
- Include only essential code - avoid lengthy example code to conserve tokens
|
||||||
|
- Add concise, meaningful comments only where necessary for clarity
|
||||||
|
- Ensure consistency with existing codebase patterns
|
||||||
|
|
||||||
|
4. **Communication Style**:
|
||||||
|
- Ask questions simply and clearly in Czech or English as appropriate
|
||||||
|
- Be direct and concise in your explanations
|
||||||
|
- When presenting options, clearly outline pros/cons of each approach
|
||||||
|
- Avoid verbose explanations - focus on actionable information
|
||||||
|
|
||||||
|
5. **Decision Making**:
|
||||||
|
- If multiple valid approaches exist, present them with brief rationale
|
||||||
|
- When you lack critical information, ask specific questions rather than making assumptions
|
||||||
|
- If project documentation conflicts with the request, point this out and ask for clarification
|
||||||
|
- Prioritize maintainability and consistency with existing patterns
|
||||||
|
|
||||||
|
## Quality Standards:
|
||||||
|
|
||||||
|
- Code must be production-ready, not prototype or example code
|
||||||
|
- Follow DRY (Don't Repeat Yourself) and SOLID principles unless project standards specify otherwise
|
||||||
|
- Ensure proper error handling appropriate to the project's patterns
|
||||||
|
- Write code that integrates seamlessly with existing architecture
|
||||||
|
- Consider performance, security, and scalability as defined in project standards
|
||||||
|
|
||||||
|
## Token Efficiency:
|
||||||
|
|
||||||
|
- Provide complete, working code but avoid unnecessary examples or demonstrations
|
||||||
|
- If showing a pattern, demonstrate it once concisely rather than multiple verbose examples
|
||||||
|
- Focus on the specific implementation needed, not general tutorials
|
||||||
|
- Use comments sparingly - only where code intent isn't self-evident
|
||||||
|
|
||||||
|
## When You Don't Know:
|
||||||
|
|
||||||
|
- Explicitly state what information you need
|
||||||
|
- Ask targeted questions to gather requirements
|
||||||
|
- If you have suggestions, present them as options with clear trade-offs
|
||||||
|
- Never guess at critical architectural decisions
|
||||||
|
|
||||||
|
Remember: Your value lies in creating code that perfectly fits the existing project architecture while maintaining high quality standards. Always prioritize consistency with project documentation and clarity in communication.
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Project Memory
|
||||||
|
|
||||||
|
## User: zuf (Ondrej Trochta)
|
||||||
|
- Git user: TrochtaOndrej / TrochtaOndrej@gmail.com
|
||||||
|
- Sudo: NOPASSWD configured via `/etc/sudoers.d/99-claude-nopasswd` — always use `sudo` directly, never ask for password
|
||||||
|
- Language: Czech preferred
|
||||||
|
|
||||||
|
## Server Environment
|
||||||
|
- Hostname: `zuf-Virtual-Machine` (VM)
|
||||||
|
- IP: 192.168.1.33 (LAN), Tailscale: 100.126.183.1
|
||||||
|
- mDNS: enabled via `/etc/systemd/resolved.conf.d/99-enable-mdns.conf`
|
||||||
|
- Accessible as: `zuf-Virtual-Machine.local` from LAN
|
||||||
|
|
||||||
|
## Projects in /home/zuf/dev/code/
|
||||||
|
- **CentralStore** — has `dev` branch, remote on GitHub
|
||||||
|
- **CSAI** — has `dev` branch (default), remote on GitHub
|
||||||
|
- **OTCryptoRobot** — has `dev` branch (default), remote on GitHub
|
||||||
|
- **ClaudeTaskManager (CTM)** — Node.js dashboard on port 3333, systemd service `claude-task-manager.service`
|
||||||
|
- **TaskManagerMobile (NudgeMobile)** — Flutter app (starší projekt), `dev` branch, remote gitea (`AI/TaskManagerMobile.git`)
|
||||||
|
- **NuggeMobile** — Flutter app (dříve KittieMobile), ADHD task manager s virtual pet, `dev` branch, remote gitea (`AI/KittieMobile.git`)
|
||||||
|
|
||||||
|
## AITM Task Creation — DULEZITE
|
||||||
|
- VZDY pouzit `usePipeline: true` a `autoMergeDev: true` (jinak task bezi jako plain session a nema auto-merge!)
|
||||||
|
- VZDY pouzit `useWorktree: true`
|
||||||
|
- VZDY pouzit heredoc format pro curl body (`--data-binary @- <<'ENDJSON'`) — nikoliv `-d` s inline JSON
|
||||||
|
- Prompt VZDY zacina "READ CLAUDE.md first" a je anglicky
|
||||||
|
- Prompt musi byt self-contained (vsechny detaily primo v promptu)
|
||||||
|
- Fix REST API: commit 2f3306c v CTM — RestApi.ts ted uklada usePipeline/autoMergeDev/skipSteps
|
||||||
|
- Monitor skripty: `ctm-monitor.mjs` (global, 30min zombie), `kittie-monitor.mjs` (tmm tab, 5min zombie)
|
||||||
|
|
||||||
|
## Parallel Task Execution — Fixes (commit c2a2901)
|
||||||
|
- **mergeTaskToDev**: async s mutex per tabRoot + push verification (viz `parallel-tasks-bugs.md`)
|
||||||
|
- **Session key**: worktree tasks pouzivaji `taskId` jako session key (ne `projectId`)
|
||||||
|
- **isTabBusy**: worktree tasky obchazeji isTabBusy check
|
||||||
|
- **pushRemote**: `"gitea"` v config.json (ne origin/GitHub)
|
||||||
|
- **task:archive**: akceptuje i stopped/failed (nejen finished)
|
||||||
|
|
||||||
|
## Workflow: TASK keyword = Full Dev Cycle
|
||||||
|
Když uživatel napíše **`TASK: popis tasku`**, provede se automaticky celý vývojový cyklus:
|
||||||
|
1. **Branch** — nová branch z `dev` (`feature/...` nebo `fix/...`)
|
||||||
|
2. **Implementace** — provedení tasku, commit
|
||||||
|
3. **Review** — self-review kódu (kvalita, SOLID, coding standards)
|
||||||
|
4. **Fix review** — oprava nalezených problémů, commit
|
||||||
|
5. **Test** — `dotnet build` + `dotnet test`, oprava padajících testů
|
||||||
|
6. **Merge** — merge do `dev`, smazání feature branch
|
||||||
|
|
||||||
|
Modifikátory (přidat za TASK):
|
||||||
|
- **"bez review"** — přeskočí kroky 3-4
|
||||||
|
- **"bez merge"** — zastaví se před mergem, čeká na souhlas
|
||||||
|
|
||||||
|
## Key Services (all on 0.0.0.0, accessible from LAN)
|
||||||
|
- CTM (ClaudeTaskManager): port 3333, systemd: `claude-task-manager.service`
|
||||||
|
- CSAI.Api: port 5208, systemd: `csai-api.service`
|
||||||
|
- CSAI.Web: port 5274, systemd: `csai-web.service`
|
||||||
|
- RobotFE: port 5000, systemd: `robot-fe.service`
|
||||||
|
- RobotCBBE: port 5010, systemd: `robot-cbbe.service`
|
||||||
|
- MarketDataService: ports 5020/5021 (already 0.0.0.0)
|
||||||
|
- CTM-WEB: port 8080, systemd: `ctm-web.service`
|
||||||
|
- n8n: port 5679 (localhost), systemd: `n8n.service`
|
||||||
|
- All systemd services are `--user` level, linger enabled
|
||||||
|
- dotnet path: `/home/zuf/.dotnet/dotnet`
|
||||||
|
|
||||||
|
## Skills a Agents - struktura
|
||||||
|
Skills a agents se nacitaji podle **CWD**. Globalni + projektove se kombinuji.
|
||||||
|
- Globalni (`~/.claude/skills/`, `~/.claude/agents/`): code-review, deploy, full-cycle, serilog-log-analyzer, status + 5 agentu
|
||||||
|
- OTCryptoRobot: cryptorobot-blazor-architect, cryptorobot-ef-expert
|
||||||
|
- CSAI: csai-blazor-architect, csai-ef-expert
|
||||||
|
|
||||||
|
## Subagenty - volba modelu (uspora nakladu)
|
||||||
|
- **haiku** - research, hledani souboru, jednoduche ukoly
|
||||||
|
- **sonnet** - code review, opravy kodu, merge branch, generovani kodu
|
||||||
|
- **opus** - jen hlavni konverzace a slozite architektonicke veci
|
||||||
|
- VZDY pouzivat nejlevnejsi model ktery zvladne dany ukol
|
||||||
|
|
||||||
|
## MCP servery - uspora tokenu
|
||||||
|
- Playwright a paretools-dotnet jsou **vypnute** v `~/.claude/settings.local.json`
|
||||||
|
- Kdyz uzivatel chce Playwright testy, upozornit ho: "Zapni Playwright pres `/mcp` a restartuj konverzaci"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Parallel Tasks — Bugs & Fixes (2026-03-18)
|
||||||
|
|
||||||
|
## Bug 1: Race condition in mergeTaskToDev (CRITICAL — DATA LOSS)
|
||||||
|
- **Fixed in**: commit `c2a2901` on dev
|
||||||
|
- **File**: `src/git/WorktreeManager.ts`
|
||||||
|
- **Root cause**: Multiple parallel tasks finishing at same time all call `mergeTaskToDev()` which does `git checkout dev` + `git merge` + `git push` — but concurrent checkouts corrupt working dir
|
||||||
|
- **Fix**: Async function with per-tabRoot mutex lock (Promise chain). Push verification before worktree/branch cleanup.
|
||||||
|
- **Evidence**: 3 tasks completed ($1.82-$2.43 each) but code lost — git reflog showed only `checkout: moving from dev to dev` with no merge commits
|
||||||
|
|
||||||
|
## Bug 2: Session key collision for parallel worktree tasks
|
||||||
|
- **Fixed in**: commit `c2a2901` on dev
|
||||||
|
- **File**: `src/sessions/SessionManager.ts`
|
||||||
|
- **Root cause**: Session key was always `projectId` — when multiple tasks share same projectId, each new session kills the previous via `killSession(ctx, projectId)`
|
||||||
|
- **Fix**: `sessionKey = (useWorktree && taskId) ? taskId : projectId` — worktree tasks use taskId as key
|
||||||
|
|
||||||
|
## Bug 3: task:archive only accepted finished tasks
|
||||||
|
- **Fixed in**: commit `c2a2901` on dev
|
||||||
|
- **File**: `src/socket/TaskSocketHandlers.ts`
|
||||||
|
- **Fix**: Archive now accepts `finished`, `stopped`, `failed` statuses
|
||||||
|
|
||||||
|
## Bug 4: pushRemote not configured
|
||||||
|
- **Fixed in**: commit `c2a2901` on dev
|
||||||
|
- **File**: `config.json`
|
||||||
|
- **Root cause**: No `pushRemote` field, defaulting to `origin` (GitHub) instead of `gitea`
|
||||||
|
- **Fix**: Added `"pushRemote": "gitea"` to config.json
|
||||||
|
|
||||||
|
## Bug 5: isTabBusy blocks parallel worktree tasks
|
||||||
|
- **Fixed in**: commit `c2a2901` on dev
|
||||||
|
- **File**: `src/socket/TaskSocketHandlers.ts`
|
||||||
|
- **Root cause**: `task:run` handler checks `isTabBusy()` and returns `queue-blocked` even for worktree tasks that can run independently
|
||||||
|
- **Fix**: Added `&& !useWorktree` condition to bypass check for worktree tasks
|
||||||
|
|
||||||
|
## Key Lesson
|
||||||
|
- Paralelní tasky s worktree MUSÍ mít:
|
||||||
|
1. Unikátní session key (taskId, ne projectId)
|
||||||
|
2. Serializované merge operace (mutex per tabRoot)
|
||||||
|
3. Push verification před smazáním worktree/branch
|
||||||
|
4. Správný pushRemote (gitea, ne origin)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(*)",
|
||||||
|
"Read(*)",
|
||||||
|
"Edit(*)",
|
||||||
|
"Write(*)",
|
||||||
|
"Glob(*)",
|
||||||
|
"Grep(*)",
|
||||||
|
"WebFetch(*)",
|
||||||
|
"WebSearch(*)",
|
||||||
|
"NotebookEdit(*)",
|
||||||
|
"mcp__playwright__*",
|
||||||
|
"mcp__nuget__*",
|
||||||
|
"mcp__dotnet__*",
|
||||||
|
"mcp__paretools-dotnet__*",
|
||||||
|
"Bash(pip list 2>/dev/null | grep -i kanban; pip3 list 2>/dev/null | grep -i kanban; echo \"---\"; pip list 2>/dev/null | grep -i vibe; pip3 list 2>/dev/null | grep -i vibe)",
|
||||||
|
"Bash(dpkg -l 2>/dev/null | grep -i kanban; echo \"---\"; snap list 2>/dev/null | grep -i kanban)",
|
||||||
|
"Bash(flatpak list 2>/dev/null | grep -i kanban; echo \"EXIT:$?\")",
|
||||||
|
"Bash(sqlite3 /home/zuf/.local/share/vibe-kanban/db.v2.sqlite \".tables\" 2>/dev/null | head -20)",
|
||||||
|
"Bash(sqlite3 /home/zuf/.local/share/vibe-kanban/db.v2.sqlite \"SELECT name FROM sqlite_master WHERE type='table';\" 2>/dev/null)",
|
||||||
|
"Bash(which sqlite3 2>/dev/null; apt list --installed 2>/dev/null | grep sqlite)",
|
||||||
|
"Bash(cp /home/zuf/.local/share/applications/vibe-kanban.desktop /home/zuf/Desktop/vibe-kanban.desktop && chmod +x /home/zuf/Desktop/vibe-kanban.desktop)",
|
||||||
|
"Bash(which snap flatpak apt dpkg 2>/dev/null; dpkg --print-architecture 2>/dev/null)",
|
||||||
|
"mcp__playwright__browser_click",
|
||||||
|
"mcp__playwright__browser_take_screenshot"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"defaultMode": "default"
|
||||||
|
},
|
||||||
|
"skipDangerousModePermissionPrompt": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: code-review
|
||||||
|
description: Performs thorough code review checking quality, security, patterns, and best practices for trading bot code
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code Review Skill
|
||||||
|
|
||||||
|
Performs comprehensive code review for the OTCryptoRobot trading bot.
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
When user asks to review code, check changes, or review a PR.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. **Identify Changes** - Run `git diff` to see changes
|
||||||
|
2. **Review Each File** - Check against patterns in CLAUDE.md
|
||||||
|
3. **Check Categories**
|
||||||
|
- [ ] Generic `<C, F>` pattern compliance
|
||||||
|
- [ ] Async/await correctness (critical for real-time trading)
|
||||||
|
- [ ] CancellationToken propagation
|
||||||
|
- [ ] EF Core patterns (AsNoTracking, DbContextFactory)
|
||||||
|
- [ ] DI registration in RegisterService.cs
|
||||||
|
- [ ] Error handling (no empty catch blocks)
|
||||||
|
- [ ] Security (API keys, credentials)
|
||||||
|
- [ ] Thread safety (singleton services)
|
||||||
|
- [ ] SignalR/gRPC patterns
|
||||||
|
4. **Output** - Summary with severity-rated issues
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
name: full-cycle
|
||||||
|
description: Full development cycle - branch, implement, review, fix, test, merge to dev
|
||||||
|
argument-hint: <task-description>
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Write
|
||||||
|
- Edit
|
||||||
|
- Glob
|
||||||
|
- Grep
|
||||||
|
- Agent
|
||||||
|
- TaskCreate
|
||||||
|
- TaskUpdate
|
||||||
|
- TaskList
|
||||||
|
---
|
||||||
|
|
||||||
|
# Full Development Cycle
|
||||||
|
|
||||||
|
Execute a complete development cycle for the given task: `$ARGUMENTS`
|
||||||
|
|
||||||
|
## Workflow Steps
|
||||||
|
|
||||||
|
### 1. Branch
|
||||||
|
- Create a feature/fix branch from `dev`: `git checkout dev && git pull && git checkout -b <type>/<short-name>`
|
||||||
|
- Branch naming: `feature/` for new features, `fix/` for bug fixes, `refactor/` for refactoring
|
||||||
|
|
||||||
|
### 2. Investigate
|
||||||
|
- Read CLAUDE.md and relevant codebase files
|
||||||
|
- Understand the current architecture and patterns
|
||||||
|
- Identify all files that need changes
|
||||||
|
|
||||||
|
### 3. Implement
|
||||||
|
- Make the necessary code changes
|
||||||
|
- Follow project conventions from CLAUDE.md (primary constructors, file-scoped namespaces, XML docs)
|
||||||
|
- Keep changes minimal and focused
|
||||||
|
|
||||||
|
### 4. Build & Verify
|
||||||
|
- Run `dotnet build OTCryptoRobot.sln` - must have 0 errors
|
||||||
|
- Check for new warnings in changed files
|
||||||
|
|
||||||
|
### 5. Self-Review
|
||||||
|
- Review all changes with `git diff`
|
||||||
|
- Check for: security issues, missing null checks, async/await correctness, proper error handling
|
||||||
|
- Fix any issues found
|
||||||
|
|
||||||
|
### 6. Test (if applicable)
|
||||||
|
- Run relevant tests: `dotnet test`
|
||||||
|
- If services need restart, use systemctl --user restart
|
||||||
|
|
||||||
|
### 7. Commit & Merge
|
||||||
|
- Stage specific files (not `git add -A`)
|
||||||
|
- Write descriptive commit message with Co-Authored-By
|
||||||
|
- Merge to dev: `git checkout dev && git merge <branch> --ff-only`
|
||||||
|
- Delete feature branch: `git branch -d <branch>`
|
||||||
|
|
||||||
|
## Important Rules
|
||||||
|
- Never push to remote without explicit user request
|
||||||
|
- Never use --force or --no-verify
|
||||||
|
- If build fails, fix and create NEW commit (don't amend)
|
||||||
|
- All I/O must be async, all public members need XML docs
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"name": "mastering-typescript",
|
||||||
|
"owner": "SpillwaveSolutions",
|
||||||
|
"repo": "mastering-typescript-skill",
|
||||||
|
"path": "mastering-typescript",
|
||||||
|
"branch": "main",
|
||||||
|
"sha": "12ab8354f759d283e6849a683f78e87ecc9dbb97",
|
||||||
|
"source": "manual"
|
||||||
|
}
|
||||||
@@ -0,0 +1,441 @@
|
|||||||
|
---
|
||||||
|
name: mastering-typescript
|
||||||
|
description: |
|
||||||
|
Master enterprise-grade TypeScript development with type-safe patterns, modern tooling, and framework integration. This skill provides comprehensive guidance for TypeScript 5.9+, covering type system fundamentals (generics, mapped types, conditional types, satisfies operator), enterprise patterns (error handling, validation with Zod), React integration for type-safe frontends, NestJS for scalable APIs, and LangChain.js for AI applications. Use when building type-safe applications, migrating JavaScript codebases, configuring modern toolchains (Vite 7, pnpm, ESLint, Vitest), implementing advanced type patterns, or comparing TypeScript with Java/Python approaches.
|
||||||
|
version: 1.0.0
|
||||||
|
category: programming-languages
|
||||||
|
triggers:
|
||||||
|
- typescript
|
||||||
|
- ts
|
||||||
|
- type-safe
|
||||||
|
- generics
|
||||||
|
- nestjs typescript
|
||||||
|
- react typescript
|
||||||
|
- typescript migration
|
||||||
|
- tsconfig
|
||||||
|
- type guards
|
||||||
|
- mapped types
|
||||||
|
- conditional types
|
||||||
|
- satisfies operator
|
||||||
|
- zod validation
|
||||||
|
author: Richard Hightower
|
||||||
|
license: MIT
|
||||||
|
tags:
|
||||||
|
- typescript
|
||||||
|
- type-safety
|
||||||
|
- enterprise
|
||||||
|
- react
|
||||||
|
- nestjs
|
||||||
|
- langchain
|
||||||
|
- vite
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mastering Modern TypeScript
|
||||||
|
|
||||||
|
Build enterprise-grade, type-safe applications with TypeScript 5.9+.
|
||||||
|
|
||||||
|
> **Compatibility:** TypeScript 5.9+, Node.js 22 LTS, Vite 7, NestJS 11, React 19
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize TypeScript project with ESM
|
||||||
|
pnpm create vite@latest my-app --template vanilla-ts
|
||||||
|
cd my-app && pnpm install
|
||||||
|
|
||||||
|
# Configure strict TypeScript
|
||||||
|
cat > tsconfig.json << 'EOF'
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2024",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Use when:
|
||||||
|
- Building type-safe React, NestJS, or Node.js applications
|
||||||
|
- Migrating JavaScript codebases to TypeScript
|
||||||
|
- Implementing advanced type patterns (generics, mapped types, conditional types)
|
||||||
|
- Configuring modern TypeScript toolchains (Vite, pnpm, ESLint)
|
||||||
|
- Designing type-safe API contracts with Zod validation
|
||||||
|
- Comparing TypeScript approaches with Java or Python
|
||||||
|
|
||||||
|
## Project Setup Checklist
|
||||||
|
|
||||||
|
Before starting any TypeScript project:
|
||||||
|
|
||||||
|
```
|
||||||
|
- [ ] Use pnpm for package management (faster, disk-efficient)
|
||||||
|
- [ ] Configure ESM-first (type: "module" in package.json)
|
||||||
|
- [ ] Enable strict mode in tsconfig.json
|
||||||
|
- [ ] Set up ESLint with @typescript-eslint
|
||||||
|
- [ ] Add Prettier for consistent formatting
|
||||||
|
- [ ] Configure Vitest for testing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type System Quick Reference
|
||||||
|
|
||||||
|
### Primitive Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const name: string = "Alice";
|
||||||
|
const age: number = 30;
|
||||||
|
const active: boolean = true;
|
||||||
|
const id: bigint = 9007199254740991n;
|
||||||
|
const key: symbol = Symbol("unique");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Union and Intersection Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Union: value can be one of several types
|
||||||
|
type Status = "pending" | "approved" | "rejected";
|
||||||
|
|
||||||
|
// Intersection: value must satisfy all types
|
||||||
|
type Employee = Person & { employeeId: string };
|
||||||
|
|
||||||
|
// Discriminated union for type-safe handling
|
||||||
|
type Result<T> =
|
||||||
|
| { success: true; data: T }
|
||||||
|
| { success: false; error: string };
|
||||||
|
|
||||||
|
function handleResult<T>(result: Result<T>): T | null {
|
||||||
|
if (result.success) {
|
||||||
|
return result.data; // TypeScript knows data exists here
|
||||||
|
}
|
||||||
|
console.error(result.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Guards
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// typeof guard
|
||||||
|
function process(value: string | number): string {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.toUpperCase();
|
||||||
|
}
|
||||||
|
return value.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom type guard
|
||||||
|
interface User { type: "user"; name: string }
|
||||||
|
interface Admin { type: "admin"; permissions: string[] }
|
||||||
|
|
||||||
|
function isAdmin(person: User | Admin): person is Admin {
|
||||||
|
return person.type === "admin";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### The `satisfies` Operator (TS 5.0+)
|
||||||
|
|
||||||
|
Validate type conformance while preserving inference:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Problem: Type assertion loses specific type info
|
||||||
|
const colors1 = {
|
||||||
|
red: "#ff0000",
|
||||||
|
green: "#00ff00"
|
||||||
|
} as Record<string, string>;
|
||||||
|
|
||||||
|
colors1.red.toUpperCase(); // OK, but red could be undefined
|
||||||
|
|
||||||
|
// Solution: satisfies preserves literal types
|
||||||
|
const colors2 = {
|
||||||
|
red: "#ff0000",
|
||||||
|
green: "#00ff00"
|
||||||
|
} satisfies Record<string, string>;
|
||||||
|
|
||||||
|
colors2.red.toUpperCase(); // OK, and TypeScript knows red exists
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generics Patterns
|
||||||
|
|
||||||
|
### Basic Generic Function
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function first<T>(items: T[]): T | undefined {
|
||||||
|
return items[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = first([1, 2, 3]); // number | undefined
|
||||||
|
const str = first(["a", "b"]); // string | undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
### Constrained Generics
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface HasLength {
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logLength<T extends HasLength>(item: T): T {
|
||||||
|
console.log(item.length);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
logLength("hello"); // OK: string has length
|
||||||
|
logLength([1, 2, 3]); // OK: array has length
|
||||||
|
logLength(42); // Error: number has no length
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic API Response Wrapper
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
data: T;
|
||||||
|
status: number;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUser(id: string): Promise<ApiResponse<User>> {
|
||||||
|
const response = await fetch(`/api/users/${id}`);
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
status: response.status,
|
||||||
|
timestamp: new Date()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utility Types Reference
|
||||||
|
|
||||||
|
| Type | Purpose | Example |
|
||||||
|
|------|---------|---------|
|
||||||
|
| `Partial<T>` | All properties optional | `Partial<User>` |
|
||||||
|
| `Required<T>` | All properties required | `Required<Config>` |
|
||||||
|
| `Pick<T, K>` | Select specific properties | `Pick<User, "id" \| "name">` |
|
||||||
|
| `Omit<T, K>` | Exclude specific properties | `Omit<User, "password">` |
|
||||||
|
| `Record<K, V>` | Object with typed keys/values | `Record<string, number>` |
|
||||||
|
| `ReturnType<F>` | Extract function return type | `ReturnType<typeof fn>` |
|
||||||
|
| `Parameters<F>` | Extract function parameters | `Parameters<typeof fn>` |
|
||||||
|
| `Awaited<T>` | Unwrap Promise type | `Awaited<Promise<User>>` |
|
||||||
|
|
||||||
|
## Conditional Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic conditional type
|
||||||
|
type IsString<T> = T extends string ? true : false;
|
||||||
|
|
||||||
|
// Extract array element type
|
||||||
|
type ArrayElement<T> = T extends (infer E)[] ? E : never;
|
||||||
|
|
||||||
|
type Numbers = ArrayElement<number[]>; // number
|
||||||
|
type Strings = ArrayElement<string[]>; // string
|
||||||
|
|
||||||
|
// Practical: Extract Promise result type
|
||||||
|
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mapped Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Make all properties readonly
|
||||||
|
type Immutable<T> = {
|
||||||
|
readonly [K in keyof T]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make all properties nullable
|
||||||
|
type Nullable<T> = {
|
||||||
|
[K in keyof T]: T[K] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create getter functions for each property
|
||||||
|
type Getters<T> = {
|
||||||
|
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Person { name: string; age: number }
|
||||||
|
type PersonGetters = Getters<Person>;
|
||||||
|
// { getName: () => string; getAge: () => number }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Framework Integration
|
||||||
|
|
||||||
|
### React with TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Typed functional component
|
||||||
|
interface ButtonProps {
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
variant?: "primary" | "secondary";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button: React.FC<ButtonProps> = ({ label, onClick, variant = "primary" }) => (
|
||||||
|
<button className={variant} onClick={onClick}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Typed hooks
|
||||||
|
const [count, setCount] = useState<number>(0);
|
||||||
|
const userRef = useRef<HTMLInputElement>(null);
|
||||||
|
```
|
||||||
|
|
||||||
|
### NestJS with TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Type-safe DTO with class-validator
|
||||||
|
import { IsString, IsEmail, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
class CreateUserDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(2)
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or with Zod (modern approach)
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const CreateUserSchema = z.object({
|
||||||
|
name: z.string().min(2),
|
||||||
|
email: z.string().email()
|
||||||
|
});
|
||||||
|
|
||||||
|
type CreateUserDto = z.infer<typeof CreateUserSchema>;
|
||||||
|
```
|
||||||
|
|
||||||
|
See [react-integration.md](references/react-integration.md) and [nestjs-integration.md](references/nestjs-integration.md) for detailed patterns.
|
||||||
|
|
||||||
|
## Validation with Zod
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Define schema
|
||||||
|
const UserSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string().min(1).max(100),
|
||||||
|
email: z.string().email(),
|
||||||
|
role: z.enum(["user", "admin", "moderator"]),
|
||||||
|
createdAt: z.coerce.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Infer TypeScript type from schema
|
||||||
|
type User = z.infer<typeof UserSchema>;
|
||||||
|
|
||||||
|
// Validate at runtime
|
||||||
|
function parseUser(data: unknown): User {
|
||||||
|
return UserSchema.parse(data); // Throws ZodError if invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe parsing (returns result object)
|
||||||
|
const result = UserSchema.safeParse(data);
|
||||||
|
if (result.success) {
|
||||||
|
console.log(result.data); // Typed as User
|
||||||
|
} else {
|
||||||
|
console.error(result.error.issues);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modern Toolchain (2025)
|
||||||
|
|
||||||
|
| Tool | Version | Purpose |
|
||||||
|
|------|---------|---------|
|
||||||
|
| TypeScript | 5.9+ | Type checking and compilation |
|
||||||
|
| Node.js | 22 LTS | Runtime environment |
|
||||||
|
| Vite | 7.x | Build tool and dev server |
|
||||||
|
| pnpm | 9.x | Package manager |
|
||||||
|
| ESLint | 9.x | Linting with flat config |
|
||||||
|
| Vitest | 3.x | Testing framework |
|
||||||
|
| Prettier | 3.x | Code formatting |
|
||||||
|
|
||||||
|
### ESLint Flat Config (ESLint 9+)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// eslint.config.js
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Strategies
|
||||||
|
|
||||||
|
### Incremental Migration
|
||||||
|
|
||||||
|
1. Add `allowJs: true` and `checkJs: false` to tsconfig.json
|
||||||
|
2. Rename files from `.js` to `.ts` one at a time
|
||||||
|
3. Add type annotations gradually
|
||||||
|
4. Enable stricter options incrementally
|
||||||
|
|
||||||
|
### JSDoc for Gradual Typing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Before full migration, use JSDoc
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {number} age
|
||||||
|
* @returns {User}
|
||||||
|
*/
|
||||||
|
function createUser(name, age) {
|
||||||
|
return { name, age };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See [enterprise-patterns.md](references/enterprise-patterns.md) for comprehensive migration guides.
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
| Mistake | Problem | Fix |
|
||||||
|
|---------|---------|-----|
|
||||||
|
| Using `any` liberally | Defeats type safety | Use `unknown` and narrow |
|
||||||
|
| Ignoring strict mode | Misses null/undefined bugs | Enable all strict options |
|
||||||
|
| Type assertions (`as`) | Can hide type errors | Use `satisfies` or guards |
|
||||||
|
| Enum for simple unions | Generates runtime code | Use literal unions instead |
|
||||||
|
| Not validating API data | Runtime type mismatches | Use Zod at boundaries |
|
||||||
|
|
||||||
|
## Cross-Language Comparison
|
||||||
|
|
||||||
|
| Feature | TypeScript | Java | Python |
|
||||||
|
|---------|------------|------|--------|
|
||||||
|
| Type System | Structural | Nominal | Gradual (duck typing) |
|
||||||
|
| Nullability | Explicit (`T \| null`) | `@Nullable` annotations | Optional via typing |
|
||||||
|
| Generics | Type-level, erased | Type-level, erased | Runtime via typing |
|
||||||
|
| Interfaces | Structural matching | Must implement | Protocol (3.8+) |
|
||||||
|
| Enums | Avoid (use unions) | First-class | Enum class |
|
||||||
|
|
||||||
|
## Reference Files
|
||||||
|
|
||||||
|
- [type-system.md](references/type-system.md) — Complete type system guide
|
||||||
|
- [generics.md](references/generics.md) — Advanced generics patterns
|
||||||
|
- [enterprise-patterns.md](references/enterprise-patterns.md) — Error handling, validation, architecture
|
||||||
|
- [react-integration.md](references/react-integration.md) — React + TypeScript patterns
|
||||||
|
- [nestjs-integration.md](references/nestjs-integration.md) — NestJS API development
|
||||||
|
- [toolchain.md](references/toolchain.md) — Modern build tools configuration
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
- [tsconfig-template.json](assets/tsconfig-template.json) — Strict enterprise config
|
||||||
|
- [eslint-template.js](assets/eslint-template.js) — ESLint 9 flat config
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
- [validate-setup.sh](scripts/validate-setup.sh) — Verify TypeScript environment
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// eslint.config.js - ESLint 9+ Flat Config for TypeScript
|
||||||
|
// Copy this file to your project root as eslint.config.js
|
||||||
|
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
// Base ESLint recommendations
|
||||||
|
eslint.configs.recommended,
|
||||||
|
|
||||||
|
// TypeScript strict type-checking
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// TypeScript parser configuration
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Custom TypeScript rules
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// Allow unused vars with underscore prefix
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', {
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_'
|
||||||
|
}],
|
||||||
|
|
||||||
|
// Enforce type imports for better tree-shaking
|
||||||
|
'@typescript-eslint/consistent-type-imports': ['error', {
|
||||||
|
prefer: 'type-imports',
|
||||||
|
fixStyle: 'inline-type-imports'
|
||||||
|
}],
|
||||||
|
|
||||||
|
// Prevent unhandled promises
|
||||||
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
|
'@typescript-eslint/no-misused-promises': 'error',
|
||||||
|
|
||||||
|
// Prevent awaiting non-promises
|
||||||
|
'@typescript-eslint/await-thenable': 'error',
|
||||||
|
|
||||||
|
// Prefer nullish coalescing
|
||||||
|
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||||
|
|
||||||
|
// Prefer optional chaining
|
||||||
|
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||||
|
|
||||||
|
// Consistent type assertions
|
||||||
|
'@typescript-eslint/consistent-type-assertions': ['error', {
|
||||||
|
assertionStyle: 'as',
|
||||||
|
objectLiteralTypeAssertions: 'never'
|
||||||
|
}],
|
||||||
|
|
||||||
|
// Naming conventions
|
||||||
|
'@typescript-eslint/naming-convention': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
selector: 'interface',
|
||||||
|
format: ['PascalCase']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'typeAlias',
|
||||||
|
format: ['PascalCase']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'enum',
|
||||||
|
format: ['PascalCase']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ignore patterns
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'dist/**',
|
||||||
|
'build/**',
|
||||||
|
'node_modules/**',
|
||||||
|
'coverage/**',
|
||||||
|
'*.config.js',
|
||||||
|
'*.config.ts'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
// Language and Environment
|
||||||
|
"target": "ES2024",
|
||||||
|
"lib": ["ES2024"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
|
||||||
|
// Strict Type Checking (Enterprise-Grade)
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
||||||
|
// Module Handling
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
|
||||||
|
// Output
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
|
||||||
|
// Path Aliases (adjust as needed)
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Performance
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"incremental": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.cache/tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,536 @@
|
|||||||
|
# Enterprise Patterns Reference
|
||||||
|
|
||||||
|
> **Load when:** User asks about error handling, validation, project architecture, migration strategies, or large-scale TypeScript patterns.
|
||||||
|
|
||||||
|
Proven patterns for building maintainable TypeScript applications.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
- [Validation Patterns](#validation-patterns)
|
||||||
|
- [Project Organization](#project-organization)
|
||||||
|
- [Migration Strategies](#migration-strategies)
|
||||||
|
- [Security Patterns](#security-patterns)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Result Type Pattern
|
||||||
|
|
||||||
|
Instead of throwing exceptions, return typed results:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Define Result type
|
||||||
|
type Result<T, E = Error> =
|
||||||
|
| { success: true; data: T }
|
||||||
|
| { success: false; error: E };
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function ok<T>(data: T): Result<T, never> {
|
||||||
|
return { success: true, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
function err<E>(error: E): Result<never, E> {
|
||||||
|
return { success: false, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
interface ValidationError {
|
||||||
|
field: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEmail(input: string): Result<string, ValidationError> {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
||||||
|
if (!emailRegex.test(input)) {
|
||||||
|
return err({ field: "email", message: "Invalid email format" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(input.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consuming Result
|
||||||
|
const result = parseEmail(userInput);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log(`Valid email: ${result.data}`);
|
||||||
|
} else {
|
||||||
|
console.error(`Error in ${result.error.field}: ${result.error.message}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typed Error Classes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Base application error
|
||||||
|
abstract class AppError extends Error {
|
||||||
|
abstract readonly code: string;
|
||||||
|
abstract readonly statusCode: number;
|
||||||
|
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific error types
|
||||||
|
class NotFoundError extends AppError {
|
||||||
|
readonly code = "NOT_FOUND";
|
||||||
|
readonly statusCode = 404;
|
||||||
|
|
||||||
|
constructor(resource: string, id: string) {
|
||||||
|
super(`${resource} with id ${id} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ValidationError extends AppError {
|
||||||
|
readonly code = "VALIDATION_ERROR";
|
||||||
|
readonly statusCode = 400;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly fields: Record<string, string[]>
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnauthorizedError extends AppError {
|
||||||
|
readonly code = "UNAUTHORIZED";
|
||||||
|
readonly statusCode = 401;
|
||||||
|
|
||||||
|
constructor(message = "Authentication required") {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type guard for app errors
|
||||||
|
function isAppError(error: unknown): error is AppError {
|
||||||
|
return error instanceof AppError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error handler
|
||||||
|
function handleError(error: unknown): { status: number; body: object } {
|
||||||
|
if (isAppError(error)) {
|
||||||
|
return {
|
||||||
|
status: error.statusCode,
|
||||||
|
body: {
|
||||||
|
code: error.code,
|
||||||
|
message: error.message,
|
||||||
|
...(error instanceof ValidationError && { fields: error.fields })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Unexpected error:", error);
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
body: { code: "INTERNAL_ERROR", message: "Internal server error" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation Patterns
|
||||||
|
|
||||||
|
### Zod Schema Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Define schemas
|
||||||
|
const UserSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string().min(1).max(100),
|
||||||
|
email: z.string().email(),
|
||||||
|
age: z.number().int().min(0).max(150).optional(),
|
||||||
|
role: z.enum(["user", "admin", "moderator"]),
|
||||||
|
metadata: z.record(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Infer TypeScript type
|
||||||
|
type User = z.infer<typeof UserSchema>;
|
||||||
|
|
||||||
|
// Create DTO schemas
|
||||||
|
const CreateUserSchema = UserSchema.omit({ id: true });
|
||||||
|
type CreateUserDto = z.infer<typeof CreateUserSchema>;
|
||||||
|
|
||||||
|
const UpdateUserSchema = UserSchema.partial().omit({ id: true });
|
||||||
|
type UpdateUserDto = z.infer<typeof UpdateUserSchema>;
|
||||||
|
|
||||||
|
// Validation functions
|
||||||
|
function validateCreateUser(data: unknown): Result<CreateUserDto, z.ZodError> {
|
||||||
|
const result = CreateUserSchema.safeParse(data);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return ok(result.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return err(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform Zod errors to user-friendly format
|
||||||
|
function formatZodError(error: z.ZodError): Record<string, string[]> {
|
||||||
|
const formatted: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
for (const issue of error.issues) {
|
||||||
|
const path = issue.path.join(".");
|
||||||
|
if (!formatted[path]) {
|
||||||
|
formatted[path] = [];
|
||||||
|
}
|
||||||
|
formatted[path].push(issue.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Branded Types for Validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Branded/Nominal types
|
||||||
|
declare const EmailBrand: unique symbol;
|
||||||
|
type Email = string & { readonly [EmailBrand]: true };
|
||||||
|
|
||||||
|
declare const UserIdBrand: unique symbol;
|
||||||
|
type UserId = string & { readonly [UserIdBrand]: true };
|
||||||
|
|
||||||
|
// Validation functions that return branded types
|
||||||
|
function validateEmail(input: string): Email {
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(input)) {
|
||||||
|
throw new ValidationError("Invalid email", { email: ["Invalid format"] });
|
||||||
|
}
|
||||||
|
return input as Email;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateUserId(input: string): UserId {
|
||||||
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
|
if (!uuidRegex.test(input)) {
|
||||||
|
throw new ValidationError("Invalid user ID", { id: ["Must be UUID"] });
|
||||||
|
}
|
||||||
|
return input as UserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage: Functions require validated types
|
||||||
|
function sendEmail(to: Email, subject: string): void {
|
||||||
|
// to is guaranteed to be valid email
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUser(id: UserId): Promise<User> {
|
||||||
|
// id is guaranteed to be valid UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compiler enforces validation
|
||||||
|
sendEmail("invalid", "Hello"); // Error: string not assignable to Email
|
||||||
|
sendEmail(validateEmail("a@b.com"), "Hello"); // OK
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Organization
|
||||||
|
|
||||||
|
### Feature-Based Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── features/
|
||||||
|
│ ├── users/
|
||||||
|
│ │ ├── index.ts # Public exports (barrel)
|
||||||
|
│ │ ├── user.types.ts # Types and interfaces
|
||||||
|
│ │ ├── user.schema.ts # Zod schemas
|
||||||
|
│ │ ├── user.service.ts # Business logic
|
||||||
|
│ │ ├── user.repository.ts # Data access
|
||||||
|
│ │ ├── user.controller.ts # HTTP handlers
|
||||||
|
│ │ └── __tests__/
|
||||||
|
│ │ ├── user.service.test.ts
|
||||||
|
│ │ └── user.controller.test.ts
|
||||||
|
│ ├── auth/
|
||||||
|
│ │ ├── index.ts
|
||||||
|
│ │ ├── auth.types.ts
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── posts/
|
||||||
|
│ └── ...
|
||||||
|
├── shared/
|
||||||
|
│ ├── types/
|
||||||
|
│ │ ├── result.ts
|
||||||
|
│ │ └── pagination.ts
|
||||||
|
│ ├── utils/
|
||||||
|
│ │ ├── validation.ts
|
||||||
|
│ │ └── date.ts
|
||||||
|
│ └── errors/
|
||||||
|
│ └── app-error.ts
|
||||||
|
├── infrastructure/
|
||||||
|
│ ├── database/
|
||||||
|
│ │ └── client.ts
|
||||||
|
│ ├── cache/
|
||||||
|
│ │ └── redis.ts
|
||||||
|
│ └── logging/
|
||||||
|
│ └── logger.ts
|
||||||
|
└── config/
|
||||||
|
├── index.ts
|
||||||
|
└── env.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Barrel Exports
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// features/users/index.ts
|
||||||
|
export type { User, CreateUserDto, UpdateUserDto } from './user.types';
|
||||||
|
export { UserSchema, CreateUserSchema } from './user.schema';
|
||||||
|
export { UserService } from './user.service';
|
||||||
|
export { UserController } from './user.controller';
|
||||||
|
|
||||||
|
// Don't export repository (internal detail)
|
||||||
|
// Don't export internal helper functions
|
||||||
|
|
||||||
|
// Usage in other modules
|
||||||
|
import { User, UserService } from '@/features/users';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Path Aliases
|
||||||
|
|
||||||
|
```json
|
||||||
|
// tsconfig.json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@features/*": ["src/features/*"],
|
||||||
|
"@shared/*": ["src/shared/*"],
|
||||||
|
"@config/*": ["src/config/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Strategies
|
||||||
|
|
||||||
|
### Incremental Migration from JavaScript
|
||||||
|
|
||||||
|
**Phase 1: Enable TypeScript alongside JavaScript**
|
||||||
|
|
||||||
|
```json
|
||||||
|
// tsconfig.json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": false,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": false,
|
||||||
|
"noImplicitAny": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Phase 2: Rename files gradually**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Convert one file at a time
|
||||||
|
mv src/utils/helpers.js src/utils/helpers.ts
|
||||||
|
|
||||||
|
# Add minimal type annotations
|
||||||
|
# Fix any type errors
|
||||||
|
# Run tests to verify
|
||||||
|
```
|
||||||
|
|
||||||
|
**Phase 3: Enable stricter checks incrementally**
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Progression of strict options
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Step 1: Basic strictness
|
||||||
|
"noImplicitAny": true,
|
||||||
|
|
||||||
|
// Step 2: Null safety
|
||||||
|
"strictNullChecks": true,
|
||||||
|
|
||||||
|
// Step 3: Full strict mode
|
||||||
|
"strict": true,
|
||||||
|
|
||||||
|
// Step 4: Extra safety (optional)
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSDoc for Gradual Typing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Before full migration, use JSDoc
|
||||||
|
/**
|
||||||
|
* @typedef {Object} User
|
||||||
|
* @property {string} id
|
||||||
|
* @property {string} name
|
||||||
|
* @property {string} email
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find user by ID
|
||||||
|
* @param {string} id - User ID
|
||||||
|
* @returns {Promise<User | null>}
|
||||||
|
*/
|
||||||
|
async function findUser(id) {
|
||||||
|
// implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {T[]} items
|
||||||
|
* @returns {T | undefined}
|
||||||
|
*/
|
||||||
|
function first(items) {
|
||||||
|
return items[0];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CommonJS to ESM Migration
|
||||||
|
|
||||||
|
```json
|
||||||
|
// package.json
|
||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before (CommonJS)
|
||||||
|
const express = require('express');
|
||||||
|
const { UserService } = require('./user.service');
|
||||||
|
module.exports = { router };
|
||||||
|
|
||||||
|
// After (ESM)
|
||||||
|
import express from 'express';
|
||||||
|
import { UserService } from './user.service.js'; // Note .js extension
|
||||||
|
export { router };
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Patterns
|
||||||
|
|
||||||
|
### Input Sanitization
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod';
|
||||||
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
|
||||||
|
// Sanitized string schema
|
||||||
|
const SanitizedString = z.string().transform((val) => {
|
||||||
|
return DOMPurify.sanitize(val.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
// HTML content schema (for rich text)
|
||||||
|
const HtmlContentSchema = z.string().transform((val) => {
|
||||||
|
return DOMPurify.sanitize(val, {
|
||||||
|
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
|
||||||
|
ALLOWED_ATTR: ['href', 'target']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// SQL-safe identifier
|
||||||
|
const SafeIdentifierSchema = z.string().regex(
|
||||||
|
/^[a-zA-Z_][a-zA-Z0-9_]*$/,
|
||||||
|
"Invalid identifier"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Safe Environment Variables
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const EnvSchema = z.object({
|
||||||
|
NODE_ENV: z.enum(['development', 'production', 'test']),
|
||||||
|
PORT: z.coerce.number().default(3000),
|
||||||
|
DATABASE_URL: z.string().url(),
|
||||||
|
JWT_SECRET: z.string().min(32),
|
||||||
|
REDIS_URL: z.string().url().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate on startup
|
||||||
|
function loadEnv() {
|
||||||
|
const result = EnvSchema.safeParse(process.env);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error('Invalid environment variables:');
|
||||||
|
console.error(result.error.format());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const env = loadEnv();
|
||||||
|
|
||||||
|
// Usage: fully typed
|
||||||
|
env.PORT; // number
|
||||||
|
env.NODE_ENV; // "development" | "production" | "test"
|
||||||
|
env.REDIS_URL; // string | undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
### Secure API Response Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Never expose internal fields
|
||||||
|
interface InternalUser {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
passwordHash: string;
|
||||||
|
internalNotes: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API response (Pick only safe fields)
|
||||||
|
type PublicUser = Pick<InternalUser, 'id' | 'name' | 'email'>;
|
||||||
|
|
||||||
|
// Or explicitly define
|
||||||
|
interface UserResponse {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform function
|
||||||
|
function toPublicUser(user: InternalUser): UserResponse {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rate Limiting Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface RateLimitConfig {
|
||||||
|
windowMs: number;
|
||||||
|
maxRequests: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RateLimitResult {
|
||||||
|
allowed: boolean;
|
||||||
|
remaining: number;
|
||||||
|
resetAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rateLimits: Record<string, RateLimitConfig> = {
|
||||||
|
api: { windowMs: 60000, maxRequests: 100 },
|
||||||
|
auth: { windowMs: 300000, maxRequests: 5 },
|
||||||
|
upload: { windowMs: 3600000, maxRequests: 10 }
|
||||||
|
} as const satisfies Record<string, RateLimitConfig>;
|
||||||
|
```
|
||||||
@@ -0,0 +1,499 @@
|
|||||||
|
# Generics Reference
|
||||||
|
|
||||||
|
> **Load when:** User asks about generics, mapped types, conditional types, template literal types, or reusable type patterns.
|
||||||
|
|
||||||
|
Advanced generics and type-level programming patterns.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Generic Fundamentals](#generic-fundamentals)
|
||||||
|
- [Generic Constraints](#generic-constraints)
|
||||||
|
- [Mapped Types](#mapped-types)
|
||||||
|
- [Conditional Types](#conditional-types)
|
||||||
|
- [Template Literal Types](#template-literal-types)
|
||||||
|
- [Variadic Tuple Types](#variadic-tuple-types)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generic Fundamentals
|
||||||
|
|
||||||
|
### Basic Generic Function
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Type parameter T can be any type
|
||||||
|
function identity<T>(value: T): T {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const str = identity("hello"); // string
|
||||||
|
const num = identity(42); // number
|
||||||
|
const obj = identity({ x: 1 }); // { x: number }
|
||||||
|
|
||||||
|
// Explicit type argument (rarely needed)
|
||||||
|
const explicit = identity<string>("hello");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic Interfaces
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Container<T> {
|
||||||
|
value: T;
|
||||||
|
getValue(): T;
|
||||||
|
setValue(value: T): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Repository<T, ID = string> {
|
||||||
|
findById(id: ID): Promise<T | null>;
|
||||||
|
findAll(): Promise<T[]>;
|
||||||
|
save(entity: T): Promise<T>;
|
||||||
|
delete(id: ID): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation
|
||||||
|
class UserRepository implements Repository<User> {
|
||||||
|
async findById(id: string): Promise<User | null> {
|
||||||
|
// implementation
|
||||||
|
}
|
||||||
|
// ... other methods
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic Classes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class Stack<T> {
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty(): boolean {
|
||||||
|
return this.items.length === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberStack = new Stack<number>();
|
||||||
|
numberStack.push(1);
|
||||||
|
numberStack.push(2);
|
||||||
|
const top = numberStack.pop(); // number | undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generic Constraints
|
||||||
|
|
||||||
|
### extends Constraint
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// T must have a length property
|
||||||
|
interface HasLength {
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logLength<T extends HasLength>(item: T): T {
|
||||||
|
console.log(`Length: ${item.length}`);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
logLength("hello"); // OK: string has length
|
||||||
|
logLength([1, 2, 3]); // OK: array has length
|
||||||
|
logLength({ length: 10 }); // OK: object has length
|
||||||
|
logLength(42); // Error: number has no length
|
||||||
|
```
|
||||||
|
|
||||||
|
### keyof Constraint
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
||||||
|
return obj[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Person {
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const person: Person = { name: "Alice", age: 30 };
|
||||||
|
|
||||||
|
const name = getProperty(person, "name"); // string
|
||||||
|
const age = getProperty(person, "age"); // number
|
||||||
|
const bad = getProperty(person, "email"); // Error: "email" not in Person
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Constraints
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Printable {
|
||||||
|
print(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Loggable {
|
||||||
|
log(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// T must satisfy both interfaces
|
||||||
|
function process<T extends Printable & Loggable>(item: T): void {
|
||||||
|
item.print();
|
||||||
|
console.log(item.log());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Default Type Parameters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ApiResponse<T = unknown, E = Error> {
|
||||||
|
data?: T;
|
||||||
|
error?: E;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uses defaults
|
||||||
|
const response1: ApiResponse = { status: 200 };
|
||||||
|
|
||||||
|
// Override data type only
|
||||||
|
const response2: ApiResponse<User> = { data: user, status: 200 };
|
||||||
|
|
||||||
|
// Override both
|
||||||
|
const response3: ApiResponse<User, ValidationError> = {
|
||||||
|
error: new ValidationError(),
|
||||||
|
status: 400
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mapped Types
|
||||||
|
|
||||||
|
### Basic Mapped Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Transform all properties to optional
|
||||||
|
type Partial<T> = {
|
||||||
|
[K in keyof T]?: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transform all properties to required
|
||||||
|
type Required<T> = {
|
||||||
|
[K in keyof T]-?: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transform all properties to readonly
|
||||||
|
type Readonly<T> = {
|
||||||
|
readonly [K in keyof T]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove readonly modifier
|
||||||
|
type Mutable<T> = {
|
||||||
|
-readonly [K in keyof T]: T[K];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Practical Mapped Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Make all properties nullable
|
||||||
|
type Nullable<T> = {
|
||||||
|
[K in keyof T]: T[K] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make all properties async getters
|
||||||
|
type AsyncGetters<T> = {
|
||||||
|
[K in keyof T as `get${Capitalize<string & K>}`]: () => Promise<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserGetters = AsyncGetters<User>;
|
||||||
|
// {
|
||||||
|
// getName: () => Promise<string>;
|
||||||
|
// getEmail: () => Promise<string>;
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Remapping (as clause)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Filter keys by type
|
||||||
|
type FilterByType<T, U> = {
|
||||||
|
[K in keyof T as T[K] extends U ? K : never]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Mixed {
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
active: boolean;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringProps = FilterByType<Mixed, string>;
|
||||||
|
// { name: string }
|
||||||
|
|
||||||
|
type NumberProps = FilterByType<Mixed, number>;
|
||||||
|
// { age: number; score: number }
|
||||||
|
|
||||||
|
// Prefix all keys
|
||||||
|
type Prefixed<T, P extends string> = {
|
||||||
|
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
type PrefixedUser = Prefixed<User, "user">;
|
||||||
|
// { userName: string; userEmail: string }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conditional Types
|
||||||
|
|
||||||
|
### Basic Conditional Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// T extends U ? X : Y
|
||||||
|
type IsString<T> = T extends string ? true : false;
|
||||||
|
|
||||||
|
type A = IsString<string>; // true
|
||||||
|
type B = IsString<number>; // false
|
||||||
|
type C = IsString<"hello">; // true
|
||||||
|
|
||||||
|
// Practical: Extract non-nullable type
|
||||||
|
type NonNullable<T> = T extends null | undefined ? never : T;
|
||||||
|
|
||||||
|
type D = NonNullable<string | null>; // string
|
||||||
|
type E = NonNullable<number | undefined>; // number
|
||||||
|
```
|
||||||
|
|
||||||
|
### Distributive Conditional Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Conditional types distribute over unions
|
||||||
|
type ToArray<T> = T extends unknown ? T[] : never;
|
||||||
|
|
||||||
|
type StringOrNumberArray = ToArray<string | number>;
|
||||||
|
// string[] | number[] (not (string | number)[])
|
||||||
|
|
||||||
|
// Prevent distribution with tuple
|
||||||
|
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
|
||||||
|
|
||||||
|
type Mixed = ToArrayNonDist<string | number>;
|
||||||
|
// (string | number)[]
|
||||||
|
```
|
||||||
|
|
||||||
|
### infer Keyword
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Extract return type
|
||||||
|
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||||
|
|
||||||
|
type FnReturn = ReturnType<() => string>; // string
|
||||||
|
|
||||||
|
// Extract array element type
|
||||||
|
type ArrayElement<T> = T extends (infer E)[] ? E : never;
|
||||||
|
|
||||||
|
type Element = ArrayElement<number[]>; // number
|
||||||
|
|
||||||
|
// Extract Promise result
|
||||||
|
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;
|
||||||
|
|
||||||
|
type Result = Awaited<Promise<Promise<string>>>; // string
|
||||||
|
|
||||||
|
// Extract function first parameter
|
||||||
|
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
|
||||||
|
|
||||||
|
type First = FirstParam<(name: string, age: number) => void>; // string
|
||||||
|
```
|
||||||
|
|
||||||
|
### Practical Conditional Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// API response helper
|
||||||
|
type ApiResult<T> =
|
||||||
|
| { success: true; data: T }
|
||||||
|
| { success: false; error: string };
|
||||||
|
|
||||||
|
// Extract data type from result
|
||||||
|
type ExtractData<T> = T extends { success: true; data: infer D } ? D : never;
|
||||||
|
|
||||||
|
type UserResult = ApiResult<User>;
|
||||||
|
type UserData = ExtractData<UserResult>; // User
|
||||||
|
|
||||||
|
// Type-safe event handlers
|
||||||
|
type EventHandler<T> = T extends `on${infer Event}`
|
||||||
|
? (event: Event) => void
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type ClickHandler = EventHandler<"onClick">; // (event: "Click") => void
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Template Literal Types
|
||||||
|
|
||||||
|
### Basic Template Literals
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type Greeting = `Hello, ${string}!`;
|
||||||
|
|
||||||
|
const valid: Greeting = "Hello, World!"; // OK
|
||||||
|
const invalid: Greeting = "Hi, World!"; // Error
|
||||||
|
|
||||||
|
// Combine with unions
|
||||||
|
type Size = "small" | "medium" | "large";
|
||||||
|
type Color = "red" | "blue" | "green";
|
||||||
|
|
||||||
|
type ColoredSize = `${Color}-${Size}`;
|
||||||
|
// "red-small" | "red-medium" | "red-large" |
|
||||||
|
// "blue-small" | "blue-medium" | "blue-large" |
|
||||||
|
// "green-small" | "green-medium" | "green-large"
|
||||||
|
```
|
||||||
|
|
||||||
|
### String Manipulation Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Built-in string manipulation types
|
||||||
|
type Upper = Uppercase<"hello">; // "HELLO"
|
||||||
|
type Lower = Lowercase<"HELLO">; // "hello"
|
||||||
|
type Cap = Capitalize<"hello">; // "Hello"
|
||||||
|
type Uncap = Uncapitalize<"Hello">; // "hello"
|
||||||
|
|
||||||
|
// Practical: Generate event names
|
||||||
|
type Event = "click" | "hover" | "focus";
|
||||||
|
type EventHandler = `on${Capitalize<Event>}`;
|
||||||
|
// "onClick" | "onHover" | "onFocus"
|
||||||
|
|
||||||
|
// CSS property with vendor prefixes
|
||||||
|
type CSSProp = "transform" | "transition";
|
||||||
|
type Prefixed = `-webkit-${CSSProp}` | `-moz-${CSSProp}` | CSSProp;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Template Patterns
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Parse dot-notation paths
|
||||||
|
type PathSegment<T> = T extends `${infer Head}.${infer Tail}`
|
||||||
|
? Head | PathSegment<Tail>
|
||||||
|
: T;
|
||||||
|
|
||||||
|
type Segments = PathSegment<"user.profile.name">;
|
||||||
|
// "user" | "profile" | "name"
|
||||||
|
|
||||||
|
// HTTP methods with paths
|
||||||
|
type Method = "GET" | "POST" | "PUT" | "DELETE";
|
||||||
|
type Endpoint = "/users" | "/posts" | "/comments";
|
||||||
|
|
||||||
|
type Route = `${Method} ${Endpoint}`;
|
||||||
|
// "GET /users" | "GET /posts" | "GET /comments" |
|
||||||
|
// "POST /users" | ... etc
|
||||||
|
|
||||||
|
// Type-safe SQL column references
|
||||||
|
type Table = "users" | "posts";
|
||||||
|
type Column<T extends Table> = T extends "users"
|
||||||
|
? "id" | "name" | "email"
|
||||||
|
: T extends "posts"
|
||||||
|
? "id" | "title" | "content"
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type UserColumn = `users.${Column<"users">}`;
|
||||||
|
// "users.id" | "users.name" | "users.email"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variadic Tuple Types
|
||||||
|
|
||||||
|
### Basic Variadic Tuples
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Spread tuple types
|
||||||
|
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
|
||||||
|
|
||||||
|
type Combined = Concat<[1, 2], [3, 4]>;
|
||||||
|
// [1, 2, 3, 4]
|
||||||
|
|
||||||
|
// Prepend element
|
||||||
|
type Prepend<T, U extends unknown[]> = [T, ...U];
|
||||||
|
|
||||||
|
type WithFirst = Prepend<0, [1, 2, 3]>;
|
||||||
|
// [0, 1, 2, 3]
|
||||||
|
|
||||||
|
// Append element
|
||||||
|
type Append<T extends unknown[], U> = [...T, U];
|
||||||
|
|
||||||
|
type WithLast = Append<[1, 2, 3], 4>;
|
||||||
|
// [1, 2, 3, 4]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Practical Variadic Patterns
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Typed curry function
|
||||||
|
type Curry<F> = F extends (...args: infer A) => infer R
|
||||||
|
? A extends [infer First, ...infer Rest]
|
||||||
|
? (arg: First) => Curry<(...args: Rest) => R>
|
||||||
|
: R
|
||||||
|
: never;
|
||||||
|
|
||||||
|
declare function curry<F extends (...args: any[]) => any>(fn: F): Curry<F>;
|
||||||
|
|
||||||
|
function add(a: number, b: number, c: number): number {
|
||||||
|
return a + b + c;
|
||||||
|
}
|
||||||
|
|
||||||
|
const curriedAdd = curry(add);
|
||||||
|
const add1 = curriedAdd(1); // (arg: number) => Curry<...>
|
||||||
|
const add1and2 = add1(2); // (arg: number) => number
|
||||||
|
const result = add1and2(3); // number (6)
|
||||||
|
|
||||||
|
// Typed pipe function
|
||||||
|
type Pipe<T extends unknown[], R> = T extends [infer First, ...infer Rest]
|
||||||
|
? First extends (arg: R) => infer Next
|
||||||
|
? Pipe<Rest, Next>
|
||||||
|
: never
|
||||||
|
: R;
|
||||||
|
|
||||||
|
function pipe<T extends ((arg: any) => any)[]>(
|
||||||
|
...fns: T
|
||||||
|
): (arg: Parameters<T[0]>[0]) => Pipe<T, Parameters<T[0]>[0]> {
|
||||||
|
return (arg) => fns.reduce((acc, fn) => fn(acc), arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const process = pipe(
|
||||||
|
(n: number) => n * 2,
|
||||||
|
(n: number) => n.toString(),
|
||||||
|
(s: string) => s.length
|
||||||
|
);
|
||||||
|
|
||||||
|
const length = process(5); // number (2 - length of "10")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Built-in Utility Types Reference
|
||||||
|
|
||||||
|
| Utility | Purpose | Example |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| `Partial<T>` | All properties optional | `Partial<User>` |
|
||||||
|
| `Required<T>` | All properties required | `Required<Partial<User>>` |
|
||||||
|
| `Readonly<T>` | All properties readonly | `Readonly<User>` |
|
||||||
|
| `Pick<T, K>` | Select properties | `Pick<User, "id" \| "name">` |
|
||||||
|
| `Omit<T, K>` | Exclude properties | `Omit<User, "password">` |
|
||||||
|
| `Record<K, V>` | Create object type | `Record<string, User>` |
|
||||||
|
| `Exclude<T, U>` | Remove union members | `Exclude<"a" \| "b", "a">` |
|
||||||
|
| `Extract<T, U>` | Keep union members | `Extract<"a" \| "b", "a">` |
|
||||||
|
| `NonNullable<T>` | Remove null/undefined | `NonNullable<string \| null>` |
|
||||||
|
| `Parameters<F>` | Function parameters | `Parameters<typeof fn>` |
|
||||||
|
| `ReturnType<F>` | Function return | `ReturnType<typeof fn>` |
|
||||||
|
| `ConstructorParameters<C>` | Constructor params | `ConstructorParameters<typeof Date>` |
|
||||||
|
| `InstanceType<C>` | Instance type | `InstanceType<typeof Date>` |
|
||||||
|
| `Awaited<T>` | Unwrap Promise | `Awaited<Promise<User>>` |
|
||||||
|
| `NoInfer<T>` | Prevent inference | `NoInfer<T>` (TS 5.4+) |
|
||||||
@@ -0,0 +1,591 @@
|
|||||||
|
# NestJS Integration Reference
|
||||||
|
|
||||||
|
> **Load when:** User asks about NestJS with TypeScript, API development, DTOs, validation, authentication, or backend patterns.
|
||||||
|
|
||||||
|
Type-safe API development with NestJS 11+.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
|
- [Controllers and Routes](#controllers-and-routes)
|
||||||
|
- [DTOs and Validation](#dtos-and-validation)
|
||||||
|
- [Services and Dependency Injection](#services-and-dependency-injection)
|
||||||
|
- [Authentication](#authentication)
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Recommended Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.ts # Application entry point
|
||||||
|
├── app.module.ts # Root module
|
||||||
|
├── common/ # Shared utilities
|
||||||
|
│ ├── decorators/
|
||||||
|
│ ├── filters/
|
||||||
|
│ ├── guards/
|
||||||
|
│ ├── interceptors/
|
||||||
|
│ └── pipes/
|
||||||
|
├── config/ # Configuration
|
||||||
|
│ ├── config.module.ts
|
||||||
|
│ └── env.validation.ts
|
||||||
|
└── modules/
|
||||||
|
├── users/
|
||||||
|
│ ├── users.module.ts
|
||||||
|
│ ├── users.controller.ts
|
||||||
|
│ ├── users.service.ts
|
||||||
|
│ ├── users.repository.ts
|
||||||
|
│ ├── dto/
|
||||||
|
│ │ ├── create-user.dto.ts
|
||||||
|
│ │ └── update-user.dto.ts
|
||||||
|
│ ├── entities/
|
||||||
|
│ │ └── user.entity.ts
|
||||||
|
│ └── __tests__/
|
||||||
|
└── auth/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Module Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// users.module.ts
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { UsersController } from './users.controller';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
import { UsersRepository } from './users.repository';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [UsersController],
|
||||||
|
providers: [UsersService, UsersRepository],
|
||||||
|
exports: [UsersService] // Export for use in other modules
|
||||||
|
})
|
||||||
|
export class UsersModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Controllers and Routes
|
||||||
|
|
||||||
|
### Basic Controller
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Param,
|
||||||
|
Body,
|
||||||
|
Query,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto';
|
||||||
|
import { PaginationDto } from '@/common/dto';
|
||||||
|
|
||||||
|
@ApiTags('users')
|
||||||
|
@Controller('users')
|
||||||
|
export class UsersController {
|
||||||
|
constructor(private readonly usersService: UsersService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'Get all users' })
|
||||||
|
@ApiResponse({ status: 200, type: [UserResponseDto] })
|
||||||
|
async findAll(@Query() query: PaginationDto): Promise<UserResponseDto[]> {
|
||||||
|
return this.usersService.findAll(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get user by ID' })
|
||||||
|
@ApiResponse({ status: 200, type: UserResponseDto })
|
||||||
|
@ApiResponse({ status: 404, description: 'User not found' })
|
||||||
|
async findOne(@Param('id') id: string): Promise<UserResponseDto> {
|
||||||
|
return this.usersService.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({ summary: 'Create new user' })
|
||||||
|
@ApiResponse({ status: 201, type: UserResponseDto })
|
||||||
|
async create(@Body() dto: CreateUserDto): Promise<UserResponseDto> {
|
||||||
|
return this.usersService.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
@ApiOperation({ summary: 'Update user' })
|
||||||
|
async update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateUserDto
|
||||||
|
): Promise<UserResponseDto> {
|
||||||
|
return this.usersService.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: 'Delete user' })
|
||||||
|
async remove(@Param('id') id: string): Promise<void> {
|
||||||
|
return this.usersService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DTOs and Validation
|
||||||
|
|
||||||
|
### Class-Validator Approach
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// dto/create-user.dto.ts
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsEmail,
|
||||||
|
MinLength,
|
||||||
|
MaxLength,
|
||||||
|
IsOptional,
|
||||||
|
IsEnum,
|
||||||
|
ValidateNested,
|
||||||
|
IsArray
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export enum UserRole {
|
||||||
|
User = 'user',
|
||||||
|
Admin = 'admin',
|
||||||
|
Moderator = 'moderator'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddressDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
street: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
city: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsString()
|
||||||
|
country: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateUserDto {
|
||||||
|
@ApiProperty({ example: 'john@example.com' })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ minLength: 2, maxLength: 50 })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(2)
|
||||||
|
@MaxLength(50)
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ minLength: 8 })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(8)
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: UserRole, default: UserRole.User })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(UserRole)
|
||||||
|
role?: UserRole = UserRole.User;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: AddressDto })
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => AddressDto)
|
||||||
|
address?: AddressDto;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: [String] })
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// dto/update-user.dto.ts
|
||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateUserDto } from './create-user.dto';
|
||||||
|
|
||||||
|
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zod-Based DTOs (Modern Approach)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// dto/user.schema.ts
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { createZodDto } from 'nestjs-zod';
|
||||||
|
|
||||||
|
// Define Zod schemas
|
||||||
|
export const UserRoleSchema = z.enum(['user', 'admin', 'moderator']);
|
||||||
|
|
||||||
|
export const AddressSchema = z.object({
|
||||||
|
street: z.string(),
|
||||||
|
city: z.string(),
|
||||||
|
country: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateUserSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
name: z.string().min(2).max(50),
|
||||||
|
password: z.string().min(8),
|
||||||
|
role: UserRoleSchema.default('user').optional(),
|
||||||
|
address: AddressSchema.optional(),
|
||||||
|
tags: z.array(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateUserSchema = CreateUserSchema.partial();
|
||||||
|
|
||||||
|
// Create DTO classes from schemas
|
||||||
|
export class CreateUserDto extends createZodDto(CreateUserSchema) {}
|
||||||
|
export class UpdateUserDto extends createZodDto(UpdateUserSchema) {}
|
||||||
|
|
||||||
|
// Infer types
|
||||||
|
export type CreateUser = z.infer<typeof CreateUserSchema>;
|
||||||
|
export type UpdateUser = z.infer<typeof UpdateUserSchema>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global Validation Pipe
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// main.ts
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true, // Strip unknown properties
|
||||||
|
forbidNonWhitelisted: true, // Throw on unknown properties
|
||||||
|
transform: true, // Transform payloads to DTO classes
|
||||||
|
transformOptions: {
|
||||||
|
enableImplicitConversion: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.listen(3000);
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Services and Dependency Injection
|
||||||
|
|
||||||
|
### Typed Service
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// users.service.ts
|
||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { UsersRepository } from './users.repository';
|
||||||
|
import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto';
|
||||||
|
import { PaginationDto } from '@/common/dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersService {
|
||||||
|
constructor(private readonly usersRepository: UsersRepository) {}
|
||||||
|
|
||||||
|
async findAll(query: PaginationDto): Promise<UserResponseDto[]> {
|
||||||
|
const users = await this.usersRepository.findAll(query);
|
||||||
|
return users.map(this.toResponseDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string): Promise<UserResponseDto> {
|
||||||
|
const user = await this.usersRepository.findById(id);
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException(`User with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.toResponseDto(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(dto: CreateUserDto): Promise<UserResponseDto> {
|
||||||
|
const user = await this.usersRepository.create(dto);
|
||||||
|
return this.toResponseDto(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, dto: UpdateUserDto): Promise<UserResponseDto> {
|
||||||
|
const user = await this.usersRepository.update(id, dto);
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException(`User with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.toResponseDto(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
const deleted = await this.usersRepository.delete(id);
|
||||||
|
if (!deleted) {
|
||||||
|
throw new NotFoundException(`User with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private toResponseDto(user: User): UserResponseDto {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
role: user.role,
|
||||||
|
createdAt: user.createdAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Repository Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// users.repository.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '@/prisma/prisma.service';
|
||||||
|
import { User, Prisma } from '@prisma/client';
|
||||||
|
import { PaginationDto } from '@/common/dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersRepository {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findAll(query: PaginationDto): Promise<User[]> {
|
||||||
|
return this.prisma.user.findMany({
|
||||||
|
skip: query.skip,
|
||||||
|
take: query.take,
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<User | null> {
|
||||||
|
return this.prisma.user.findUnique({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByEmail(email: string): Promise<User | null> {
|
||||||
|
return this.prisma.user.findUnique({ where: { email } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(data: Prisma.UserCreateInput): Promise<User> {
|
||||||
|
return this.prisma.user.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, data: Prisma.UserUpdateInput): Promise<User | null> {
|
||||||
|
try {
|
||||||
|
return await this.prisma.user.update({ where: { id }, data });
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.prisma.user.delete({ where: { id } });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### JWT Authentication
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// auth/strategies/jwt.strategy.ts
|
||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { UsersService } from '@/modules/users/users.service';
|
||||||
|
|
||||||
|
interface JwtPayload {
|
||||||
|
sub: string;
|
||||||
|
email: string;
|
||||||
|
role: string;
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(
|
||||||
|
configService: ConfigService,
|
||||||
|
private readonly usersService: UsersService
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: configService.getOrThrow<string>('JWT_SECRET')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: JwtPayload) {
|
||||||
|
const user = await this.usersService.findOne(payload.sub);
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
return { id: payload.sub, email: payload.email, role: payload.role };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Role-Based Access Control
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// common/decorators/roles.decorator.ts
|
||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const ROLES_KEY = 'roles';
|
||||||
|
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||||
|
|
||||||
|
// common/guards/roles.guard.ts
|
||||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { ROLES_KEY } from '../decorators/roles.decorator';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
constructor(private reflector: Reflector) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
|
||||||
|
ROLES_KEY,
|
||||||
|
[context.getHandler(), context.getClass()]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!requiredRoles) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = context.switchToHttp().getRequest();
|
||||||
|
return requiredRoles.includes(user.role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in controller
|
||||||
|
@Controller('admin')
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
export class AdminController {
|
||||||
|
@Get('users')
|
||||||
|
@Roles('admin')
|
||||||
|
findAllUsers() {
|
||||||
|
return this.adminService.findAllUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('users/:id')
|
||||||
|
@Roles('admin', 'moderator')
|
||||||
|
removeUser(@Param('id') id: string) {
|
||||||
|
return this.adminService.removeUser(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Exception Filters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// common/filters/http-exception.filter.ts
|
||||||
|
import {
|
||||||
|
ExceptionFilter,
|
||||||
|
Catch,
|
||||||
|
ArgumentsHost,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
interface ErrorResponse {
|
||||||
|
statusCode: number;
|
||||||
|
message: string;
|
||||||
|
error: string;
|
||||||
|
timestamp: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class AllExceptionsFilter implements ExceptionFilter {
|
||||||
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
const request = ctx.getRequest();
|
||||||
|
|
||||||
|
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
let message = 'Internal server error';
|
||||||
|
let error = 'Internal Server Error';
|
||||||
|
|
||||||
|
if (exception instanceof HttpException) {
|
||||||
|
status = exception.getStatus();
|
||||||
|
const exceptionResponse = exception.getResponse();
|
||||||
|
|
||||||
|
if (typeof exceptionResponse === 'string') {
|
||||||
|
message = exceptionResponse;
|
||||||
|
} else if (typeof exceptionResponse === 'object') {
|
||||||
|
const responseObj = exceptionResponse as Record<string, unknown>;
|
||||||
|
message = (responseObj.message as string) || message;
|
||||||
|
error = (responseObj.error as string) || exception.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorResponse: ErrorResponse = {
|
||||||
|
statusCode: status,
|
||||||
|
message,
|
||||||
|
error,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
path: request.url
|
||||||
|
};
|
||||||
|
|
||||||
|
response.status(status).json(errorResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// main.ts
|
||||||
|
app.useGlobalFilters(new AllExceptionsFilter());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Exceptions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// common/exceptions/business.exception.ts
|
||||||
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
|
export class BusinessException extends HttpException {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly code: string,
|
||||||
|
status: HttpStatus = HttpStatus.BAD_REQUEST
|
||||||
|
) {
|
||||||
|
super({ message, code }, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InsufficientFundsException extends BusinessException {
|
||||||
|
constructor(required: number, available: number) {
|
||||||
|
super(
|
||||||
|
`Insufficient funds: required ${required}, available ${available}`,
|
||||||
|
'INSUFFICIENT_FUNDS'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DuplicateEmailException extends BusinessException {
|
||||||
|
constructor(email: string) {
|
||||||
|
super(`Email ${email} is already registered`, 'DUPLICATE_EMAIL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
throw new InsufficientFundsException(100, 50);
|
||||||
|
```
|
||||||
@@ -0,0 +1,618 @@
|
|||||||
|
# React Integration Reference
|
||||||
|
|
||||||
|
> **Load when:** User asks about React with TypeScript, typed components, hooks, state management, or React patterns.
|
||||||
|
|
||||||
|
Type-safe React development patterns for React 19+.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Component Patterns](#component-patterns)
|
||||||
|
- [Hooks with TypeScript](#hooks-with-typescript)
|
||||||
|
- [State Management](#state-management)
|
||||||
|
- [Event Handling](#event-handling)
|
||||||
|
- [Context API](#context-api)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Component Patterns
|
||||||
|
|
||||||
|
### Functional Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic typed component
|
||||||
|
interface GreetingProps {
|
||||||
|
name: string;
|
||||||
|
age?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Greeting({ name, age }: GreetingProps) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Hello, {name}!
|
||||||
|
{age && <span> You are {age} years old.</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// With React.FC (optional, some prefer explicit return type)
|
||||||
|
const GreetingFC: React.FC<GreetingProps> = ({ name, age }) => {
|
||||||
|
return <div>Hello, {name}!</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Component with children
|
||||||
|
interface CardProps {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Card({ title, children }: CardProps) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<div className="card-body">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Generic list component
|
||||||
|
interface ListProps<T> {
|
||||||
|
items: T[];
|
||||||
|
renderItem: (item: T, index: number) => React.ReactNode;
|
||||||
|
keyExtractor: (item: T) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
|
||||||
|
return (
|
||||||
|
<ul>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
<List<User>
|
||||||
|
items={users}
|
||||||
|
keyExtractor={(user) => user.id}
|
||||||
|
renderItem={(user) => <span>{user.name}</span>}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
// Generic select component
|
||||||
|
interface SelectProps<T> {
|
||||||
|
options: T[];
|
||||||
|
value: T | null;
|
||||||
|
onChange: (value: T) => void;
|
||||||
|
getLabel: (option: T) => string;
|
||||||
|
getValue: (option: T) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Select<T>({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
getLabel,
|
||||||
|
getValue
|
||||||
|
}: SelectProps<T>) {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
value={value ? getValue(value) : ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const selected = options.find((opt) => getValue(opt) === e.target.value);
|
||||||
|
if (selected) onChange(selected);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">Select...</option>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<option key={getValue(opt)} value={getValue(opt)}>
|
||||||
|
{getLabel(opt)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Polymorphic Components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Component that can render as different elements
|
||||||
|
type ButtonProps<T extends React.ElementType> = {
|
||||||
|
as?: T;
|
||||||
|
children: React.ReactNode;
|
||||||
|
variant?: "primary" | "secondary";
|
||||||
|
} & Omit<React.ComponentPropsWithoutRef<T>, "as" | "children">;
|
||||||
|
|
||||||
|
function Button<T extends React.ElementType = "button">({
|
||||||
|
as,
|
||||||
|
children,
|
||||||
|
variant = "primary",
|
||||||
|
...props
|
||||||
|
}: ButtonProps<T>) {
|
||||||
|
const Component = as || "button";
|
||||||
|
return (
|
||||||
|
<Component className={`btn btn-${variant}`} {...props}>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<Button>Click me</Button>
|
||||||
|
<Button as="a" href="/about">Link Button</Button>
|
||||||
|
<Button as={Link} to="/home">Router Link</Button>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hooks with TypeScript
|
||||||
|
|
||||||
|
### useState
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic usage (type inferred)
|
||||||
|
const [count, setCount] = useState(0);
|
||||||
|
|
||||||
|
// Explicit type (for complex types or initial null)
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
|
||||||
|
// With union types
|
||||||
|
type Status = "idle" | "loading" | "success" | "error";
|
||||||
|
const [status, setStatus] = useState<Status>("idle");
|
||||||
|
|
||||||
|
// Lazy initialization
|
||||||
|
const [state, setState] = useState<ExpensiveState>(() => {
|
||||||
|
return computeExpensiveInitialState();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### useRef
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// DOM element ref
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
function focusInput() {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutable ref (no initial render)
|
||||||
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
intervalRef.current = setInterval(() => {}, 1000);
|
||||||
|
return () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Ref to store previous value
|
||||||
|
function usePrevious<T>(value: T): T | undefined {
|
||||||
|
const ref = useRef<T | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### useReducer
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Define state and actions
|
||||||
|
interface CounterState {
|
||||||
|
count: number;
|
||||||
|
step: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CounterAction =
|
||||||
|
| { type: "increment" }
|
||||||
|
| { type: "decrement" }
|
||||||
|
| { type: "setStep"; payload: number }
|
||||||
|
| { type: "reset" };
|
||||||
|
|
||||||
|
function counterReducer(
|
||||||
|
state: CounterState,
|
||||||
|
action: CounterAction
|
||||||
|
): CounterState {
|
||||||
|
switch (action.type) {
|
||||||
|
case "increment":
|
||||||
|
return { ...state, count: state.count + state.step };
|
||||||
|
case "decrement":
|
||||||
|
return { ...state, count: state.count - state.step };
|
||||||
|
case "setStep":
|
||||||
|
return { ...state, step: action.payload };
|
||||||
|
case "reset":
|
||||||
|
return { count: 0, step: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const [state, dispatch] = useReducer(counterReducer, { count: 0, step: 1 });
|
||||||
|
|
||||||
|
dispatch({ type: "increment" });
|
||||||
|
dispatch({ type: "setStep", payload: 5 });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Hooks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Async data fetching hook
|
||||||
|
interface UseAsyncResult<T> {
|
||||||
|
data: T | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
refetch: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAsync<T>(
|
||||||
|
asyncFn: () => Promise<T>,
|
||||||
|
deps: React.DependencyList = []
|
||||||
|
): UseAsyncResult<T> {
|
||||||
|
const [data, setData] = useState<T | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
const execute = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await asyncFn();
|
||||||
|
setData(result);
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e : new Error(String(e)));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, deps);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
execute();
|
||||||
|
}, [execute]);
|
||||||
|
|
||||||
|
return { data, loading, error, refetch: execute };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const { data: users, loading, error } = useAsync(
|
||||||
|
() => fetch("/api/users").then((r) => r.json()),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Local storage hook
|
||||||
|
function useLocalStorage<T>(
|
||||||
|
key: string,
|
||||||
|
initialValue: T
|
||||||
|
): [T, (value: T | ((prev: T) => T)) => void] {
|
||||||
|
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||||
|
try {
|
||||||
|
const item = localStorage.getItem(key);
|
||||||
|
return item ? JSON.parse(item) : initialValue;
|
||||||
|
} catch {
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setValue = (value: T | ((prev: T) => T)) => {
|
||||||
|
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||||
|
setStoredValue(valueToStore);
|
||||||
|
localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||||
|
};
|
||||||
|
|
||||||
|
return [storedValue, setValue];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
### Zustand with TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
// Define state interface
|
||||||
|
interface AuthState {
|
||||||
|
user: User | null;
|
||||||
|
token: string | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
login: (email: string, password: string) => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
setUser: (user: User) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create typed store
|
||||||
|
const useAuthStore = create<AuthState>()(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
user: null,
|
||||||
|
token: null,
|
||||||
|
isAuthenticated: false,
|
||||||
|
|
||||||
|
login: async (email, password) => {
|
||||||
|
const response = await api.login(email, password);
|
||||||
|
set({
|
||||||
|
user: response.user,
|
||||||
|
token: response.token,
|
||||||
|
isAuthenticated: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: () => {
|
||||||
|
set({ user: null, token: null, isAuthenticated: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
setUser: (user) => set({ user })
|
||||||
|
}),
|
||||||
|
{ name: 'auth-storage' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Usage with selectors
|
||||||
|
const user = useAuthStore((state) => state.user);
|
||||||
|
const login = useAuthStore((state) => state.login);
|
||||||
|
|
||||||
|
// Shallow comparison for multiple values
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
|
const { user, isAuthenticated } = useAuthStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
user: state.user,
|
||||||
|
isAuthenticated: state.isAuthenticated
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redux Toolkit with TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createSlice, PayloadAction, configureStore } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
// Define slice state
|
||||||
|
interface TodosState {
|
||||||
|
items: Todo[];
|
||||||
|
filter: "all" | "active" | "completed";
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: TodosState = {
|
||||||
|
items: [],
|
||||||
|
filter: "all",
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create typed slice
|
||||||
|
const todosSlice = createSlice({
|
||||||
|
name: "todos",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
addTodo: (state, action: PayloadAction<string>) => {
|
||||||
|
state.items.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
text: action.payload,
|
||||||
|
completed: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
toggleTodo: (state, action: PayloadAction<string>) => {
|
||||||
|
const todo = state.items.find((t) => t.id === action.payload);
|
||||||
|
if (todo) {
|
||||||
|
todo.completed = !todo.completed;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setFilter: (state, action: PayloadAction<TodosState["filter"]>) => {
|
||||||
|
state.filter = action.payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure store with type inference
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
todos: todosSlice.reducer
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Infer types from store
|
||||||
|
type RootState = ReturnType<typeof store.getState>;
|
||||||
|
type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
// Typed hooks
|
||||||
|
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
||||||
|
|
||||||
|
const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||||
|
const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const todos = useAppSelector((state) => state.todos.items);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
dispatch(todosSlice.actions.addTodo("New todo"));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Event Handling
|
||||||
|
|
||||||
|
### Common Event Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Click events
|
||||||
|
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
|
||||||
|
console.log(event.currentTarget.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form events
|
||||||
|
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
const formData = new FormData(event.currentTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input change
|
||||||
|
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const { name, value, checked, type } = event.target;
|
||||||
|
const inputValue = type === "checkbox" ? checked : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard events
|
||||||
|
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
// submit form
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus events
|
||||||
|
function handleFocus(event: React.FocusEvent<HTMLInputElement>) {
|
||||||
|
event.target.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag events
|
||||||
|
function handleDrag(event: React.DragEvent<HTMLDivElement>) {
|
||||||
|
event.dataTransfer.setData("text/plain", "dragged data");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form with TypeScript
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface FormData {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
role: "user" | "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
function RegistrationForm() {
|
||||||
|
const [formData, setFormData] = useState<FormData>({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
role: "user"
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||||
|
) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<select name="role" value={formData.role} onChange={handleChange}>
|
||||||
|
<option value="user">User</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit">Register</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context API
|
||||||
|
|
||||||
|
### Typed Context
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Define context type
|
||||||
|
interface ThemeContextType {
|
||||||
|
theme: "light" | "dark";
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context with undefined default
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
// Provider component
|
||||||
|
function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||||
|
|
||||||
|
const toggleTheme = useCallback(() => {
|
||||||
|
setTheme((prev) => (prev === "light" ? "dark" : "light"));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom hook with type safety
|
||||||
|
function useTheme(): ThemeContextType {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useTheme must be used within ThemeProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
function ThemeToggle() {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
return <button onClick={toggleTheme}>Current: {theme}</button>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic Context Factory
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Factory function for creating typed contexts
|
||||||
|
function createContext<T>(displayName: string) {
|
||||||
|
const Context = React.createContext<T | undefined>(undefined);
|
||||||
|
Context.displayName = displayName;
|
||||||
|
|
||||||
|
function useContextHook(): T {
|
||||||
|
const context = React.useContext(Context);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error(`use${displayName} must be used within ${displayName}Provider`);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [Context.Provider, useContextHook] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
interface AuthContextType {
|
||||||
|
user: User | null;
|
||||||
|
login: (credentials: Credentials) => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [AuthProvider, useAuth] = createContext<AuthContextType>("Auth");
|
||||||
|
```
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
# Modern Toolchain Reference
|
||||||
|
|
||||||
|
> **Load when:** User asks about Vite, pnpm, ESLint, Vitest, tsconfig, build tools, or project configuration.
|
||||||
|
|
||||||
|
Modern TypeScript toolchain configuration for 2025.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [TypeScript Configuration](#typescript-configuration)
|
||||||
|
- [Package Manager (pnpm)](#package-manager-pnpm)
|
||||||
|
- [Build Tool (Vite)](#build-tool-vite)
|
||||||
|
- [Linting (ESLint 9)](#linting-eslint-9)
|
||||||
|
- [Testing (Vitest)](#testing-vitest)
|
||||||
|
- [Formatting (Prettier)](#formatting-prettier)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TypeScript Configuration
|
||||||
|
|
||||||
|
### Strict Enterprise Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
// tsconfig.json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Language and Environment
|
||||||
|
"target": "ES2024",
|
||||||
|
"lib": ["ES2024"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
|
||||||
|
// Strict Type Checking
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
|
||||||
|
// Module Handling
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
|
||||||
|
// Output
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
|
||||||
|
// Path Aliases
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Performance
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"incremental": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.cache/tsbuildinfo"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### React Project Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
// tsconfig.json for React
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"@components/*": ["./src/components/*"],
|
||||||
|
"@hooks/*": ["./src/hooks/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js Backend Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
// tsconfig.json for Node.js/NestJS
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Package Manager (pnpm)
|
||||||
|
|
||||||
|
### Why pnpm
|
||||||
|
|
||||||
|
| Feature | npm | pnpm |
|
||||||
|
|---------|-----|------|
|
||||||
|
| Disk usage | Duplicates packages | Shared store, symlinks |
|
||||||
|
| Install speed | Slower | 2-3x faster |
|
||||||
|
| Strictness | Allows phantom deps | Strict by default |
|
||||||
|
| Monorepo support | Basic workspaces | First-class support |
|
||||||
|
|
||||||
|
### Basic Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Add packages
|
||||||
|
pnpm add typescript
|
||||||
|
pnpm add -D vitest @types/node
|
||||||
|
|
||||||
|
# Run scripts
|
||||||
|
pnpm run build
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
# Update packages
|
||||||
|
pnpm update
|
||||||
|
pnpm update --interactive
|
||||||
|
|
||||||
|
# List packages
|
||||||
|
pnpm list
|
||||||
|
pnpm why lodash
|
||||||
|
|
||||||
|
# Clean install
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workspace Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# pnpm-workspace.yaml
|
||||||
|
packages:
|
||||||
|
- 'packages/*'
|
||||||
|
- 'apps/*'
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
// package.json (root)
|
||||||
|
{
|
||||||
|
"name": "my-monorepo",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm -r run build",
|
||||||
|
"test": "pnpm -r run test",
|
||||||
|
"lint": "pnpm -r run lint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build Tool (Vite)
|
||||||
|
|
||||||
|
### Vite Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tsconfigPaths()
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
host: true
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'es2022',
|
||||||
|
sourcemap: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
vendor: ['react', 'react-dom'],
|
||||||
|
utils: ['lodash-es', 'date-fns']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['./src/test/setup.ts']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Library Mode
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts for library
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import dts from 'vite-plugin-dts';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: './src/index.ts',
|
||||||
|
name: 'MyLibrary',
|
||||||
|
fileName: 'my-library',
|
||||||
|
formats: ['es', 'cjs']
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['react', 'react-dom'],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
react: 'React',
|
||||||
|
'react-dom': 'ReactDOM'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
dts({ insertTypesEntry: true })
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Linting (ESLint 9)
|
||||||
|
|
||||||
|
### Flat Config Format
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// eslint.config.js
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import reactPlugin from 'eslint-plugin-react';
|
||||||
|
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
// Base ESLint recommendations
|
||||||
|
eslint.configs.recommended,
|
||||||
|
|
||||||
|
// TypeScript strict type-checking
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// TypeScript parser options
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// React configuration
|
||||||
|
{
|
||||||
|
files: ['**/*.tsx'],
|
||||||
|
plugins: {
|
||||||
|
react: reactPlugin,
|
||||||
|
'react-hooks': reactHooksPlugin
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactPlugin.configs.recommended.rules,
|
||||||
|
...reactHooksPlugin.configs.recommended.rules,
|
||||||
|
'react/react-in-jsx-scope': 'off'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: { version: 'detect' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Custom rules
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', {
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_'
|
||||||
|
}],
|
||||||
|
'@typescript-eslint/consistent-type-imports': ['error', {
|
||||||
|
prefer: 'type-imports'
|
||||||
|
}],
|
||||||
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
|
'@typescript-eslint/await-thenable': 'error'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ignore patterns
|
||||||
|
{
|
||||||
|
ignores: ['dist/**', 'node_modules/**', '*.config.js']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Rules Explained
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Important TypeScript ESLint rules
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// Enforce type imports for better tree-shaking
|
||||||
|
'@typescript-eslint/consistent-type-imports': 'error',
|
||||||
|
|
||||||
|
// Prevent unhandled promises
|
||||||
|
'@typescript-eslint/no-floating-promises': 'error',
|
||||||
|
'@typescript-eslint/no-misused-promises': 'error',
|
||||||
|
|
||||||
|
// Prevent awaiting non-promises
|
||||||
|
'@typescript-eslint/await-thenable': 'error',
|
||||||
|
|
||||||
|
// Require return types on functions
|
||||||
|
'@typescript-eslint/explicit-function-return-type': ['error', {
|
||||||
|
allowExpressions: true
|
||||||
|
}],
|
||||||
|
|
||||||
|
// Prefer nullish coalescing
|
||||||
|
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||||
|
|
||||||
|
// Prefer optional chaining
|
||||||
|
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||||
|
|
||||||
|
// No any type
|
||||||
|
'@typescript-eslint/no-explicit-any': 'error',
|
||||||
|
|
||||||
|
// Enforce strict boolean expressions
|
||||||
|
'@typescript-eslint/strict-boolean-expressions': 'error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing (Vitest)
|
||||||
|
|
||||||
|
### Vitest Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vitest.config.ts
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tsconfigPaths()],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['./src/test/setup.ts'],
|
||||||
|
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'json', 'html'],
|
||||||
|
exclude: ['**/*.d.ts', '**/*.config.*', '**/test/**']
|
||||||
|
},
|
||||||
|
typecheck: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Setup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/test/setup.ts
|
||||||
|
import '@testing-library/jest-dom/vitest';
|
||||||
|
import { cleanup } from '@testing-library/react';
|
||||||
|
import { afterEach, beforeAll, afterAll, vi } from 'vitest';
|
||||||
|
|
||||||
|
// Cleanup after each test
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock environment variables
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.stubEnv('API_URL', 'http://localhost:3000');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Tests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/utils/format.test.ts
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { formatCurrency, formatDate } from './format';
|
||||||
|
|
||||||
|
describe('formatCurrency', () => {
|
||||||
|
it('formats USD correctly', () => {
|
||||||
|
expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles zero', () => {
|
||||||
|
expect(formatCurrency(0, 'USD')).toBe('$0.00');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// src/components/Button.test.tsx
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { Button } from './Button';
|
||||||
|
|
||||||
|
describe('Button', () => {
|
||||||
|
it('renders with label', () => {
|
||||||
|
render(<Button label="Click me" onClick={() => {}} />);
|
||||||
|
expect(screen.getByRole('button')).toHaveTextContent('Click me');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onClick when clicked', () => {
|
||||||
|
const handleClick = vi.fn();
|
||||||
|
render(<Button label="Click" onClick={handleClick} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button'));
|
||||||
|
|
||||||
|
expect(handleClick).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Formatting (Prettier)
|
||||||
|
|
||||||
|
### Prettier Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
// .prettierrc
|
||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration with ESLint
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// eslint.config.js
|
||||||
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
// ... other configs
|
||||||
|
eslintConfigPrettier // Must be last to disable conflicting rules
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package Scripts
|
||||||
|
|
||||||
|
```json
|
||||||
|
// package.json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint --fix .",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Project Setup
|
||||||
|
|
||||||
|
### Quick Start Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# setup-ts-project.sh
|
||||||
|
|
||||||
|
PROJECT_NAME=${1:-my-app}
|
||||||
|
|
||||||
|
# Create project with Vite
|
||||||
|
pnpm create vite@latest $PROJECT_NAME --template react-ts
|
||||||
|
cd $PROJECT_NAME
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Add development dependencies
|
||||||
|
pnpm add -D \
|
||||||
|
typescript-eslint \
|
||||||
|
@eslint/js \
|
||||||
|
eslint-plugin-react \
|
||||||
|
eslint-plugin-react-hooks \
|
||||||
|
eslint-config-prettier \
|
||||||
|
prettier \
|
||||||
|
vitest \
|
||||||
|
@vitest/coverage-v8 \
|
||||||
|
@testing-library/react \
|
||||||
|
@testing-library/jest-dom \
|
||||||
|
vite-tsconfig-paths
|
||||||
|
|
||||||
|
echo "Project setup complete! Run: cd $PROJECT_NAME && pnpm dev"
|
||||||
|
```
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
# TypeScript Type System Reference
|
||||||
|
|
||||||
|
> **Load when:** User asks about type annotations, interfaces vs types, unions, intersections, or type system fundamentals.
|
||||||
|
|
||||||
|
Complete guide to TypeScript's structural type system.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Type Annotations](#type-annotations)
|
||||||
|
- [Interfaces vs Type Aliases](#interfaces-vs-type-aliases)
|
||||||
|
- [Union and Intersection Types](#union-and-intersection-types)
|
||||||
|
- [Literal Types](#literal-types)
|
||||||
|
- [Type Guards and Narrowing](#type-guards-and-narrowing)
|
||||||
|
- [The satisfies Operator](#the-satisfies-operator)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Annotations
|
||||||
|
|
||||||
|
### Variable Annotations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Explicit type annotations
|
||||||
|
const name: string = "Alice";
|
||||||
|
const age: number = 30;
|
||||||
|
const active: boolean = true;
|
||||||
|
|
||||||
|
// Type inference (preferred when obvious)
|
||||||
|
const inferredName = "Bob"; // TypeScript infers string
|
||||||
|
const inferredAge = 25; // TypeScript infers number
|
||||||
|
|
||||||
|
// Arrays
|
||||||
|
const numbers: number[] = [1, 2, 3];
|
||||||
|
const strings: Array<string> = ["a", "b", "c"];
|
||||||
|
|
||||||
|
// Tuples (fixed-length arrays with specific types)
|
||||||
|
const pair: [string, number] = ["age", 30];
|
||||||
|
const triple: [string, number, boolean] = ["name", 1, true];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function Annotations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Function with typed parameters and return
|
||||||
|
function greet(name: string): string {
|
||||||
|
return `Hello, ${name}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrow function
|
||||||
|
const add = (a: number, b: number): number => a + b;
|
||||||
|
|
||||||
|
// Optional parameters
|
||||||
|
function greetOptional(name: string, greeting?: string): string {
|
||||||
|
return `${greeting ?? "Hello"}, ${name}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default parameters
|
||||||
|
function greetDefault(name: string, greeting: string = "Hello"): string {
|
||||||
|
return `${greeting}, ${name}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rest parameters
|
||||||
|
function sum(...numbers: number[]): number {
|
||||||
|
return numbers.reduce((a, b) => a + b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function type alias
|
||||||
|
type Comparator<T> = (a: T, b: T) => number;
|
||||||
|
const numberCompare: Comparator<number> = (a, b) => a - b;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interfaces vs Type Aliases
|
||||||
|
|
||||||
|
### When to Use Interfaces
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Interfaces are ideal for object shapes
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interfaces can be extended
|
||||||
|
interface Employee extends User {
|
||||||
|
employeeId: string;
|
||||||
|
department: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interfaces can be implemented by classes
|
||||||
|
class Manager implements Employee {
|
||||||
|
constructor(
|
||||||
|
public id: string,
|
||||||
|
public name: string,
|
||||||
|
public email: string,
|
||||||
|
public employeeId: string,
|
||||||
|
public department: string
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declaration merging (interfaces only)
|
||||||
|
interface Config {
|
||||||
|
apiUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
timeout: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config now has both apiUrl and timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use Type Aliases
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Type aliases for unions
|
||||||
|
type Status = "pending" | "approved" | "rejected";
|
||||||
|
|
||||||
|
// Type aliases for complex types
|
||||||
|
type Handler = (event: Event) => void;
|
||||||
|
|
||||||
|
// Type aliases for mapped types
|
||||||
|
type Nullable<T> = { [K in keyof T]: T[K] | null };
|
||||||
|
|
||||||
|
// Type aliases for conditional types
|
||||||
|
type NonNullable<T> = T extends null | undefined ? never : T;
|
||||||
|
|
||||||
|
// Type aliases for tuples
|
||||||
|
type Point = [x: number, y: number];
|
||||||
|
type RGB = [red: number, green: number, blue: number];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decision Guide
|
||||||
|
|
||||||
|
| Use Case | Prefer |
|
||||||
|
|----------|--------|
|
||||||
|
| Object shapes | `interface` |
|
||||||
|
| Extending objects | `interface` |
|
||||||
|
| Class contracts | `interface` |
|
||||||
|
| Union types | `type` |
|
||||||
|
| Tuple types | `type` |
|
||||||
|
| Mapped types | `type` |
|
||||||
|
| Conditional types | `type` |
|
||||||
|
| Primitive aliases | `type` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Union and Intersection Types
|
||||||
|
|
||||||
|
### Union Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Value can be one of several types
|
||||||
|
type StringOrNumber = string | number;
|
||||||
|
|
||||||
|
// Discriminated unions (tagged unions)
|
||||||
|
interface Dog {
|
||||||
|
kind: "dog";
|
||||||
|
bark(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Cat {
|
||||||
|
kind: "cat";
|
||||||
|
meow(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pet = Dog | Cat;
|
||||||
|
|
||||||
|
function speak(pet: Pet): void {
|
||||||
|
switch (pet.kind) {
|
||||||
|
case "dog":
|
||||||
|
pet.bark();
|
||||||
|
break;
|
||||||
|
case "cat":
|
||||||
|
pet.meow();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Intersection Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Value must satisfy all types
|
||||||
|
interface HasName {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HasAge {
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Person = HasName & HasAge;
|
||||||
|
|
||||||
|
const person: Person = {
|
||||||
|
name: "Alice",
|
||||||
|
age: 30
|
||||||
|
};
|
||||||
|
|
||||||
|
// Practical: Extending with additional properties
|
||||||
|
type WithTimestamp<T> = T & { createdAt: Date; updatedAt: Date };
|
||||||
|
|
||||||
|
interface Article {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimestampedArticle = WithTimestamp<Article>;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Literal Types
|
||||||
|
|
||||||
|
### String Literals
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Specific string values
|
||||||
|
type Direction = "north" | "south" | "east" | "west";
|
||||||
|
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
||||||
|
|
||||||
|
function move(direction: Direction): void {
|
||||||
|
console.log(`Moving ${direction}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
move("north"); // OK
|
||||||
|
move("up"); // Error: Argument of type '"up"' is not assignable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Numeric Literals
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
type BinaryDigit = 0 | 1;
|
||||||
|
|
||||||
|
function roll(): DiceRoll {
|
||||||
|
return Math.ceil(Math.random() * 6) as DiceRoll;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template Literal Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Construct string literal types
|
||||||
|
type EventName = "click" | "hover" | "focus";
|
||||||
|
type HandlerName = `on${Capitalize<EventName>}`;
|
||||||
|
// "onClick" | "onHover" | "onFocus"
|
||||||
|
|
||||||
|
// CSS unit types
|
||||||
|
type CSSUnit = "px" | "em" | "rem" | "%";
|
||||||
|
type CSSValue = `${number}${CSSUnit}`;
|
||||||
|
|
||||||
|
const width: CSSValue = "100px"; // OK
|
||||||
|
const height: CSSValue = "50%"; // OK
|
||||||
|
const bad: CSSValue = "100"; // Error
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Guards and Narrowing
|
||||||
|
|
||||||
|
### Built-in Type Guards
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function process(value: string | number | null): string {
|
||||||
|
// typeof guard
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// typeof guard for number
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return value.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// null/undefined narrowing
|
||||||
|
if (value === null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exhaustiveness check
|
||||||
|
const _exhaustive: never = value;
|
||||||
|
throw new Error(`Unhandled case: ${_exhaustive}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### instanceof Guard
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class ApiError extends Error {
|
||||||
|
constructor(public statusCode: number, message: string) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ValidationError extends Error {
|
||||||
|
constructor(public fields: string[]) {
|
||||||
|
super("Validation failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(error: Error): void {
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
console.log(`API Error ${error.statusCode}: ${error.message}`);
|
||||||
|
} else if (error instanceof ValidationError) {
|
||||||
|
console.log(`Validation Error on fields: ${error.fields.join(", ")}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Unknown error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Type Guards
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface User {
|
||||||
|
type: "user";
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Admin {
|
||||||
|
type: "admin";
|
||||||
|
name: string;
|
||||||
|
permissions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type Account = User | Admin;
|
||||||
|
|
||||||
|
// Type predicate: returns boolean but narrows type
|
||||||
|
function isAdmin(account: Account): account is Admin {
|
||||||
|
return account.type === "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPermissions(account: Account): string[] {
|
||||||
|
if (isAdmin(account)) {
|
||||||
|
return account.permissions; // TypeScript knows this is Admin
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assertion Functions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function assertDefined<T>(value: T | undefined, message: string): asserts value is T {
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processUser(user: User | undefined): void {
|
||||||
|
assertDefined(user, "User is required");
|
||||||
|
// After assertion, user is narrowed to User
|
||||||
|
console.log(user.name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The satisfies Operator
|
||||||
|
|
||||||
|
### Problem: Type Assertions Hide Bugs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Using 'as' can hide type errors
|
||||||
|
const config = {
|
||||||
|
port: 3000,
|
||||||
|
host: "localhost"
|
||||||
|
} as Record<string, string | number>;
|
||||||
|
|
||||||
|
// No error, but port is now string | number
|
||||||
|
const portString = config.port.toFixed(2); // Runtime error if port is string!
|
||||||
|
```
|
||||||
|
|
||||||
|
### Solution: satisfies Validates Without Widening
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// satisfies checks conformance but preserves literal types
|
||||||
|
const config = {
|
||||||
|
port: 3000,
|
||||||
|
host: "localhost"
|
||||||
|
} satisfies Record<string, string | number>;
|
||||||
|
|
||||||
|
// TypeScript knows port is number, host is string
|
||||||
|
config.port.toFixed(2); // OK - port is number
|
||||||
|
config.host.toUpperCase(); // OK - host is string
|
||||||
|
```
|
||||||
|
|
||||||
|
### Practical Use Cases
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Color palette with constrained values
|
||||||
|
const palette = {
|
||||||
|
primary: "#007bff",
|
||||||
|
secondary: "#6c757d",
|
||||||
|
success: "#28a745"
|
||||||
|
} satisfies Record<string, `#${string}`>;
|
||||||
|
|
||||||
|
// TypeScript knows each property exists and is a hex string
|
||||||
|
palette.primary.startsWith("#"); // OK
|
||||||
|
|
||||||
|
// Route configuration
|
||||||
|
type RouteConfig = {
|
||||||
|
path: string;
|
||||||
|
method: "GET" | "POST";
|
||||||
|
handler: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
home: { path: "/", method: "GET", handler: () => {} },
|
||||||
|
login: { path: "/login", method: "POST", handler: () => {} }
|
||||||
|
} satisfies Record<string, RouteConfig>;
|
||||||
|
|
||||||
|
// TypeScript preserves literal types for each route
|
||||||
|
routes.home.method; // "GET" (not "GET" | "POST")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Special Types
|
||||||
|
|
||||||
|
### any vs unknown
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// any: Opt out of type checking (avoid)
|
||||||
|
let anyValue: any = "hello";
|
||||||
|
anyValue.toFixed(2); // No error, but crashes at runtime
|
||||||
|
|
||||||
|
// unknown: Type-safe any (prefer)
|
||||||
|
let unknownValue: unknown = "hello";
|
||||||
|
unknownValue.toFixed(2); // Error: Object is of type 'unknown'
|
||||||
|
|
||||||
|
// Must narrow unknown before use
|
||||||
|
if (typeof unknownValue === "string") {
|
||||||
|
unknownValue.toUpperCase(); // OK after narrowing
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### never
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// never: Represents impossible values
|
||||||
|
function fail(message: string): never {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exhaustiveness checking with never
|
||||||
|
type Shape = "circle" | "square";
|
||||||
|
|
||||||
|
function getArea(shape: Shape): number {
|
||||||
|
switch (shape) {
|
||||||
|
case "circle":
|
||||||
|
return Math.PI;
|
||||||
|
case "square":
|
||||||
|
return 1;
|
||||||
|
default:
|
||||||
|
// If we add a new shape, this will error
|
||||||
|
const _exhaustive: never = shape;
|
||||||
|
throw new Error(`Unknown shape: ${_exhaustive}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### void vs undefined
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// void: Function doesn't return anything meaningful
|
||||||
|
function log(message: string): void {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// undefined: Explicit undefined value
|
||||||
|
function findUser(id: string): User | undefined {
|
||||||
|
return users.get(id);
|
||||||
|
}
|
||||||
|
```
|
||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# TypeScript Project Setup Validator
|
||||||
|
#
|
||||||
|
# Checks that TypeScript project is properly configured for development.
|
||||||
|
# Run before starting development or deploying.
|
||||||
|
#
|
||||||
|
# Usage: ./validate-setup.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
pass() { echo -e "${GREEN}✓${NC} $1"; }
|
||||||
|
fail() { echo -e "${RED}✗${NC} $1"; ((ERRORS++)); }
|
||||||
|
warn() { echo -e "${YELLOW}!${NC} $1"; ((WARNINGS++)); }
|
||||||
|
info() { echo -e "${BLUE}ℹ${NC} $1"; }
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "TypeScript Project Setup Validator"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check 1: Node.js version
|
||||||
|
echo "Checking Node.js..."
|
||||||
|
if command -v node &> /dev/null; then
|
||||||
|
NODE_VERSION=$(node -v | sed 's/v//')
|
||||||
|
MAJOR_VERSION=$(echo $NODE_VERSION | cut -d. -f1)
|
||||||
|
if [[ $MAJOR_VERSION -ge 20 ]]; then
|
||||||
|
pass "Node.js $NODE_VERSION (recommended: 20+)"
|
||||||
|
else
|
||||||
|
warn "Node.js $NODE_VERSION (recommended: 20+ for full ES2024 support)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "Node.js not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 2: Package manager
|
||||||
|
echo ""
|
||||||
|
echo "Checking package manager..."
|
||||||
|
if command -v pnpm &> /dev/null; then
|
||||||
|
PNPM_VERSION=$(pnpm -v)
|
||||||
|
pass "pnpm $PNPM_VERSION installed (recommended)"
|
||||||
|
elif command -v npm &> /dev/null; then
|
||||||
|
NPM_VERSION=$(npm -v)
|
||||||
|
warn "npm $NPM_VERSION installed (pnpm recommended for better performance)"
|
||||||
|
else
|
||||||
|
fail "No package manager found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 3: TypeScript installation
|
||||||
|
echo ""
|
||||||
|
echo "Checking TypeScript..."
|
||||||
|
if [[ -f "node_modules/typescript/package.json" ]]; then
|
||||||
|
TS_VERSION=$(node -p "require('./node_modules/typescript/package.json').version")
|
||||||
|
MAJOR_VERSION=$(echo $TS_VERSION | cut -d. -f1)
|
||||||
|
MINOR_VERSION=$(echo $TS_VERSION | cut -d. -f2)
|
||||||
|
if [[ $MAJOR_VERSION -ge 5 ]] && [[ $MINOR_VERSION -ge 5 ]]; then
|
||||||
|
pass "TypeScript $TS_VERSION installed"
|
||||||
|
else
|
||||||
|
warn "TypeScript $TS_VERSION installed (5.5+ recommended)"
|
||||||
|
fi
|
||||||
|
elif command -v tsc &> /dev/null; then
|
||||||
|
TS_VERSION=$(tsc --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||||
|
pass "TypeScript $TS_VERSION (global)"
|
||||||
|
else
|
||||||
|
fail "TypeScript not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 4: tsconfig.json
|
||||||
|
echo ""
|
||||||
|
echo "Checking TypeScript configuration..."
|
||||||
|
if [[ -f "tsconfig.json" ]]; then
|
||||||
|
pass "tsconfig.json exists"
|
||||||
|
|
||||||
|
# Check for strict mode
|
||||||
|
if grep -q '"strict":\s*true' tsconfig.json 2>/dev/null; then
|
||||||
|
pass "Strict mode enabled"
|
||||||
|
else
|
||||||
|
warn "Strict mode not enabled (recommended)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for target
|
||||||
|
if grep -qE '"target":\s*"ES202[234]"' tsconfig.json 2>/dev/null; then
|
||||||
|
pass "Modern ES target configured"
|
||||||
|
else
|
||||||
|
info "Consider updating target to ES2024"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "tsconfig.json not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 5: package.json type field
|
||||||
|
echo ""
|
||||||
|
echo "Checking ESM configuration..."
|
||||||
|
if [[ -f "package.json" ]]; then
|
||||||
|
if grep -q '"type":\s*"module"' package.json 2>/dev/null; then
|
||||||
|
pass "ESM mode enabled (type: module)"
|
||||||
|
else
|
||||||
|
info "Consider adding \"type\": \"module\" for ESM"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "package.json not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 6: ESLint configuration
|
||||||
|
echo ""
|
||||||
|
echo "Checking linting setup..."
|
||||||
|
if [[ -f "eslint.config.js" ]] || [[ -f "eslint.config.mjs" ]]; then
|
||||||
|
pass "ESLint flat config found"
|
||||||
|
elif [[ -f ".eslintrc.js" ]] || [[ -f ".eslintrc.json" ]]; then
|
||||||
|
warn "Legacy ESLint config found (migrate to flat config for ESLint 9+)"
|
||||||
|
else
|
||||||
|
warn "ESLint not configured"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 7: Prettier configuration
|
||||||
|
echo ""
|
||||||
|
echo "Checking formatting setup..."
|
||||||
|
if [[ -f ".prettierrc" ]] || [[ -f ".prettierrc.json" ]] || [[ -f "prettier.config.js" ]]; then
|
||||||
|
pass "Prettier configured"
|
||||||
|
else
|
||||||
|
info "Consider adding Prettier for consistent formatting"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 8: Test framework
|
||||||
|
echo ""
|
||||||
|
echo "Checking test setup..."
|
||||||
|
if [[ -f "vitest.config.ts" ]] || [[ -f "vitest.config.js" ]]; then
|
||||||
|
pass "Vitest configured"
|
||||||
|
elif [[ -f "jest.config.ts" ]] || [[ -f "jest.config.js" ]]; then
|
||||||
|
pass "Jest configured"
|
||||||
|
elif grep -q '"vitest"' package.json 2>/dev/null; then
|
||||||
|
pass "Vitest in dependencies"
|
||||||
|
elif grep -q '"jest"' package.json 2>/dev/null; then
|
||||||
|
pass "Jest in dependencies"
|
||||||
|
else
|
||||||
|
warn "No test framework detected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 9: Git hooks (optional)
|
||||||
|
echo ""
|
||||||
|
echo "Checking Git hooks..."
|
||||||
|
if [[ -d ".husky" ]]; then
|
||||||
|
pass "Husky Git hooks configured"
|
||||||
|
elif [[ -f ".git/hooks/pre-commit" ]]; then
|
||||||
|
pass "Git pre-commit hook exists"
|
||||||
|
else
|
||||||
|
info "Consider adding pre-commit hooks for quality checks"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 10: Dependencies up to date
|
||||||
|
echo ""
|
||||||
|
echo "Checking for outdated packages..."
|
||||||
|
if command -v pnpm &> /dev/null && [[ -f "pnpm-lock.yaml" ]]; then
|
||||||
|
OUTDATED=$(pnpm outdated --format json 2>/dev/null | grep -c '"' || echo "0")
|
||||||
|
if [[ "$OUTDATED" == "0" ]] || [[ "$OUTDATED" == "" ]]; then
|
||||||
|
pass "All packages up to date"
|
||||||
|
else
|
||||||
|
info "Some packages may be outdated (run: pnpm outdated)"
|
||||||
|
fi
|
||||||
|
elif command -v npm &> /dev/null && [[ -f "package-lock.json" ]]; then
|
||||||
|
info "Run 'npm outdated' to check for updates"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Summary"
|
||||||
|
echo "=========================================="
|
||||||
|
if [[ $ERRORS -eq 0 ]] && [[ $WARNINGS -eq 0 ]]; then
|
||||||
|
echo -e "${GREEN}All checks passed!${NC}"
|
||||||
|
echo "Your TypeScript project is properly configured."
|
||||||
|
exit 0
|
||||||
|
elif [[ $ERRORS -eq 0 ]]; then
|
||||||
|
echo -e "${YELLOW}Passed with $WARNINGS warning(s)${NC}"
|
||||||
|
echo "Project is functional but could be improved."
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}Failed with $ERRORS error(s) and $WARNINGS warning(s)${NC}"
|
||||||
|
echo "Fix errors before proceeding."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
---
|
||||||
|
name: serilog-log-analyzer
|
||||||
|
description: |
|
||||||
|
Expert Serilog JSON log analyst for OTCryptoRobot.
|
||||||
|
MUST BE USED for analyzing BE (RobotCBBE) and FE (RobotFE) logs.
|
||||||
|
Reads CLEF (Compact Log Event Format) JSON logs written by Serilog.
|
||||||
|
|
||||||
|
Log file locations:
|
||||||
|
- BE JSON logs: `RobotCBBE/Logs/CryptoRobot_Log-{date}.json` (CompactJsonFormatter, min level: Information)
|
||||||
|
- BE text logs: `RobotCBBE/Logs/log-{date}.txt` (all levels including Verbose)
|
||||||
|
- FE logs: `RobotFE/Logs/`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- <example>
|
||||||
|
Context: Investigating errors
|
||||||
|
user: "Check today's logs for errors"
|
||||||
|
assistant: "I'll use @serilog-log-analyzer to analyze the logs"
|
||||||
|
<commentary>
|
||||||
|
Reads JSON log, groups errors, finds patterns.
|
||||||
|
</commentary>
|
||||||
|
</example>
|
||||||
|
- <example>
|
||||||
|
Context: Auth failures
|
||||||
|
user: "Why am I getting 401 errors?"
|
||||||
|
assistant: "Let me use @serilog-log-analyzer to trace the auth failures"
|
||||||
|
<commentary>
|
||||||
|
Finds HTTP 401 responses, correlates with request URLs and JWT auth.
|
||||||
|
</commentary>
|
||||||
|
</example>
|
||||||
|
- <example>
|
||||||
|
Context: Performance
|
||||||
|
user: "The app seems slow today"
|
||||||
|
assistant: "I'll use @serilog-log-analyzer to find slow operations"
|
||||||
|
<commentary>
|
||||||
|
Analyzes API-Perf entries, WebSocket timeouts, DB operation durations.
|
||||||
|
</commentary>
|
||||||
|
</example>
|
||||||
|
|
||||||
|
Delegations:
|
||||||
|
- <delegation>
|
||||||
|
Trigger: Database errors found
|
||||||
|
Target: cryptorobot-ef-expert
|
||||||
|
Handoff: "Log analysis found DB issues. Investigate: [entity/query details]"
|
||||||
|
</delegation>
|
||||||
|
- <delegation>
|
||||||
|
Trigger: Frontend rendering issues found
|
||||||
|
Target: cryptorobot-blazor-architect
|
||||||
|
Handoff: "Log analysis found FE issues. Investigate: [component details]"
|
||||||
|
</delegation>
|
||||||
|
---
|
||||||
|
|
||||||
|
# Serilog JSON Log Analyzer
|
||||||
|
|
||||||
|
You are an expert log analyst for the OTCryptoRobot trading system. Analyze Serilog JSON logs and provide actionable insights.
|
||||||
|
|
||||||
|
## Input Format
|
||||||
|
|
||||||
|
Logs are in CLEF (Compact Log Event Format) written by `Serilog.Formatting.Compact.CompactJsonFormatter`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"@t":"2026-02-16T19:05:52.787+04:00","@l":"Warning","@m":"HTTP 401 from GET https://api.coinbase.com/...","@x":"Exception...","StatusCode":401,"Url":"..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field mapping:**
|
||||||
|
- `@t` = timestamp (ISO 8601)
|
||||||
|
- `@l` = log level (Verbose/Debug/Information/Warning/Error/Fatal). If missing, level is Information.
|
||||||
|
- `@m` = rendered message
|
||||||
|
- `@mt` = message template (with placeholders like `{Url}`)
|
||||||
|
- `@x` = exception (full stack trace)
|
||||||
|
- `@i` = event id (hex)
|
||||||
|
- `@r` = renderings
|
||||||
|
- All other properties are structured data (e.g., `StatusCode`, `MarketCode`, `ThreadId`, `SourceContext`)
|
||||||
|
|
||||||
|
## Analysis Steps
|
||||||
|
|
||||||
|
### 1. Overview
|
||||||
|
- Total log count
|
||||||
|
- Time range (first to last entry)
|
||||||
|
- Log level distribution (count + percentage for each level)
|
||||||
|
- Events per minute/hour throughput
|
||||||
|
|
||||||
|
### 2. Errors & Exceptions (PRIORITY)
|
||||||
|
- Group all Error/Fatal entries by exception type or message pattern
|
||||||
|
- For each group: count, first/last occurrence, sample message with timestamp
|
||||||
|
- Identify error patterns (do errors cluster in time? repeat periodically?)
|
||||||
|
- Trace root cause chains (which Warning leads to which Error?)
|
||||||
|
- Flag Fatal level events immediately
|
||||||
|
|
||||||
|
### 3. Performance Analysis
|
||||||
|
- Find slow operations (look for properties: elapsed, duration, ExecMs, WaitMs)
|
||||||
|
- Identify `[API-Perf]` entries and their timings
|
||||||
|
- Find timeout patterns (TimeoutError, RequestTimeout)
|
||||||
|
- Database operation timings if present
|
||||||
|
|
||||||
|
### 4. External Services (Coinbase API)
|
||||||
|
- HTTP response failures (401, 429, 500) — group by status code and URL
|
||||||
|
- WebSocket disconnections/reconnections
|
||||||
|
- Authentication failures (`authentication failure`, `Unauthorized`)
|
||||||
|
- Rate limiting events
|
||||||
|
|
||||||
|
### 5. Trading Operations
|
||||||
|
- Order state changes (Open, Filled, Cancelled)
|
||||||
|
- Subscription failures (Ticker, User)
|
||||||
|
- Price update gaps (missing data periods)
|
||||||
|
|
||||||
|
### 6. Patterns & Anomalies
|
||||||
|
- Repeating error cycles
|
||||||
|
- Sudden spikes in error rate
|
||||||
|
- Gaps in logging (service downtime?)
|
||||||
|
- Unusual event sequences
|
||||||
|
|
||||||
|
### 7. Structured Properties
|
||||||
|
- Extract and analyze: CorrelationId, OrderId, MarketCode, ThreadId, SourceContext
|
||||||
|
- Group events by request flow
|
||||||
|
- Identify which entities/operations cause most errors
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
### CRITICAL ISSUES (fix immediately)
|
||||||
|
- [Issue with exact log evidence: timestamp + message]
|
||||||
|
|
||||||
|
### WARNINGS (investigate soon)
|
||||||
|
- [Pattern that may become critical]
|
||||||
|
|
||||||
|
### OBSERVATIONS
|
||||||
|
- [Performance notes, interesting patterns]
|
||||||
|
|
||||||
|
### TIMELINE
|
||||||
|
- Chronological summary of significant events
|
||||||
|
|
||||||
|
### RECOMMENDATIONS
|
||||||
|
- Specific actionable fixes based on log evidence
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
- Always quote specific log entries as evidence (timestamp + message)
|
||||||
|
- If logs contain sensitive data (API keys, passwords, JWT tokens), flag it as a security issue
|
||||||
|
- Calculate metrics: error rate %, mean time between failures, avg response times
|
||||||
|
- For `DbUpdateConcurrencyException` — analyze entity, frequency, suggest specific fix
|
||||||
|
- For WebSocket errors — analyze reconnection pattern and connection stability
|
||||||
|
- For HTTP 401 — check if JWT auth is present, correlate with `/time` sync calls
|
||||||
|
- Focus on actionable insights, not just listing errors
|
||||||
|
- When reading large logs, use the `.json` file for structured analysis, `.txt` for full context with Verbose entries
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"name": "solid",
|
||||||
|
"owner": "ramziddin",
|
||||||
|
"repo": "solid-skills",
|
||||||
|
"path": "skills/solid",
|
||||||
|
"branch": "main",
|
||||||
|
"sha": "3f43af8adc4bddd607640a7571365dca69ec2285",
|
||||||
|
"source": "manual"
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
---
|
||||||
|
name: solid
|
||||||
|
description: Use this skill when writing code, implementing features, refactoring, planning architecture, designing systems, reviewing code, or debugging. This skill transforms junior-level code into senior-engineer quality software through SOLID principles, TDD, clean code practices, and professional software design.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Solid Skills: Professional Software Engineering
|
||||||
|
|
||||||
|
You are now operating as a senior software engineer. Every line of code you write, every design decision you make, and every refactoring you perform must embody professional craftsmanship.
|
||||||
|
|
||||||
|
## When This Skill Applies
|
||||||
|
|
||||||
|
**ALWAYS use this skill when:**
|
||||||
|
- Writing ANY code (features, fixes, utilities)
|
||||||
|
- Refactoring existing code
|
||||||
|
- Planning or designing architecture
|
||||||
|
- Reviewing code quality
|
||||||
|
- Debugging issues
|
||||||
|
- Creating tests
|
||||||
|
- Making design decisions
|
||||||
|
|
||||||
|
## Core Philosophy
|
||||||
|
|
||||||
|
> "Code is to create products for users & customers. Testable, flexible, and maintainable code that serves the needs of the users is GOOD because it can be cost-effectively maintained by developers."
|
||||||
|
|
||||||
|
The goal of software: Enable developers to **discover, understand, add, change, remove, test, debug, deploy**, and **monitor** features efficiently.
|
||||||
|
|
||||||
|
## The Non-Negotiable Process
|
||||||
|
|
||||||
|
### 1. ALWAYS Start with Tests (TDD)
|
||||||
|
|
||||||
|
**Red-Green-Refactor is not optional:**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. RED - Write a failing test that describes the behavior
|
||||||
|
2. GREEN - Write the SIMPLEST code to make it pass
|
||||||
|
3. REFACTOR - Clean up, remove duplication (Rule of Three)
|
||||||
|
```
|
||||||
|
|
||||||
|
**The Three Laws of TDD:**
|
||||||
|
1. You cannot write production code unless it makes a failing test pass
|
||||||
|
2. You cannot write more test code than is sufficient to fail
|
||||||
|
3. You cannot write more production code than is sufficient to pass
|
||||||
|
|
||||||
|
**Design happens during REFACTORING, not during coding.**
|
||||||
|
|
||||||
|
See: [references/tdd.md](references/tdd.md)
|
||||||
|
|
||||||
|
### 2. Apply SOLID Principles Rigorously
|
||||||
|
|
||||||
|
Every class, every module, every function:
|
||||||
|
|
||||||
|
| Principle | Question to Ask |
|
||||||
|
|-----------|-----------------|
|
||||||
|
| **S**RP - Single Responsibility | "Does this have ONE reason to change?" |
|
||||||
|
| **O**CP - Open/Closed | "Can I extend without modifying?" |
|
||||||
|
| **L**SP - Liskov Substitution | "Can subtypes replace base types safely?" |
|
||||||
|
| **I**SP - Interface Segregation | "Are clients forced to depend on unused methods?" |
|
||||||
|
| **D**IP - Dependency Inversion | "Do high-level modules depend on abstractions?" |
|
||||||
|
|
||||||
|
See: [references/solid-principles.md](references/solid-principles.md)
|
||||||
|
|
||||||
|
### 3. Write Clean, Human-Readable Code
|
||||||
|
|
||||||
|
**Naming (in order of priority):**
|
||||||
|
1. **Consistency** - Same concept = same name everywhere
|
||||||
|
2. **Understandability** - Domain language, not technical jargon
|
||||||
|
3. **Specificity** - Precise, not vague (avoid `data`, `info`, `manager`)
|
||||||
|
4. **Brevity** - Short but not cryptic
|
||||||
|
5. **Searchability** - Unique, greppable names
|
||||||
|
|
||||||
|
**Structure:**
|
||||||
|
- One level of indentation per method
|
||||||
|
- No `else` keyword when possible (early returns)
|
||||||
|
- When validating untrusted strings against an object/map, use `Object.hasOwn(...)` (or `Object.prototype.hasOwnProperty.call(...)`) — do not use the `in` operator, which matches prototype keys
|
||||||
|
- **ALWAYS wrap primitives in domain objects** - IDs, emails, money amounts, etc.
|
||||||
|
- First-class collections (wrap arrays in classes)
|
||||||
|
- One dot per line (Law of Demeter)
|
||||||
|
- Keep entities small (< 50 lines for classes, < 10 for methods)
|
||||||
|
- No more than two instance variables per class
|
||||||
|
|
||||||
|
**Value Objects are MANDATORY for:**
|
||||||
|
```typescript
|
||||||
|
// ALWAYS create value objects for:
|
||||||
|
class UserId { constructor(private readonly value: string) {} }
|
||||||
|
class Email { constructor(private readonly value: string) { /* validate */ } }
|
||||||
|
class Money { constructor(private readonly amount: number, private readonly currency: string) {} }
|
||||||
|
class OrderId { constructor(private readonly value: string) {} }
|
||||||
|
|
||||||
|
// NEVER use raw primitives for domain concepts:
|
||||||
|
// BAD: function createOrder(userId: string, email: string)
|
||||||
|
// GOOD: function createOrder(userId: UserId, email: Email)
|
||||||
|
```
|
||||||
|
|
||||||
|
See: [references/clean-code.md](references/clean-code.md)
|
||||||
|
|
||||||
|
### 4. Design with Responsibility in Mind
|
||||||
|
|
||||||
|
**Ask these questions for every class:**
|
||||||
|
1. "What pattern is this?" (Entity, Service, Repository, Factory, etc.)
|
||||||
|
2. "Is it doing too much?" (Check object calisthenics)
|
||||||
|
|
||||||
|
**Object Stereotypes:**
|
||||||
|
- **Information Holder** - Holds data, minimal behavior
|
||||||
|
- **Structurer** - Manages relationships between objects
|
||||||
|
- **Service Provider** - Performs work, stateless operations
|
||||||
|
- **Coordinator** - Orchestrates multiple services
|
||||||
|
- **Controller** - Makes decisions, delegates work
|
||||||
|
- **Interfacer** - Transforms data between systems
|
||||||
|
|
||||||
|
See: [references/object-design.md](references/object-design.md)
|
||||||
|
|
||||||
|
### 5. Manage Complexity Ruthlessly
|
||||||
|
|
||||||
|
**Essential complexity** = inherent to the problem domain
|
||||||
|
**Accidental complexity** = introduced by our solutions
|
||||||
|
|
||||||
|
**Detect complexity through:**
|
||||||
|
- Change amplification (small change = many files)
|
||||||
|
- Cognitive load (hard to understand)
|
||||||
|
- Unknown unknowns (surprises in behavior)
|
||||||
|
|
||||||
|
**Fight complexity with:**
|
||||||
|
- YAGNI - Don't build what you don't need NOW
|
||||||
|
- KISS - Simplest solution that works
|
||||||
|
- DRY - But only after Rule of Three (wait for 3 duplications)
|
||||||
|
|
||||||
|
See: [references/complexity.md](references/complexity.md)
|
||||||
|
|
||||||
|
### 6. Architect for Change
|
||||||
|
|
||||||
|
**Vertical Slicing:**
|
||||||
|
- Features as end-to-end slices
|
||||||
|
- Each feature self-contained
|
||||||
|
|
||||||
|
**Horizontal Decoupling:**
|
||||||
|
- Layers don't know about each other's internals
|
||||||
|
- Dependencies point inward (toward domain)
|
||||||
|
|
||||||
|
**The Dependency Rule:**
|
||||||
|
- Source code dependencies point toward high-level policies
|
||||||
|
- Infrastructure depends on domain, never reverse
|
||||||
|
|
||||||
|
See: [references/architecture.md](references/architecture.md)
|
||||||
|
|
||||||
|
## The Four Elements of Simple Design (XP)
|
||||||
|
|
||||||
|
In priority order:
|
||||||
|
1. **Runs all the tests** - Must work correctly
|
||||||
|
2. **Expresses intent** - Readable, reveals purpose
|
||||||
|
3. **No duplication** - DRY (but Rule of Three)
|
||||||
|
4. **Minimal** - Fewest classes, methods possible
|
||||||
|
|
||||||
|
## Code Smell Detection
|
||||||
|
|
||||||
|
**Stop and refactor when you see:**
|
||||||
|
|
||||||
|
| Smell | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| Long Method | Extract methods, compose method pattern |
|
||||||
|
| Large Class | Extract class, single responsibility |
|
||||||
|
| Long Parameter List | Introduce parameter object |
|
||||||
|
| Divergent Change | Split into focused classes |
|
||||||
|
| Shotgun Surgery | Move related code together |
|
||||||
|
| Feature Envy | Move method to the envied class |
|
||||||
|
| Data Clumps | Extract class for grouped data |
|
||||||
|
| Primitive Obsession | Wrap in value objects |
|
||||||
|
| Switch Statements | Replace with polymorphism |
|
||||||
|
| Parallel Inheritance | Merge hierarchies |
|
||||||
|
| Speculative Generality | YAGNI - remove unused abstractions |
|
||||||
|
|
||||||
|
See: [references/code-smells.md](references/code-smells.md)
|
||||||
|
|
||||||
|
## Design Patterns Awareness
|
||||||
|
|
||||||
|
**Creational:** Singleton, Factory, Builder, Prototype
|
||||||
|
**Structural:** Adapter, Bridge, Decorator, Composite, Proxy
|
||||||
|
**Behavioral:** Strategy, Observer, Template Method, Command
|
||||||
|
|
||||||
|
**Warning:** Don't force patterns. Let them emerge from refactoring.
|
||||||
|
|
||||||
|
See: [references/design-patterns.md](references/design-patterns.md)
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
**Test Types (from inner to outer):**
|
||||||
|
1. **Unit Tests** - Single class/function, fast, isolated
|
||||||
|
2. **Integration Tests** - Multiple components together
|
||||||
|
3. **E2E/Acceptance Tests** - Full system, user perspective
|
||||||
|
|
||||||
|
**Arrange-Act-Assert Pattern:**
|
||||||
|
```typescript
|
||||||
|
// Arrange - Set up test state
|
||||||
|
const calculator = new Calculator();
|
||||||
|
|
||||||
|
// Act - Execute the behavior
|
||||||
|
const result = calculator.add(2, 3);
|
||||||
|
|
||||||
|
// Assert - Verify the outcome
|
||||||
|
expect(result).toBe(5);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Naming:** Use concrete examples, not abstract statements
|
||||||
|
```typescript
|
||||||
|
// BAD: 'can add numbers'
|
||||||
|
// GOOD: 'when adding 2 + 3, returns 5'
|
||||||
|
```
|
||||||
|
|
||||||
|
See: [references/testing.md](references/testing.md)
|
||||||
|
|
||||||
|
## Behavioral Principles
|
||||||
|
|
||||||
|
- **Tell, Don't Ask** - Command objects, don't query and decide
|
||||||
|
- **Design by Contract** - Preconditions, postconditions, invariants
|
||||||
|
- **Hollywood Principle** - "Don't call us, we'll call you" (IoC)
|
||||||
|
- **Law of Demeter** - Only talk to immediate friends
|
||||||
|
|
||||||
|
## Pre-Code Checklist
|
||||||
|
|
||||||
|
Before writing ANY code, answer:
|
||||||
|
|
||||||
|
1. [ ] Do I understand the requirement? (Write acceptance criteria first)
|
||||||
|
2. [ ] What test will I write first?
|
||||||
|
3. [ ] What is the simplest solution?
|
||||||
|
4. [ ] What patterns might apply? (Don't force them)
|
||||||
|
5. [ ] Am I solving a real problem or a hypothetical one?
|
||||||
|
|
||||||
|
## During-Code Checklist
|
||||||
|
|
||||||
|
While coding, continuously ask:
|
||||||
|
|
||||||
|
1. [ ] Is this the simplest thing that could work?
|
||||||
|
2. [ ] Does this class have a single responsibility?
|
||||||
|
3. [ ] Am I depending on abstractions or concretions?
|
||||||
|
4. [ ] Can I name this more clearly?
|
||||||
|
5. [ ] Is there duplication I should extract? (Rule of Three)
|
||||||
|
|
||||||
|
## Post-Code Checklist
|
||||||
|
|
||||||
|
After the code works:
|
||||||
|
|
||||||
|
1. [ ] Do all tests pass?
|
||||||
|
2. [ ] Is there any dead code to remove?
|
||||||
|
3. [ ] Can I simplify any complex conditions?
|
||||||
|
4. [ ] Are names still accurate after changes?
|
||||||
|
5. [ ] Would a junior understand this in 6 months?
|
||||||
|
|
||||||
|
## Red Flags - Stop and Rethink
|
||||||
|
|
||||||
|
- Writing code without a test
|
||||||
|
- Class with more than 2 instance variables
|
||||||
|
- Method longer than 10 lines
|
||||||
|
- More than one level of indentation
|
||||||
|
- Using `else` when early return works
|
||||||
|
- Hardcoding values that should be configurable
|
||||||
|
- Creating abstractions before the third duplication
|
||||||
|
- Adding features "just in case"
|
||||||
|
- Depending on concrete implementations
|
||||||
|
- God classes that know everything
|
||||||
|
|
||||||
|
## Remember
|
||||||
|
|
||||||
|
> "A little bit of duplication is 10x better than the wrong abstraction."
|
||||||
|
|
||||||
|
> "Focus on WHAT needs to happen, not HOW it needs to happen."
|
||||||
|
|
||||||
|
> "Design principles become second nature through practice. Eventually, you won't think about SOLID - you'll just write SOLID code."
|
||||||
|
|
||||||
|
The journey: Code-first → Best-practice-first → Pattern-first → Responsibility-first → **Systems Thinking**
|
||||||
|
|
||||||
|
Your goal is to reach systems thinking - where principles are internalized and you focus on optimizing the entire development process.
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
# Software Architecture
|
||||||
|
|
||||||
|
## The Goal of Architecture
|
||||||
|
|
||||||
|
Enable the development team to:
|
||||||
|
1. **Add** features with minimal friction
|
||||||
|
2. **Change** existing features safely
|
||||||
|
3. **Remove** features cleanly
|
||||||
|
4. **Test** features in isolation
|
||||||
|
5. **Deploy** independently when possible
|
||||||
|
|
||||||
|
## Architectural Principles
|
||||||
|
|
||||||
|
### 1. Vertical Boundaries (Features/Slices)
|
||||||
|
|
||||||
|
Organize by **feature**, not by technical layer.
|
||||||
|
|
||||||
|
```
|
||||||
|
BAD: Layer-first
|
||||||
|
src/
|
||||||
|
controllers/
|
||||||
|
UserController.ts
|
||||||
|
OrderController.ts
|
||||||
|
services/
|
||||||
|
UserService.ts
|
||||||
|
OrderService.ts
|
||||||
|
repositories/
|
||||||
|
UserRepository.ts
|
||||||
|
OrderRepository.ts
|
||||||
|
|
||||||
|
GOOD: Feature-first
|
||||||
|
src/
|
||||||
|
users/
|
||||||
|
UserController.ts
|
||||||
|
UserService.ts
|
||||||
|
UserRepository.ts
|
||||||
|
orders/
|
||||||
|
OrderController.ts
|
||||||
|
OrderService.ts
|
||||||
|
OrderRepository.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Changes to "users" feature stay in `users/`. High cohesion within features.
|
||||||
|
|
||||||
|
### 2. Horizontal Boundaries (Layers)
|
||||||
|
|
||||||
|
Separate concerns into layers with clear dependencies.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────┐
|
||||||
|
│ Presentation │ UI, Controllers, CLI
|
||||||
|
├──────────────────────────────────────┤
|
||||||
|
│ Application │ Use Cases, Orchestration
|
||||||
|
├──────────────────────────────────────┤
|
||||||
|
│ Domain │ Business Logic, Entities
|
||||||
|
├──────────────────────────────────────┤
|
||||||
|
│ Infrastructure │ Database, APIs, External
|
||||||
|
└──────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. The Dependency Rule
|
||||||
|
|
||||||
|
**Dependencies point INWARD.**
|
||||||
|
|
||||||
|
```
|
||||||
|
Infrastructure → Application → Domain
|
||||||
|
↓ ↓ ↓
|
||||||
|
(outer) (middle) (inner)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Inner layers know NOTHING about outer layers
|
||||||
|
- Domain has zero dependencies on infrastructure
|
||||||
|
- Use interfaces to invert dependencies
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Domain defines the interface (inner)
|
||||||
|
interface UserRepository {
|
||||||
|
save(user: User): Promise<void>;
|
||||||
|
findById(id: UserId): Promise<User | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infrastructure implements it (outer)
|
||||||
|
class PostgresUserRepository implements UserRepository {
|
||||||
|
save(user: User): Promise<void> {
|
||||||
|
// SQL here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain service uses the interface
|
||||||
|
class UserService {
|
||||||
|
constructor(private repo: UserRepository) {} // Depends on abstraction
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Contracts
|
||||||
|
|
||||||
|
Interfaces define boundaries between components.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// The contract
|
||||||
|
interface PaymentGateway {
|
||||||
|
charge(amount: Money, card: CardDetails): Promise<ChargeResult>;
|
||||||
|
refund(chargeId: string): Promise<RefundResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple implementations possible
|
||||||
|
class StripeGateway implements PaymentGateway { }
|
||||||
|
class PayPalGateway implements PaymentGateway { }
|
||||||
|
class MockGateway implements PaymentGateway { } // For tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Cross-Cutting Concerns
|
||||||
|
|
||||||
|
Concerns that span multiple features: logging, auth, validation, error handling.
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- Middleware/interceptors
|
||||||
|
- Decorators
|
||||||
|
- Aspect-oriented approaches
|
||||||
|
- Base classes (use sparingly)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Middleware approach
|
||||||
|
class LoggingMiddleware {
|
||||||
|
handle(request: Request, next: Handler): Response {
|
||||||
|
console.log(`Request: ${request.path}`);
|
||||||
|
const response = next(request);
|
||||||
|
console.log(`Response: ${response.status}`);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Conway's Law
|
||||||
|
|
||||||
|
> "Organizations design systems that mirror their communication structure."
|
||||||
|
|
||||||
|
**Implication:** Team structure affects architecture. Align both intentionally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Architectural Styles
|
||||||
|
|
||||||
|
### Layered Architecture
|
||||||
|
|
||||||
|
Traditional layers: Presentation → Business → Persistence
|
||||||
|
|
||||||
|
**Pros:** Simple, well-understood
|
||||||
|
**Cons:** Can become a "big ball of mud" without discipline
|
||||||
|
|
||||||
|
### Hexagonal Architecture (Ports & Adapters)
|
||||||
|
|
||||||
|
Domain at center, adapters around the edges.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ HTTP Adapter │
|
||||||
|
└─────────┬───────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────▼─────────────────┐
|
||||||
|
│ DOMAIN │
|
||||||
|
│ ┌─────────────────────────┐ │
|
||||||
|
│ │ Business Logic │ │
|
||||||
|
│ │ Use Cases │ │
|
||||||
|
│ └─────────────────────────┘ │
|
||||||
|
└─────────────────┬─────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────▼───────────┐
|
||||||
|
│ Database Adapter │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ports:** Interfaces defined by the domain
|
||||||
|
**Adapters:** Implementations that connect to the outside world
|
||||||
|
|
||||||
|
### Clean Architecture
|
||||||
|
|
||||||
|
Similar to Hexagonal, with explicit layers:
|
||||||
|
|
||||||
|
1. **Entities** - Enterprise business rules
|
||||||
|
2. **Use Cases** - Application business rules
|
||||||
|
3. **Interface Adapters** - Controllers, Presenters, Gateways
|
||||||
|
4. **Frameworks & Drivers** - Web, DB, External interfaces
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature-Driven Structure (Frontend)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
features/
|
||||||
|
auth/
|
||||||
|
components/
|
||||||
|
LoginForm.tsx
|
||||||
|
SignupForm.tsx
|
||||||
|
hooks/
|
||||||
|
useAuth.ts
|
||||||
|
services/
|
||||||
|
authService.ts
|
||||||
|
types/
|
||||||
|
auth.types.ts
|
||||||
|
index.ts # Public API
|
||||||
|
checkout/
|
||||||
|
components/
|
||||||
|
hooks/
|
||||||
|
services/
|
||||||
|
types/
|
||||||
|
index.ts
|
||||||
|
shared/
|
||||||
|
components/ # Truly shared UI
|
||||||
|
hooks/ # Truly shared hooks
|
||||||
|
utils/ # Truly shared utilities
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature-Driven Structure (Backend)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
modules/
|
||||||
|
users/
|
||||||
|
domain/
|
||||||
|
User.ts
|
||||||
|
UserRepository.ts # Interface
|
||||||
|
application/
|
||||||
|
CreateUser.ts # Use case
|
||||||
|
GetUser.ts # Use case
|
||||||
|
infrastructure/
|
||||||
|
PostgresUserRepo.ts
|
||||||
|
presentation/
|
||||||
|
UserController.ts
|
||||||
|
UserDTO.ts
|
||||||
|
orders/
|
||||||
|
domain/
|
||||||
|
application/
|
||||||
|
infrastructure/
|
||||||
|
presentation/
|
||||||
|
shared/
|
||||||
|
domain/ # Shared value objects
|
||||||
|
infrastructure/ # Shared infra utilities
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Walking Skeleton
|
||||||
|
|
||||||
|
Start with a minimal end-to-end slice:
|
||||||
|
|
||||||
|
1. **Thinnest possible feature** that touches all layers
|
||||||
|
2. **Deployable** from day one
|
||||||
|
3. **Proves the architecture** works
|
||||||
|
|
||||||
|
Example walking skeleton for e-commerce:
|
||||||
|
- User can view ONE product (hardcoded)
|
||||||
|
- User can add it to cart
|
||||||
|
- User can "checkout" (just logs)
|
||||||
|
|
||||||
|
From there, flesh out each feature fully.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────┐
|
||||||
|
│ E2E / Acceptance Tests │ Few, slow, high confidence
|
||||||
|
├────────────────────────────────────────────┤
|
||||||
|
│ Integration Tests │ Some, medium speed
|
||||||
|
├────────────────────────────────────────────┤
|
||||||
|
│ Unit Tests │ Many, fast, isolated
|
||||||
|
└────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test by layer:**
|
||||||
|
- **Domain:** Unit tests (most tests here)
|
||||||
|
- **Application:** Integration tests with mocked infra
|
||||||
|
- **Infrastructure:** Integration tests with real dependencies
|
||||||
|
- **E2E:** Critical paths only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Decision Records (ADRs)
|
||||||
|
|
||||||
|
Document significant decisions:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# ADR 001: Use PostgreSQL for persistence
|
||||||
|
|
||||||
|
## Status
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
We need a database. Options: PostgreSQL, MongoDB, MySQL
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
PostgreSQL for:
|
||||||
|
- ACID compliance
|
||||||
|
- Team familiarity
|
||||||
|
- JSON support for flexibility
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- Need PostgreSQL expertise
|
||||||
|
- Schema migrations required
|
||||||
|
- Excellent query capabilities
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Red Flags in Architecture
|
||||||
|
|
||||||
|
- **Circular dependencies** between modules
|
||||||
|
- **Domain depending on infrastructure**
|
||||||
|
- **Framework code in business logic**
|
||||||
|
- **No clear boundaries** between features
|
||||||
|
- **Shared mutable state** across modules
|
||||||
|
- **"Util" or "Common" packages** that grow forever
|
||||||
|
- **Database schema driving domain model**
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
# Clean Code Practices
|
||||||
|
|
||||||
|
## What is Clean Code?
|
||||||
|
|
||||||
|
Code that is:
|
||||||
|
- **Easy to understand** - reveals intent clearly
|
||||||
|
- **Easy to change** - modifications are localized
|
||||||
|
- **Easy to test** - dependencies are injectable
|
||||||
|
- **Simple** - no unnecessary complexity
|
||||||
|
|
||||||
|
## The Human-Centered Approach
|
||||||
|
|
||||||
|
Code has THREE consumers:
|
||||||
|
1. **Users** - get their needs met
|
||||||
|
2. **Customers** - make or save money
|
||||||
|
3. **Developers** - must maintain it
|
||||||
|
|
||||||
|
Design for all three, but remember: **developers read code 10x more than they write it.**
|
||||||
|
|
||||||
|
## Naming Principles
|
||||||
|
|
||||||
|
### 1. Consistency & Uniqueness (HIGHEST PRIORITY)
|
||||||
|
Same concept = same name everywhere. One name per concept.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Inconsistent names for same concept
|
||||||
|
getUserById(id)
|
||||||
|
fetchCustomerById(id)
|
||||||
|
retrieveClientById(id)
|
||||||
|
|
||||||
|
// GOOD: Consistent
|
||||||
|
getUser(id)
|
||||||
|
getOrder(id)
|
||||||
|
getProduct(id)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Understandability
|
||||||
|
Use domain language, not technical jargon.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Technical
|
||||||
|
const arr = users.filter(u => u.isActive);
|
||||||
|
|
||||||
|
// GOOD: Domain language
|
||||||
|
const activeCustomers = users.filter(user => user.isActive);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Specificity
|
||||||
|
Avoid vague names: `data`, `info`, `manager`, `handler`, `processor`, `utils`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Vague
|
||||||
|
class DataManager { }
|
||||||
|
function processInfo(data) { }
|
||||||
|
|
||||||
|
// GOOD: Specific
|
||||||
|
class OrderRepository { }
|
||||||
|
function validatePayment(payment) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Brevity (but not at cost of clarity)
|
||||||
|
Short names are good only if meaning is preserved.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Too cryptic
|
||||||
|
const usrLst = getUsrs();
|
||||||
|
|
||||||
|
// BAD: Unnecessarily long
|
||||||
|
const listOfAllActiveUsersInTheSystem = getActiveUsers();
|
||||||
|
|
||||||
|
// GOOD: Brief but clear
|
||||||
|
const activeUsers = getActiveUsers();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Searchability
|
||||||
|
Names should be unique enough to grep/search.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Common word, hard to search
|
||||||
|
const data = fetch();
|
||||||
|
|
||||||
|
// GOOD: Unique, searchable
|
||||||
|
const orderSummary = fetchOrderSummary();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Pronounceability
|
||||||
|
You should be able to say it in conversation.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD
|
||||||
|
const genymdhms = generateYearMonthDayHourMinuteSecond();
|
||||||
|
|
||||||
|
// GOOD
|
||||||
|
const timestamp = generateTimestamp();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Austerity
|
||||||
|
Avoid unnecessary filler words.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Redundant
|
||||||
|
const userData = user; // 'Data' adds nothing
|
||||||
|
class UserClass { } // 'Class' adds nothing
|
||||||
|
|
||||||
|
// GOOD
|
||||||
|
const user = user;
|
||||||
|
class User { }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Object Calisthenics (9 Rules)
|
||||||
|
|
||||||
|
Exercises to improve OO design. Follow strictly during practice, relax slightly in production.
|
||||||
|
|
||||||
|
### 1. One Level of Indentation per Method
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Multiple levels
|
||||||
|
function process(orders: Order[]) {
|
||||||
|
for (const order of orders) {
|
||||||
|
if (order.isValid()) {
|
||||||
|
for (const item of order.items) {
|
||||||
|
if (item.inStock) {
|
||||||
|
// process...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Extract methods
|
||||||
|
function process(orders: Order[]) {
|
||||||
|
orders.filter(o => o.isValid()).forEach(processOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
function processOrder(order: Order) {
|
||||||
|
order.items.filter(i => i.inStock).forEach(processItem);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Don't Use the ELSE Keyword
|
||||||
|
|
||||||
|
Use early returns, guard clauses, or polymorphism.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: else
|
||||||
|
function getDiscount(user: User): number {
|
||||||
|
if (user.isPremium) {
|
||||||
|
return 20;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Early return
|
||||||
|
function getDiscount(user: User): number {
|
||||||
|
if (user.isPremium) return 20;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Wrap All Primitives and Strings
|
||||||
|
|
||||||
|
Primitives should be wrapped in domain objects when they have meaning.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Primitive obsession
|
||||||
|
function createUser(email: string, age: number) { }
|
||||||
|
|
||||||
|
// GOOD: Value objects
|
||||||
|
class Email {
|
||||||
|
constructor(private value: string) {
|
||||||
|
if (!this.isValid(value)) throw new InvalidEmail();
|
||||||
|
}
|
||||||
|
private isValid(email: string): boolean { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Age {
|
||||||
|
constructor(private value: number) {
|
||||||
|
if (value < 0 || value > 150) throw new InvalidAge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUser(email: Email, age: Age) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. First-Class Collections
|
||||||
|
|
||||||
|
Any class with a collection should have no other instance variables.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Collection mixed with other state
|
||||||
|
class Order {
|
||||||
|
items: OrderItem[] = [];
|
||||||
|
customerId: string;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Collection is its own class
|
||||||
|
class OrderItems {
|
||||||
|
constructor(private items: OrderItem[] = []) {}
|
||||||
|
|
||||||
|
add(item: OrderItem): void { ... }
|
||||||
|
total(): Money { ... }
|
||||||
|
isEmpty(): boolean { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Order {
|
||||||
|
constructor(
|
||||||
|
private items: OrderItems,
|
||||||
|
private customerId: CustomerId
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. One Dot per Line (Law of Demeter)
|
||||||
|
|
||||||
|
Don't chain through object graphs.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Train wreck
|
||||||
|
const city = order.customer.address.city;
|
||||||
|
|
||||||
|
// GOOD: Tell, don't ask
|
||||||
|
const city = order.getShippingCity();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Don't Abbreviate
|
||||||
|
|
||||||
|
If a name is too long to type, the class is doing too much.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD
|
||||||
|
const custRepo = new CustRepo();
|
||||||
|
const ord = new Ord();
|
||||||
|
|
||||||
|
// GOOD
|
||||||
|
const customerRepository = new CustomerRepository();
|
||||||
|
const order = new Order();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Keep All Entities Small
|
||||||
|
|
||||||
|
- Classes: < 50 lines
|
||||||
|
- Methods: < 10 lines
|
||||||
|
- Files: < 100 lines
|
||||||
|
|
||||||
|
If larger, it's probably doing too much. Split it.
|
||||||
|
|
||||||
|
### 8. No Classes with More Than Two Instance Variables
|
||||||
|
|
||||||
|
Forces small, focused classes.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Too many variables
|
||||||
|
class Order {
|
||||||
|
id: string;
|
||||||
|
customerId: string;
|
||||||
|
items: Item[];
|
||||||
|
total: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Composed of smaller objects
|
||||||
|
class Order {
|
||||||
|
constructor(
|
||||||
|
private id: OrderId,
|
||||||
|
private details: OrderDetails
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderDetails {
|
||||||
|
constructor(
|
||||||
|
private customer: Customer,
|
||||||
|
private lineItems: LineItems
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9. No Getters/Setters/Properties
|
||||||
|
|
||||||
|
Objects should have behavior, not just data. Tell objects what to do.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Data bag with getters
|
||||||
|
class Account {
|
||||||
|
getBalance(): number { return this.balance; }
|
||||||
|
setBalance(value: number) { this.balance = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caller does the work
|
||||||
|
if (account.getBalance() >= amount) {
|
||||||
|
account.setBalance(account.getBalance() - amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Behavior-rich object
|
||||||
|
class Account {
|
||||||
|
withdraw(amount: Money): WithdrawResult {
|
||||||
|
if (!this.canWithdraw(amount)) {
|
||||||
|
return WithdrawResult.insufficientFunds();
|
||||||
|
}
|
||||||
|
this.balance = this.balance.subtract(amount);
|
||||||
|
return WithdrawResult.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caller tells, object decides
|
||||||
|
const result = account.withdraw(amount);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
### When to Write Comments
|
||||||
|
|
||||||
|
**Only write comments to explain WHY, not WHAT or HOW.**
|
||||||
|
|
||||||
|
Code explains what and how. Comments explain business reasons, non-obvious decisions, or warnings.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Explains what (redundant)
|
||||||
|
// Add 1 to counter
|
||||||
|
counter++;
|
||||||
|
|
||||||
|
// GOOD: Explains why
|
||||||
|
// Compensate for 0-based indexing in legacy API
|
||||||
|
counter++;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prefer Self-Documenting Code
|
||||||
|
|
||||||
|
Instead of commenting, rename to make intent clear.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Comment needed
|
||||||
|
// Check if user can access premium features
|
||||||
|
if (user.subscriptionLevel >= 2 && !user.isBanned) { }
|
||||||
|
|
||||||
|
// GOOD: Self-documenting
|
||||||
|
if (user.canAccessPremiumFeatures()) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
### Vertical Spacing
|
||||||
|
- Related code together
|
||||||
|
- Blank lines between concepts
|
||||||
|
- Most important/public at top
|
||||||
|
|
||||||
|
### Horizontal Spacing
|
||||||
|
- Consistent indentation
|
||||||
|
- Space around operators
|
||||||
|
- Max line length ~80-120 characters
|
||||||
|
|
||||||
|
### Storytelling
|
||||||
|
Code should read top-to-bottom like a story. High-level at top, details below.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class OrderProcessor {
|
||||||
|
// Public API first
|
||||||
|
process(order: Order): ProcessResult {
|
||||||
|
this.validate(order);
|
||||||
|
this.calculateTotals(order);
|
||||||
|
return this.save(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supporting methods below, in order of appearance
|
||||||
|
private validate(order: Order): void { ... }
|
||||||
|
private calculateTotals(order: Order): void { ... }
|
||||||
|
private save(order: Order): ProcessResult { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
# Code Smells & Anti-Patterns
|
||||||
|
|
||||||
|
## What Are Code Smells?
|
||||||
|
|
||||||
|
Indicators that something MAY be wrong. Not bugs, but design problems that make code hard to understand, change, or test.
|
||||||
|
|
||||||
|
## The Five Categories
|
||||||
|
|
||||||
|
### 1. Bloaters
|
||||||
|
Code that has grown too large.
|
||||||
|
|
||||||
|
| Smell | Symptom | Refactoring |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| **Long Method** | > 10 lines | Extract Method |
|
||||||
|
| **Large Class** | > 50 lines, multiple responsibilities | Extract Class |
|
||||||
|
| **Long Parameter List** | > 3 parameters | Introduce Parameter Object |
|
||||||
|
| **Data Clumps** | Same group of variables appear together | Extract Class |
|
||||||
|
| **Primitive Obsession** | Primitives instead of small objects | Wrap in Value Object |
|
||||||
|
|
||||||
|
### 2. Object-Orientation Abusers
|
||||||
|
Misuse of OO principles.
|
||||||
|
|
||||||
|
| Smell | Symptom | Refactoring |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| **Switch Statements** | Type checking, large switch/if-else | Replace with Polymorphism |
|
||||||
|
| **Parallel Inheritance** | Adding subclass requires adding another | Merge Hierarchies |
|
||||||
|
| **Refused Bequest** | Subclass doesn't use parent methods | Replace Inheritance with Delegation |
|
||||||
|
| **Alternative Classes** | Different interfaces, same concept | Rename, Extract Superclass |
|
||||||
|
|
||||||
|
### 3. Change Preventers
|
||||||
|
Code that makes changes difficult.
|
||||||
|
|
||||||
|
| Smell | Symptom | Refactoring |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| **Divergent Change** | One class changed for many reasons | Extract Class (SRP) |
|
||||||
|
| **Shotgun Surgery** | One change touches many classes | Move Method/Field together |
|
||||||
|
| **Parallel Inheritance** | (see above) | Merge Hierarchies |
|
||||||
|
|
||||||
|
### 4. Dispensables
|
||||||
|
Code that can be removed.
|
||||||
|
|
||||||
|
| Smell | Symptom | Refactoring |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| **Comments** | Explaining bad code | Rename, Extract Method |
|
||||||
|
| **Duplicate Code** | Copy-paste | Extract Method, Pull Up Method |
|
||||||
|
| **Dead Code** | Unreachable code | Delete |
|
||||||
|
| **Speculative Generality** | "Just in case" code | Delete (YAGNI) |
|
||||||
|
| **Lazy Class** | Class that does almost nothing | Inline Class |
|
||||||
|
|
||||||
|
### 5. Couplers
|
||||||
|
Excessive coupling between classes.
|
||||||
|
|
||||||
|
| Smell | Symptom | Refactoring |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| **Feature Envy** | Method uses another class's data extensively | Move Method |
|
||||||
|
| **Inappropriate Intimacy** | Classes know too much about each other | Move Method, Extract Class |
|
||||||
|
| **Message Chains** | `a.getB().getC().getD()` | Hide Delegate |
|
||||||
|
| **Middle Man** | Class only delegates | Inline Class |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Seven Most Common Code Smells
|
||||||
|
|
||||||
|
### 1. Long Method
|
||||||
|
|
||||||
|
**Symptom:** Method > 10 lines, doing multiple things.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SMELL
|
||||||
|
function processOrder(order: Order) {
|
||||||
|
// Validate
|
||||||
|
if (!order.items.length) throw new Error('Empty');
|
||||||
|
if (!order.customer) throw new Error('No customer');
|
||||||
|
|
||||||
|
// Calculate
|
||||||
|
let total = 0;
|
||||||
|
for (const item of order.items) {
|
||||||
|
total += item.price * item.quantity;
|
||||||
|
if (item.discount) {
|
||||||
|
total -= item.discount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply tax
|
||||||
|
const taxRate = getTaxRate(order.customer.state);
|
||||||
|
total = total * (1 + taxRate);
|
||||||
|
|
||||||
|
// Save
|
||||||
|
db.orders.insert({ ...order, total });
|
||||||
|
|
||||||
|
// Notify
|
||||||
|
emailService.send(order.customer.email, 'Order confirmed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// REFACTORED
|
||||||
|
function processOrder(order: Order) {
|
||||||
|
validateOrder(order);
|
||||||
|
const total = calculateTotal(order);
|
||||||
|
saveOrder(order, total);
|
||||||
|
notifyCustomer(order);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Large Class
|
||||||
|
|
||||||
|
**Symptom:** Class with many responsibilities, > 50 lines.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SMELL: God class
|
||||||
|
class User {
|
||||||
|
// User data
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
login() { }
|
||||||
|
logout() { }
|
||||||
|
resetPassword() { }
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
setTheme() { }
|
||||||
|
setLanguage() { }
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
sendEmail() { }
|
||||||
|
sendSMS() { }
|
||||||
|
|
||||||
|
// Billing
|
||||||
|
charge() { }
|
||||||
|
refund() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// REFACTORED: Separate classes
|
||||||
|
class User { name: string; email: string; }
|
||||||
|
class AuthService { login(); logout(); resetPassword(); }
|
||||||
|
class UserPreferences { setTheme(); setLanguage(); }
|
||||||
|
class NotificationService { sendEmail(); sendSMS(); }
|
||||||
|
class BillingService { charge(); refund(); }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Feature Envy
|
||||||
|
|
||||||
|
**Symptom:** Method uses another class's data more than its own.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SMELL: Order envies Customer
|
||||||
|
class Order {
|
||||||
|
calculateShipping(customer: Customer): number {
|
||||||
|
if (customer.country === 'US') {
|
||||||
|
if (customer.state === 'CA') return 10;
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// REFACTORED: Move to Customer
|
||||||
|
class Customer {
|
||||||
|
getShippingCost(): number {
|
||||||
|
if (this.country === 'US') {
|
||||||
|
if (this.state === 'CA') return 10;
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Order {
|
||||||
|
calculateShipping(): number {
|
||||||
|
return this.customer.getShippingCost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Primitive Obsession
|
||||||
|
|
||||||
|
**Symptom:** Using primitives for domain concepts.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SMELL
|
||||||
|
function createUser(email: string, age: number, zipCode: string) {
|
||||||
|
// No validation, easy to pass wrong values
|
||||||
|
if (!email.includes('@')) throw new Error();
|
||||||
|
if (age < 0) throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
// REFACTORED: Value objects
|
||||||
|
class Email {
|
||||||
|
constructor(private value: string) {
|
||||||
|
if (!value.includes('@')) throw new InvalidEmail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Age {
|
||||||
|
constructor(private value: number) {
|
||||||
|
if (value < 0 || value > 150) throw new InvalidAge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createUser(email: Email, age: Age, address: Address) {
|
||||||
|
// Type system prevents invalid data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Switch Statements
|
||||||
|
|
||||||
|
**Symptom:** Switching on type, repeated across codebase.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SMELL
|
||||||
|
function getArea(shape: Shape): number {
|
||||||
|
switch (shape.type) {
|
||||||
|
case 'circle': return Math.PI * shape.radius ** 2;
|
||||||
|
case 'rectangle': return shape.width * shape.height;
|
||||||
|
case 'triangle': return 0.5 * shape.base * shape.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPerimeter(shape: Shape): number {
|
||||||
|
switch (shape.type) { // Same switch again!
|
||||||
|
case 'circle': return 2 * Math.PI * shape.radius;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// REFACTORED: Polymorphism
|
||||||
|
interface Shape {
|
||||||
|
getArea(): number;
|
||||||
|
getPerimeter(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Circle implements Shape {
|
||||||
|
constructor(private radius: number) {}
|
||||||
|
getArea(): number { return Math.PI * this.radius ** 2; }
|
||||||
|
getPerimeter(): number { return 2 * Math.PI * this.radius; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Inappropriate Intimacy
|
||||||
|
|
||||||
|
**Symptom:** Classes know too much about each other's internals.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SMELL
|
||||||
|
class Order {
|
||||||
|
process() {
|
||||||
|
const inventory = new Inventory();
|
||||||
|
// Reaching into inventory's internals
|
||||||
|
for (const item of this.items) {
|
||||||
|
const stock = inventory.stockLevels[item.sku];
|
||||||
|
if (stock.quantity < item.quantity) {
|
||||||
|
throw new Error('Out of stock');
|
||||||
|
}
|
||||||
|
inventory.stockLevels[item.sku].quantity -= item.quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// REFACTORED: Tell, don't ask
|
||||||
|
class Inventory {
|
||||||
|
reserve(items: OrderItem[]): ReserveResult {
|
||||||
|
// Inventory manages its own state
|
||||||
|
for (const item of items) {
|
||||||
|
if (!this.canReserve(item)) {
|
||||||
|
return ReserveResult.outOfStock(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.deductStock(items);
|
||||||
|
return ReserveResult.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Order {
|
||||||
|
process(inventory: Inventory) {
|
||||||
|
const result = inventory.reserve(this.items);
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
throw new OutOfStockError(result.failedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Speculative Generality
|
||||||
|
|
||||||
|
**Symptom:** "Just in case" abstractions that aren't used.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SMELL: Over-engineered for hypothetical needs
|
||||||
|
interface PaymentProcessor {
|
||||||
|
process(): void;
|
||||||
|
rollback(): void;
|
||||||
|
audit(): void;
|
||||||
|
generateReport(): void;
|
||||||
|
scheduleRecurring(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StripeProcessor implements PaymentProcessor {
|
||||||
|
process() { /* actual code */ }
|
||||||
|
rollback() { throw new Error('Not implemented'); }
|
||||||
|
audit() { throw new Error('Not implemented'); }
|
||||||
|
generateReport() { throw new Error('Not implemented'); }
|
||||||
|
scheduleRecurring() { throw new Error('Not implemented'); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// REFACTORED: YAGNI
|
||||||
|
interface PaymentProcessor {
|
||||||
|
process(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StripeProcessor implements PaymentProcessor {
|
||||||
|
process() { /* actual code */ }
|
||||||
|
}
|
||||||
|
// Add other methods when actually needed
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prevention Strategies
|
||||||
|
|
||||||
|
1. **Follow Object Calisthenics** - Rules prevent most smells
|
||||||
|
2. **Practice TDD** - Tests reveal design problems early
|
||||||
|
3. **Review in pairs** - Fresh eyes catch smells
|
||||||
|
4. **Refactor continuously** - Don't let smells accumulate
|
||||||
|
5. **Apply SOLID** - Prevents structural smells
|
||||||
|
6. **Use static analysis** - Tools catch common issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When You Find a Smell
|
||||||
|
|
||||||
|
1. **Confirm it's a problem** - Not all smells need fixing
|
||||||
|
2. **Ensure test coverage** - Before refactoring
|
||||||
|
3. **Refactor in small steps** - Keep tests passing
|
||||||
|
4. **Commit frequently** - Easy to revert if needed
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
# Managing Complexity
|
||||||
|
|
||||||
|
## The Two Types of Complexity
|
||||||
|
|
||||||
|
### Essential Complexity
|
||||||
|
Inherent to the problem domain. Cannot be removed, only managed.
|
||||||
|
- Business rules
|
||||||
|
- Domain logic
|
||||||
|
- User requirements
|
||||||
|
|
||||||
|
### Accidental Complexity
|
||||||
|
Introduced by our solutions. CAN and SHOULD be minimized.
|
||||||
|
- Poor abstractions
|
||||||
|
- Unnecessary indirection
|
||||||
|
- Framework ceremony
|
||||||
|
- Technical debt
|
||||||
|
|
||||||
|
**Goal: Minimize accidental complexity while clearly expressing essential complexity.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detecting Complexity
|
||||||
|
|
||||||
|
### 1. Change Amplification
|
||||||
|
Small changes require touching many files.
|
||||||
|
|
||||||
|
**Symptom:** "To add this field, I need to update 15 files."
|
||||||
|
|
||||||
|
**Cause:** Scattered responsibilities, poor abstraction boundaries.
|
||||||
|
|
||||||
|
### 2. Cognitive Load
|
||||||
|
Code is hard to understand, requires holding too much in memory.
|
||||||
|
|
||||||
|
**Symptom:** "I need to understand 10 other classes to understand this one."
|
||||||
|
|
||||||
|
**Cause:** Tight coupling, hidden dependencies, unclear naming.
|
||||||
|
|
||||||
|
### 3. Unknown Unknowns
|
||||||
|
Behavior is surprising, side effects are hidden.
|
||||||
|
|
||||||
|
**Symptom:** "I changed this, and something completely unrelated broke."
|
||||||
|
|
||||||
|
**Cause:** Global state, hidden dependencies, implicit contracts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The XP Values for Fighting Complexity
|
||||||
|
|
||||||
|
From Extreme Programming:
|
||||||
|
|
||||||
|
### 1. Communication
|
||||||
|
Code should communicate clearly. Names, structure, tests all contribute.
|
||||||
|
|
||||||
|
### 2. Simplicity
|
||||||
|
Do the simplest thing that could possibly work.
|
||||||
|
|
||||||
|
### 3. Feedback
|
||||||
|
Fast feedback loops catch complexity early. TDD, CI, code review.
|
||||||
|
|
||||||
|
### 4. Courage
|
||||||
|
Refactor aggressively. Don't let complexity accumulate.
|
||||||
|
|
||||||
|
### 5. Respect
|
||||||
|
Respect future readers (including yourself). Write for humans first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KISS - Keep It Simple, Silly
|
||||||
|
|
||||||
|
> "The simplest solution that works is usually the best."
|
||||||
|
|
||||||
|
### How to Apply:
|
||||||
|
1. Start with the obvious solution
|
||||||
|
2. Only add complexity when REQUIRED
|
||||||
|
3. Prefer boring, well-understood approaches
|
||||||
|
4. Question every abstraction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Over-engineered
|
||||||
|
class UserServiceFactoryProvider {
|
||||||
|
private static instance: UserServiceFactoryProvider;
|
||||||
|
|
||||||
|
static getInstance(): UserServiceFactoryProvider { ... }
|
||||||
|
createFactory(): UserServiceFactory { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
// KISS
|
||||||
|
class UserService {
|
||||||
|
getUser(id: string): User { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## YAGNI - You Aren't Gonna Need It
|
||||||
|
|
||||||
|
> "Don't build features until they're actually needed."
|
||||||
|
|
||||||
|
### Warning Signs:
|
||||||
|
- "We might need this later"
|
||||||
|
- "It would be nice to have"
|
||||||
|
- "Just in case"
|
||||||
|
- "For future extensibility"
|
||||||
|
|
||||||
|
### The Cost of YAGNI Violations:
|
||||||
|
1. **Development time** - Building unused features
|
||||||
|
2. **Maintenance burden** - Code that must be maintained
|
||||||
|
3. **Cognitive load** - More to understand
|
||||||
|
4. **Wrong abstraction** - Guessing future needs incorrectly
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// YAGNI violation: Building for hypothetical needs
|
||||||
|
class User {
|
||||||
|
// "We might need these someday"
|
||||||
|
middleName?: string;
|
||||||
|
secondaryEmail?: string;
|
||||||
|
faxNumber?: string;
|
||||||
|
linkedinProfile?: string;
|
||||||
|
twitterHandle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// YAGNI: Only what's needed NOW
|
||||||
|
class User {
|
||||||
|
name: string;
|
||||||
|
email: Email;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DRY - Don't Repeat Yourself (with The Rule of Three)
|
||||||
|
|
||||||
|
> "Every piece of knowledge should have a single, unambiguous representation."
|
||||||
|
|
||||||
|
### BUT: The Rule of Three
|
||||||
|
|
||||||
|
**Don't extract duplication until you see it THREE times.**
|
||||||
|
|
||||||
|
Why? The wrong abstraction is worse than duplication.
|
||||||
|
|
||||||
|
```
|
||||||
|
Duplication #1 → Leave it
|
||||||
|
Duplication #2 → Note it, leave it
|
||||||
|
Duplication #3 → NOW extract it
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example:
|
||||||
|
```typescript
|
||||||
|
// First time - leave it
|
||||||
|
function processUserOrder(order) {
|
||||||
|
validate(order);
|
||||||
|
calculateTax(order);
|
||||||
|
save(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second time - note the similarity, but leave it
|
||||||
|
function processGuestOrder(order) {
|
||||||
|
validate(order);
|
||||||
|
calculateTax(order);
|
||||||
|
save(order);
|
||||||
|
sendGuestEmail(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third time - NOW extract
|
||||||
|
function processCorporateOrder(order) {
|
||||||
|
validate(order);
|
||||||
|
calculateTax(order);
|
||||||
|
save(order);
|
||||||
|
applyCorporateDiscount(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
// After three, extract the common parts
|
||||||
|
function processOrder(order: Order, postProcessing: (o: Order) => void) {
|
||||||
|
validate(order);
|
||||||
|
calculateTax(order);
|
||||||
|
save(order);
|
||||||
|
postProcessing(order);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Separation of Concerns
|
||||||
|
|
||||||
|
> "Each module should address a single concern."
|
||||||
|
|
||||||
|
### Concerns to Separate:
|
||||||
|
- **Business logic** vs **Infrastructure**
|
||||||
|
- **What** (policy) vs **How** (mechanism)
|
||||||
|
- **Input** vs **Processing** vs **Output**
|
||||||
|
- **Data** vs **Behavior**
|
||||||
|
|
||||||
|
### Example:
|
||||||
|
```typescript
|
||||||
|
// BAD: Mixed concerns
|
||||||
|
class OrderProcessor {
|
||||||
|
process(order: Order) {
|
||||||
|
// Validation
|
||||||
|
if (!order.items.length) throw new Error('Empty');
|
||||||
|
|
||||||
|
// Business logic
|
||||||
|
let total = 0;
|
||||||
|
for (const item of order.items) {
|
||||||
|
total += item.price * item.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persistence
|
||||||
|
const db = new Database();
|
||||||
|
db.query(`INSERT INTO orders...`);
|
||||||
|
|
||||||
|
// Notification
|
||||||
|
const email = new EmailClient();
|
||||||
|
email.send(order.customer.email, 'Order confirmed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Separated concerns
|
||||||
|
class OrderProcessor {
|
||||||
|
constructor(
|
||||||
|
private validator: OrderValidator,
|
||||||
|
private calculator: OrderCalculator,
|
||||||
|
private repository: OrderRepository,
|
||||||
|
private notifier: OrderNotifier
|
||||||
|
) {}
|
||||||
|
|
||||||
|
process(order: Order): ProcessResult {
|
||||||
|
this.validator.validate(order);
|
||||||
|
const total = this.calculator.calculateTotal(order);
|
||||||
|
const savedOrder = this.repository.save(order);
|
||||||
|
this.notifier.notifyConfirmation(savedOrder);
|
||||||
|
return ProcessResult.success(savedOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Managing Technical Debt
|
||||||
|
|
||||||
|
### Types of Technical Debt:
|
||||||
|
1. **Deliberate** - Conscious trade-off for speed
|
||||||
|
2. **Accidental** - Mistakes, lack of knowledge
|
||||||
|
3. **Bit rot** - Code degrades over time
|
||||||
|
|
||||||
|
### The Boy Scout Rule:
|
||||||
|
> "Leave the code better than you found it."
|
||||||
|
|
||||||
|
Every time you touch code:
|
||||||
|
- Improve one small thing
|
||||||
|
- Fix one naming issue
|
||||||
|
- Extract one method
|
||||||
|
- Add one missing test
|
||||||
|
|
||||||
|
### When to Pay Down Debt:
|
||||||
|
- When it's in your path (you're already there)
|
||||||
|
- When it's blocking new features
|
||||||
|
- When it's causing bugs
|
||||||
|
- During dedicated refactoring time
|
||||||
|
|
||||||
|
### When NOT to Refactor:
|
||||||
|
- Code that works and won't change
|
||||||
|
- Code being replaced soon
|
||||||
|
- When you don't have tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Four Elements of Simple Design
|
||||||
|
|
||||||
|
In priority order (from XP):
|
||||||
|
|
||||||
|
1. **Runs all the tests**
|
||||||
|
- If it doesn't work, nothing else matters
|
||||||
|
|
||||||
|
2. **Expresses intent**
|
||||||
|
- Clear names, obvious structure
|
||||||
|
- Code tells the story
|
||||||
|
|
||||||
|
3. **No duplication**
|
||||||
|
- DRY (but Rule of Three)
|
||||||
|
- Single source of truth
|
||||||
|
|
||||||
|
4. **Minimal**
|
||||||
|
- Fewest classes and methods possible
|
||||||
|
- Remove anything unnecessary
|
||||||
|
|
||||||
|
If these four are true, the design is simple enough.
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
# Design Patterns
|
||||||
|
|
||||||
|
## What Are Design Patterns?
|
||||||
|
|
||||||
|
Reusable solutions to common design problems. A shared vocabulary for discussing design.
|
||||||
|
|
||||||
|
## WARNING: Don't Force Patterns
|
||||||
|
|
||||||
|
> "Let patterns emerge from refactoring, don't force them upfront."
|
||||||
|
|
||||||
|
Patterns should solve problems you HAVE, not problems you MIGHT have.
|
||||||
|
|
||||||
|
## When to Use Patterns
|
||||||
|
|
||||||
|
1. **You recognize the problem** - You've seen it before
|
||||||
|
2. **The pattern fits** - Not forcing it
|
||||||
|
3. **It simplifies** - Doesn't add unnecessary complexity
|
||||||
|
4. **Team understands it** - Shared knowledge
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creational Patterns
|
||||||
|
|
||||||
|
### Singleton
|
||||||
|
|
||||||
|
**Purpose:** Ensure only one instance exists.
|
||||||
|
|
||||||
|
**When to use:** Global configuration, connection pools, logging.
|
||||||
|
|
||||||
|
**Warning:** Often overused. Consider dependency injection instead.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class Logger {
|
||||||
|
private static instance: Logger;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static getInstance(): Logger {
|
||||||
|
if (!Logger.instance) {
|
||||||
|
Logger.instance = new Logger();
|
||||||
|
}
|
||||||
|
return Logger.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(message: string): void { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Factory
|
||||||
|
|
||||||
|
**Purpose:** Create objects without specifying exact class.
|
||||||
|
|
||||||
|
**When to use:** Object creation logic is complex, or varies by type.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Notification {
|
||||||
|
send(message: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailNotification implements Notification { ... }
|
||||||
|
class SMSNotification implements Notification { ... }
|
||||||
|
class PushNotification implements Notification { ... }
|
||||||
|
|
||||||
|
class NotificationFactory {
|
||||||
|
create(type: 'email' | 'sms' | 'push'): Notification {
|
||||||
|
switch (type) {
|
||||||
|
case 'email': return new EmailNotification();
|
||||||
|
case 'sms': return new SMSNotification();
|
||||||
|
case 'push': return new PushNotification();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Builder
|
||||||
|
|
||||||
|
**Purpose:** Construct complex objects step by step.
|
||||||
|
|
||||||
|
**When to use:** Objects with many optional parameters, test data creation.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class UserBuilder {
|
||||||
|
private user: Partial<User> = {};
|
||||||
|
|
||||||
|
withName(name: string): UserBuilder {
|
||||||
|
this.user.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withEmail(email: string): UserBuilder {
|
||||||
|
this.user.email = email;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withAge(age: number): UserBuilder {
|
||||||
|
this.user.age = age;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
build(): User {
|
||||||
|
return new User(
|
||||||
|
this.user.name!,
|
||||||
|
this.user.email!,
|
||||||
|
this.user.age
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const user = new UserBuilder()
|
||||||
|
.withName('Alice')
|
||||||
|
.withEmail('alice@example.com')
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prototype
|
||||||
|
|
||||||
|
**Purpose:** Create new objects by cloning existing ones.
|
||||||
|
|
||||||
|
**When to use:** Object creation is expensive, or you need copies with slight variations.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Prototype {
|
||||||
|
clone(): Prototype;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Document implements Prototype {
|
||||||
|
constructor(
|
||||||
|
public title: string,
|
||||||
|
public content: string,
|
||||||
|
public metadata: Metadata
|
||||||
|
) {}
|
||||||
|
|
||||||
|
clone(): Document {
|
||||||
|
return new Document(
|
||||||
|
this.title,
|
||||||
|
this.content,
|
||||||
|
{ ...this.metadata }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structural Patterns
|
||||||
|
|
||||||
|
### Adapter
|
||||||
|
|
||||||
|
**Purpose:** Make incompatible interfaces work together.
|
||||||
|
|
||||||
|
**When to use:** Integrating third-party libraries, legacy code.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Third-party library with different interface
|
||||||
|
class OldPaymentAPI {
|
||||||
|
makePayment(cents: number): boolean { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our interface
|
||||||
|
interface PaymentGateway {
|
||||||
|
charge(amount: Money): ChargeResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapter
|
||||||
|
class OldPaymentAdapter implements PaymentGateway {
|
||||||
|
constructor(private oldAPI: OldPaymentAPI) {}
|
||||||
|
|
||||||
|
charge(amount: Money): ChargeResult {
|
||||||
|
const cents = amount.toCents();
|
||||||
|
const success = this.oldAPI.makePayment(cents);
|
||||||
|
return success ? ChargeResult.success() : ChargeResult.failed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decorator
|
||||||
|
|
||||||
|
**Purpose:** Add behavior to objects dynamically.
|
||||||
|
|
||||||
|
**When to use:** Adding features without modifying existing code.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Notifier {
|
||||||
|
send(message: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailNotifier implements Notifier {
|
||||||
|
send(message: string): void {
|
||||||
|
console.log(`Email: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decorators
|
||||||
|
class SMSDecorator implements Notifier {
|
||||||
|
constructor(private wrapped: Notifier) {}
|
||||||
|
|
||||||
|
send(message: string): void {
|
||||||
|
this.wrapped.send(message);
|
||||||
|
console.log(`SMS: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SlackDecorator implements Notifier {
|
||||||
|
constructor(private wrapped: Notifier) {}
|
||||||
|
|
||||||
|
send(message: string): void {
|
||||||
|
this.wrapped.send(message);
|
||||||
|
console.log(`Slack: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage - compose behaviors
|
||||||
|
const notifier = new SlackDecorator(
|
||||||
|
new SMSDecorator(
|
||||||
|
new EmailNotifier()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
notifier.send('Alert!'); // Sends to all three
|
||||||
|
```
|
||||||
|
|
||||||
|
### Proxy
|
||||||
|
|
||||||
|
**Purpose:** Control access to an object.
|
||||||
|
|
||||||
|
**When to use:** Lazy loading, access control, logging, caching.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Image {
|
||||||
|
display(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RealImage implements Image {
|
||||||
|
constructor(private filename: string) {
|
||||||
|
this.loadFromDisk(); // Expensive
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadFromDisk(): void { ... }
|
||||||
|
|
||||||
|
display(): void { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy loading proxy
|
||||||
|
class ImageProxy implements Image {
|
||||||
|
private realImage: RealImage | null = null;
|
||||||
|
|
||||||
|
constructor(private filename: string) {}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
if (!this.realImage) {
|
||||||
|
this.realImage = new RealImage(this.filename);
|
||||||
|
}
|
||||||
|
this.realImage.display();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Composite
|
||||||
|
|
||||||
|
**Purpose:** Treat individual objects and compositions uniformly.
|
||||||
|
|
||||||
|
**When to use:** Tree structures, hierarchies (files/folders, UI components).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Component {
|
||||||
|
getPrice(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Product implements Component {
|
||||||
|
constructor(private price: number) {}
|
||||||
|
|
||||||
|
getPrice(): number {
|
||||||
|
return this.price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Box implements Component {
|
||||||
|
private children: Component[] = [];
|
||||||
|
|
||||||
|
add(component: Component): void {
|
||||||
|
this.children.push(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrice(): number {
|
||||||
|
return this.children.reduce(
|
||||||
|
(sum, child) => sum + child.getPrice(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const smallBox = new Box();
|
||||||
|
smallBox.add(new Product(10));
|
||||||
|
smallBox.add(new Product(20));
|
||||||
|
|
||||||
|
const bigBox = new Box();
|
||||||
|
bigBox.add(smallBox);
|
||||||
|
bigBox.add(new Product(50));
|
||||||
|
|
||||||
|
console.log(bigBox.getPrice()); // 80
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Behavioral Patterns
|
||||||
|
|
||||||
|
### Strategy
|
||||||
|
|
||||||
|
**Purpose:** Define a family of algorithms, make them interchangeable.
|
||||||
|
|
||||||
|
**When to use:** Multiple ways to do something, switchable at runtime.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface PricingStrategy {
|
||||||
|
calculate(basePrice: number): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegularPricing implements PricingStrategy {
|
||||||
|
calculate(basePrice: number): number {
|
||||||
|
return basePrice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PremiumDiscount implements PricingStrategy {
|
||||||
|
calculate(basePrice: number): number {
|
||||||
|
return basePrice * 0.8; // 20% off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlackFriday implements PricingStrategy {
|
||||||
|
calculate(basePrice: number): number {
|
||||||
|
return basePrice * 0.5; // 50% off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShoppingCart {
|
||||||
|
constructor(private pricing: PricingStrategy) {}
|
||||||
|
|
||||||
|
calculateTotal(items: Item[]): number {
|
||||||
|
const base = items.reduce((sum, i) => sum + i.price, 0);
|
||||||
|
return this.pricing.calculate(base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Observer
|
||||||
|
|
||||||
|
**Purpose:** Notify multiple objects about state changes.
|
||||||
|
|
||||||
|
**When to use:** Event systems, pub/sub, reactive updates.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Observer {
|
||||||
|
update(event: Event): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventEmitter {
|
||||||
|
private observers: Observer[] = [];
|
||||||
|
|
||||||
|
subscribe(observer: Observer): void {
|
||||||
|
this.observers.push(observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(observer: Observer): void {
|
||||||
|
this.observers = this.observers.filter(o => o !== observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(event: Event): void {
|
||||||
|
this.observers.forEach(o => o.update(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
class OrderService extends EventEmitter {
|
||||||
|
placeOrder(order: Order): void {
|
||||||
|
// Process order...
|
||||||
|
this.notify({ type: 'ORDER_PLACED', order });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailService implements Observer {
|
||||||
|
update(event: Event): void {
|
||||||
|
if (event.type === 'ORDER_PLACED') {
|
||||||
|
this.sendConfirmation(event.order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Template Method
|
||||||
|
|
||||||
|
**Purpose:** Define algorithm skeleton, let subclasses override steps.
|
||||||
|
|
||||||
|
**When to use:** Common algorithm with varying steps.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
abstract class DataExporter {
|
||||||
|
// Template method - defines the algorithm
|
||||||
|
export(data: Data[]): void {
|
||||||
|
this.validate(data);
|
||||||
|
const formatted = this.format(data);
|
||||||
|
this.write(formatted);
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common steps
|
||||||
|
private validate(data: Data[]): void { ... }
|
||||||
|
private notify(): void { ... }
|
||||||
|
|
||||||
|
// Steps to override
|
||||||
|
protected abstract format(data: Data[]): string;
|
||||||
|
protected abstract write(content: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CSVExporter extends DataExporter {
|
||||||
|
protected format(data: Data[]): string {
|
||||||
|
return data.map(d => d.toCSV()).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected write(content: string): void {
|
||||||
|
fs.writeFileSync('export.csv', content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JSONExporter extends DataExporter {
|
||||||
|
protected format(data: Data[]): string {
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected write(content: string): void {
|
||||||
|
fs.writeFileSync('export.json', content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command
|
||||||
|
|
||||||
|
**Purpose:** Encapsulate a request as an object.
|
||||||
|
|
||||||
|
**When to use:** Undo/redo, queuing, logging actions.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Command {
|
||||||
|
execute(): void;
|
||||||
|
undo(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddItemCommand implements Command {
|
||||||
|
constructor(
|
||||||
|
private cart: Cart,
|
||||||
|
private item: Item
|
||||||
|
) {}
|
||||||
|
|
||||||
|
execute(): void {
|
||||||
|
this.cart.add(this.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
undo(): void {
|
||||||
|
this.cart.remove(this.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommandHistory {
|
||||||
|
private history: Command[] = [];
|
||||||
|
|
||||||
|
execute(command: Command): void {
|
||||||
|
command.execute();
|
||||||
|
this.history.push(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
undo(): void {
|
||||||
|
const command = this.history.pop();
|
||||||
|
command?.undo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pattern Awareness
|
||||||
|
|
||||||
|
### The Four-Dimensional Lens
|
||||||
|
|
||||||
|
When analyzing new code/libraries, ask:
|
||||||
|
|
||||||
|
1. **What problem does it solve?** (Creational, Structural, Behavioral)
|
||||||
|
2. **What scope?** (Object-level, Class-level, System-level)
|
||||||
|
3. **When is it applied?** (Compile-time, Runtime)
|
||||||
|
4. **How coupled?** (Tight, Loose)
|
||||||
|
|
||||||
|
This helps recognize patterns even in unfamiliar code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-Patterns to Avoid
|
||||||
|
|
||||||
|
| Anti-Pattern | Problem | Solution |
|
||||||
|
|--------------|---------|----------|
|
||||||
|
| **God Object** | Class does everything | Split by responsibility |
|
||||||
|
| **Spaghetti Code** | Tangled, no structure | Refactor to layers |
|
||||||
|
| **Golden Hammer** | Using one pattern for everything | Match pattern to problem |
|
||||||
|
| **Premature Optimization** | Optimizing before needed | YAGNI, profile first |
|
||||||
|
| **Copy-Paste Programming** | Duplication | Extract, Rule of Three |
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
# Object-Oriented Design
|
||||||
|
|
||||||
|
## Responsibility-Driven Design (RDD)
|
||||||
|
|
||||||
|
The key insight: **Objects are defined by their responsibilities, not their data.**
|
||||||
|
|
||||||
|
### Finding Objects
|
||||||
|
|
||||||
|
Start with:
|
||||||
|
1. **Nouns** in requirements → candidate objects
|
||||||
|
2. **Verbs** → candidate methods/behaviors
|
||||||
|
3. **Domain concepts** → value objects
|
||||||
|
|
||||||
|
### Finding Responsibilities
|
||||||
|
|
||||||
|
Each object should answer:
|
||||||
|
- What does this object **know**?
|
||||||
|
- What does this object **do**?
|
||||||
|
- What does this object **decide**?
|
||||||
|
|
||||||
|
### Object Stereotypes
|
||||||
|
|
||||||
|
Every class fits one (or maybe two) stereotypes:
|
||||||
|
|
||||||
|
| Stereotype | Purpose | Example |
|
||||||
|
|------------|---------|---------|
|
||||||
|
| **Information Holder** | Knows things, holds data | `User`, `Product`, `Address` |
|
||||||
|
| **Structurer** | Maintains relationships | `OrderItems`, `UserGroup` |
|
||||||
|
| **Service Provider** | Performs work | `PaymentProcessor`, `EmailSender` |
|
||||||
|
| **Coordinator** | Orchestrates workflow | `OrderFulfillmentService` |
|
||||||
|
| **Controller** | Makes decisions, delegates | `CheckoutController` |
|
||||||
|
| **Interfacer** | Transforms between systems | `UserAPIAdapter`, `DatabaseMapper` |
|
||||||
|
|
||||||
|
### The Two Questions
|
||||||
|
|
||||||
|
For every class, ask:
|
||||||
|
1. **"What pattern is this?"** - Which stereotype? Which design pattern?
|
||||||
|
2. **"Is it doing too much?"** - Check object calisthenics rules
|
||||||
|
|
||||||
|
If you can't answer clearly, the class needs refactoring.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tell, Don't Ask
|
||||||
|
|
||||||
|
**Command objects to do work. Don't interrogate them and do the work yourself.**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Asking, then doing
|
||||||
|
if (account.getBalance() >= amount) {
|
||||||
|
account.setBalance(account.getBalance() - amount);
|
||||||
|
// more logic here...
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Telling
|
||||||
|
const result = account.withdraw(amount);
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The object that has the data should have the behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design by Contract (DbC)
|
||||||
|
|
||||||
|
Every method has:
|
||||||
|
- **Preconditions** - What must be true BEFORE calling
|
||||||
|
- **Postconditions** - What will be true AFTER calling
|
||||||
|
- **Invariants** - What is ALWAYS true about the object
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class BankAccount {
|
||||||
|
private balance: Money;
|
||||||
|
|
||||||
|
// INVARIANT: balance is never negative
|
||||||
|
|
||||||
|
// PRECONDITION: amount > 0
|
||||||
|
// POSTCONDITION: balance decreased by amount OR error returned
|
||||||
|
withdraw(amount: Money): WithdrawResult {
|
||||||
|
if (amount.isNegativeOrZero()) {
|
||||||
|
return WithdrawResult.invalidAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.balance.isLessThan(amount)) {
|
||||||
|
return WithdrawResult.insufficientFunds();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.balance = this.balance.minus(amount);
|
||||||
|
return WithdrawResult.success(this.balance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Composition Over Inheritance
|
||||||
|
|
||||||
|
**Prefer composing objects over extending classes.**
|
||||||
|
|
||||||
|
### Why Inheritance is Problematic:
|
||||||
|
- Tight coupling between parent and child
|
||||||
|
- Fragile base class problem
|
||||||
|
- Difficult to change parent without breaking children
|
||||||
|
- Forces "is-a" relationship that may not fit
|
||||||
|
|
||||||
|
### When to Use Inheritance:
|
||||||
|
- True "is-a" relationship (rare)
|
||||||
|
- Framework requirements
|
||||||
|
- Template Method pattern (intentional)
|
||||||
|
|
||||||
|
### Prefer Composition:
|
||||||
|
```typescript
|
||||||
|
// BAD: Inheritance
|
||||||
|
class PremiumUser extends User {
|
||||||
|
getDiscount(): number { return 20; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Composition
|
||||||
|
class User {
|
||||||
|
constructor(private discountPolicy: DiscountPolicy) {}
|
||||||
|
|
||||||
|
getDiscount(): number {
|
||||||
|
return this.discountPolicy.calculate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now discount behavior is pluggable
|
||||||
|
new User(new PremiumDiscount());
|
||||||
|
new User(new StandardDiscount());
|
||||||
|
new User(new NoDiscount());
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Law of Demeter (Principle of Least Knowledge)
|
||||||
|
|
||||||
|
**Only talk to your immediate friends.**
|
||||||
|
|
||||||
|
A method should only call:
|
||||||
|
1. Methods on `this`
|
||||||
|
2. Methods on parameters
|
||||||
|
3. Methods on objects it creates
|
||||||
|
4. Methods on its direct components
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Reaching through objects
|
||||||
|
order.getCustomer().getAddress().getCity();
|
||||||
|
|
||||||
|
// GOOD: Ask the immediate friend
|
||||||
|
order.getShippingCity();
|
||||||
|
```
|
||||||
|
|
||||||
|
This reduces coupling - changes to `Address` don't ripple through all callers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Encapsulation
|
||||||
|
|
||||||
|
**Hide internal details, expose behavior.**
|
||||||
|
|
||||||
|
### Levels of Encapsulation:
|
||||||
|
1. **Data** - private fields, no direct access
|
||||||
|
2. **Implementation** - how things work internally
|
||||||
|
3. **Type** - concrete class hidden behind interface
|
||||||
|
4. **Design** - architectural decisions hidden from clients
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Exposed internals
|
||||||
|
class Order {
|
||||||
|
public items: Item[] = [];
|
||||||
|
public total: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client can corrupt state
|
||||||
|
order.items.push(item);
|
||||||
|
order.total = -999; // Oops!
|
||||||
|
|
||||||
|
// GOOD: Encapsulated
|
||||||
|
class Order {
|
||||||
|
private items: OrderItems;
|
||||||
|
private total: Money;
|
||||||
|
|
||||||
|
addItem(item: Item): void {
|
||||||
|
this.items.add(item);
|
||||||
|
this.recalculateTotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTotal(): Money {
|
||||||
|
return this.total; // Returns copy or immutable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Polymorphism
|
||||||
|
|
||||||
|
**Replace conditionals with types.**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Type checking
|
||||||
|
function calculateShipping(method: string, value: number): number {
|
||||||
|
if (method === 'standard') return value < 50 ? 5 : 0;
|
||||||
|
if (method === 'express') return 15;
|
||||||
|
if (method === 'overnight') return 25;
|
||||||
|
throw new Error('Unknown method');
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Polymorphism
|
||||||
|
interface ShippingMethod {
|
||||||
|
calculateCost(orderValue: number): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StandardShipping implements ShippingMethod {
|
||||||
|
calculateCost(orderValue: number): number {
|
||||||
|
return orderValue < 50 ? 5 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExpressShipping implements ShippingMethod {
|
||||||
|
calculateCost(orderValue: number): number {
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage - no conditionals
|
||||||
|
function calculateShipping(method: ShippingMethod, value: number): number {
|
||||||
|
return method.calculateCost(value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Value Objects vs Entities
|
||||||
|
|
||||||
|
### Value Objects
|
||||||
|
- Defined by their attributes (no identity)
|
||||||
|
- Immutable
|
||||||
|
- Comparable by value
|
||||||
|
- Examples: `Money`, `Email`, `Address`, `DateRange`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class Money {
|
||||||
|
constructor(
|
||||||
|
private readonly amount: number,
|
||||||
|
private readonly currency: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
equals(other: Money): boolean {
|
||||||
|
return this.amount === other.amount &&
|
||||||
|
this.currency === other.currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(other: Money): Money {
|
||||||
|
if (this.currency !== other.currency) {
|
||||||
|
throw new CurrencyMismatch();
|
||||||
|
}
|
||||||
|
return new Money(this.amount + other.amount, this.currency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entities
|
||||||
|
- Have identity (survives attribute changes)
|
||||||
|
- Usually mutable (via methods)
|
||||||
|
- Comparable by identity
|
||||||
|
- Examples: `User`, `Order`, `Product`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class User {
|
||||||
|
constructor(
|
||||||
|
private readonly id: UserId,
|
||||||
|
private email: Email,
|
||||||
|
private name: Name
|
||||||
|
) {}
|
||||||
|
|
||||||
|
equals(other: User): boolean {
|
||||||
|
return this.id.equals(other.id); // Identity comparison
|
||||||
|
}
|
||||||
|
|
||||||
|
changeEmail(newEmail: Email): void {
|
||||||
|
this.email = newEmail; // Still same user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Aggregates
|
||||||
|
|
||||||
|
A cluster of objects treated as a single unit for data changes.
|
||||||
|
|
||||||
|
- One object is the **aggregate root** (entry point)
|
||||||
|
- External code only references the root
|
||||||
|
- Root enforces invariants for the entire cluster
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Order is the aggregate root
|
||||||
|
class Order {
|
||||||
|
private items: OrderItem[] = [];
|
||||||
|
|
||||||
|
// All access through the root
|
||||||
|
addItem(product: Product, quantity: number): void {
|
||||||
|
const item = new OrderItem(product, quantity);
|
||||||
|
this.items.push(item);
|
||||||
|
this.validateTotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(itemId: ItemId): void {
|
||||||
|
this.items = this.items.filter(i => !i.id.equals(itemId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root enforces invariants
|
||||||
|
private validateTotal(): void {
|
||||||
|
if (this.calculateTotal().exceeds(MAX_ORDER_VALUE)) {
|
||||||
|
throw new OrderTotalExceeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BAD: Accessing items directly
|
||||||
|
order.items.push(new OrderItem(...)); // Bypasses validation!
|
||||||
|
|
||||||
|
// GOOD: Through the root
|
||||||
|
order.addItem(product, 2); // Validation happens
|
||||||
|
```
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
# SOLID Principles
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
SOLID helps structure software to be flexible, maintainable, and testable. These principles reduce coupling and increase cohesion.
|
||||||
|
|
||||||
|
## S - Single Responsibility Principle (SRP)
|
||||||
|
|
||||||
|
> "A class should have one, and only one, reason to change."
|
||||||
|
|
||||||
|
### Problem It Solves
|
||||||
|
God objects that do everything - hard to test, hard to change, hard to understand.
|
||||||
|
|
||||||
|
### How to Apply
|
||||||
|
Each class handles ONE responsibility. If you find yourself saying "and" when describing what a class does, split it.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Multiple responsibilities
|
||||||
|
class Order {
|
||||||
|
calculateTotal(): number { ... }
|
||||||
|
saveToDatabase(): void { ... } // Persistence
|
||||||
|
generateInvoice(): string { ... } // Presentation
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Single responsibility each
|
||||||
|
class Order {
|
||||||
|
private items: OrderItem[] = [];
|
||||||
|
|
||||||
|
addItem(item: OrderItem): void { ... }
|
||||||
|
calculateTotal(): number { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderRepository {
|
||||||
|
save(order: Order): Promise<void> { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvoiceGenerator {
|
||||||
|
generate(order: Order): Invoice { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detection Questions
|
||||||
|
- Does this class have multiple reasons to change?
|
||||||
|
- Can I describe it without using "and"?
|
||||||
|
- Would different stakeholders request changes to different parts?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## O - Open/Closed Principle (OCP)
|
||||||
|
|
||||||
|
> "Software entities should be open for extension but closed for modification."
|
||||||
|
|
||||||
|
### Problem It Solves
|
||||||
|
Having to modify existing, tested code every time requirements change. Risk of breaking working features.
|
||||||
|
|
||||||
|
### How to Apply
|
||||||
|
Design abstractions that allow new behavior through new classes, not edits to existing ones.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Must modify to add new shipping
|
||||||
|
class ShippingCalculator {
|
||||||
|
calculate(type: string, value: number): number {
|
||||||
|
if (type === 'standard') return value < 50 ? 5 : 0;
|
||||||
|
if (type === 'express') return 15;
|
||||||
|
// Must add more ifs for new types!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Open for extension
|
||||||
|
interface ShippingMethod {
|
||||||
|
calculateCost(orderValue: number): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StandardShipping implements ShippingMethod {
|
||||||
|
calculateCost(orderValue: number): number {
|
||||||
|
return orderValue < 50 ? 5 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExpressShipping implements ShippingMethod {
|
||||||
|
calculateCost(orderValue: number): number {
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new shipping by creating new class, not modifying existing
|
||||||
|
class SameDayShipping implements ShippingMethod {
|
||||||
|
calculateCost(orderValue: number): number {
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Architectural Insight
|
||||||
|
OCP at architecture level means: **design your codebase so new features are added by adding code, not changing existing code.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## L - Liskov Substitution Principle (LSP)
|
||||||
|
|
||||||
|
> "Subtypes must be substitutable for their base types without altering program correctness."
|
||||||
|
|
||||||
|
### Problem It Solves
|
||||||
|
Subclasses that break expectations, requiring type-checking and special cases.
|
||||||
|
|
||||||
|
### How to Apply
|
||||||
|
Subclasses must honor the contract of the parent. If the parent returns positive numbers, subclasses cannot return negatives.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Violates parent's contract
|
||||||
|
class DiscountPolicy {
|
||||||
|
getDiscount(value: number): number {
|
||||||
|
return 0; // Non-negative expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WeirdDiscount extends DiscountPolicy {
|
||||||
|
getDiscount(value: number): number {
|
||||||
|
return -5; // Increases cost! Breaks expectations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Enforces contract
|
||||||
|
class DiscountPolicy {
|
||||||
|
constructor(private discount: number) {
|
||||||
|
if (discount < 0) throw new Error("Discount must be non-negative");
|
||||||
|
}
|
||||||
|
|
||||||
|
getDiscount(): number {
|
||||||
|
return this.discount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Insight
|
||||||
|
This is why you can swap `InMemoryUserRepo` for `PostgresUserRepo` - they both honor the `UserRepo` interface contract.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## I - Interface Segregation Principle (ISP)
|
||||||
|
|
||||||
|
> "Clients should not be forced to depend on methods they do not use."
|
||||||
|
|
||||||
|
### Problem It Solves
|
||||||
|
Fat interfaces that force partial implementations, empty methods, or throws.
|
||||||
|
|
||||||
|
### How to Apply
|
||||||
|
Split large interfaces into smaller, cohesive ones. Clients depend only on what they need.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Fat interface
|
||||||
|
interface WarehouseDevice {
|
||||||
|
printLabel(orderId: string): void;
|
||||||
|
scanBarcode(): string;
|
||||||
|
packageItem(orderId: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BasicPrinter implements WarehouseDevice {
|
||||||
|
printLabel(orderId: string): void { /* works */ }
|
||||||
|
scanBarcode(): string { throw new Error("Not supported"); } // Forced!
|
||||||
|
packageItem(orderId: string): void { throw new Error("Not supported"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Segregated interfaces
|
||||||
|
interface LabelPrinter {
|
||||||
|
printLabel(orderId: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BarcodeScanner {
|
||||||
|
scanBarcode(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ItemPackager {
|
||||||
|
packageItem(orderId: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BasicPrinter implements LabelPrinter {
|
||||||
|
printLabel(orderId: string): void { /* only what it does */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Detection
|
||||||
|
If you see `throw new Error("Not implemented")` or empty method bodies, the interface is too fat.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D - Dependency Inversion Principle (DIP)
|
||||||
|
|
||||||
|
> "High-level modules should not depend on low-level modules. Both should depend on abstractions."
|
||||||
|
|
||||||
|
### Problem It Solves
|
||||||
|
Tight coupling to specific implementations (databases, APIs, frameworks). Hard to test, hard to swap.
|
||||||
|
|
||||||
|
### How to Apply
|
||||||
|
Depend on interfaces, inject implementations.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Direct dependency on concrete class
|
||||||
|
class OrderService {
|
||||||
|
private emailService = new SendGridEmailService(); // Locked in!
|
||||||
|
|
||||||
|
confirmOrder(email: string): void {
|
||||||
|
this.emailService.send(email, "Order confirmed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOD: Depend on abstraction
|
||||||
|
interface EmailService {
|
||||||
|
send(to: string, message: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService {
|
||||||
|
constructor(private emailService: EmailService) {}
|
||||||
|
|
||||||
|
confirmOrder(email: string): void {
|
||||||
|
this.emailService.send(email, "Order confirmed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now can inject any implementation
|
||||||
|
new OrderService(new SendGridEmailService());
|
||||||
|
new OrderService(new SESEmailService());
|
||||||
|
new OrderService(new MockEmailService()); // For tests!
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Dependency Rule
|
||||||
|
Source code dependencies should point **inward** toward high-level policies (domain logic), never toward low-level details (infrastructure).
|
||||||
|
|
||||||
|
```
|
||||||
|
Infrastructure → Application → Domain
|
||||||
|
↑ ↑ ↑
|
||||||
|
(outer) (middle) (inner)
|
||||||
|
|
||||||
|
Dependencies flow: outer → inner
|
||||||
|
Never: inner → outer
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Applying SOLID at Architecture Level
|
||||||
|
|
||||||
|
These principles scale beyond classes:
|
||||||
|
|
||||||
|
| Principle | Architecture Application |
|
||||||
|
|-----------|--------------------------|
|
||||||
|
| SRP | Each bounded context has one responsibility |
|
||||||
|
| OCP | New features = new modules, not edits to existing |
|
||||||
|
| LSP | Microservices with same contract are substitutable |
|
||||||
|
| ISP | Thin interfaces between services |
|
||||||
|
| DIP | High-level business logic doesn't know about databases/frameworks |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Principle | One-Liner | Red Flag |
|
||||||
|
|-----------|-----------|----------|
|
||||||
|
| SRP | One reason to change | "This class handles X and Y and Z" |
|
||||||
|
| OCP | Add, don't modify | `if/else` chains for types |
|
||||||
|
| LSP | Subtypes are substitutable | Type-checking in calling code |
|
||||||
|
| ISP | Small, focused interfaces | Empty method implementations |
|
||||||
|
| DIP | Depend on abstractions | `new ConcreteClass()` in business logic |
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# Test-Driven Development
|
||||||
|
|
||||||
|
## The Core Loop
|
||||||
|
|
||||||
|
```
|
||||||
|
RED → GREEN → REFACTOR → RED → ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### RED Phase
|
||||||
|
Write a failing test that describes the behavior you want. The test should:
|
||||||
|
- Use domain language, not technical jargon
|
||||||
|
- Describe WHAT, not HOW
|
||||||
|
- Be a concrete example, not an abstract statement
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Abstract
|
||||||
|
it('can add numbers', () => { ... });
|
||||||
|
|
||||||
|
// GOOD: Concrete example
|
||||||
|
it('when adding 2 + 3, returns 5', () => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
### GREEN Phase
|
||||||
|
Write the **simplest possible code** to make the test pass. Two strategies:
|
||||||
|
|
||||||
|
1. **Fake It** - Return a hardcoded value
|
||||||
|
```typescript
|
||||||
|
add(a: number, b: number): number {
|
||||||
|
return 5; // Simplest thing!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Obvious Implementation** - If you know the solution
|
||||||
|
```typescript
|
||||||
|
add(a: number, b: number): number {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prefer Fake It** when learning or unsure. Let more tests drive the real implementation.
|
||||||
|
|
||||||
|
### REFACTOR Phase
|
||||||
|
This is where **design happens**. Look for:
|
||||||
|
- Duplication (but wait for Rule of Three)
|
||||||
|
- Long methods to extract
|
||||||
|
- Poor names to improve
|
||||||
|
- Complex conditions to simplify
|
||||||
|
|
||||||
|
## The Three Laws of TDD
|
||||||
|
|
||||||
|
1. **No production code** without a failing test
|
||||||
|
2. **No more test code** than sufficient to fail (compilation failures count)
|
||||||
|
3. **No more production code** than sufficient to pass the one failing test
|
||||||
|
|
||||||
|
## The Rule of Three
|
||||||
|
|
||||||
|
**Only extract duplication when you see it THREE times.**
|
||||||
|
|
||||||
|
Why? Wrong abstractions are worse than duplication. Wait for the pattern to emerge.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Duplication #1 - Leave it
|
||||||
|
// Duplication #2 - Note it, leave it
|
||||||
|
// Duplication #3 - NOW extract it
|
||||||
|
```
|
||||||
|
|
||||||
|
## Triangulation
|
||||||
|
|
||||||
|
Each new test "sculpts" the solution toward a general, robust implementation.
|
||||||
|
|
||||||
|
Think of **degrees of freedom** - like a car that needs forward/back, left/right, and rotation. Each test carves out one degree of freedom until the implementation handles all cases.
|
||||||
|
|
||||||
|
## Transformation Priority Premise
|
||||||
|
|
||||||
|
When going from RED to GREEN, prefer simpler transformations:
|
||||||
|
|
||||||
|
| Priority | Transformation |
|
||||||
|
|----------|----------------|
|
||||||
|
| 1 | {} → nil |
|
||||||
|
| 2 | nil → constant |
|
||||||
|
| 3 | constant → variable |
|
||||||
|
| 4 | unconditional → conditional |
|
||||||
|
| 5 | scalar → collection |
|
||||||
|
| 6 | statement → recursion |
|
||||||
|
| 7 | value → mutated value |
|
||||||
|
|
||||||
|
Higher priority = simpler. Avoid jumping to complex transformations too early.
|
||||||
|
|
||||||
|
## Arrange-Act-Assert
|
||||||
|
|
||||||
|
Structure every test:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
it('calculates total with discount', () => {
|
||||||
|
// ARRANGE - Set up the world
|
||||||
|
const order = new Order();
|
||||||
|
order.addItem({ price: 100 });
|
||||||
|
const discount = new PercentDiscount(10);
|
||||||
|
|
||||||
|
// ACT - Execute the behavior
|
||||||
|
const total = order.calculateTotal(discount);
|
||||||
|
|
||||||
|
// ASSERT - Verify the outcome
|
||||||
|
expect(total).toBe(90);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing Tests Backwards
|
||||||
|
|
||||||
|
Sometimes it helps to write AAA in reverse:
|
||||||
|
1. Write the ASSERT first - what do you want to verify?
|
||||||
|
2. Write the ACT - what action produces that result?
|
||||||
|
3. Write the ARRANGE - what setup is needed?
|
||||||
|
|
||||||
|
## Test Naming Principles
|
||||||
|
|
||||||
|
- Use **behavior-driven names** with domain language
|
||||||
|
- Provide **concrete examples**, not abstract statements
|
||||||
|
- **One example per test** for easy debugging
|
||||||
|
- Avoid leaking implementation details
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// BAD: Technical, implementation-focused
|
||||||
|
it('should set the data property to 1', () => { ... });
|
||||||
|
|
||||||
|
// GOOD: Behavior-focused, domain language
|
||||||
|
it('should recognize "mom" as a palindrome', () => { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Classic vs Mockist TDD
|
||||||
|
|
||||||
|
**Classic (Detroit/Chicago) TDD:**
|
||||||
|
- Test with real dependencies
|
||||||
|
- Higher confidence, slower tests
|
||||||
|
- Best for: Pure functions, integration tests
|
||||||
|
|
||||||
|
**Mockist (London) TDD:**
|
||||||
|
- Mock external dependencies
|
||||||
|
- Faster tests, more isolated
|
||||||
|
- Best for: Classes with infrastructure dependencies
|
||||||
|
|
||||||
|
Start with Classic TDD to learn the technique. Add mocks when testing code with databases, APIs, etc.
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
1. **Writing code before tests** - Violates the fundamental principle
|
||||||
|
2. **Writing too much test** - Just enough to fail
|
||||||
|
3. **Writing too much code** - Just enough to pass
|
||||||
|
4. **Skipping refactor** - This is where design lives
|
||||||
|
5. **Testing implementation** - Test behavior, not how it's done
|
||||||
|
6. **Abstract test names** - Use concrete examples
|
||||||
|
7. **Extracting too early** - Wait for Rule of Three
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
# Testing Strategy
|
||||||
|
|
||||||
|
## The Testing Pyramid
|
||||||
|
|
||||||
|
```
|
||||||
|
/\
|
||||||
|
/ \ E2E Tests (Few)
|
||||||
|
/----\ - Full system
|
||||||
|
/ \ - Slow, brittle
|
||||||
|
/--------\
|
||||||
|
/ \ Integration Tests (Some)
|
||||||
|
/------------\ - Multiple components
|
||||||
|
/ \ - Medium speed
|
||||||
|
----------------
|
||||||
|
Unit Tests (Many)
|
||||||
|
- Single unit
|
||||||
|
- Fast, isolated
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Types
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
Test ONE class or function in isolation.
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Fast (milliseconds)
|
||||||
|
- No external dependencies (mocked)
|
||||||
|
- Most of your tests should be unit tests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('Order', () => {
|
||||||
|
it('calculates total correctly', () => {
|
||||||
|
const order = new Order();
|
||||||
|
order.addItem({ price: 100 });
|
||||||
|
order.addItem({ price: 50 });
|
||||||
|
|
||||||
|
expect(order.calculateTotal()).toBe(150);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
Test multiple components together.
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Slower (may use real DB)
|
||||||
|
- Test boundaries between components
|
||||||
|
- Fewer than unit tests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('OrderService Integration', () => {
|
||||||
|
let db: Database;
|
||||||
|
let service: OrderService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = await Database.connect();
|
||||||
|
service = new OrderService(new PostgresOrderRepo(db));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saves and retrieves an order', async () => {
|
||||||
|
const order = Order.create({ customerId: '123' });
|
||||||
|
await service.save(order);
|
||||||
|
|
||||||
|
const retrieved = await service.findById(order.id);
|
||||||
|
expect(retrieved).toEqual(order);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### E2E / Acceptance Tests
|
||||||
|
|
||||||
|
Test the entire system from user perspective.
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Slowest
|
||||||
|
- Most brittle (many moving parts)
|
||||||
|
- Test critical paths only
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('Checkout Flow', () => {
|
||||||
|
it('user can complete purchase', async () => {
|
||||||
|
await page.goto('/products');
|
||||||
|
await page.click('[data-testid="add-to-cart"]');
|
||||||
|
await page.click('[data-testid="checkout"]');
|
||||||
|
await page.fill('[name="card"]', '4242424242424242');
|
||||||
|
await page.click('[data-testid="pay"]');
|
||||||
|
|
||||||
|
expect(await page.textContent('h1')).toBe('Order Confirmed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arrange-Act-Assert (AAA)
|
||||||
|
|
||||||
|
Structure EVERY test this way:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
it('applies discount to premium users', () => {
|
||||||
|
// ARRANGE - Set up the test world
|
||||||
|
const user = new User({ isPremium: true });
|
||||||
|
const cart = new Cart(user);
|
||||||
|
cart.addItem({ price: 100 });
|
||||||
|
|
||||||
|
// ACT - Execute the behavior under test
|
||||||
|
const total = cart.calculateTotal();
|
||||||
|
|
||||||
|
// ASSERT - Verify the expected outcome
|
||||||
|
expect(total).toBe(80); // 20% discount
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing AAA Backwards
|
||||||
|
|
||||||
|
Sometimes easier to write in reverse:
|
||||||
|
|
||||||
|
1. **Assert first** - What do you want to verify?
|
||||||
|
2. **Act** - What action produces that result?
|
||||||
|
3. **Arrange** - What setup is needed?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Naming
|
||||||
|
|
||||||
|
### Bad: Abstract, Technical
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
it('should work correctly')
|
||||||
|
it('handles the edge case')
|
||||||
|
it('sets the data property')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Good: Concrete Examples, Domain Language
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
it('calculates 20% discount for premium users')
|
||||||
|
it('returns error when cart is empty')
|
||||||
|
it('recognizes "racecar" as a palindrome')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Option 1: should + behavior
|
||||||
|
it('should apply tax based on shipping state')
|
||||||
|
|
||||||
|
// Option 2: when + then
|
||||||
|
it('when adding 2 + 3, then returns 5')
|
||||||
|
|
||||||
|
// Option 3: Given-When-Then (for complex scenarios)
|
||||||
|
describe('given a premium user', () => {
|
||||||
|
describe('when they checkout', () => {
|
||||||
|
it('then they receive 20% discount', () => { ... });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Doubles
|
||||||
|
|
||||||
|
### Dummy
|
||||||
|
|
||||||
|
Object passed but never used.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const dummyLogger = {} as Logger;
|
||||||
|
new UserService(realRepo, dummyLogger);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stub
|
||||||
|
|
||||||
|
Returns predefined values.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const stubRepo: UserRepo = {
|
||||||
|
findById: () => Promise.resolve(new User({ name: 'Test' })),
|
||||||
|
save: () => Promise.resolve(),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spy
|
||||||
|
|
||||||
|
Records how it was called.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const emailSpy = {
|
||||||
|
sentEmails: [] as string[],
|
||||||
|
send(to: string, message: string) {
|
||||||
|
this.sentEmails.push(to);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Later
|
||||||
|
expect(emailSpy.sentEmails).toContain('user@example.com');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mock
|
||||||
|
|
||||||
|
Verifies expected interactions.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const mockRepo = jest.fn<UserRepo>();
|
||||||
|
mockRepo.save.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
// After test
|
||||||
|
expect(mockRepo.save).toHaveBeenCalledWith(expectedUser);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fake
|
||||||
|
|
||||||
|
Working implementation (simplified).
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class InMemoryUserRepo implements UserRepo {
|
||||||
|
private users: Map<string, User> = new Map();
|
||||||
|
|
||||||
|
async save(user: User): Promise<void> {
|
||||||
|
this.users.set(user.id, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<User | null> {
|
||||||
|
return this.users.get(id) || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Strategies by Layer
|
||||||
|
|
||||||
|
### Domain Layer (Most Tests)
|
||||||
|
|
||||||
|
- Unit tests with no mocks
|
||||||
|
- Test business rules, value objects, entities
|
||||||
|
- Fast, comprehensive
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('Money', () => {
|
||||||
|
it('adds amounts with same currency', () => {
|
||||||
|
const a = Money.dollars(10);
|
||||||
|
const b = Money.dollars(20);
|
||||||
|
expect(a.add(b).equals(Money.dollars(30))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when adding different currencies', () => {
|
||||||
|
const usd = Money.dollars(10);
|
||||||
|
const eur = Money.euros(10);
|
||||||
|
expect(() => usd.add(eur)).toThrow(CurrencyMismatch);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application Layer
|
||||||
|
|
||||||
|
- Integration tests with mocked infrastructure
|
||||||
|
- Test use case orchestration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('CreateOrderUseCase', () => {
|
||||||
|
it('creates order and sends confirmation', async () => {
|
||||||
|
const orderRepo = new InMemoryOrderRepo();
|
||||||
|
const emailService = { send: jest.fn() };
|
||||||
|
const useCase = new CreateOrderUseCase(orderRepo, emailService);
|
||||||
|
|
||||||
|
await useCase.execute({ customerId: '123', items: [...] });
|
||||||
|
|
||||||
|
expect(orderRepo.count()).toBe(1);
|
||||||
|
expect(emailService.send).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Infrastructure Layer
|
||||||
|
|
||||||
|
- Integration tests with real dependencies
|
||||||
|
- Test database, API integrations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('PostgresOrderRepo', () => {
|
||||||
|
let repo: PostgresOrderRepo;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
repo = new PostgresOrderRepo(testDb);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists and retrieves order', async () => {
|
||||||
|
const order = Order.create({ ... });
|
||||||
|
await repo.save(order);
|
||||||
|
|
||||||
|
const found = await repo.findById(order.id);
|
||||||
|
expect(found).toEqual(order);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## High-Value Integration Tests
|
||||||
|
|
||||||
|
Focus integration tests on:
|
||||||
|
|
||||||
|
1. **Boundaries** - Where systems meet
|
||||||
|
2. **Critical paths** - Money, security, core features
|
||||||
|
3. **Complex queries** - Database operations
|
||||||
|
|
||||||
|
### Contract Tests
|
||||||
|
|
||||||
|
Verify implementations match interfaces.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Shared contract test
|
||||||
|
function testUserRepoContract(createRepo: () => UserRepo) {
|
||||||
|
describe('UserRepo Contract', () => {
|
||||||
|
let repo: UserRepo;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
repo = createRepo();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saves and retrieves user', async () => {
|
||||||
|
const user = User.create({ name: 'Test' });
|
||||||
|
await repo.save(user);
|
||||||
|
const found = await repo.findById(user.id);
|
||||||
|
expect(found).toEqual(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for missing user', async () => {
|
||||||
|
const found = await repo.findById('nonexistent');
|
||||||
|
expect(found).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply to all implementations
|
||||||
|
testUserRepoContract(() => new InMemoryUserRepo());
|
||||||
|
testUserRepoContract(() => new PostgresUserRepo(testDb));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Builders
|
||||||
|
|
||||||
|
Create test objects easily.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class OrderBuilder {
|
||||||
|
private props: Partial<OrderProps> = {
|
||||||
|
id: 'order-1',
|
||||||
|
customerId: 'cust-1',
|
||||||
|
items: [],
|
||||||
|
status: 'pending',
|
||||||
|
};
|
||||||
|
|
||||||
|
withId(id: string): OrderBuilder {
|
||||||
|
this.props.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withItems(items: Item[]): OrderBuilder {
|
||||||
|
this.props.items = items;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
paid(): OrderBuilder {
|
||||||
|
this.props.status = 'paid';
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
build(): Order {
|
||||||
|
return Order.create(this.props as OrderProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const order = new OrderBuilder()
|
||||||
|
.withItems([{ sku: 'ABC', price: 100 }])
|
||||||
|
.paid()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Testing Mistakes
|
||||||
|
|
||||||
|
| Mistake | Problem | Solution |
|
||||||
|
|---------|---------|----------|
|
||||||
|
| Testing implementation | Brittle tests | Test behavior only |
|
||||||
|
| Too many mocks | Tests prove nothing | Use real objects when possible |
|
||||||
|
| Shared state | Flaky tests | Isolate each test |
|
||||||
|
| No assertions | False confidence | Always assert something meaningful |
|
||||||
|
| Testing trivial code | Wasted effort | Focus on logic and edge cases |
|
||||||
|
| Slow tests | Reduced feedback | Optimize, use unit tests |
|
||||||
Reference in New Issue
Block a user