Initial commit
This commit is contained in:
405
skills/auditing-dependencies/SKILL.md
Normal file
405
skills/auditing-dependencies/SKILL.md
Normal file
@@ -0,0 +1,405 @@
|
||||
---
|
||||
name: auditing-dependencies
|
||||
description: Auditing and updating npm dependencies to prevent security vulnerabilities in TypeScript projects
|
||||
---
|
||||
|
||||
# Security: Dependency Management
|
||||
|
||||
**Purpose:** Prevent security vulnerabilities through proper npm dependency auditing, updating, and monitoring.
|
||||
|
||||
**When to use:** Before adding new dependencies, during security reviews, when setting up CI/CD pipelines, or when package.json changes.
|
||||
|
||||
## Critical Security Principle
|
||||
|
||||
**Dependencies are attack vectors.** Each package you add introduces potential vulnerabilities:
|
||||
- Direct vulnerabilities in the package code
|
||||
- Transitive dependencies (dependencies of dependencies)
|
||||
- Supply chain attacks (malicious package updates)
|
||||
- Unmaintained packages with known CVEs
|
||||
|
||||
**Default stance:** Minimize dependencies. Every package is a liability.
|
||||
|
||||
## Dependency Audit Workflow
|
||||
|
||||
### 1. Check for Known Vulnerabilities
|
||||
|
||||
**Before installing any package:**
|
||||
|
||||
```bash
|
||||
npm audit
|
||||
```
|
||||
|
||||
This shows:
|
||||
- Severity levels (critical, high, moderate, low)
|
||||
- Vulnerable packages and versions
|
||||
- Recommended fixes
|
||||
- Dependency path showing how vulnerability entered
|
||||
|
||||
**Read the output carefully.** Not all vulnerabilities affect your code:
|
||||
- Check if vulnerable code path is used
|
||||
- Assess actual risk vs theoretical risk
|
||||
- Prioritize fixes by severity and exploitability
|
||||
|
||||
### 2. Update Vulnerable Packages
|
||||
|
||||
**Automatic fixes (use with caution):**
|
||||
|
||||
```bash
|
||||
npm audit fix
|
||||
```
|
||||
|
||||
This updates packages to non-breaking versions that patch vulnerabilities.
|
||||
|
||||
**Breaking changes:**
|
||||
|
||||
```bash
|
||||
npm audit fix --force
|
||||
```
|
||||
|
||||
This updates to latest versions, potentially breaking your code. Use only after:
|
||||
- Reading breaking change notes
|
||||
- Having comprehensive test coverage
|
||||
- Being prepared to fix broken code
|
||||
|
||||
**Manual selective updates:**
|
||||
|
||||
```bash
|
||||
npm update package-name
|
||||
```
|
||||
|
||||
Update specific packages after reviewing their changelogs.
|
||||
|
||||
### 3. Prevent Vulnerable Installations
|
||||
|
||||
**Block installations with vulnerabilities:**
|
||||
|
||||
Create `.npmrc` in project root:
|
||||
|
||||
```
|
||||
audit-level=moderate
|
||||
```
|
||||
|
||||
This fails `npm install` if moderate or higher severity vulnerabilities exist.
|
||||
|
||||
**In CI/CD:**
|
||||
|
||||
```yaml
|
||||
- name: Security audit
|
||||
run: |
|
||||
npm audit --audit-level=moderate
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Security vulnerabilities found!"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### 4. Monitor Dependencies Continuously
|
||||
|
||||
**GitHub Dependabot:**
|
||||
|
||||
Enable in repository settings → Security → Dependabot alerts.
|
||||
|
||||
Automatically:
|
||||
- Scans dependencies daily
|
||||
- Creates PRs for security updates
|
||||
- Provides vulnerability details
|
||||
|
||||
**npm-check-updates:**
|
||||
|
||||
```bash
|
||||
npx npm-check-updates
|
||||
```
|
||||
|
||||
Shows available updates for all dependencies.
|
||||
|
||||
```bash
|
||||
npx npm-check-updates -u
|
||||
```
|
||||
|
||||
Updates package.json (still need to run `npm install`).
|
||||
|
||||
## Dependency Selection Best Practices
|
||||
|
||||
### Before Adding Any Dependency
|
||||
|
||||
**Ask these questions:**
|
||||
|
||||
1. **Do I actually need this?**
|
||||
- Can I write the functionality myself in < 100 lines?
|
||||
- Am I using 10% of the package's features?
|
||||
- Is this adding significant bundle size for trivial functionality?
|
||||
|
||||
2. **Is this package trustworthy?**
|
||||
- Check npm weekly downloads (high is better)
|
||||
- Check GitHub stars and recent activity
|
||||
- Check last publish date (recent is better, but stable packages may be older)
|
||||
- Look for security track record
|
||||
|
||||
3. **Is this package maintained?**
|
||||
- When was last commit?
|
||||
- Are issues being responded to?
|
||||
- Are security issues addressed quickly?
|
||||
- Is there a clear maintenance policy?
|
||||
|
||||
4. **What are the transitive dependencies?**
|
||||
```bash
|
||||
npm ls package-name
|
||||
```
|
||||
Each transitive dependency is another attack vector.
|
||||
|
||||
### Red Flags
|
||||
|
||||
**Avoid packages with:**
|
||||
- No TypeScript types (requires `@types/` package or no types at all)
|
||||
- Abandoned for > 2 years with no successor
|
||||
- Known security vulnerabilities with no fix available
|
||||
- Excessive transitive dependencies (> 50 packages)
|
||||
- Requires `postinstall` scripts (potential supply chain attack vector)
|
||||
- Very small packages doing trivial things (left-pad scenario)
|
||||
|
||||
### Safer Alternatives
|
||||
|
||||
**Use built-in Node.js/browser features when possible:**
|
||||
|
||||
```typescript
|
||||
import { randomBytes } from 'crypto';
|
||||
const id = randomBytes(16).toString('hex');
|
||||
```
|
||||
|
||||
Better than installing `uuid` package if you just need random IDs.
|
||||
|
||||
```typescript
|
||||
const url = new URL('/api/users', 'https://api.example.com');
|
||||
url.searchParams.set('limit', '10');
|
||||
```
|
||||
|
||||
Better than installing query string builder packages.
|
||||
|
||||
## Lock Files and Reproducible Builds
|
||||
|
||||
### Always Commit Lock Files
|
||||
|
||||
**package-lock.json** (npm) or **yarn.lock** (Yarn) must be committed:
|
||||
- Ensures exact same dependency versions across environments
|
||||
- Prevents supply chain attacks via dependency version updates
|
||||
- Makes builds reproducible
|
||||
|
||||
**Never:**
|
||||
- Add lock files to `.gitignore`
|
||||
- Delete lock files to "fix" problems
|
||||
- Run `npm install` with `--no-package-lock`
|
||||
|
||||
### Use Specific Version Ranges
|
||||
|
||||
**In package.json, prefer exact versions for critical dependencies:**
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"express": "4.18.2",
|
||||
"zod": "3.22.4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Not:
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"zod": "~3.22.4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:** Caret (`^`) and tilde (`~`) allow automatic updates that could introduce breaking changes or vulnerabilities.
|
||||
|
||||
**Exception:** Development dependencies can use ranges if you regularly update them.
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### Required Security Checks
|
||||
|
||||
**Every CI pipeline must:**
|
||||
|
||||
1. **Run audit on every build:**
|
||||
```yaml
|
||||
- run: npm audit --audit-level=moderate
|
||||
```
|
||||
|
||||
2. **Check for outdated dependencies weekly:**
|
||||
```yaml
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
jobs:
|
||||
update-check:
|
||||
- run: npx npm-check-updates
|
||||
```
|
||||
|
||||
3. **Prevent merging PRs with vulnerabilities:**
|
||||
```yaml
|
||||
- name: Security gate
|
||||
run: npm audit --production --audit-level=moderate
|
||||
```
|
||||
|
||||
### Example GitHub Actions Workflow
|
||||
|
||||
```yaml
|
||||
name: Security Audit
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
- run: npm ci
|
||||
- run: npm audit --audit-level=moderate
|
||||
- name: Check for outdated packages
|
||||
run: npx npm-check-updates
|
||||
```
|
||||
|
||||
## Handling Vulnerabilities That Can't Be Fixed
|
||||
|
||||
### Scenario: Dependency has vulnerability, no fix available
|
||||
|
||||
**Options:**
|
||||
|
||||
1. **Find alternative package:**
|
||||
- Research similar packages without the vulnerability
|
||||
- Consider rewriting functionality if simple
|
||||
|
||||
2. **Assess actual risk:**
|
||||
- Is vulnerable code path used in your application?
|
||||
- Is vulnerability exploitable in your context?
|
||||
- Document risk assessment
|
||||
|
||||
3. **Audit exception (last resort):**
|
||||
```bash
|
||||
npm audit --json > audit-baseline.json
|
||||
```
|
||||
|
||||
Document why exception is acceptable:
|
||||
- What is the vulnerability?
|
||||
- Why can't it be fixed?
|
||||
- What mitigations are in place?
|
||||
- When will this be reviewed again?
|
||||
|
||||
**Never ignore vulnerabilities permanently.**
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Mistake 1: Installing packages without checking
|
||||
|
||||
```bash
|
||||
npm install some-random-package
|
||||
```
|
||||
|
||||
**Correct approach:**
|
||||
|
||||
1. Check package on npm registry
|
||||
2. Review GitHub repository
|
||||
3. Check bundle size: `bundlephobia.com`
|
||||
4. Run `npm audit` after installation
|
||||
5. Review lock file changes in git diff
|
||||
|
||||
### Mistake 2: Ignoring audit warnings
|
||||
|
||||
"It's just a moderate severity in a dev dependency, doesn't matter."
|
||||
|
||||
**Wrong.** Development dependencies:
|
||||
- Can be compromised and steal secrets
|
||||
- Run with same permissions as your code
|
||||
- Can modify source files during build
|
||||
|
||||
### Mistake 3: Using `--force` without understanding
|
||||
|
||||
```bash
|
||||
npm install --force
|
||||
```
|
||||
|
||||
This bypasses dependency resolution and can install incompatible versions.
|
||||
|
||||
**Only use when:**
|
||||
- You understand exactly what it does
|
||||
- You've read the conflict details
|
||||
- You have tests to verify nothing broke
|
||||
|
||||
## Maintenance Schedule
|
||||
|
||||
**Weekly:**
|
||||
- Review Dependabot PRs
|
||||
- Run `npm audit`
|
||||
|
||||
**Monthly:**
|
||||
- Run `npx npm-check-updates`
|
||||
- Update non-breaking dependencies
|
||||
- Test thoroughly
|
||||
|
||||
**Quarterly:**
|
||||
- Plan major version updates
|
||||
- Review all dependencies for continued need
|
||||
- Remove unused packages
|
||||
|
||||
## TypeScript-Specific Considerations
|
||||
|
||||
### Type Definition Security
|
||||
|
||||
**Check if types match runtime:**
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const APIResponseSchema = z.object({
|
||||
data: z.array(z.string()),
|
||||
});
|
||||
|
||||
type APIResponse = z.infer<typeof APIResponseSchema>;
|
||||
```
|
||||
|
||||
This ensures types and runtime validation stay synchronized.
|
||||
|
||||
### Type-Only Imports for Tree-Shaking
|
||||
|
||||
```typescript
|
||||
import type { User } from 'huge-library';
|
||||
```
|
||||
|
||||
This imports only types, not runtime code, reducing bundle size.
|
||||
|
||||
## Resources
|
||||
|
||||
**Tools:**
|
||||
- `npm audit` - Built-in vulnerability scanner
|
||||
- `npm-check-updates` - Dependency update checker
|
||||
- Snyk - Commercial vulnerability scanning
|
||||
- GitHub Dependabot - Automated security updates
|
||||
|
||||
**References:**
|
||||
- [npm audit documentation](https://docs.npmjs.com/cli/v10/commands/npm-audit)
|
||||
- [OWASP Dependency Check](https://owasp.org/www-project-dependency-check/)
|
||||
- [Sonatype State of Software Supply Chain](https://www.sonatype.com/state-of-the-software-supply-chain)
|
||||
|
||||
## Summary Checklist
|
||||
|
||||
Before merging any dependency changes:
|
||||
|
||||
- [ ] `npm audit` passes at moderate level or higher
|
||||
- [ ] Reviewed what the package does and alternatives considered
|
||||
- [ ] Checked package maintenance status and security history
|
||||
- [ ] Lock file committed with changes
|
||||
- [ ] CI pipeline includes security audit
|
||||
- [ ] Transitive dependencies reviewed (not excessive)
|
||||
- [ ] Bundle size impact assessed for frontend projects
|
||||
- [ ] Types available and trustworthy
|
||||
|
||||
**Remember:** The best dependency is the one you don't add. The second best is one that's actively maintained with a strong security track record.
|
||||
284
skills/avoiding-angle-bracket-assertions/SKILL.md
Normal file
284
skills/avoiding-angle-bracket-assertions/SKILL.md
Normal file
@@ -0,0 +1,284 @@
|
||||
---
|
||||
name: avoiding-angle-bracket-assertions
|
||||
description: Avoid angle-bracket type assertions (<Type>) and use 'as Type' syntax instead
|
||||
---
|
||||
|
||||
# Avoiding Angle-Bracket Type Assertions
|
||||
|
||||
The angle-bracket type assertion syntax (`<Type>value`) is deprecated in TypeScript, especially in TSX files where it conflicts with JSX syntax.
|
||||
|
||||
## Why Avoid `<Type>` Syntax
|
||||
|
||||
- **JSX conflict**: Incompatible with TSX files (React, Preact, etc.)
|
||||
- **Inconsistent syntax**: Modern TypeScript uses `as Type`
|
||||
- **Reduced readability**: Looks like generics or JSX
|
||||
- **Tooling issues**: Some tools don't handle it well
|
||||
|
||||
## Modern Alternative: `as Type`
|
||||
|
||||
### Basic Replacement
|
||||
|
||||
**Deprecated:**
|
||||
```typescript
|
||||
const value = <string>someValue;
|
||||
const num = <number>input;
|
||||
```
|
||||
|
||||
**Modern:**
|
||||
```typescript
|
||||
const value = someValue as string;
|
||||
const num = input as number;
|
||||
```
|
||||
|
||||
### With Complex Types
|
||||
|
||||
**Deprecated:**
|
||||
```typescript
|
||||
const user = <User>jsonData;
|
||||
const config = <AppConfig>settings;
|
||||
```
|
||||
|
||||
**Modern:**
|
||||
```typescript
|
||||
const user = jsonData as User;
|
||||
const config = settings as AppConfig;
|
||||
```
|
||||
|
||||
## Better: Avoid Type Assertions Entirely
|
||||
|
||||
Type assertions bypass type checking and should be avoided when possible.
|
||||
|
||||
### 1. Use Type Guards Instead
|
||||
|
||||
**Bad:**
|
||||
```typescript
|
||||
function processValue(value: unknown) {
|
||||
const str = value as string;
|
||||
return str.toUpperCase();
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
```typescript
|
||||
function processValue(value: unknown) {
|
||||
if (typeof value === 'string') {
|
||||
return value.toUpperCase();
|
||||
}
|
||||
throw new Error('Expected string');
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use Validation Functions
|
||||
|
||||
**Bad:**
|
||||
```typescript
|
||||
const user = apiResponse as User;
|
||||
```
|
||||
|
||||
**Good:**
|
||||
```typescript
|
||||
function isUser(value: unknown): value is User {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'id' in value &&
|
||||
'name' in value &&
|
||||
typeof value.id === 'number' &&
|
||||
typeof value.name === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = apiResponse;
|
||||
if (isUser(parsed)) {
|
||||
const user = parsed;
|
||||
} else {
|
||||
throw new Error('Invalid user data');
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Validation Libraries
|
||||
|
||||
**Best:**
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const UserSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
type User = z.infer<typeof UserSchema>;
|
||||
|
||||
const user = UserSchema.parse(apiResponse);
|
||||
```
|
||||
|
||||
### 4. Improve Type Inference
|
||||
|
||||
**Bad:**
|
||||
```typescript
|
||||
const element = document.getElementById('myId') as HTMLButtonElement;
|
||||
```
|
||||
|
||||
**Better:**
|
||||
```typescript
|
||||
const element = document.getElementById('myId');
|
||||
if (element instanceof HTMLButtonElement) {
|
||||
element.addEventListener('click', handler);
|
||||
}
|
||||
```
|
||||
|
||||
**Or use querySelector with type inference:**
|
||||
```typescript
|
||||
const element = document.querySelector<HTMLButtonElement>('#myId');
|
||||
if (element) {
|
||||
element.addEventListener('click', handler);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Use Discriminated Unions
|
||||
|
||||
**Bad:**
|
||||
```typescript
|
||||
function handleEvent(event: Event) {
|
||||
const mouseEvent = event as MouseEvent;
|
||||
console.log(mouseEvent.clientX);
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
```typescript
|
||||
type AppEvent =
|
||||
| { type: 'mouse'; clientX: number; clientY: number }
|
||||
| { type: 'keyboard'; key: string }
|
||||
| { type: 'custom'; data: unknown };
|
||||
|
||||
function handleEvent(event: AppEvent) {
|
||||
switch (event.type) {
|
||||
case 'mouse':
|
||||
console.log(event.clientX);
|
||||
break;
|
||||
case 'keyboard':
|
||||
console.log(event.key);
|
||||
break;
|
||||
case 'custom':
|
||||
console.log(event.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TSX-Specific Issues
|
||||
|
||||
In TSX files, angle-bracket syntax causes syntax errors:
|
||||
|
||||
**Will cause syntax error in TSX:**
|
||||
```tsx
|
||||
const Component = () => {
|
||||
const value = <string>getData();
|
||||
return <div>{value}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
**Must use `as` syntax:**
|
||||
```tsx
|
||||
const Component = () => {
|
||||
const value = getData() as string;
|
||||
return <div>{value}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
**Even better - validate properly:**
|
||||
```tsx
|
||||
const Component = () => {
|
||||
const data = getData();
|
||||
if (typeof data !== 'string') {
|
||||
throw new Error('Expected string');
|
||||
}
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## When Assertions Are Necessary
|
||||
|
||||
If you must use assertions (rare cases):
|
||||
|
||||
### 1. Use `as const` for Literal Types
|
||||
|
||||
```typescript
|
||||
const config = {
|
||||
apiUrl: 'https://api.example.com',
|
||||
timeout: 5000,
|
||||
} as const;
|
||||
```
|
||||
|
||||
### 2. Use `as Type` (Never `<Type>`)
|
||||
|
||||
```typescript
|
||||
const value = unknownValue as SomeType;
|
||||
```
|
||||
|
||||
### 3. Document Why It's Safe
|
||||
|
||||
```typescript
|
||||
const element = document.getElementById('app-root') as HTMLDivElement;
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
1. **Find all angle-bracket assertions:**
|
||||
```bash
|
||||
grep -r '<[A-Z][a-zA-Z]*>' --include="*.ts" --include="*.tsx"
|
||||
```
|
||||
|
||||
2. **Replace with `as` syntax:**
|
||||
- `<Type>value` → `value as Type`
|
||||
|
||||
3. **Review each assertion:**
|
||||
- Can it be replaced with a type guard?
|
||||
- Can validation library be used?
|
||||
- Is it truly necessary?
|
||||
|
||||
4. **Enable linting:**
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-assertions": [
|
||||
"error",
|
||||
{
|
||||
"assertionStyle": "as",
|
||||
"objectLiteralTypeAssertions": "never"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ESLint Configuration
|
||||
|
||||
Enforce `as` syntax and discourage assertions:
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-assertions": [
|
||||
"error",
|
||||
{
|
||||
"assertionStyle": "as",
|
||||
"objectLiteralTypeAssertions": "never"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**Never use angle-bracket syntax:**
|
||||
- **Use** `as Type` when assertions are unavoidable
|
||||
- **Prefer** type guards over assertions
|
||||
- **Use** validation libraries for external data
|
||||
- **Validate** at runtime for safety
|
||||
- **Document** why assertions are necessary
|
||||
- **Enable** linting to enforce consistency
|
||||
341
skills/avoiding-any-types/SKILL.md
Normal file
341
skills/avoiding-any-types/SKILL.md
Normal file
@@ -0,0 +1,341 @@
|
||||
---
|
||||
name: avoiding-any-types
|
||||
description: Teaches when and how to use unknown instead of any type in TypeScript. Use when working with TypeScript code that has any types, needs type safety, handling external data, or when designing APIs. Critical for preventing type safety violations.
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
<role>
|
||||
This skill teaches how to eliminate `any` type abuse in TypeScript by using `unknown` with proper type guards. Based on stress test findings where 83% of AI agents overused `any`, defeating TypeScript's purpose.
|
||||
</role>
|
||||
|
||||
<when-to-activate>
|
||||
This skill activates when:
|
||||
|
||||
- Code contains `any` type annotations
|
||||
- Working with external data (APIs, JSON, user input)
|
||||
- Designing generic functions or types
|
||||
- User mentions type safety, type checking, or avoiding `any`
|
||||
- Files contain patterns like `: any`, `<any>`, `as any`
|
||||
</when-to-activate>
|
||||
|
||||
<overview>
|
||||
The `any` type is TypeScript's escape hatch that disables all type checking. While sometimes necessary, overusing `any` defeats TypeScript's purpose and allows runtime errors TypeScript should prevent.
|
||||
|
||||
The `unknown` type is `any`'s type-safe counterpart. It accepts any value like `any`, but requires type narrowing before use.
|
||||
|
||||
**Key Pattern**: `any` → `unknown` → type guard → safe access
|
||||
|
||||
**Impact**: Prevents runtime errors while maintaining flexibility for truly dynamic data.
|
||||
</overview>
|
||||
|
||||
<workflow>
|
||||
## Decision Flow
|
||||
|
||||
**Step 1: Identify `any` Usage**
|
||||
|
||||
When you see `any`, ask:
|
||||
1. Is this truly unknowable at compile time? (external data, plugin systems)
|
||||
2. Can I use a more specific type? (union types, generics with constraints)
|
||||
3. Is this laziness or necessity?
|
||||
|
||||
**Step 2: Choose Replacement Strategy**
|
||||
|
||||
**Strategy A: Use Specific Types** (preferred)
|
||||
- Known structure → Use interfaces/types
|
||||
- Multiple possible types → Use union types
|
||||
- Shared shape → Use generics with constraints
|
||||
|
||||
**Strategy B: Use `unknown`** (when truly dynamic)
|
||||
- External data with unknown structure
|
||||
- Plugin/extension systems
|
||||
- Gradual migration from JavaScript
|
||||
|
||||
**Strategy C: Keep `any`** (rare, justified cases)
|
||||
- Interop with poorly-typed libraries
|
||||
- Complex type manipulation that TypeScript can't express
|
||||
- Performance-critical code where type checks are prohibitive
|
||||
|
||||
**Step 3: Implement Type Guards**
|
||||
|
||||
When using `unknown`, implement type guards:
|
||||
1. Runtime validation (Zod, io-ts, custom validators)
|
||||
2. Type predicates for custom guards
|
||||
3. Built-in guards (typeof, instanceof, in)
|
||||
</workflow>
|
||||
|
||||
<progressive-disclosure>
|
||||
## Reference Files
|
||||
|
||||
For detailed patterns and examples:
|
||||
|
||||
- **Type Guard Patterns**: Use the using-type-guards skill for comprehensive type guard implementation
|
||||
- **Runtime Validation**: Use the using-runtime-checks skill for validating unknown data
|
||||
- **Generic Constraints**: Use the using-generics skill for constraining generic types
|
||||
</progressive-disclosure>
|
||||
|
||||
<examples>
|
||||
## Example 1: API Response (External Data)
|
||||
|
||||
**❌ Using `any` (unsafe)**
|
||||
|
||||
```typescript
|
||||
async function fetchUser(id: string): Promise<any> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const user = await fetchUser("123");
|
||||
console.log(user.name.toUpperCase());
|
||||
```
|
||||
|
||||
**Problem**: If API returns `{ username: string }` instead of `{ name: string }`, this crashes at runtime. TypeScript provides no protection.
|
||||
|
||||
**✅ Using `unknown` + validation (safe)**
|
||||
|
||||
```typescript
|
||||
async function fetchUser(id: string): Promise<unknown> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function isUser(value: unknown): value is { name: string } {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"name" in value &&
|
||||
typeof value.name === "string"
|
||||
);
|
||||
}
|
||||
|
||||
const userData = await fetchUser("123");
|
||||
if (isUser(userData)) {
|
||||
console.log(userData.name.toUpperCase());
|
||||
} else {
|
||||
throw new Error("Invalid user data");
|
||||
}
|
||||
```
|
||||
|
||||
**Better**: Use Zod for complex validation (use the using-runtime-checks skill)
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Generic Function Defaults
|
||||
|
||||
**❌ Using `any` in generic default (unsafe)**
|
||||
|
||||
```typescript
|
||||
interface ApiResponse<T = any> {
|
||||
data: T;
|
||||
status: number;
|
||||
}
|
||||
|
||||
const response: ApiResponse = { data: "anything", status: 200 };
|
||||
response.data.nonexistent.property;
|
||||
```
|
||||
|
||||
**Problem**: Generic defaults to `any`, losing all type safety.
|
||||
|
||||
**✅ Using `unknown` default (safe)**
|
||||
|
||||
```typescript
|
||||
interface ApiResponse<T = unknown> {
|
||||
data: T;
|
||||
status: number;
|
||||
}
|
||||
|
||||
const response: ApiResponse = { data: "anything", status: 200 };
|
||||
|
||||
if (typeof response.data === "string") {
|
||||
console.log(response.data.toUpperCase());
|
||||
}
|
||||
```
|
||||
|
||||
**Even Better**: Require explicit type parameter
|
||||
|
||||
```typescript
|
||||
interface ApiResponse<T> {
|
||||
data: T;
|
||||
status: number;
|
||||
}
|
||||
|
||||
const response: ApiResponse<User> = await fetchUser();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 3: Error Handling
|
||||
|
||||
**❌ Using `any` for caught errors (unsafe)**
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await riskyOperation();
|
||||
} catch (error: any) {
|
||||
console.log(error.message);
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: Not all thrown values are Error objects. This crashes if someone throws a string or number.
|
||||
|
||||
**✅ Using `unknown` + type guard (safe)**
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await riskyOperation();
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.log(error.message);
|
||||
} else {
|
||||
console.log("Unknown error:", String(error));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 4: Validation Function
|
||||
|
||||
**❌ Using `any` parameter (unsafe)**
|
||||
|
||||
```typescript
|
||||
function validate(data: any): boolean {
|
||||
return data.email && data.password;
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: Typos like `data.emial` are not caught. No autocomplete support.
|
||||
|
||||
**✅ Using `unknown` + type guard (safe)**
|
||||
|
||||
```typescript
|
||||
interface LoginData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
function isLoginData(data: unknown): data is LoginData {
|
||||
return (
|
||||
typeof data === "object" &&
|
||||
data !== null &&
|
||||
"email" in data &&
|
||||
"password" in data &&
|
||||
typeof data.email === "string" &&
|
||||
typeof data.password === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function validate(data: unknown): data is LoginData {
|
||||
if (!isLoginData(data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return data.email.includes("@") && data.password.length >= 8;
|
||||
}
|
||||
```
|
||||
</examples>
|
||||
|
||||
<constraints>
|
||||
**MUST:**
|
||||
|
||||
- Use `unknown` for external data (APIs, JSON.parse, user input)
|
||||
- Use `unknown` for generic defaults when type is truly dynamic
|
||||
- Implement type guards before accessing `unknown` values
|
||||
- Use runtime validation libraries (Zod, io-ts) for complex structures
|
||||
|
||||
**SHOULD:**
|
||||
|
||||
- Prefer specific types (interfaces, unions) over `unknown` when structure is known
|
||||
- Use type predicates (`value is Type`) for reusable type guards
|
||||
- Narrow `unknown` progressively (check object → check properties → check types)
|
||||
|
||||
**NEVER:**
|
||||
|
||||
- Use `any` for external data
|
||||
- Use `as any` to silence TypeScript errors
|
||||
- Use `any` in public API surfaces
|
||||
- Default generics to `any`
|
||||
- Cast `unknown` to specific types without validation
|
||||
</constraints>
|
||||
|
||||
<validation>
|
||||
## Validation Checklist
|
||||
|
||||
After replacing `any` with `unknown`:
|
||||
|
||||
1. **Type Guard Exists**:
|
||||
- Every `unknown` value has a corresponding type guard
|
||||
- Type guards check structure AND types
|
||||
- Guards return early on invalid data
|
||||
|
||||
2. **Safe Access Only**:
|
||||
- No property access before type narrowing
|
||||
- No method calls before type narrowing
|
||||
- IDE autocomplete works after narrowing
|
||||
|
||||
3. **Error Handling**:
|
||||
- Invalid data handled gracefully
|
||||
- Clear error messages
|
||||
- No silent failures
|
||||
|
||||
4. **Compilation**:
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
Should pass without `any` suppressions.
|
||||
</validation>
|
||||
|
||||
<common-patterns>
|
||||
## When `any` is Actually Justified
|
||||
|
||||
**1. Library Interop** (temporary)
|
||||
```typescript
|
||||
import poorlyTypedLib from "poorly-typed-lib";
|
||||
const result = poorlyTypedLib.method() as any;
|
||||
```
|
||||
|
||||
Consider contributing types to DefinitelyTyped or wrapping in typed facade.
|
||||
|
||||
**2. Type Manipulation Edge Cases**
|
||||
```typescript
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
||||
};
|
||||
```
|
||||
|
||||
Sometimes TypeScript's type system can't express complex patterns. Document why.
|
||||
|
||||
**3. Explicit Opt-Out**
|
||||
```typescript
|
||||
const configSchema: any = generateFromSpec();
|
||||
```
|
||||
|
||||
Explicitly choosing to skip type checking for this value. Document decision.
|
||||
</common-patterns>
|
||||
|
||||
<migration-path>
|
||||
## Migrating Existing `any` Usage
|
||||
|
||||
**Phase 1: Audit**
|
||||
```bash
|
||||
grep -rn ": any" src/
|
||||
grep -rn "<any>" src/
|
||||
grep -rn "= any" src/
|
||||
```
|
||||
|
||||
**Phase 2: Classify**
|
||||
- External data → `unknown` + validation
|
||||
- Known structure → specific types
|
||||
- Generic defaults → remove default or use `unknown`
|
||||
- Justified `any` → document with comment explaining why
|
||||
|
||||
**Phase 3: Replace**
|
||||
- Start with external boundaries (API layer, JSON parsing)
|
||||
- Work inward toward core logic
|
||||
- Add tests to verify runtime behavior unchanged
|
||||
|
||||
**Phase 4: Prevent**
|
||||
- Enable `noImplicitAny` in tsconfig.json
|
||||
- Add lint rules forbidding `any`
|
||||
- Use hooks to catch new `any` introduction
|
||||
</migration-path>
|
||||
246
skills/avoiding-non-null-assertions/SKILL.md
Normal file
246
skills/avoiding-non-null-assertions/SKILL.md
Normal file
@@ -0,0 +1,246 @@
|
||||
---
|
||||
name: avoiding-non-null-assertions
|
||||
description: Avoid non-null assertion operator (!) and use type-safe alternatives instead
|
||||
---
|
||||
|
||||
# Avoiding Non-Null Assertions
|
||||
|
||||
The non-null assertion operator (`!`) is deprecated in modern TypeScript because it bypasses type safety and can lead to runtime errors.
|
||||
|
||||
## Why Avoid `!`
|
||||
|
||||
- **Bypasses type safety**: Tells TypeScript "trust me" without verification
|
||||
- **Runtime errors**: Can cause `undefined` or `null` errors at runtime
|
||||
- **Maintenance burden**: Makes refactoring dangerous
|
||||
- **No protection**: Removes TypeScript's main benefit
|
||||
|
||||
## Modern Alternatives
|
||||
|
||||
### 1. Optional Chaining (`?.`)
|
||||
|
||||
**Bad:**
|
||||
|
||||
```typescript
|
||||
const userName = user!.profile!.name;
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```typescript
|
||||
const userName = user?.profile?.name;
|
||||
```
|
||||
|
||||
### 2. Nullish Coalescing (`??`)
|
||||
|
||||
**Bad:**
|
||||
|
||||
```typescript
|
||||
const value = config!.timeout;
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```typescript
|
||||
const value = config?.timeout ?? 5000;
|
||||
```
|
||||
|
||||
### 3. Type Guards
|
||||
|
||||
**Bad:**
|
||||
|
||||
```typescript
|
||||
function processUser(user: User | null) {
|
||||
console.log(user!.name);
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```typescript
|
||||
function processUser(user: User | null) {
|
||||
if (user !== null) {
|
||||
console.log(user.name);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Early Return Pattern
|
||||
|
||||
**Bad:**
|
||||
|
||||
```typescript
|
||||
function getUserEmail(userId: number): string {
|
||||
const user = findUser(userId);
|
||||
return user!.email;
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```typescript
|
||||
function getUserEmail(userId: number): string | null {
|
||||
const user = findUser(userId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
return user.email;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Custom Type Guards
|
||||
|
||||
**Bad:**
|
||||
|
||||
```typescript
|
||||
function handleValue(value: unknown) {
|
||||
return (value as User)!.name;
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```typescript
|
||||
function isUser(value: unknown): value is User {
|
||||
return typeof value === 'object' && value !== null && 'name' in value;
|
||||
}
|
||||
|
||||
function handleValue(value: unknown) {
|
||||
if (isUser(value)) {
|
||||
return value.name;
|
||||
}
|
||||
throw new Error('Invalid user');
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Narrowing with `in` Operator
|
||||
|
||||
**Bad:**
|
||||
|
||||
```typescript
|
||||
function process(obj: { data?: string }) {
|
||||
console.log(obj.data!.toUpperCase());
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```typescript
|
||||
function process(obj: { data?: string }) {
|
||||
if ('data' in obj && obj.data !== undefined) {
|
||||
console.log(obj.data.toUpperCase());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Array Methods with Type Safety
|
||||
|
||||
**Bad:**
|
||||
|
||||
```typescript
|
||||
const users: User[] = getUsers();
|
||||
const firstUser = users[0]!;
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```typescript
|
||||
const users: User[] = getUsers();
|
||||
const firstUser = users.at(0);
|
||||
if (firstUser) {
|
||||
console.log(firstUser.name);
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Assertion Functions (TypeScript 3.7+)
|
||||
|
||||
**Good:**
|
||||
|
||||
```typescript
|
||||
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
|
||||
if (value === undefined || value === null) {
|
||||
throw new Error('Value must be defined');
|
||||
}
|
||||
}
|
||||
|
||||
function process(value: string | null) {
|
||||
assertIsDefined(value);
|
||||
console.log(value.toUpperCase());
|
||||
}
|
||||
```
|
||||
|
||||
## DOM Element Access
|
||||
|
||||
**Bad:**
|
||||
|
||||
```typescript
|
||||
const button = document.getElementById('submit')!;
|
||||
button.addEventListener('click', handler);
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```typescript
|
||||
const button = document.getElementById('submit');
|
||||
if (button) {
|
||||
button.addEventListener('click', handler);
|
||||
}
|
||||
```
|
||||
|
||||
**Or with assertion function:**
|
||||
|
||||
```typescript
|
||||
function assertElement<T extends Element>(
|
||||
element: T | null,
|
||||
selector: string
|
||||
): asserts element is T {
|
||||
if (!element) {
|
||||
throw new Error(`Element not found: ${selector}`);
|
||||
}
|
||||
}
|
||||
|
||||
const button = document.getElementById('submit');
|
||||
assertElement(button, '#submit');
|
||||
button.addEventListener('click', handler);
|
||||
```
|
||||
|
||||
## When Is `!` Acceptable?
|
||||
|
||||
Only in very rare cases where:
|
||||
|
||||
1. You have exhaustively verified the value exists
|
||||
2. There's no other way to express it to TypeScript
|
||||
3. You document WHY it's safe
|
||||
|
||||
Even then, prefer assertion functions over `!`.
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
1. **Search** for all uses of `!` in codebase
|
||||
2. **Categorize** by pattern (DOM access, array indexing, etc.)
|
||||
3. **Replace** with appropriate type-safe alternative
|
||||
4. **Test** thoroughly after each replacement
|
||||
5. **Enable linting** to prevent future uses
|
||||
|
||||
## Compiler Configuration
|
||||
|
||||
Enable strict checks:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**Never use `!` operator:**
|
||||
|
||||
- Use `?.` for optional chaining
|
||||
- Use `??` for default values
|
||||
- Use type guards for narrowing
|
||||
- Use assertion functions when validation is needed
|
||||
- Let TypeScript protect you from null/undefined errors
|
||||
177
skills/diagnosing-type-errors/SKILL.md
Normal file
177
skills/diagnosing-type-errors/SKILL.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: diagnosing-type-errors
|
||||
description: Analyze TypeScript errors and provide detailed diagnostics with root cause analysis and specific fix recommendations
|
||||
---
|
||||
|
||||
<role>
|
||||
You are a TypeScript Expert specializing in type system diagnostics. You excel at identifying type issues, classifying errors, tracing root causes, and providing actionable recommendations.
|
||||
</role>
|
||||
|
||||
<context>
|
||||
The user has specified a target file to analyze.
|
||||
|
||||
Project configuration:
|
||||
@tsconfig.json
|
||||
@package.json
|
||||
</context>
|
||||
|
||||
<task>
|
||||
Analyze TypeScript errors and provide comprehensive diagnostics:
|
||||
|
||||
## 1. Run Type Check
|
||||
|
||||
Execute type checking and collect all errors for the target file:
|
||||
|
||||
```bash
|
||||
pnpm type-check 2>&1 | grep "target-file"
|
||||
```
|
||||
|
||||
Replace `target-file` with the actual file path from the user's request.
|
||||
|
||||
If no errors found, report success and exit.
|
||||
|
||||
## 2. Classify Errors
|
||||
|
||||
Group errors by category:
|
||||
- Type mismatches (assignability errors)
|
||||
- Missing properties (required fields)
|
||||
- Null/undefined safety violations
|
||||
- Generic constraint violations
|
||||
- Function signature mismatches
|
||||
- Import/export type errors
|
||||
- Configuration issues (module resolution, lib types)
|
||||
|
||||
Identify error severity and impact:
|
||||
- **Critical**: Prevents compilation, blocks functionality
|
||||
- **High**: Type unsoundness, runtime risk
|
||||
- **Medium**: Poor type design, maintenance burden
|
||||
- **Low**: Minor inconsistencies, style issues
|
||||
|
||||
Detect patterns across multiple errors:
|
||||
- Common root cause affecting multiple locations
|
||||
- Cascading errors from single source
|
||||
- Repeated anti-patterns
|
||||
|
||||
## 3. Root Cause Analysis
|
||||
|
||||
For each error, trace to its source:
|
||||
|
||||
**Read the target file** specified by the user
|
||||
|
||||
**Identify error origin**:
|
||||
- Incorrect type annotations (wrong type specified)
|
||||
- Missing type definitions (implicit any)
|
||||
- Third-party library types (DefinitelyTyped issues)
|
||||
- Configuration issues (tsconfig strictness)
|
||||
- Type narrowing failures (guard logic errors)
|
||||
- Generic inference failures (constraints needed)
|
||||
|
||||
**Explain why each error occurs**:
|
||||
- What TypeScript rule is being violated
|
||||
- Why the types are incompatible
|
||||
- What the type system expected vs. received
|
||||
- How the error propagates through code
|
||||
|
||||
## 4. Provide Recommendations
|
||||
|
||||
For each error, suggest specific fixes:
|
||||
|
||||
**Immediate fixes**:
|
||||
- Exact code changes needed (line numbers, syntax)
|
||||
- Type annotations to add/modify
|
||||
- Type guards to implement
|
||||
- Assertions to safely apply (when justified)
|
||||
|
||||
**Type refactoring opportunities**:
|
||||
- Overly broad types that need narrowing
|
||||
- Union types that need discrimination
|
||||
- Generic types that need constraints
|
||||
- Interfaces that need extension
|
||||
|
||||
**Type extraction candidates**:
|
||||
- Inline types used multiple times
|
||||
- Complex type expressions needing names
|
||||
- Shared type patterns across files
|
||||
- Utility types that could simplify code
|
||||
|
||||
**Type safety improvements**:
|
||||
- Replace `any` with `unknown` + guards
|
||||
- Add strict null checks where missing
|
||||
- Strengthen generic constraints
|
||||
- Use branded types for validation
|
||||
|
||||
</task>
|
||||
|
||||
<constraints>
|
||||
**Analysis Requirements:**
|
||||
- NEVER suggest using `any` type
|
||||
- NEVER recommend suppressing errors with `@ts-ignore`
|
||||
- ALWAYS verify type structures from source definitions
|
||||
- ALWAYS explain the "why" behind each error
|
||||
- MUST trace errors to root cause, not symptoms
|
||||
- MUST provide line numbers and exact syntax
|
||||
|
||||
**Communication Requirements:**
|
||||
- Use clear, educational explanations
|
||||
- Reference TypeScript documentation for complex issues
|
||||
- Provide actionable, specific recommendations
|
||||
- Prioritize fixes by severity and impact
|
||||
- Group related errors together
|
||||
|
||||
**Code Quality Requirements:**
|
||||
- MUST maintain existing code structure
|
||||
- MUST follow project naming conventions
|
||||
- NEVER introduce breaking changes
|
||||
- Consider backward compatibility
|
||||
</constraints>
|
||||
|
||||
<output>
|
||||
Provide clear diagnostic report:
|
||||
|
||||
## Error Summary
|
||||
|
||||
- **Total errors**: {count}
|
||||
- **Categories**: {list with counts}
|
||||
- **Severity**: {critical/high/medium/low breakdown}
|
||||
|
||||
## Error Analysis
|
||||
|
||||
For each error:
|
||||
|
||||
### Error {n}: {category} - {severity}
|
||||
|
||||
**Location**: `{file}:{line}:{column}`
|
||||
|
||||
**Error message**:
|
||||
```
|
||||
{TypeScript error message}
|
||||
```
|
||||
|
||||
**Root cause**:
|
||||
{Explanation of why this error occurs}
|
||||
|
||||
**Affected code**:
|
||||
```typescript
|
||||
{Code snippet showing the error}
|
||||
```
|
||||
|
||||
**Recommended fix**:
|
||||
```typescript
|
||||
{Exact code change needed}
|
||||
```
|
||||
|
||||
**Explanation**:
|
||||
{Why this fix resolves the error}
|
||||
|
||||
## Refactoring Opportunities
|
||||
|
||||
- {List of type improvements that could be made}
|
||||
- {Extraction candidates for reusable types}
|
||||
- {Type safety enhancements beyond error fixes}
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. {Prioritized action items}
|
||||
2. {Suggested order of fixes}
|
||||
3. {Long-term type improvements to consider}
|
||||
</output>
|
||||
294
skills/hashing-passwords/SKILL.md
Normal file
294
skills/hashing-passwords/SKILL.md
Normal file
@@ -0,0 +1,294 @@
|
||||
---
|
||||
name: hashing-passwords
|
||||
description: CRITICAL security skill teaching proper credential and password handling. NEVER store passwords, use bcrypt/argon2, NEVER accept third-party credentials. Use when handling authentication, passwords, API keys, or any sensitive credentials.
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
<role>
|
||||
This skill prevents CRITICAL security failures found in stress testing where 33% of agents had severe security vulnerabilities including Base64 "encryption" for passwords and accepting PayPal passwords directly.
|
||||
|
||||
**THIS IS A ZERO-TOLERANCE SECURITY SKILL. NO EXCEPTIONS.**
|
||||
</role>
|
||||
|
||||
<when-to-activate>
|
||||
This skill activates when:
|
||||
|
||||
- Working with passwords or authentication
|
||||
- Handling API keys, tokens, or credentials
|
||||
- Storing sensitive user data
|
||||
- Implementing login, signup, or password reset
|
||||
- User mentions passwords, credentials, encryption, hashing, or authentication
|
||||
- Code contains password storage, credential handling, or third-party auth
|
||||
</when-to-activate>
|
||||
|
||||
<overview>
|
||||
## Critical Security Rules
|
||||
|
||||
**RULE 1: NEVER STORE PASSWORDS**
|
||||
|
||||
Store password HASHES only, using bcrypt or argon2. Passwords must be:
|
||||
- Hashed (NOT encrypted, NOT Base64, NOT plaintext)
|
||||
- Salted automatically by bcrypt/argon2
|
||||
- Using modern algorithms (bcrypt cost 12+, argon2id)
|
||||
|
||||
**RULE 2: NEVER ACCEPT THIRD-PARTY CREDENTIALS**
|
||||
|
||||
NEVER ask users for passwords to other services (PayPal, Google, etc.):
|
||||
- Use OAuth instead
|
||||
- Use API keys from the service
|
||||
- Use service-provided SDKs
|
||||
|
||||
**RULE 3: NEVER USE ENCODING AS ENCRYPTION**
|
||||
|
||||
- Base64 is NOT encryption (trivially reversible)
|
||||
- URL encoding is NOT security
|
||||
- Hex encoding is NOT security
|
||||
|
||||
**RULE 4: USE PROPER CRYPTOGRAPHY**
|
||||
|
||||
- For passwords: bcrypt or argon2
|
||||
- For encryption: Use established libraries (crypto module, Web Crypto API)
|
||||
- For API keys: Store in environment variables, use secret management
|
||||
</overview>
|
||||
|
||||
<critical-anti-patterns>
|
||||
## NEVER DO THIS
|
||||
|
||||
**❌ Base64 "Encryption"**: `Buffer.from(password).toString("base64")` is encoding, NOT encryption. Trivially reversible.
|
||||
|
||||
**❌ Third-Party Passwords**: Never accept PayPal/Google/etc passwords. Use OAuth.
|
||||
|
||||
**❌ Plaintext Storage**: Never store raw passwords. Always hash.
|
||||
|
||||
**❌ Weak Hashing**: MD5/SHA-1/SHA-256 too fast. Use bcrypt/argon2.
|
||||
|
||||
See `references/never-do-this.md` for detailed examples and failures.
|
||||
</critical-anti-patterns>
|
||||
|
||||
<correct-patterns>
|
||||
## ✅ CORRECT: bcrypt Password Hashing
|
||||
|
||||
```typescript
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
const SALT_ROUNDS = 12;
|
||||
|
||||
async function hashPassword(password: string): Promise<string> {
|
||||
return await bcrypt.hash(password, SALT_ROUNDS);
|
||||
}
|
||||
|
||||
async function verifyPassword(
|
||||
password: string,
|
||||
hash: string
|
||||
): Promise<boolean> {
|
||||
return await bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
passwordHash: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- bcrypt designed for passwords
|
||||
- Automatic salting
|
||||
- Cost factor 12+ (prevents brute force)
|
||||
- Never stores actual password
|
||||
|
||||
## ✅ CORRECT: argon2 (More Modern)
|
||||
|
||||
```typescript
|
||||
import argon2 from "argon2";
|
||||
|
||||
async function hashPassword(password: string): Promise<string> {
|
||||
return await argon2.hash(password, {
|
||||
type: argon2.argon2id,
|
||||
memoryCost: 2 ** 16,
|
||||
timeCost: 3,
|
||||
parallelism: 1
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Advantages**: Memory-hard, resists GPU attacks, latest standard.
|
||||
|
||||
## ✅ CORRECT: OAuth for Third-Party Services
|
||||
|
||||
```typescript
|
||||
import { google } from "googleapis";
|
||||
|
||||
const oauth2Client = new google.auth.OAuth2(
|
||||
process.env.GOOGLE_CLIENT_ID,
|
||||
process.env.GOOGLE_CLIENT_SECRET,
|
||||
"http://localhost:3000/auth/callback"
|
||||
);
|
||||
|
||||
function getAuthUrl(): string {
|
||||
return oauth2Client.generateAuthUrl({
|
||||
access_type: "offline",
|
||||
scope: ["https://www.googleapis.com/auth/userinfo.email"]
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**: Token-based, never sees user password, revocable.
|
||||
|
||||
## ✅ CORRECT: Environment Variables for API Keys
|
||||
|
||||
```typescript
|
||||
function loadConfig(): Config {
|
||||
const apiKey = process.env.STRIPE_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("Missing required API key");
|
||||
}
|
||||
return { apiKey };
|
||||
}
|
||||
```
|
||||
|
||||
See `references/correct-implementations.md` for complete examples.
|
||||
</correct-patterns>
|
||||
|
||||
<constraints>
|
||||
**MUST:**
|
||||
|
||||
- Use bcrypt (cost 12+) or argon2id for password hashing
|
||||
- Store password HASHES only, never passwords
|
||||
- Use OAuth for third-party service authentication
|
||||
- Store API keys in environment variables
|
||||
- Validate password strength (min length, complexity)
|
||||
- Use HTTPS for all authentication endpoints
|
||||
|
||||
**NEVER:**
|
||||
|
||||
- Store passwords in any form (plaintext, Base64, encrypted)
|
||||
- Use MD5, SHA-1, or SHA-256 for passwords
|
||||
- Accept third-party credentials (PayPal, Google, etc.)
|
||||
- Hardcode API keys or secrets
|
||||
- Use encoding (Base64, hex) as "encryption"
|
||||
- Email passwords to users
|
||||
- Log passwords (even in dev mode)
|
||||
|
||||
**SHOULD:**
|
||||
|
||||
- Implement password strength requirements
|
||||
- Rate-limit login attempts
|
||||
- Use two-factor authentication (2FA)
|
||||
- Implement account lockout after failed attempts
|
||||
- Rotate API keys periodically
|
||||
- Use secret management services (AWS Secrets Manager, HashiCorp Vault)
|
||||
</constraints>
|
||||
|
||||
<password-requirements>
|
||||
## Minimum Requirements
|
||||
|
||||
- **Length**: 12+ characters (prefer 16+)
|
||||
- **Complexity**: Uppercase, lowercase, numbers, special chars
|
||||
- **Validation**: Reject common passwords ("password", "12345678")
|
||||
|
||||
See `references/password-validation.md` for complete implementation.
|
||||
</password-requirements>
|
||||
|
||||
<installation>
|
||||
## Installing Password Libraries
|
||||
|
||||
**bcrypt**:
|
||||
```bash
|
||||
npm install bcrypt
|
||||
npm install -D @types/bcrypt
|
||||
```
|
||||
|
||||
**argon2**:
|
||||
```bash
|
||||
npm install argon2
|
||||
npm install -D @types/argon2
|
||||
```
|
||||
|
||||
**Note**: Both require native compilation. Ensure build tools are available.
|
||||
</installation>
|
||||
|
||||
<progressive-disclosure>
|
||||
## Reference Files
|
||||
|
||||
**Detailed Examples**:
|
||||
- `references/never-do-this.md` - Security failures and anti-patterns
|
||||
- `references/correct-implementations.md` - Complete working examples
|
||||
- `references/password-validation.md` - Password strength validation
|
||||
- `references/emergency-response.md` - Breach response and migration
|
||||
|
||||
**Related Skills**:
|
||||
- **Input Validation**: Use the sanitizing-user-inputs skill
|
||||
- **Dependencies**: Use the auditing-dependencies skill
|
||||
- **External Data**: Use the validating-external-data skill
|
||||
</progressive-disclosure>
|
||||
|
||||
<validation>
|
||||
## Security Implementation Checklist
|
||||
|
||||
1. **Password Storage**:
|
||||
- [ ] Uses bcrypt (cost 12+) or argon2id
|
||||
- [ ] NEVER stores actual passwords
|
||||
- [ ] Password hashes stored in separate column
|
||||
- [ ] No way to retrieve original password
|
||||
|
||||
2. **Third-Party Auth**:
|
||||
- [ ] Uses OAuth/OpenID Connect
|
||||
- [ ] NEVER asks for third-party passwords
|
||||
- [ ] Tokens stored securely
|
||||
- [ ] Follows service Terms of Service
|
||||
|
||||
3. **API Keys**:
|
||||
- [ ] Stored in environment variables
|
||||
- [ ] Not hardcoded in source
|
||||
- [ ] Not committed to git
|
||||
- [ ] `.env` file in `.gitignore`
|
||||
|
||||
4. **Password Requirements**:
|
||||
- [ ] Minimum 12 characters (prefer 16+)
|
||||
- [ ] Complexity requirements enforced
|
||||
- [ ] Common passwords rejected
|
||||
- [ ] Strength meter for users
|
||||
|
||||
5. **Additional Security**:
|
||||
- [ ] HTTPS required for auth endpoints
|
||||
- [ ] Rate limiting on login
|
||||
- [ ] Account lockout after failures
|
||||
- [ ] Password reset via email only
|
||||
- [ ] No password hints or security questions
|
||||
</validation>
|
||||
|
||||
<common-mistakes>
|
||||
## Why Developers Make These Mistakes
|
||||
|
||||
**"I need to retrieve the password later"**
|
||||
→ You never need to retrieve passwords. Use password reset instead.
|
||||
|
||||
**"Base64 is encryption"**
|
||||
→ Base64 is encoding for transport, not security.
|
||||
|
||||
**"I'll encrypt passwords"**
|
||||
→ If you can decrypt, so can attackers. Hash, don't encrypt.
|
||||
|
||||
**"SHA-256 is secure"**
|
||||
→ SHA-256 is too fast. Use bcrypt/argon2.
|
||||
|
||||
**"I need PayPal credentials to check balance"**
|
||||
→ Use PayPal's API with OAuth tokens.
|
||||
</common-mistakes>
|
||||
|
||||
<emergency-response>
|
||||
## If You Find Insecure Password Storage
|
||||
|
||||
**IMMEDIATE ACTIONS**:
|
||||
|
||||
1. **Stop the application** (if running)
|
||||
2. **Do NOT commit the code**
|
||||
3. **Implement proper hashing** (bcrypt/argon2)
|
||||
4. **Force password reset for all users**
|
||||
5. **Notify security team**
|
||||
6. **Assess breach scope**
|
||||
|
||||
See `references/emergency-response.md` for complete migration guide.
|
||||
</emergency-response>
|
||||
195
skills/hashing-passwords/references/correct-implementations.md
Normal file
195
skills/hashing-passwords/references/correct-implementations.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Correct Security Implementations
|
||||
|
||||
## Password Hashing with bcrypt
|
||||
|
||||
```typescript
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
const SALT_ROUNDS = 12;
|
||||
|
||||
async function hashPassword(password: string): Promise<string> {
|
||||
return await bcrypt.hash(password, SALT_ROUNDS);
|
||||
}
|
||||
|
||||
async function verifyPassword(
|
||||
password: string,
|
||||
hash: string
|
||||
): Promise<boolean> {
|
||||
return await bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
passwordHash: string;
|
||||
}
|
||||
|
||||
async function createUser(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<User> {
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
return {
|
||||
id: generateId(),
|
||||
email,
|
||||
passwordHash
|
||||
};
|
||||
}
|
||||
|
||||
async function loginUser(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<User | null> {
|
||||
const user = await database.findByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValid = await verifyPassword(password, user.passwordHash);
|
||||
|
||||
return isValid ? user : null;
|
||||
}
|
||||
```
|
||||
|
||||
**Why this is correct**:
|
||||
- Uses bcrypt (designed for passwords)
|
||||
- Automatic salting
|
||||
- Slow (intentional, prevents brute force)
|
||||
- Cost factor 12 (good balance)
|
||||
- Never stores actual password
|
||||
- Async to avoid blocking
|
||||
|
||||
## argon2 Implementation
|
||||
|
||||
```typescript
|
||||
import argon2 from "argon2";
|
||||
|
||||
async function hashPassword(password: string): Promise<string> {
|
||||
return await argon2.hash(password, {
|
||||
type: argon2.argon2id,
|
||||
memoryCost: 2 ** 16,
|
||||
timeCost: 3,
|
||||
parallelism: 1
|
||||
});
|
||||
}
|
||||
|
||||
async function verifyPassword(
|
||||
password: string,
|
||||
hash: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
return await argon2.verify(hash, password);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this is correct**:
|
||||
- argon2id (latest standard, winner of Password Hashing Competition)
|
||||
- Memory-hard (resists GPU attacks)
|
||||
- Configurable parameters
|
||||
- Better than bcrypt for new projects
|
||||
|
||||
## OAuth for Third-Party Services
|
||||
|
||||
```typescript
|
||||
import { google } from "googleapis";
|
||||
|
||||
const oauth2Client = new google.auth.OAuth2(
|
||||
process.env.GOOGLE_CLIENT_ID,
|
||||
process.env.GOOGLE_CLIENT_SECRET,
|
||||
"http://localhost:3000/auth/callback"
|
||||
);
|
||||
|
||||
function getAuthUrl(): string {
|
||||
return oauth2Client.generateAuthUrl({
|
||||
access_type: "offline",
|
||||
scope: ["https://www.googleapis.com/auth/userinfo.email"]
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCallback(code: string) {
|
||||
const { tokens } = await oauth2Client.getToken(code);
|
||||
oauth2Client.setCredentials(tokens);
|
||||
|
||||
return tokens;
|
||||
}
|
||||
```
|
||||
|
||||
**Why this is correct**:
|
||||
- Uses OAuth (industry standard)
|
||||
- Never sees user's Google password
|
||||
- Token-based authentication
|
||||
- Revocable access
|
||||
- Follows Terms of Service
|
||||
|
||||
## API Key Storage
|
||||
|
||||
```typescript
|
||||
interface Config {
|
||||
stripeApiKey: string;
|
||||
sendgridApiKey: string;
|
||||
}
|
||||
|
||||
function loadConfig(): Config {
|
||||
const stripeApiKey = process.env.STRIPE_API_KEY;
|
||||
const sendgridApiKey = process.env.SENDGRID_API_KEY;
|
||||
|
||||
if (!stripeApiKey || !sendgridApiKey) {
|
||||
throw new Error("Missing required API keys");
|
||||
}
|
||||
|
||||
return {
|
||||
stripeApiKey,
|
||||
sendgridApiKey
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Why this is correct**:
|
||||
- Reads from environment variables
|
||||
- Never hardcoded
|
||||
- Not committed to git
|
||||
- Validated at startup
|
||||
|
||||
## Session Tokens
|
||||
|
||||
```typescript
|
||||
import crypto from "crypto";
|
||||
|
||||
function generateSessionToken(): string {
|
||||
return crypto.randomBytes(32).toString("hex");
|
||||
}
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
userId: string;
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
async function createSession(userId: string): Promise<Session> {
|
||||
const token = generateSessionToken();
|
||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
|
||||
const session = {
|
||||
id: generateId(),
|
||||
userId,
|
||||
token,
|
||||
expiresAt
|
||||
};
|
||||
|
||||
await database.sessions.insert(session);
|
||||
|
||||
return session;
|
||||
}
|
||||
```
|
||||
|
||||
**Why this is correct**:
|
||||
- Cryptographically random tokens
|
||||
- Time-based expiration
|
||||
- Separate from password
|
||||
- Can be revoked
|
||||
52
skills/hashing-passwords/references/emergency-response.md
Normal file
52
skills/hashing-passwords/references/emergency-response.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Emergency Response for Security Breaches
|
||||
|
||||
## If You Find Insecure Password Storage
|
||||
|
||||
**IMMEDIATE ACTIONS**:
|
||||
|
||||
1. **Stop the application** (if running)
|
||||
2. **Do NOT commit the code**
|
||||
3. **Implement proper hashing** (bcrypt/argon2)
|
||||
4. **Force password reset for all users**
|
||||
5. **Notify security team**
|
||||
6. **Assess breach scope**
|
||||
7. **Notify users if breached**
|
||||
|
||||
## Migration Path from Insecure Storage
|
||||
|
||||
```typescript
|
||||
async function migratePasswords() {
|
||||
const users = await database.users.find({ passwordMigrated: false });
|
||||
|
||||
for (const user of users) {
|
||||
|
||||
if (user.plaintextPassword) {
|
||||
user.passwordHash = await hashPassword(user.plaintextPassword);
|
||||
delete user.plaintextPassword;
|
||||
user.passwordMigrated = true;
|
||||
await database.users.update(user);
|
||||
} else {
|
||||
|
||||
user.requirePasswordReset = true;
|
||||
user.passwordMigrated = true;
|
||||
await database.users.update(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Legal and Compliance Considerations
|
||||
|
||||
- Document the incident
|
||||
- Follow breach notification laws (GDPR, CCPA, etc.)
|
||||
- Preserve evidence for investigation
|
||||
- Consider engaging legal counsel
|
||||
- Prepare public communications if needed
|
||||
|
||||
## Prevention for Future
|
||||
|
||||
- Implement code review processes
|
||||
- Use automated security scanning
|
||||
- Train developers on security best practices
|
||||
- Establish security champions in teams
|
||||
- Regular security audits
|
||||
87
skills/hashing-passwords/references/never-do-this.md
Normal file
87
skills/hashing-passwords/references/never-do-this.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Critical Security Anti-Patterns
|
||||
|
||||
This document contains detailed examples of security failures found in stress testing where 33% of agents had severe security vulnerabilities.
|
||||
|
||||
## ❌ Base64 "Encryption" for Passwords
|
||||
|
||||
```typescript
|
||||
function storePassword(password: string): string {
|
||||
return Buffer.from(password).toString("base64");
|
||||
}
|
||||
|
||||
function retrievePassword(encoded: string): string {
|
||||
return Buffer.from(encoded, "base64").toString();
|
||||
}
|
||||
```
|
||||
|
||||
**CRITICAL FAILURE**:
|
||||
- Base64 is ENCODING, not encryption
|
||||
- Trivially reversible: `atob("cGFzc3dvcmQxMjM=")` → `"password123"`
|
||||
- Provides ZERO security
|
||||
- Violates PCI-DSS, GDPR, SOC2, every security standard
|
||||
- Leads to data breaches and lawsuits
|
||||
|
||||
## ❌ Accepting Third-Party Passwords
|
||||
|
||||
```typescript
|
||||
interface UserCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
paypalEmail: string;
|
||||
paypalPassword: string;
|
||||
}
|
||||
|
||||
function saveCredentials(creds: UserCredentials) {
|
||||
database.insert({
|
||||
...creds,
|
||||
paypalPassword: encrypt(creds.paypalPassword)
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**CRITICAL FAILURE**:
|
||||
- Violates PayPal Terms of Service
|
||||
- Violates PCI compliance
|
||||
- Exposes user to account takeover
|
||||
- Creates liability for your company
|
||||
- Even encrypted storage is wrong
|
||||
|
||||
## ❌ Plaintext Password Storage
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
function createUser(email: string, password: string): User {
|
||||
return {
|
||||
id: generateId(),
|
||||
email,
|
||||
password
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**CRITICAL FAILURE**:
|
||||
- Database breach exposes all passwords
|
||||
- Password reuse attacks
|
||||
- Criminal liability
|
||||
- Regulatory fines
|
||||
|
||||
## ❌ Weak Hashing (MD5, SHA-1)
|
||||
|
||||
```typescript
|
||||
import crypto from "crypto";
|
||||
|
||||
function hashPassword(password: string): string {
|
||||
return crypto.createHash("md5").update(password).digest("hex");
|
||||
}
|
||||
```
|
||||
|
||||
**CRITICAL FAILURE**:
|
||||
- MD5/SHA-1 designed for speed (bad for passwords)
|
||||
- Rainbow table attacks
|
||||
- GPU cracking (billions of hashes/second)
|
||||
- No salt (identical passwords = identical hashes)
|
||||
66
skills/hashing-passwords/references/password-validation.md
Normal file
66
skills/hashing-passwords/references/password-validation.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Password Strength Validation
|
||||
|
||||
## Complete Implementation
|
||||
|
||||
```typescript
|
||||
interface PasswordRequirements {
|
||||
minLength: number;
|
||||
requireUppercase: boolean;
|
||||
requireLowercase: boolean;
|
||||
requireNumbers: boolean;
|
||||
requireSpecialChars: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_REQUIREMENTS: PasswordRequirements = {
|
||||
minLength: 12,
|
||||
requireUppercase: true,
|
||||
requireLowercase: true,
|
||||
requireNumbers: true,
|
||||
requireSpecialChars: true
|
||||
};
|
||||
|
||||
function validatePasswordStrength(
|
||||
password: string,
|
||||
requirements: PasswordRequirements = DEFAULT_REQUIREMENTS
|
||||
): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (password.length < requirements.minLength) {
|
||||
errors.push(`Password must be at least ${requirements.minLength} characters`);
|
||||
}
|
||||
|
||||
if (requirements.requireUppercase && !/[A-Z]/.test(password)) {
|
||||
errors.push("Password must contain at least one uppercase letter");
|
||||
}
|
||||
|
||||
if (requirements.requireLowercase && !/[a-z]/.test(password)) {
|
||||
errors.push("Password must contain at least one lowercase letter");
|
||||
}
|
||||
|
||||
if (requirements.requireNumbers && !/[0-9]/.test(password)) {
|
||||
errors.push("Password must contain at least one number");
|
||||
}
|
||||
|
||||
if (requirements.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
||||
errors.push("Password must contain at least one special character");
|
||||
}
|
||||
|
||||
const commonPasswords = ["password", "12345678", "qwerty", "admin"];
|
||||
if (commonPasswords.some(common => password.toLowerCase().includes(common))) {
|
||||
errors.push("Password contains common patterns");
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Minimum 12 characters (prefer 16+)
|
||||
- Mix of uppercase, lowercase, numbers, special characters
|
||||
- Reject common passwords and patterns
|
||||
- Provide user-friendly feedback
|
||||
- Consider using zxcvbn for entropy estimation
|
||||
391
skills/refactoring-inline-types/SKILL.md
Normal file
391
skills/refactoring-inline-types/SKILL.md
Normal file
@@ -0,0 +1,391 @@
|
||||
---
|
||||
name: refactoring-inline-types
|
||||
description: Refactor inline types into reusable, well-organized type definitions using interfaces, type aliases, and generics
|
||||
---
|
||||
|
||||
<role>
|
||||
You are a TypeScript Expert specializing in type refactoring and architecture. You excel at identifying inline types that need extraction, designing type hierarchies, and creating reusable type definitions.
|
||||
</role>
|
||||
|
||||
<context>
|
||||
The user has specified a target file for type extraction.
|
||||
|
||||
Project configuration:
|
||||
@tsconfig.json
|
||||
@package.json
|
||||
</context>
|
||||
|
||||
<task>
|
||||
Refactor inline types into reusable, well-organized type definitions:
|
||||
|
||||
## 1. Analyze Current Types
|
||||
|
||||
Read the target file specified by the user.
|
||||
|
||||
**Identify all inline type definitions**:
|
||||
- Object type literals in function parameters
|
||||
- Union types without names
|
||||
- Intersection types without names
|
||||
- Complex type expressions
|
||||
- Mapped types without names
|
||||
- Conditional types without names
|
||||
|
||||
**Find duplicated type patterns**:
|
||||
- Same object shapes in multiple places
|
||||
- Repeated union combinations
|
||||
- Similar generic patterns
|
||||
- Common property subsets
|
||||
|
||||
**Locate complex inline types**:
|
||||
- Nested object types (3+ levels deep)
|
||||
- Large union types (4+ members)
|
||||
- Complex mapped types
|
||||
- Utility type compositions
|
||||
|
||||
**Detect types that could be reused**:
|
||||
- Function signatures used multiple times
|
||||
- Data structures shared across functions
|
||||
- API response/request shapes
|
||||
- Event handler types
|
||||
|
||||
## 2. Design Type Structure
|
||||
|
||||
Determine appropriate type organization:
|
||||
|
||||
**Interfaces for object shapes** (extensible):
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
interface AdminUser extends User {
|
||||
permissions: string[]
|
||||
}
|
||||
```
|
||||
|
||||
**Type aliases for unions/primitives/functions**:
|
||||
```typescript
|
||||
type Status = "pending" | "active" | "inactive"
|
||||
type ID = string | number
|
||||
type Handler = (event: Event) => void
|
||||
```
|
||||
|
||||
**Generics for reusable patterns**:
|
||||
```typescript
|
||||
type Result<T, E = Error> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: E }
|
||||
|
||||
type Nullable<T> = T | null
|
||||
type Optional<T> = T | undefined
|
||||
```
|
||||
|
||||
**Utility types for transformations**:
|
||||
```typescript
|
||||
type PartialUser = Partial<User>
|
||||
type UserCredentials = Pick<User, "email" | "password">
|
||||
type PublicUser = Omit<User, "password">
|
||||
type ReadonlyUser = Readonly<User>
|
||||
```
|
||||
|
||||
**Plan type hierarchy**:
|
||||
- Base types first
|
||||
- Derived types after
|
||||
- Utility types at end
|
||||
- Group related types together
|
||||
|
||||
## 3. Extract and Refactor
|
||||
|
||||
**Create types module if needed**:
|
||||
- `types.ts` for single domain
|
||||
- `types/index.ts` for multiple domains
|
||||
- `types/{domain}.ts` for large projects
|
||||
|
||||
**Extract inline types to named definitions**:
|
||||
|
||||
Before:
|
||||
```typescript
|
||||
function createUser(data: {
|
||||
name: string
|
||||
email: string
|
||||
age: number
|
||||
}): { id: string; name: string; email: string; age: number } {
|
||||
return { id: generateId(), ...data }
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```typescript
|
||||
interface UserInput {
|
||||
name: string
|
||||
email: string
|
||||
age: number
|
||||
}
|
||||
|
||||
interface User extends UserInput {
|
||||
id: string
|
||||
}
|
||||
|
||||
function createUser(data: UserInput): User {
|
||||
return { id: generateId(), ...data }
|
||||
}
|
||||
```
|
||||
|
||||
**Give descriptive names**:
|
||||
- Follow project conventions
|
||||
- Use domain language
|
||||
- Be specific and clear
|
||||
- Avoid generic names (Data, Info, Params)
|
||||
|
||||
**Add JSDoc for complex types**:
|
||||
```typescript
|
||||
/**
|
||||
* Represents the result of an async operation.
|
||||
* Success case includes data, failure case includes error.
|
||||
*/
|
||||
type AsyncResult<T, E = Error> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: E }
|
||||
```
|
||||
|
||||
**Use generic constraints**:
|
||||
```typescript
|
||||
interface Repository<T extends { id: string }> {
|
||||
findById(id: string): Promise<T | null>
|
||||
save(item: T): Promise<T>
|
||||
}
|
||||
```
|
||||
|
||||
**Organize types logically**:
|
||||
|
||||
```typescript
|
||||
// Base types
|
||||
export interface User {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
// Derived types
|
||||
export interface AdminUser extends User {
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
// Input/output types
|
||||
export type CreateUserInput = Omit<User, "id">
|
||||
export type UserResponse = Readonly<User>
|
||||
|
||||
// Utility types
|
||||
export type PartialUser = Partial<User>
|
||||
```
|
||||
|
||||
**Export public types**:
|
||||
```typescript
|
||||
export interface User { }
|
||||
export type Status = "active" | "inactive"
|
||||
```
|
||||
|
||||
**Keep internal types private**:
|
||||
```typescript
|
||||
interface InternalCache { }
|
||||
type ValidationState = "valid" | "invalid"
|
||||
```
|
||||
|
||||
**Update original file**:
|
||||
```typescript
|
||||
import { User, CreateUserInput, UserResponse } from "./types"
|
||||
```
|
||||
|
||||
## 4. Optimization
|
||||
|
||||
**Replace duplicate types**:
|
||||
- Single source of truth
|
||||
- Import instead of redefine
|
||||
- Use extends for variations
|
||||
|
||||
**Simplify complex inline types**:
|
||||
```typescript
|
||||
// Before
|
||||
function process(
|
||||
data: { id: string } & ({ type: "user"; name: string } | { type: "admin"; permissions: string[] })
|
||||
): void { }
|
||||
|
||||
// After
|
||||
interface BaseData {
|
||||
id: string
|
||||
}
|
||||
|
||||
type UserData = BaseData & {
|
||||
type: "user"
|
||||
name: string
|
||||
}
|
||||
|
||||
type AdminData = BaseData & {
|
||||
type: "admin"
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
type ProcessData = UserData | AdminData
|
||||
|
||||
function process(data: ProcessData): void { }
|
||||
```
|
||||
|
||||
**Add type aliases for readability**:
|
||||
```typescript
|
||||
type UserID = string
|
||||
type Timestamp = number
|
||||
type EmailAddress = string
|
||||
```
|
||||
|
||||
**Use discriminated unions**:
|
||||
```typescript
|
||||
type Shape =
|
||||
| { kind: "circle"; radius: number }
|
||||
| { kind: "square"; size: number }
|
||||
| { kind: "rectangle"; width: number; height: number }
|
||||
|
||||
function area(shape: Shape): number {
|
||||
switch (shape.kind) {
|
||||
case "circle":
|
||||
return Math.PI * shape.radius ** 2
|
||||
case "square":
|
||||
return shape.size ** 2
|
||||
case "rectangle":
|
||||
return shape.width * shape.height
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Apply utility types**:
|
||||
```typescript
|
||||
type UserInput = Omit<User, "id" | "createdAt">
|
||||
type PartialUpdate = Partial<UserInput>
|
||||
type ReadonlyConfig = Readonly<Config>
|
||||
type RequiredFields = Required<OptionalConfig>
|
||||
```
|
||||
|
||||
## 5. Validation
|
||||
|
||||
Run type check on the target file:
|
||||
|
||||
```bash
|
||||
pnpm type-check 2>&1 | grep "target-file"
|
||||
```
|
||||
|
||||
Replace `target-file` with the actual file path.
|
||||
|
||||
**Verify**:
|
||||
- No type errors introduced
|
||||
- Type definitions properly exported/imported
|
||||
- Refactored code maintains same type safety
|
||||
- All references updated correctly
|
||||
|
||||
**If errors found**:
|
||||
- Fix import/export issues
|
||||
- Correct type definitions
|
||||
- Update all references
|
||||
- Re-run validation
|
||||
|
||||
</task>
|
||||
|
||||
<constraints>
|
||||
**Type Safety Requirements:**
|
||||
- NEVER use `any` type
|
||||
- ALWAYS preserve type safety during refactoring
|
||||
- MUST verify all type references updated
|
||||
- MUST maintain backward compatibility
|
||||
|
||||
**Code Quality Requirements:**
|
||||
- MUST use descriptive type names
|
||||
- MUST add JSDoc for complex types
|
||||
- MUST follow project naming conventions
|
||||
- NEVER introduce breaking changes
|
||||
|
||||
**Extraction Requirements:**
|
||||
- Only extract types that provide value (reusability/clarity)
|
||||
- Consider discoverability of extracted types
|
||||
- Maintain logical organization
|
||||
- Group related types together
|
||||
|
||||
**Type Design Principles:**
|
||||
- Interfaces for object shapes (extensible)
|
||||
- Type aliases for unions/primitives/functions
|
||||
- Generics for reusable patterns
|
||||
- Utility types to reduce boilerplate
|
||||
</constraints>
|
||||
|
||||
<validation>
|
||||
**MANDATORY Validation**:
|
||||
|
||||
```bash
|
||||
pnpm type-check 2>&1 | grep "target-file"
|
||||
```
|
||||
|
||||
Replace `target-file` with the actual file path. Must show zero errors.
|
||||
|
||||
**File Integrity**:
|
||||
- Verify syntactically valid TypeScript
|
||||
- Ensure imports/exports correct
|
||||
- Confirm type definitions accessible
|
||||
- Verify no runtime behavior changes
|
||||
|
||||
**Failure Handling**:
|
||||
- Fix type errors immediately
|
||||
- Update incorrect imports
|
||||
- Correct type definitions
|
||||
- Re-run until clean
|
||||
</validation>
|
||||
|
||||
<output>
|
||||
Provide clear summary of extraction:
|
||||
|
||||
## Extraction Summary
|
||||
|
||||
- **Types extracted**: {count}
|
||||
- **New types file**: {path}
|
||||
- **Types optimized**: {count}
|
||||
- **Duplicates removed**: {count}
|
||||
|
||||
## Types Extracted
|
||||
|
||||
For each extracted type:
|
||||
|
||||
### Type {n}: {name}
|
||||
|
||||
**Location**: `{types-file}:{line}`
|
||||
|
||||
**Definition**:
|
||||
```typescript
|
||||
{Type definition}
|
||||
```
|
||||
|
||||
**Usage**: {Where and how it's used}
|
||||
|
||||
**Rationale**: {Why extraction provides value}
|
||||
|
||||
## Refactoring Improvements
|
||||
|
||||
- {Duplicates eliminated}
|
||||
- {Complex types simplified}
|
||||
- {Utility types applied}
|
||||
- {Discriminated unions added}
|
||||
|
||||
## Import Statements
|
||||
|
||||
Add to original file:
|
||||
```typescript
|
||||
import { Type1, Type2, Type3 } from "./types"
|
||||
```
|
||||
|
||||
## Validation Results
|
||||
|
||||
```
|
||||
{Output showing zero errors}
|
||||
```
|
||||
|
||||
✅ All types properly extracted
|
||||
✅ No type errors introduced
|
||||
✅ Type safety maintained
|
||||
✅ Imports/exports correct
|
||||
</output>
|
||||
285
skills/resolving-type-errors/SKILL.md
Normal file
285
skills/resolving-type-errors/SKILL.md
Normal file
@@ -0,0 +1,285 @@
|
||||
---
|
||||
name: resolving-type-errors
|
||||
description: Resolve all TypeScript errors using root cause analysis, targeted fixes, and mandatory validation
|
||||
---
|
||||
|
||||
Project configuration:
|
||||
read tsconfig.json if haven't already
|
||||
read package.json if haven't already
|
||||
|
||||
## 1. Comprehensive Error Discovery
|
||||
|
||||
Run type check and collect all errors for the target file:
|
||||
|
||||
```bash
|
||||
pnpm type-check 2>&1 | grep "target-file"
|
||||
```
|
||||
|
||||
Replace `target-file` with the actual file path from the user's request.
|
||||
|
||||
List all errors with:
|
||||
|
||||
- File path and line numbers
|
||||
- Error codes (TS####)
|
||||
- Full error descriptions
|
||||
- Related information
|
||||
|
||||
Prioritize errors by dependency order:
|
||||
|
||||
- Base type definition errors first
|
||||
- Cascading errors after root causes
|
||||
- Independent errors in parallel
|
||||
|
||||
## 2. Root Cause Analysis
|
||||
|
||||
Read the target file specified by the user.
|
||||
|
||||
For each error, trace to underlying cause:
|
||||
|
||||
**Verify type structures from source**:
|
||||
|
||||
- Check imported type definitions
|
||||
- Verify function signatures
|
||||
- Confirm interface/type shapes
|
||||
- Validate generic constraints
|
||||
|
||||
**Identify root cause categories**:
|
||||
|
||||
- Type annotation errors (wrong type specified)
|
||||
- Type narrowing failures (missing guards)
|
||||
- Generic constraint violations (needs extends)
|
||||
- Null/undefined unsafety (missing checks)
|
||||
- Function signature mismatches (args/return)
|
||||
- Import/export type issues (wrong imports)
|
||||
|
||||
**Consider impact on dependent code**:
|
||||
|
||||
- Will fix break other code?
|
||||
- Are there cascading implications?
|
||||
- Does this affect public API?
|
||||
|
||||
## 3. Resolution Implementation
|
||||
|
||||
Apply targeted fixes using Edit tool.
|
||||
|
||||
### Type Safety Patterns
|
||||
|
||||
**For type narrowing**:
|
||||
|
||||
```typescript
|
||||
if (typeof value === 'string') {
|
||||
}
|
||||
if (value !== null && value !== undefined) {
|
||||
}
|
||||
if ('property' in object) {
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
}
|
||||
```
|
||||
|
||||
**For generic constraints**:
|
||||
|
||||
```typescript
|
||||
function process<T extends SomeType>(value: T): void {}
|
||||
```
|
||||
|
||||
**For union discrimination**:
|
||||
|
||||
```typescript
|
||||
type Result = { success: true; data: Data } | { success: false; error: Error };
|
||||
|
||||
if (result.success) {
|
||||
result.data;
|
||||
} else {
|
||||
result.error;
|
||||
}
|
||||
```
|
||||
|
||||
**For null safety**:
|
||||
|
||||
```typescript
|
||||
value?.property;
|
||||
value ?? defaultValue;
|
||||
const nonNull = value!;
|
||||
```
|
||||
|
||||
**For unknown types**:
|
||||
|
||||
```typescript
|
||||
function parse(input: unknown): Result {
|
||||
if (typeof input !== 'object' || input === null) {
|
||||
throw new Error('Invalid input');
|
||||
}
|
||||
|
||||
const obj = input as Record<string, unknown>;
|
||||
|
||||
if (typeof obj.property !== 'string') {
|
||||
throw new Error('Invalid property');
|
||||
}
|
||||
|
||||
return { property: obj.property };
|
||||
}
|
||||
```
|
||||
|
||||
### Principles
|
||||
|
||||
**Minimal changes**:
|
||||
|
||||
- Fix only what's broken
|
||||
- Preserve existing logic
|
||||
- Maintain code structure
|
||||
|
||||
**Address root causes**:
|
||||
|
||||
- Don't suppress symptoms
|
||||
- Fix source of error, not just error site
|
||||
- Consider why type system caught this
|
||||
|
||||
**Maintain consistency**:
|
||||
|
||||
- Follow project patterns
|
||||
- Use existing type definitions
|
||||
- Match naming conventions
|
||||
|
||||
**Prefer interfaces over types** (for objects):
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Use type aliases for unions/primitives**:
|
||||
|
||||
```typescript
|
||||
type Status = 'pending' | 'complete' | 'error';
|
||||
type ID = string;
|
||||
```
|
||||
|
||||
**Use unknown over any**:
|
||||
|
||||
```typescript
|
||||
const data: unknown = JSON.parse(input);
|
||||
```
|
||||
|
||||
## 4. Validation
|
||||
|
||||
Run type check after fixes on the target file:
|
||||
|
||||
```bash
|
||||
pnpm type-check 2>&1 | grep "target-file"
|
||||
```
|
||||
|
||||
Replace `target-file` with the actual file path.
|
||||
|
||||
**Success criteria**: MUST show zero TypeScript errors
|
||||
|
||||
**If errors remain**:
|
||||
|
||||
1. Analyze remaining errors
|
||||
2. Identify if new errors introduced
|
||||
3. Apply additional fixes
|
||||
4. Re-run validation
|
||||
5. Iterate until clean
|
||||
|
||||
**NEVER leave file in broken state**
|
||||
|
||||
</task>
|
||||
|
||||
<constraints>
|
||||
**Type Safety Requirements:**
|
||||
- NEVER use `any` type (use `unknown` + guards)
|
||||
- NEVER use `@ts-ignore` or `@ts-expect-error`
|
||||
- ALWAYS verify type structures from source
|
||||
- ALWAYS preserve type safety during fixes
|
||||
- MUST add type guards for narrowing
|
||||
- MUST use generic constraints appropriately
|
||||
|
||||
**Code Quality Requirements:**
|
||||
|
||||
- MUST maintain existing code structure
|
||||
- MUST follow project naming conventions
|
||||
- MUST apply minimal changes to resolve errors
|
||||
- NEVER introduce breaking changes
|
||||
- NEVER change runtime behavior
|
||||
|
||||
**Resolution Requirements:**
|
||||
|
||||
- Address root causes, not symptoms
|
||||
- Fix cascading errors in dependency order
|
||||
- Iterate until zero errors remain
|
||||
- Validate after every batch of fixes
|
||||
</constraints>
|
||||
|
||||
<validation>
|
||||
**MANDATORY Validation**:
|
||||
|
||||
```bash
|
||||
pnpm type-check 2>&1 | grep "target-file"
|
||||
```
|
||||
|
||||
Replace `target-file` with the actual file path. Must show zero errors or report "No errors found".
|
||||
|
||||
**File Integrity**:
|
||||
|
||||
- Verify syntactically valid TypeScript
|
||||
- Ensure imports/exports correct
|
||||
- Confirm no runtime behavior changes
|
||||
|
||||
**Failure Handling**:
|
||||
|
||||
- If validation fails, analyze remaining errors
|
||||
- Apply additional fixes
|
||||
- Re-run validation until all checks pass
|
||||
- NEVER mark complete with errors remaining
|
||||
</validation>
|
||||
|
||||
<output>
|
||||
Provide clear summary of resolution:
|
||||
|
||||
## Resolution Summary
|
||||
|
||||
- **Errors resolved**: {count}
|
||||
- **Changes made**: {summary}
|
||||
- **Files modified**: {list}
|
||||
|
||||
## Changes Applied
|
||||
|
||||
For each fix:
|
||||
|
||||
### Fix {n}: {error category}
|
||||
|
||||
**Location**: `{file}:{line}`
|
||||
|
||||
**Original error**:
|
||||
|
||||
```
|
||||
{TypeScript error message}
|
||||
```
|
||||
|
||||
**Change made**:
|
||||
|
||||
```typescript
|
||||
{Code change applied}
|
||||
```
|
||||
|
||||
**Rationale**:
|
||||
{Why this fix resolves the error}
|
||||
|
||||
## Validation Results
|
||||
|
||||
```
|
||||
{Output of type check showing zero errors}
|
||||
```
|
||||
|
||||
✅ All TypeScript errors resolved
|
||||
✅ Type safety maintained
|
||||
✅ Zero errors remaining
|
||||
|
||||
## Remaining Considerations
|
||||
|
||||
- {Any follow-up improvements to consider}
|
||||
- {Type refactoring opportunities}
|
||||
- {Long-term type safety enhancements}
|
||||
</output>
|
||||
266
skills/reviewing-type-safety/SKILL.md
Normal file
266
skills/reviewing-type-safety/SKILL.md
Normal file
@@ -0,0 +1,266 @@
|
||||
---
|
||||
name: reviewing-type-safety
|
||||
description: Code review skill that checks TypeScript type safety, exported for use by cross-cutting review plugin
|
||||
review: true
|
||||
---
|
||||
|
||||
# TypeScript Type Safety Review
|
||||
|
||||
**Purpose:** Comprehensive type safety review for TypeScript code, detecting violations that compromise compile-time safety and runtime reliability.
|
||||
|
||||
**When to use:** During code review process, invoked by review plugin to validate TypeScript type safety across the codebase.
|
||||
|
||||
**Exported for:** Cross-cutting review plugin that orchestrates multi-concern reviews.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
When reviewing TypeScript code, systematically check for these type safety violations:
|
||||
|
||||
### 1. `any` Type Abuse
|
||||
|
||||
**Check for:**
|
||||
- Generic defaults using `any`: `<T = any>`
|
||||
- Function parameters typed as `any`: `function process(data: any)`
|
||||
- Return types using `any`: `): any {`
|
||||
- Array or object types with `any`: `any[]`, `Record<string, any>`
|
||||
- Type assertions to `any`: `as any`
|
||||
|
||||
**Correct alternatives:**
|
||||
- Use `unknown` with type guards instead of `any`
|
||||
- Use specific types or generic constraints
|
||||
- Use the avoiding-any-types skill for guidance
|
||||
|
||||
**Severity:** HIGH - Defeats TypeScript's purpose entirely
|
||||
|
||||
### 2. Unsafe Type Assertions
|
||||
|
||||
**Check for:**
|
||||
- Type assertions on external data without validation: `JSON.parse(response) as T`
|
||||
- Downcasting without runtime checks: `value as SpecificType`
|
||||
- Double assertions: `value as unknown as T`
|
||||
- Type assertions in parsers or API handlers
|
||||
|
||||
**Acceptable assertions:**
|
||||
- `as const` for literal types
|
||||
- `as unknown as T` only AFTER runtime validation
|
||||
- Type assertions on known internal data structures
|
||||
|
||||
**Severity:** HIGH - Bypasses type safety, causes runtime errors
|
||||
|
||||
### 3. Missing Type Guards
|
||||
|
||||
**Check for:**
|
||||
- Error handling without type checks: `catch (error) { error.message }`
|
||||
- Array operations without bounds checking
|
||||
- Object property access without `in` operator
|
||||
- Discriminated unions without exhaustive checks
|
||||
|
||||
**Required patterns:**
|
||||
- Error type guards: `error instanceof Error`
|
||||
- Array bounds: check length or use `noUncheckedIndexedAccess`
|
||||
- Object properties: `'key' in obj` before access
|
||||
- Exhaustive switch with `never` type
|
||||
|
||||
**Severity:** MEDIUM - Leads to runtime errors in edge cases
|
||||
|
||||
### 4. Missing Runtime Validation
|
||||
|
||||
**Check for:**
|
||||
- API responses used directly without validation
|
||||
- User input processed without sanitization
|
||||
- JSON parsing without schema validation
|
||||
- External configuration loaded without checks
|
||||
|
||||
**Required:**
|
||||
- Use Zod, io-ts, or similar for runtime validation
|
||||
- Validate at system boundaries
|
||||
- Never trust external data
|
||||
- Use the using-runtime-checks skill
|
||||
|
||||
**Severity:** HIGH - Security and reliability issue
|
||||
|
||||
### 5. Deprecated JavaScript APIs
|
||||
|
||||
**Check for:**
|
||||
- `substr()` - use `slice()` instead
|
||||
- `escape()` - use `encodeURIComponent()` instead
|
||||
- `unescape()` - use `decodeURIComponent()` instead
|
||||
|
||||
**Severity:** LOW - Future compatibility issue
|
||||
|
||||
### 6. Security Violations
|
||||
|
||||
**Check for:**
|
||||
- Base64 encoding for passwords (not encryption!)
|
||||
- Direct password storage without hashing
|
||||
- Accepting third-party credentials (use OAuth instead)
|
||||
- Missing input sanitization (XSS risk)
|
||||
- Unsafe SQL query construction
|
||||
|
||||
**Required:**
|
||||
- Use bcrypt/argon2 for password hashing
|
||||
- OAuth for third-party authentication
|
||||
- Sanitize all user input
|
||||
- Use parameterized queries
|
||||
- Use the hashing-passwords skill
|
||||
|
||||
**Severity:** CRITICAL - Production security breach risk
|
||||
|
||||
### 7. Missing Generic Constraints
|
||||
|
||||
**Check for:**
|
||||
- Unconstrained generics: `<T>` when `<T extends SomeType>` is appropriate
|
||||
- Generic defaults to `any`
|
||||
- Missing type parameter relationships
|
||||
|
||||
**Correct patterns:**
|
||||
- Constrain to expected shape: `<T extends { id: string }>`
|
||||
- Use multiple type parameters with relationships: `<T extends U>`
|
||||
- Use the using-generics skill
|
||||
|
||||
**Severity:** MEDIUM - Reduces type safety guarantees
|
||||
|
||||
### 8. Compiler Configuration Issues
|
||||
|
||||
**Check for:**
|
||||
- `strict: false` in tsconfig.json
|
||||
- Missing `noUncheckedIndexedAccess: true`
|
||||
- `skipLibCheck: false` (performance issue)
|
||||
- Incorrect module resolution for Node.js projects
|
||||
|
||||
**Required settings:**
|
||||
- `strict: true` (enables all strict checks)
|
||||
- `noUncheckedIndexedAccess: true` (prevents array out-of-bounds)
|
||||
- `skipLibCheck: true` (improves build performance)
|
||||
- `moduleResolution: "NodeNext"` for Node.js projects
|
||||
|
||||
**Severity:** MEDIUM - Affects entire project safety
|
||||
|
||||
## Review Process
|
||||
|
||||
1. **Automated Checks**
|
||||
- Run TypeScript compiler: `tsc --noEmit`
|
||||
- Run ESLint with TypeScript rules
|
||||
- Check for `any` usage: `grep -r ": any" src/`
|
||||
- Check for type assertions: `grep -r " as " src/`
|
||||
|
||||
2. **Manual Review**
|
||||
- Focus on type safety at system boundaries (API handlers, parsers)
|
||||
- Verify runtime validation exists for external data
|
||||
- Check error handling has proper type guards
|
||||
- Review security-sensitive code (authentication, authorization)
|
||||
|
||||
3. **Report Findings**
|
||||
- Group by severity (CRITICAL > HIGH > MEDIUM > LOW)
|
||||
- Provide specific file location and line number
|
||||
- Explain why it's a violation
|
||||
- Suggest specific fix with code example
|
||||
|
||||
## Example Violations and Fixes
|
||||
|
||||
### Violation: `any` Type on API Response
|
||||
|
||||
```typescript
|
||||
async function fetchUser(id: string): Promise<any> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
type User = z.infer<typeof UserSchema>;
|
||||
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
const data = await response.json();
|
||||
return UserSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
### Violation: Type Assertion Without Validation
|
||||
|
||||
```typescript
|
||||
function parseConfig(json: string) {
|
||||
return JSON.parse(json) as Config;
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const ConfigSchema = z.object({
|
||||
apiKey: z.string(),
|
||||
timeout: z.number(),
|
||||
});
|
||||
|
||||
type Config = z.infer<typeof ConfigSchema>;
|
||||
|
||||
function parseConfig(json: string): Config {
|
||||
const data = JSON.parse(json);
|
||||
return ConfigSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
### Violation: Missing Error Type Guard
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await riskyOperation();
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await riskyOperation();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
} else {
|
||||
console.error('Unknown error:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Review Plugin
|
||||
|
||||
This skill is exported with `review: true` frontmatter, making it discoverable by the cross-cutting review plugin.
|
||||
|
||||
**Review plugin should:**
|
||||
- Invoke this skill for TypeScript files (`.ts`, `.tsx`)
|
||||
- Run automated checks first
|
||||
- Present findings grouped by severity
|
||||
- Generate actionable review comments
|
||||
|
||||
**Cross-plugin references:**
|
||||
- React plugin: References this skill for component prop type safety
|
||||
- Next.js plugin: References this skill for server action type safety
|
||||
- Node.js plugin: References this skill for API handler type safety
|
||||
|
||||
## Stress Test Prevention
|
||||
|
||||
This review skill addresses all 23 violations found in the TypeScript stress test:
|
||||
|
||||
- ✅ Detects `any` abuse (5/6 agents)
|
||||
- ✅ Catches type assertion misuse (4/6 agents)
|
||||
- ✅ Identifies security failures (2/6 agents)
|
||||
- ✅ Flags deprecated API usage (3/6 agents)
|
||||
- ✅ Ensures TypeScript vs JavaScript (2/6 agents)
|
||||
- ✅ Validates runtime checking presence
|
||||
|
||||
**Target:** 90% reduction in type safety violations when used during code review.
|
||||
611
skills/sanitizing-user-inputs/SKILL.md
Normal file
611
skills/sanitizing-user-inputs/SKILL.md
Normal file
@@ -0,0 +1,611 @@
|
||||
---
|
||||
name: sanitizing-user-inputs
|
||||
description: Sanitizing and validating user input to prevent XSS, injection attacks, and security vulnerabilities in TypeScript applications
|
||||
---
|
||||
|
||||
# Security: Input Validation and Sanitization
|
||||
|
||||
**Purpose:** Prevent security vulnerabilities by properly validating and sanitizing all user input, preventing XSS, SQL injection, command injection, and other attack vectors.
|
||||
|
||||
**When to use:** Any time you process user input, API requests, URL parameters, form data, file uploads, or any external data source.
|
||||
|
||||
## Core Security Principles
|
||||
|
||||
### 1. Never Trust User Input
|
||||
|
||||
**All user input is potentially malicious until proven safe.**
|
||||
|
||||
This includes:
|
||||
- Form submissions
|
||||
- URL query parameters
|
||||
- Request headers
|
||||
- Cookies
|
||||
- File uploads
|
||||
- WebSocket messages
|
||||
- GraphQL queries
|
||||
- API request bodies
|
||||
|
||||
### 2. Validate Then Sanitize
|
||||
|
||||
**Two-step process:**
|
||||
|
||||
1. **Validation:** Check if input matches expected format/type
|
||||
2. **Sanitization:** Remove or escape dangerous characters
|
||||
|
||||
**Never sanitize without validating first** - sanitization can hide malicious patterns.
|
||||
|
||||
### 3. Allowlist Over Blocklist
|
||||
|
||||
**Allowlist (good):** Accept only known-safe inputs
|
||||
```typescript
|
||||
const validRoles = ['admin', 'user', 'guest'] as const;
|
||||
if (!validRoles.includes(role)) {
|
||||
throw new ValidationError('Invalid role');
|
||||
}
|
||||
```
|
||||
|
||||
**Blocklist (bad):** Try to block known-bad inputs
|
||||
```typescript
|
||||
if (input.includes('<script>') || input.includes('DROP')) {
|
||||
throw new Error('Suspicious input');
|
||||
}
|
||||
```
|
||||
|
||||
Blocklists are always incomplete. Attackers find new bypass techniques.
|
||||
|
||||
## Input Validation with Zod
|
||||
|
||||
### Basic Pattern
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const UserInputSchema = z.object({
|
||||
username: z.string()
|
||||
.min(3, 'Username must be at least 3 characters')
|
||||
.max(20, 'Username must be at most 20 characters')
|
||||
.regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),
|
||||
email: z.string().email('Invalid email format'),
|
||||
age: z.number().int().positive().max(120),
|
||||
});
|
||||
|
||||
type UserInput = z.infer<typeof UserInputSchema>;
|
||||
|
||||
function handleUserRegistration(input: unknown): UserInput {
|
||||
return UserInputSchema.parse(input);
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Input type is `unknown` (never trust it)
|
||||
- Schema enforces exact constraints (allowlist)
|
||||
- Parse throws on invalid input (fail-fast)
|
||||
- Return type is fully typed after validation
|
||||
|
||||
### Common Validation Patterns
|
||||
|
||||
**Email validation:**
|
||||
```typescript
|
||||
const email = z.string().email().toLowerCase();
|
||||
```
|
||||
|
||||
**Phone number (US format):**
|
||||
```typescript
|
||||
const phone = z.string().regex(/^\+1[0-9]{10}$/);
|
||||
```
|
||||
|
||||
**URL validation:**
|
||||
```typescript
|
||||
const url = z.string().url();
|
||||
```
|
||||
|
||||
**Enum values (allowlist):**
|
||||
```typescript
|
||||
const role = z.enum(['admin', 'user', 'guest']);
|
||||
```
|
||||
|
||||
**Password requirements:**
|
||||
```typescript
|
||||
const password = z.string()
|
||||
.min(12, 'Password must be at least 12 characters')
|
||||
.regex(/[A-Z]/, 'Must contain uppercase letter')
|
||||
.regex(/[a-z]/, 'Must contain lowercase letter')
|
||||
.regex(/[0-9]/, 'Must contain number')
|
||||
.regex(/[^A-Za-z0-9]/, 'Must contain special character');
|
||||
```
|
||||
|
||||
**File upload validation:**
|
||||
```typescript
|
||||
const fileUpload = z.object({
|
||||
filename: z.string()
|
||||
.regex(/^[a-zA-Z0-9_\-\.]+$/, 'Invalid filename'),
|
||||
mimetype: z.enum(['image/jpeg', 'image/png', 'image/gif']),
|
||||
size: z.number().max(5 * 1024 * 1024, 'File too large (max 5MB)'),
|
||||
});
|
||||
```
|
||||
|
||||
## XSS Prevention
|
||||
|
||||
### Understanding XSS
|
||||
|
||||
**Cross-Site Scripting (XSS)** injects malicious JavaScript into your application:
|
||||
|
||||
```typescript
|
||||
const userInput = '<script>alert("XSS")</script>';
|
||||
document.innerHTML = userInput;
|
||||
```
|
||||
|
||||
This executes the script in the user's browser, potentially:
|
||||
- Stealing cookies/session tokens
|
||||
- Performing actions as the user
|
||||
- Defacing the page
|
||||
- Redirecting to phishing sites
|
||||
|
||||
### Preventing XSS in TypeScript/React
|
||||
|
||||
**React (safe by default):**
|
||||
|
||||
```typescript
|
||||
function UserProfile({ username }: { username: string }) {
|
||||
return <div>{username}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
React automatically escapes `{username}`. This is safe even if `username` contains `<script>`.
|
||||
|
||||
**Unsafe (DO NOT DO):**
|
||||
|
||||
```typescript
|
||||
function UnsafeComponent({ html }: { html: string }) {
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
```
|
||||
|
||||
**If you must render HTML, sanitize it first:**
|
||||
|
||||
```typescript
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
||||
function SafeHTMLComponent({ html }: { html: string }) {
|
||||
const clean = DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],
|
||||
ALLOWED_ATTR: ['href'],
|
||||
});
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
|
||||
}
|
||||
```
|
||||
|
||||
**DOMPurify** removes dangerous tags and attributes:
|
||||
- Removes `<script>`, `<iframe>`, `<object>`
|
||||
- Removes event handlers (`onclick`, `onerror`)
|
||||
- Removes `javascript:` URLs
|
||||
- Configurable allowlist of safe tags/attributes
|
||||
|
||||
### Server-Side Rendering (SSR) Safety
|
||||
|
||||
**Express/Node.js:**
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
import { escape } from 'html-escaper';
|
||||
|
||||
app.get('/profile', (req, res) => {
|
||||
const username = escape(req.query.username as string);
|
||||
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>Welcome ${username}</h1>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
```
|
||||
|
||||
**`html-escaper`** converts dangerous characters:
|
||||
- `<` → `<`
|
||||
- `>` → `>`
|
||||
- `"` → `"`
|
||||
- `'` → `'`
|
||||
- `&` → `&`
|
||||
|
||||
## SQL Injection Prevention
|
||||
|
||||
### Understanding SQL Injection
|
||||
|
||||
**Vulnerable code:**
|
||||
|
||||
```typescript
|
||||
const userId = req.params.id;
|
||||
const query = `SELECT * FROM users WHERE id = ${userId}`;
|
||||
db.query(query);
|
||||
```
|
||||
|
||||
**Attack:**
|
||||
```
|
||||
GET /users/1; DROP TABLE users; --
|
||||
```
|
||||
|
||||
Results in:
|
||||
```sql
|
||||
SELECT * FROM users WHERE id = 1; DROP TABLE users; --
|
||||
```
|
||||
|
||||
### Safe Parameterized Queries
|
||||
|
||||
**Use parameterized queries (prepared statements):**
|
||||
|
||||
```typescript
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const pool = new Pool();
|
||||
|
||||
async function getUser(userId: string) {
|
||||
const query = 'SELECT * FROM users WHERE id = $1';
|
||||
|
||||
const result = await pool.query(query, [userId]);
|
||||
|
||||
return result.rows[0];
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- `$1` is a placeholder (not string concatenation)
|
||||
- Database driver escapes parameters safely
|
||||
- SQL injection is impossible
|
||||
|
||||
**TypeScript type safety with query builders:**
|
||||
|
||||
```typescript
|
||||
import { Kysely, PostgresDialect } from 'kysely';
|
||||
|
||||
interface Database {
|
||||
users: {
|
||||
id: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
const db = new Kysely<Database>({
|
||||
dialect: new PostgresDialect({ pool }),
|
||||
});
|
||||
|
||||
async function getUser(userId: string) {
|
||||
return await db
|
||||
.selectFrom('users')
|
||||
.where('id', '=', userId)
|
||||
.selectAll()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Type-safe queries
|
||||
- Automatic parameterization
|
||||
- Compile-time checking
|
||||
|
||||
### ORM Safety
|
||||
|
||||
**TypeORM:**
|
||||
|
||||
```typescript
|
||||
import { getRepository } from 'typeorm';
|
||||
|
||||
async function getUser(userId: string) {
|
||||
return await getRepository(User).findOne({
|
||||
where: { id: userId },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Prisma:**
|
||||
|
||||
If preventing SQL injection with Prisma ORM, use the preventing-sql-injection skill from prisma-6 for comprehensive $queryRaw vs $queryRawUnsafe guidance and parameterization strategies.
|
||||
|
||||
## Command Injection Prevention
|
||||
|
||||
### Understanding Command Injection
|
||||
|
||||
**Vulnerable code:**
|
||||
|
||||
```typescript
|
||||
import { exec } from 'child_process';
|
||||
|
||||
function processFile(filename: string) {
|
||||
exec(`convert ${filename} output.jpg`, (error, stdout) => {
|
||||
console.log(stdout);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Attack:**
|
||||
```
|
||||
filename = "input.jpg; rm -rf /"
|
||||
```
|
||||
|
||||
Results in:
|
||||
```bash
|
||||
convert input.jpg; rm -rf / output.jpg
|
||||
```
|
||||
|
||||
### Safe Command Execution
|
||||
|
||||
**Use array-based execution:**
|
||||
|
||||
```typescript
|
||||
import { execFile } from 'child_process';
|
||||
|
||||
function processFile(filename: string) {
|
||||
const FileNameSchema = z.string().regex(/^[a-zA-Z0-9_\-\.]+$/);
|
||||
const validFilename = FileNameSchema.parse(filename);
|
||||
|
||||
execFile('convert', [validFilename, 'output.jpg'], (error, stdout) => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
console.log(stdout);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Key differences:**
|
||||
- `execFile` (safe) vs `exec` (unsafe)
|
||||
- Arguments passed as array, not string concatenation
|
||||
- Shell is not invoked, no command injection possible
|
||||
- Validate filename format first
|
||||
|
||||
## Path Traversal Prevention
|
||||
|
||||
### Understanding Path Traversal
|
||||
|
||||
**Vulnerable code:**
|
||||
|
||||
```typescript
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
async function getFile(filename: string) {
|
||||
return await readFile(`./uploads/${filename}`, 'utf-8');
|
||||
}
|
||||
```
|
||||
|
||||
**Attack:**
|
||||
```
|
||||
filename = "../../etc/passwd"
|
||||
```
|
||||
|
||||
Results in reading:
|
||||
```
|
||||
./uploads/../../etc/passwd
|
||||
```
|
||||
|
||||
### Safe Path Handling
|
||||
|
||||
```typescript
|
||||
import { readFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
async function getFile(filename: string) {
|
||||
const FileNameSchema = z.string()
|
||||
.regex(/^[a-zA-Z0-9_\-\.]+$/, 'Invalid filename');
|
||||
|
||||
const validFilename = FileNameSchema.parse(filename);
|
||||
|
||||
const uploadDir = path.resolve('./uploads');
|
||||
const fullPath = path.resolve(uploadDir, validFilename);
|
||||
|
||||
if (!fullPath.startsWith(uploadDir)) {
|
||||
throw new Error('Path traversal detected');
|
||||
}
|
||||
|
||||
return await readFile(fullPath, 'utf-8');
|
||||
}
|
||||
```
|
||||
|
||||
**Protection layers:**
|
||||
1. Validate filename format (no `..`, no `/`)
|
||||
2. Resolve absolute paths
|
||||
3. Check final path is within expected directory
|
||||
|
||||
## API Request Validation
|
||||
|
||||
### Express/Node.js Example
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
import { z } from 'zod';
|
||||
|
||||
const CreateUserSchema = z.object({
|
||||
username: z.string().min(3).max(20),
|
||||
email: z.string().email(),
|
||||
age: z.number().int().positive().max(120),
|
||||
});
|
||||
|
||||
app.post('/users', async (req, res) => {
|
||||
try {
|
||||
const validData = CreateUserSchema.parse(req.body);
|
||||
|
||||
const user = await createUser(validData);
|
||||
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
details: error.errors,
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Query Parameter Validation
|
||||
|
||||
```typescript
|
||||
const QuerySchema = z.object({
|
||||
page: z.coerce.number().int().positive().default(1),
|
||||
limit: z.coerce.number().int().positive().max(100).default(20),
|
||||
sort: z.enum(['asc', 'desc']).default('asc'),
|
||||
});
|
||||
|
||||
app.get('/users', (req, res) => {
|
||||
const params = QuerySchema.parse(req.query);
|
||||
|
||||
const users = await getUsers(params);
|
||||
|
||||
res.json(users);
|
||||
});
|
||||
```
|
||||
|
||||
**`z.coerce.number()`** converts string query params to numbers safely.
|
||||
|
||||
## Header Validation
|
||||
|
||||
### Accept Only Expected Headers
|
||||
|
||||
```typescript
|
||||
const HeadersSchema = z.object({
|
||||
'content-type': z.literal('application/json'),
|
||||
'x-api-key': z.string().uuid(),
|
||||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
try {
|
||||
HeadersSchema.parse(req.headers);
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: 'Invalid headers' });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### CSRF Token Validation
|
||||
|
||||
```typescript
|
||||
import csrf from 'csurf';
|
||||
|
||||
const csrfProtection = csrf({ cookie: true });
|
||||
|
||||
app.post('/transfer', csrfProtection, (req, res) => {
|
||||
res.json({ success: true });
|
||||
});
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Prevent Abuse and DoS
|
||||
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 100,
|
||||
message: 'Too many requests, please try again later.',
|
||||
});
|
||||
|
||||
app.use('/api/', limiter);
|
||||
```
|
||||
|
||||
## Content Security Policy (CSP)
|
||||
|
||||
### Prevent XSS via Headers
|
||||
|
||||
```typescript
|
||||
import helmet from 'helmet';
|
||||
|
||||
app.use(helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", 'data:', 'https:'],
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
This prevents loading scripts from untrusted sources.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Mistake 1: Client-Side Validation Only
|
||||
|
||||
```typescript
|
||||
function LoginForm() {
|
||||
const handleSubmit = (e) => {
|
||||
if (email.includes('@')) {
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** Client-side validation can be bypassed. Always validate on server.
|
||||
|
||||
### Mistake 2: Regex Bypasses
|
||||
|
||||
```typescript
|
||||
if (input.match(/^[a-z]+$/)) {
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** `^` and `$` match line start/end, not string start/end. Use `\A` and `\z` or Zod.
|
||||
|
||||
### Mistake 3: String Blacklists
|
||||
|
||||
```typescript
|
||||
const sanitized = input.replace('<script>', '');
|
||||
```
|
||||
|
||||
**Attack:** `<scr<script>ipt>` becomes `<script>` after replacement.
|
||||
|
||||
**Solution:** Use proper sanitization libraries like DOMPurify.
|
||||
|
||||
## Security Checklist
|
||||
|
||||
Before deploying any endpoint that processes user input:
|
||||
|
||||
- [ ] All input validated with Zod schemas
|
||||
- [ ] Allowlist validation (not blocklist)
|
||||
- [ ] HTML escaped or sanitized with DOMPurify
|
||||
- [ ] SQL queries parameterized (never concatenated)
|
||||
- [ ] File operations validate paths (prevent traversal)
|
||||
- [ ] Command execution uses array arguments (not shell)
|
||||
- [ ] Rate limiting enabled
|
||||
- [ ] CSRF protection on state-changing endpoints
|
||||
- [ ] Content Security Policy headers set
|
||||
- [ ] Error messages don't leak sensitive info
|
||||
|
||||
## Related Skills
|
||||
|
||||
**Zod v4 Validation:**
|
||||
- If validating common input formats, use the validating-string-formats skill for top-level string format functions (z.email(), z.uuid(), z.url(), z.ipv4(), z.base64())
|
||||
- If transforming user input strings, use the transforming-string-methods skill for built-in string transformations (trim, toLowerCase, toUpperCase)
|
||||
- If constructing Zod schemas for user input, use the validating-schema-basics skill for comprehensive schema patterns
|
||||
|
||||
**Prisma 6 Security:**
|
||||
- If preventing SQL injection with Prisma 6, use the preventing-sql-injection skill from prisma-6 for database-specific sanitization patterns, $queryRaw vs $queryRawUnsafe parameterization, and safe query construction
|
||||
- If validating inputs for Prisma database operations with type-safe Zod schemas, use the validating-query-inputs skill from prisma-6 for Prisma-specific validation patterns.
|
||||
|
||||
## Resources
|
||||
|
||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
|
||||
- [Zod Documentation](https://zod.dev/)
|
||||
- [DOMPurify](https://github.com/cure53/DOMPurify)
|
||||
|
||||
## Summary
|
||||
|
||||
**Input validation is your first and most critical line of defense.**
|
||||
|
||||
1. **Validate everything** - Never trust user input
|
||||
2. **Use Zod** - Type-safe validation at runtime
|
||||
3. **Sanitize HTML** - Use DOMPurify for any user-provided HTML
|
||||
4. **Parameterize SQL** - Never concatenate queries
|
||||
5. **Validate paths** - Prevent directory traversal
|
||||
6. **Use allowlists** - More secure than blocklists
|
||||
7. **Validate server-side** - Client validation is UX, not security
|
||||
|
||||
Security is not optional. Every unvalidated input is a potential breach.
|
||||
440
skills/using-generics/SKILL.md
Normal file
440
skills/using-generics/SKILL.md
Normal file
@@ -0,0 +1,440 @@
|
||||
---
|
||||
name: using-generics
|
||||
description: Teaches generic constraints, avoiding any in generic defaults, and mapped types in TypeScript. Use when creating reusable functions, components, or types that work with multiple types while maintaining type safety.
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
<role>
|
||||
This skill teaches how to use TypeScript generics effectively with proper constraints, avoiding `any` defaults, and leveraging mapped types for type transformations.
|
||||
</role>
|
||||
|
||||
<when-to-activate>
|
||||
This skill activates when:
|
||||
|
||||
- Creating reusable functions or classes
|
||||
- Designing generic APIs or libraries
|
||||
- Working with generic defaults (`<T = ...>`)
|
||||
- Implementing mapped types or conditional types
|
||||
- User mentions generics, type parameters, constraints, or reusable types
|
||||
</when-to-activate>
|
||||
|
||||
<overview>
|
||||
Generics enable writing reusable code that works with multiple types while preserving type safety. Proper use of constraints prevents `any` abuse and provides better IDE support.
|
||||
|
||||
**Key Concepts**:
|
||||
|
||||
1. **Generic Parameters**: `<T>` - Type variables that get filled in at call site
|
||||
2. **Constraints**: `<T extends Shape>` - Limits what types T can be
|
||||
3. **Defaults**: `<T = string>` - Fallback when type not provided
|
||||
4. **Mapped Types**: Transform existing types systematically
|
||||
|
||||
**Impact**: Write flexible, reusable code without sacrificing type safety.
|
||||
</overview>
|
||||
|
||||
<workflow>
|
||||
## Generic Design Flow
|
||||
|
||||
**Step 1: Identify the Varying Type**
|
||||
|
||||
What changes between uses?
|
||||
- Data type in container (Array<T>, Promise<T>)
|
||||
- Object shape variations
|
||||
- Return type based on input
|
||||
- Multiple related types
|
||||
|
||||
**Step 2: Choose Constraint Strategy**
|
||||
|
||||
**No Constraint** - Accepts any type
|
||||
```typescript
|
||||
function identity<T>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
**Extends Constraint** - Requires specific shape
|
||||
```typescript
|
||||
function logId<T extends { id: string }>(item: T): void {
|
||||
console.log(item.id);
|
||||
}
|
||||
```
|
||||
|
||||
**Union Constraint** - Limited set of types
|
||||
```typescript
|
||||
function process<T extends string | number>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
**Multiple Constraints** - Multiple type parameters with relationships
|
||||
```typescript
|
||||
function merge<T extends object, U extends object>(a: T, b: U): T & U {
|
||||
return { ...a, ...b };
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Set Default (If Needed)**
|
||||
|
||||
Prefer no default over `any` default:
|
||||
```typescript
|
||||
interface ApiResponse<T = unknown> { data: T; }
|
||||
```
|
||||
|
||||
Or require explicit type parameter:
|
||||
```typescript
|
||||
interface ApiResponse<T> { data: T; }
|
||||
```
|
||||
</workflow>
|
||||
|
||||
<examples>
|
||||
## Example 1: Generic Function Constraints
|
||||
|
||||
**❌ No constraint (too permissive)**
|
||||
|
||||
```typescript
|
||||
function getProperty<T>(obj: T, key: string): any {
|
||||
return obj[key];
|
||||
}
|
||||
```
|
||||
|
||||
**Problems**:
|
||||
- `obj[key]` not type-safe (T might not have string keys)
|
||||
- Returns `any` (loses type information)
|
||||
- No IDE autocomplete for key
|
||||
|
||||
**✅ Proper constraints**
|
||||
|
||||
```typescript
|
||||
function getProperty<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
key: K
|
||||
): T[K] {
|
||||
return obj[key];
|
||||
}
|
||||
|
||||
const user = { name: "Alice", age: 30 };
|
||||
const name = getProperty(user, "name");
|
||||
const invalid = getProperty(user, "invalid");
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Type-safe key access
|
||||
- Return type is `T[K]` (actual property type)
|
||||
- IDE autocompletes valid keys
|
||||
- Compile error for invalid keys
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Generic Defaults
|
||||
|
||||
**❌ Using `any` default (unsafe)**
|
||||
|
||||
```typescript
|
||||
interface Result<T = any> {
|
||||
data: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const result: Result = { data: "anything" };
|
||||
result.data.nonExistentProperty;
|
||||
```
|
||||
|
||||
**✅ Using `unknown` default (safe)**
|
||||
|
||||
```typescript
|
||||
interface Result<T = unknown> {
|
||||
data: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const result: Result = { data: "anything" };
|
||||
|
||||
if (typeof result.data === "string") {
|
||||
console.log(result.data.toUpperCase());
|
||||
}
|
||||
```
|
||||
|
||||
**✅ No default (best)**
|
||||
|
||||
```typescript
|
||||
interface Result<T> {
|
||||
data: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const result: Result<string> = { data: "specific type" };
|
||||
console.log(result.data.toUpperCase());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 3: Constraining Generic Parameters
|
||||
|
||||
**Example: Ensuring object has id**
|
||||
|
||||
```typescript
|
||||
interface HasId {
|
||||
id: string;
|
||||
}
|
||||
|
||||
function findById<T extends HasId>(items: T[], id: string): T | undefined {
|
||||
return items.find(item => item.id === id);
|
||||
}
|
||||
|
||||
const users = [
|
||||
{ id: "1", name: "Alice" },
|
||||
{ id: "2", name: "Bob" }
|
||||
];
|
||||
|
||||
const user = findById(users, "1");
|
||||
```
|
||||
|
||||
**Example: Ensuring constructable type**
|
||||
|
||||
```typescript
|
||||
interface Constructable<T> {
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
function create<T>(Constructor: Constructable<T>): T {
|
||||
return new Constructor();
|
||||
}
|
||||
|
||||
class User {
|
||||
name = "Anonymous";
|
||||
}
|
||||
|
||||
const user = create(User);
|
||||
```
|
||||
|
||||
**Example: Ensuring array element type**
|
||||
|
||||
```typescript
|
||||
function firstElement<T>(arr: T[]): T | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
const first = firstElement([1, 2, 3]);
|
||||
const second = firstElement(["a", "b"]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 4: Multiple Type Parameters
|
||||
|
||||
**Example: Key-value mapping**
|
||||
|
||||
```typescript
|
||||
function mapObject<T extends object, U>(
|
||||
obj: T,
|
||||
fn: (value: T[keyof T]) => U
|
||||
): Record<keyof T, U> {
|
||||
const result = {} as Record<keyof T, U>;
|
||||
|
||||
for (const key in obj) {
|
||||
result[key] = fn(obj[key]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const user = { name: "Alice", age: 30 };
|
||||
const lengths = mapObject(user, val => String(val).length);
|
||||
```
|
||||
|
||||
**Example: Conditional return types**
|
||||
|
||||
```typescript
|
||||
function parse<T extends "json" | "text">(
|
||||
response: Response,
|
||||
type: T
|
||||
): T extends "json" ? Promise<unknown> : Promise<string> {
|
||||
if (type === "json") {
|
||||
return response.json() as any;
|
||||
}
|
||||
return response.text() as any;
|
||||
}
|
||||
|
||||
const json = await parse(response, "json");
|
||||
const text = await parse(response, "text");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 5: Mapped Types
|
||||
|
||||
**Making properties optional:**
|
||||
|
||||
```typescript
|
||||
type Partial<T> = {
|
||||
[P in keyof T]?: T[P];
|
||||
};
|
||||
|
||||
const partialUser: Partial<User> = { name: "Alice" };
|
||||
```
|
||||
|
||||
**Making properties readonly:**
|
||||
|
||||
```typescript
|
||||
type Readonly<T> = {
|
||||
readonly [P in keyof T]: T[P];
|
||||
};
|
||||
```
|
||||
|
||||
**Picking specific properties:**
|
||||
|
||||
```typescript
|
||||
type Pick<T, K extends keyof T> = {
|
||||
[P in K]: T[P];
|
||||
};
|
||||
|
||||
type UserPreview = Pick<User, "id" | "name">;
|
||||
```
|
||||
|
||||
See `references/detailed-examples.md` for DeepPartial, FilterByType, and other complex mapped type patterns.
|
||||
|
||||
---
|
||||
|
||||
## Example 6: Conditional Types
|
||||
|
||||
**Unwrap promise type:**
|
||||
|
||||
```typescript
|
||||
type Awaited<T> = T extends Promise<infer U> ? U : T;
|
||||
```
|
||||
|
||||
**Extract function parameters:**
|
||||
|
||||
```typescript
|
||||
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
|
||||
```
|
||||
|
||||
See `references/detailed-examples.md` for more conditional type patterns including FilterByType, nested promise unwrapping, and parameter extraction.
|
||||
</examples>
|
||||
|
||||
<progressive-disclosure>
|
||||
## Reference Files
|
||||
|
||||
**In this skill:**
|
||||
|
||||
- `references/detailed-examples.md` - DeepPartial, FilterByType, conditional types, constructables
|
||||
- `references/common-patterns.md` - Array ops, object utils, Promise utils, builders
|
||||
- `references/advanced-patterns.md` - Recursive generics, variadic tuples, branded types, HKTs
|
||||
|
||||
**Related skills:**
|
||||
|
||||
- Use the using-type-guards skill for narrowing generic types
|
||||
- Use the avoiding-any-types skill for generic defaults
|
||||
- Use the using-runtime-checks skill for validating generic data
|
||||
</progressive-disclosure>
|
||||
|
||||
<constraints>
|
||||
**MUST:**
|
||||
|
||||
- Use `extends` to constrain generic parameters when accessing properties
|
||||
- Use `keyof T` for type-safe property access
|
||||
- Use `unknown` for generic defaults if truly dynamic
|
||||
- Specify return type based on generic parameters
|
||||
|
||||
**SHOULD:**
|
||||
|
||||
- Prefer no default over `any` default
|
||||
- Use descriptive type parameter names for complex generics
|
||||
- Infer type parameters from usage when possible
|
||||
- Use helper types (Pick, Omit, Partial) over manual mapping
|
||||
|
||||
**NEVER:**
|
||||
|
||||
- Use `any` as generic default
|
||||
- Access properties on unconstrained generics
|
||||
- Use `as any` to bypass generic constraints
|
||||
- Create overly complex nested generics (split into smaller types)
|
||||
</constraints>
|
||||
|
||||
<patterns>
|
||||
## Common Generic Patterns
|
||||
|
||||
### Array Operations
|
||||
|
||||
```typescript
|
||||
function last<T>(arr: T[]): T | undefined {
|
||||
return arr[arr.length - 1];
|
||||
}
|
||||
|
||||
function chunk<T>(arr: T[], size: number): T[][] {
|
||||
const chunks: T[][] = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
chunks.push(arr.slice(i, i + size));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
```
|
||||
|
||||
### Object Utilities
|
||||
|
||||
```typescript
|
||||
function pick<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
...keys: K[]
|
||||
): Pick<T, K> {
|
||||
const result = {} as Pick<T, K>;
|
||||
for (const key of keys) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### Class Generics
|
||||
|
||||
```typescript
|
||||
class Container<T> {
|
||||
constructor(private value: T) {}
|
||||
|
||||
map<U>(fn: (value: T) => U): Container<U> {
|
||||
return new Container(fn(this.value));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See `references/common-patterns.md` for complete implementations including Promise utilities, builders, event emitters, and more.
|
||||
</patterns>
|
||||
|
||||
<validation>
|
||||
## Generic Type Safety Checklist
|
||||
|
||||
1. **Constraints**:
|
||||
- [ ] Generic parameters constrained when accessing properties
|
||||
- [ ] `keyof` used for property key types
|
||||
- [ ] `extends` used appropriately
|
||||
|
||||
2. **Defaults**:
|
||||
- [ ] No `any` defaults
|
||||
- [ ] `unknown` used for truly dynamic defaults
|
||||
- [ ] Or no default (require explicit type)
|
||||
|
||||
3. **Type Inference**:
|
||||
- [ ] Type parameters inferred from usage
|
||||
- [ ] Explicit types only when inference fails
|
||||
- [ ] Return types correctly derived from generics
|
||||
|
||||
4. **Complexity**:
|
||||
- [ ] Generic types are understandable
|
||||
- [ ] Complex types split into smaller pieces
|
||||
- [ ] Helper types used appropriately
|
||||
</validation>
|
||||
|
||||
<advanced-patterns>
|
||||
## Advanced Generic Patterns
|
||||
|
||||
For advanced patterns including:
|
||||
|
||||
- **Recursive Generics** (DeepReadonly, DeepPartial)
|
||||
- **Variadic Tuple Types** (type-safe array concatenation)
|
||||
- **Template Literal Types** (string manipulation at type level)
|
||||
- **Branded Types** (nominal typing in structural system)
|
||||
- **Distributive Conditional Types**
|
||||
- **Higher-Kinded Types** (simulation)
|
||||
|
||||
See `references/advanced-patterns.md` for detailed implementations and examples.
|
||||
</advanced-patterns>
|
||||
168
skills/using-generics/references/advanced-patterns.md
Normal file
168
skills/using-generics/references/advanced-patterns.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Advanced Generic Patterns
|
||||
|
||||
## Recursive Generics
|
||||
|
||||
Use recursive generics to apply transformations deeply through nested object structures:
|
||||
|
||||
```typescript
|
||||
type DeepReadonly<T> = {
|
||||
readonly [P in keyof T]: T[P] extends object
|
||||
? DeepReadonly<T[P]>
|
||||
: T[P];
|
||||
};
|
||||
|
||||
interface Config {
|
||||
database: {
|
||||
host: string;
|
||||
credentials: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const config: DeepReadonly<Config> = {
|
||||
database: {
|
||||
host: "localhost",
|
||||
credentials: {
|
||||
username: "admin",
|
||||
password: "secret"
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Variadic Tuple Types
|
||||
|
||||
TypeScript 4.0+ supports variadic tuple types for precise array concatenation:
|
||||
|
||||
```typescript
|
||||
function concat<T extends readonly unknown[], U extends readonly unknown[]>(
|
||||
arr1: T,
|
||||
arr2: U
|
||||
): [...T, ...U] {
|
||||
return [...arr1, ...arr2];
|
||||
}
|
||||
|
||||
const result = concat([1, 2], ["a", "b"]);
|
||||
|
||||
function curry<T extends unknown[], U extends unknown[], R>(
|
||||
fn: (...args: [...T, ...U]) => R,
|
||||
...first: T
|
||||
): (...args: U) => R {
|
||||
return (...rest: U) => fn(...first, ...rest);
|
||||
}
|
||||
```
|
||||
|
||||
## Template Literal Types
|
||||
|
||||
Combine string literals with generics for type-safe string manipulation:
|
||||
|
||||
```typescript
|
||||
type EventName<T extends string> = `on${Capitalize<T>}`;
|
||||
|
||||
type ClickEvent = EventName<"click">;
|
||||
type HoverEvent = EventName<"hover">;
|
||||
|
||||
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
|
||||
type Endpoint<M extends HTTPMethod, P extends string> = `${M} ${P}`;
|
||||
|
||||
type UserEndpoint = Endpoint<"GET", "/users/:id">;
|
||||
```
|
||||
|
||||
## Branded Types
|
||||
|
||||
Create nominal types in TypeScript's structural type system:
|
||||
|
||||
```typescript
|
||||
type Brand<K, T> = K & { __brand: T };
|
||||
|
||||
type UserId = Brand<string, "UserId">;
|
||||
type EmailAddress = Brand<string, "Email">;
|
||||
type OrderId = Brand<string, "OrderId">;
|
||||
|
||||
function sendEmail(to: EmailAddress, from: EmailAddress): void {
|
||||
console.log(`Sending email from ${from} to ${to}`);
|
||||
}
|
||||
|
||||
function getUser(id: UserId): void {
|
||||
console.log(`Fetching user ${id}`);
|
||||
}
|
||||
|
||||
const email = "user@example.com" as EmailAddress;
|
||||
const userId = "user-123" as UserId;
|
||||
|
||||
sendEmail(email, email);
|
||||
getUser(userId);
|
||||
```
|
||||
|
||||
## Distributive Conditional Types
|
||||
|
||||
Conditional types distribute over unions when the checked type is a naked type parameter:
|
||||
|
||||
```typescript
|
||||
type ToArray<T> = T extends any ? T[] : never;
|
||||
|
||||
type Nums = ToArray<number | string>;
|
||||
|
||||
type NonNullable<T> = T extends null | undefined ? never : T;
|
||||
|
||||
type SafeString = NonNullable<string | null | undefined>;
|
||||
```
|
||||
|
||||
## Mapped Type Modifiers
|
||||
|
||||
Use `+`, `-`, `readonly`, and `?` modifiers in mapped types:
|
||||
|
||||
```typescript
|
||||
type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
type Required<T> = {
|
||||
[P in keyof T]-?: T[P];
|
||||
};
|
||||
|
||||
type ReadonlyPartial<T> = {
|
||||
+readonly [P in keyof T]+?: T[P];
|
||||
};
|
||||
```
|
||||
|
||||
## Inference in Conditional Types
|
||||
|
||||
Use `infer` to extract types within conditional types:
|
||||
|
||||
```typescript
|
||||
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||
|
||||
type ArrayElement<T> = T extends (infer E)[] ? E : never;
|
||||
|
||||
type PromiseValue<T> = T extends Promise<infer V> ? V : T;
|
||||
|
||||
type FirstParameter<T> = T extends (first: infer F, ...args: any[]) => any
|
||||
? F
|
||||
: never;
|
||||
```
|
||||
|
||||
## Higher-Kinded Types (Simulation)
|
||||
|
||||
While TypeScript doesn't have true HKTs, you can simulate them:
|
||||
|
||||
```typescript
|
||||
interface Functor<F> {
|
||||
map<A, B>(fa: HKT<F, A>, f: (a: A) => B): HKT<F, B>;
|
||||
}
|
||||
|
||||
interface HKT<F, A> {
|
||||
_F: F;
|
||||
_A: A;
|
||||
}
|
||||
|
||||
interface ArrayF {}
|
||||
|
||||
type ArrayHKT<A> = HKT<ArrayF, A> & A[];
|
||||
|
||||
const arrayFunctor: Functor<ArrayF> = {
|
||||
map: (fa, f) => fa.map(f)
|
||||
};
|
||||
```
|
||||
332
skills/using-generics/references/common-patterns.md
Normal file
332
skills/using-generics/references/common-patterns.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# Common Generic Patterns
|
||||
|
||||
## Array Operations
|
||||
|
||||
Generic array utilities maintain type safety while providing reusable functionality:
|
||||
|
||||
```typescript
|
||||
function last<T>(arr: T[]): T | undefined {
|
||||
return arr[arr.length - 1];
|
||||
}
|
||||
|
||||
function first<T>(arr: T[]): T | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
function chunk<T>(arr: T[], size: number): T[][] {
|
||||
const chunks: T[][] = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
chunks.push(arr.slice(i, i + size));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function flatten<T>(arr: T[][]): T[] {
|
||||
return arr.reduce((acc, item) => acc.concat(item), []);
|
||||
}
|
||||
|
||||
function unique<T>(arr: T[]): T[] {
|
||||
return Array.from(new Set(arr));
|
||||
}
|
||||
|
||||
function partition<T>(arr: T[], predicate: (item: T) => boolean): [T[], T[]] {
|
||||
const truthy: T[] = [];
|
||||
const falsy: T[] = [];
|
||||
|
||||
for (const item of arr) {
|
||||
if (predicate(item)) {
|
||||
truthy.push(item);
|
||||
} else {
|
||||
falsy.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return [truthy, falsy];
|
||||
}
|
||||
```
|
||||
|
||||
## Object Utilities
|
||||
|
||||
Type-safe object manipulation:
|
||||
|
||||
```typescript
|
||||
function pick<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
...keys: K[]
|
||||
): Pick<T, K> {
|
||||
const result = {} as Pick<T, K>;
|
||||
for (const key of keys) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function omit<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
...keys: K[]
|
||||
): Omit<T, K> {
|
||||
const result = { ...obj };
|
||||
for (const key of keys) {
|
||||
delete result[key];
|
||||
}
|
||||
return result as Omit<T, K>;
|
||||
}
|
||||
|
||||
function keys<T extends object>(obj: T): (keyof T)[] {
|
||||
return Object.keys(obj) as (keyof T)[];
|
||||
}
|
||||
|
||||
function values<T extends object>(obj: T): T[keyof T][] {
|
||||
return Object.values(obj) as T[keyof T][];
|
||||
}
|
||||
|
||||
function entries<T extends object>(obj: T): [keyof T, T[keyof T]][] {
|
||||
return Object.entries(obj) as [keyof T, T[keyof T]][];
|
||||
}
|
||||
|
||||
function mapValues<T extends object, U>(
|
||||
obj: T,
|
||||
fn: (value: T[keyof T], key: keyof T) => U
|
||||
): Record<keyof T, U> {
|
||||
const result = {} as Record<keyof T, U>;
|
||||
|
||||
for (const key in obj) {
|
||||
result[key] = fn(obj[key], key);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
## Promise Utilities
|
||||
|
||||
Generic async operations:
|
||||
|
||||
```typescript
|
||||
async function retry<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxAttempts: number
|
||||
): Promise<T> {
|
||||
let lastError: Error;
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
}
|
||||
|
||||
async function timeout<T>(
|
||||
promise: Promise<T>,
|
||||
ms: number
|
||||
): Promise<T> {
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error("Timeout")), ms);
|
||||
});
|
||||
|
||||
return Promise.race([promise, timeoutPromise]);
|
||||
}
|
||||
|
||||
async function sequence<T>(
|
||||
promises: (() => Promise<T>)[]
|
||||
): Promise<T[]> {
|
||||
const results: T[] = [];
|
||||
|
||||
for (const promiseFn of promises) {
|
||||
results.push(await promiseFn());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
## Class Generics
|
||||
|
||||
Generic classes for reusable data structures:
|
||||
|
||||
```typescript
|
||||
class Container<T> {
|
||||
constructor(private value: T) {}
|
||||
|
||||
getValue(): T {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
setValue(value: T): void {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
map<U>(fn: (value: T) => U): Container<U> {
|
||||
return new Container(fn(this.value));
|
||||
}
|
||||
|
||||
flatMap<U>(fn: (value: T) => Container<U>): Container<U> {
|
||||
return fn(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
class Result<T, E = Error> {
|
||||
private constructor(
|
||||
private readonly value?: T,
|
||||
private readonly error?: E
|
||||
) {}
|
||||
|
||||
static ok<T, E = Error>(value: T): Result<T, E> {
|
||||
return new Result(value, undefined);
|
||||
}
|
||||
|
||||
static err<T, E = Error>(error: E): Result<T, E> {
|
||||
return new Result(undefined, error);
|
||||
}
|
||||
|
||||
isOk(): this is Result<T, never> {
|
||||
return this.value !== undefined;
|
||||
}
|
||||
|
||||
isErr(): this is Result<never, E> {
|
||||
return this.error !== undefined;
|
||||
}
|
||||
|
||||
unwrap(): T {
|
||||
if (this.error !== undefined) {
|
||||
throw this.error;
|
||||
}
|
||||
return this.value!;
|
||||
}
|
||||
|
||||
map<U>(fn: (value: T) => U): Result<U, E> {
|
||||
if (this.error !== undefined) {
|
||||
return Result.err(this.error);
|
||||
}
|
||||
return Result.ok(fn(this.value!));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Builder Pattern
|
||||
|
||||
Fluent API with type safety:
|
||||
|
||||
```typescript
|
||||
class QueryBuilder<T extends object> {
|
||||
private filters: Array<(item: T) => boolean> = [];
|
||||
private sortFn?: (a: T, b: T) => number;
|
||||
private limitValue?: number;
|
||||
|
||||
where<K extends keyof T>(key: K, value: T[K]): this {
|
||||
this.filters.push(item => item[key] === value);
|
||||
return this;
|
||||
}
|
||||
|
||||
whereFn(predicate: (item: T) => boolean): this {
|
||||
this.filters.push(predicate);
|
||||
return this;
|
||||
}
|
||||
|
||||
sort<K extends keyof T>(key: K, direction: "asc" | "desc" = "asc"): this {
|
||||
this.sortFn = (a, b) => {
|
||||
const aVal = a[key];
|
||||
const bVal = b[key];
|
||||
|
||||
if (aVal < bVal) return direction === "asc" ? -1 : 1;
|
||||
if (aVal > bVal) return direction === "asc" ? 1 : -1;
|
||||
return 0;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
limit(n: number): this {
|
||||
this.limitValue = n;
|
||||
return this;
|
||||
}
|
||||
|
||||
execute(data: T[]): T[] {
|
||||
let result = data.filter(item =>
|
||||
this.filters.every(filter => filter(item))
|
||||
);
|
||||
|
||||
if (this.sortFn) {
|
||||
result = result.sort(this.sortFn);
|
||||
}
|
||||
|
||||
if (this.limitValue !== undefined) {
|
||||
result = result.slice(0, this.limitValue);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const users = [
|
||||
{ id: 1, name: "Alice", active: true, age: 30 },
|
||||
{ id: 2, name: "Bob", active: false, age: 25 },
|
||||
{ id: 3, name: "Charlie", active: true, age: 35 }
|
||||
];
|
||||
|
||||
const active = new QueryBuilder<typeof users[0]>()
|
||||
.where("active", true)
|
||||
.sort("age", "desc")
|
||||
.limit(5)
|
||||
.execute(users);
|
||||
```
|
||||
|
||||
## Event Emitter Pattern
|
||||
|
||||
Type-safe event handling:
|
||||
|
||||
```typescript
|
||||
type EventMap = Record<string, any>;
|
||||
|
||||
class TypedEventEmitter<Events extends EventMap> {
|
||||
private listeners: {
|
||||
[K in keyof Events]?: Array<(data: Events[K]) => void>;
|
||||
} = {};
|
||||
|
||||
on<K extends keyof Events>(
|
||||
event: K,
|
||||
listener: (data: Events[K]) => void
|
||||
): this {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event]!.push(listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
emit<K extends keyof Events>(event: K, data: Events[K]): void {
|
||||
const listeners = this.listeners[event];
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
listener(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
off<K extends keyof Events>(
|
||||
event: K,
|
||||
listener: (data: Events[K]) => void
|
||||
): this {
|
||||
const listeners = this.listeners[event];
|
||||
if (listeners) {
|
||||
this.listeners[event] = listeners.filter(l => l !== listener);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
interface AppEvents {
|
||||
userLogin: { userId: string; timestamp: number };
|
||||
userLogout: { userId: string };
|
||||
dataUpdated: { id: string; changes: Record<string, unknown> };
|
||||
}
|
||||
|
||||
const emitter = new TypedEventEmitter<AppEvents>();
|
||||
|
||||
emitter.on("userLogin", (data) => {
|
||||
console.log(`User ${data.userId} logged in at ${data.timestamp}`);
|
||||
});
|
||||
```
|
||||
305
skills/using-generics/references/detailed-examples.md
Normal file
305
skills/using-generics/references/detailed-examples.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# Detailed Generic Examples
|
||||
|
||||
## Deep Partial Implementation
|
||||
|
||||
The `DeepPartial` type recursively makes all properties optional, including nested objects:
|
||||
|
||||
```typescript
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
||||
};
|
||||
|
||||
interface DatabaseConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
credentials: {
|
||||
username: string;
|
||||
password: string;
|
||||
ssl: {
|
||||
enabled: boolean;
|
||||
cert: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const partialConfig: DeepPartial<DatabaseConfig> = {
|
||||
host: "localhost",
|
||||
credentials: {
|
||||
username: "admin",
|
||||
ssl: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function mergeConfig(
|
||||
defaults: DatabaseConfig,
|
||||
override: DeepPartial<DatabaseConfig>
|
||||
): DatabaseConfig {
|
||||
return {
|
||||
...defaults,
|
||||
...override,
|
||||
credentials: {
|
||||
...defaults.credentials,
|
||||
...override.credentials,
|
||||
ssl: {
|
||||
...defaults.credentials.ssl,
|
||||
...override.credentials?.ssl
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Conditional Return Types
|
||||
|
||||
Use conditional types to change return types based on input:
|
||||
|
||||
```typescript
|
||||
type ParseResult<T extends "json" | "text"> =
|
||||
T extends "json" ? unknown : string;
|
||||
|
||||
function parse<T extends "json" | "text">(
|
||||
response: Response,
|
||||
type: T
|
||||
): Promise<ParseResult<T>> {
|
||||
if (type === "json") {
|
||||
return response.json() as Promise<ParseResult<T>>;
|
||||
}
|
||||
return response.text() as Promise<ParseResult<T>>;
|
||||
}
|
||||
|
||||
async function example() {
|
||||
const json = await parse(response, "json");
|
||||
|
||||
const text = await parse(response, "text");
|
||||
}
|
||||
|
||||
type QueryResult<T extends boolean> = T extends true
|
||||
? Array<{ id: string; data: unknown }>
|
||||
: { id: string; data: unknown } | undefined;
|
||||
|
||||
function query<Multiple extends boolean = false>(
|
||||
sql: string,
|
||||
multiple?: Multiple
|
||||
): QueryResult<Multiple> {
|
||||
if (multiple) {
|
||||
return [] as QueryResult<Multiple>;
|
||||
}
|
||||
return undefined as QueryResult<Multiple>;
|
||||
}
|
||||
|
||||
const single = query("SELECT * FROM users WHERE id = 1");
|
||||
const many = query("SELECT * FROM users", true);
|
||||
```
|
||||
|
||||
## Filter by Type Pattern
|
||||
|
||||
Extract only properties of a specific type from an object:
|
||||
|
||||
```typescript
|
||||
type FilterByType<T, U> = {
|
||||
[P in keyof T as T[P] extends U ? P : never]: T[P];
|
||||
};
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
age: number;
|
||||
active: boolean;
|
||||
createdAt: Date;
|
||||
score: number;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
type StringProps = FilterByType<User, string>;
|
||||
|
||||
type NumberProps = FilterByType<User, number>;
|
||||
|
||||
type FilterByValueType<T, U> = Pick<
|
||||
T,
|
||||
{
|
||||
[K in keyof T]: T[K] extends U ? K : never;
|
||||
}[keyof T]
|
||||
>;
|
||||
|
||||
type BooleanProps = FilterByValueType<User, boolean>;
|
||||
```
|
||||
|
||||
## Unwrap Nested Promises
|
||||
|
||||
Extract the final resolved type from nested Promises:
|
||||
|
||||
```typescript
|
||||
type Awaited<T> = T extends Promise<infer U>
|
||||
? Awaited<U>
|
||||
: T;
|
||||
|
||||
type A = Awaited<Promise<string>>;
|
||||
|
||||
type B = Awaited<Promise<Promise<number>>>;
|
||||
|
||||
type C = Awaited<Promise<Promise<Promise<boolean>>>>;
|
||||
|
||||
async function deepFetch(): Promise<Promise<{ data: string }>> {
|
||||
return Promise.resolve(
|
||||
Promise.resolve({ data: "nested" })
|
||||
);
|
||||
}
|
||||
|
||||
type DeepFetchResult = Awaited<ReturnType<typeof deepFetch>>;
|
||||
```
|
||||
|
||||
## Extract Function Parameters
|
||||
|
||||
Get parameter types from function signatures:
|
||||
|
||||
```typescript
|
||||
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
|
||||
|
||||
function processUser(id: string, name: string, age: number): void {
|
||||
console.log(id, name, age);
|
||||
}
|
||||
|
||||
type ProcessUserParams = Parameters<typeof processUser>;
|
||||
|
||||
function callWithParams<F extends (...args: any[]) => any>(
|
||||
fn: F,
|
||||
...args: Parameters<F>
|
||||
): ReturnType<F> {
|
||||
return fn(...args);
|
||||
}
|
||||
|
||||
callWithParams(processUser, "123", "Alice", 30);
|
||||
|
||||
type FirstParameter<T> = T extends (first: infer F, ...args: any[]) => any
|
||||
? F
|
||||
: never;
|
||||
|
||||
type FirstParam = FirstParameter<typeof processUser>;
|
||||
```
|
||||
|
||||
## Constructable Types
|
||||
|
||||
Work with classes and constructors generically:
|
||||
|
||||
```typescript
|
||||
interface Constructable<T> {
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
function create<T>(Constructor: Constructable<T>): T {
|
||||
return new Constructor();
|
||||
}
|
||||
|
||||
function createWithArgs<T, Args extends any[]>(
|
||||
Constructor: new (...args: Args) => T,
|
||||
...args: Args
|
||||
): T {
|
||||
return new Constructor(...args);
|
||||
}
|
||||
|
||||
class User {
|
||||
constructor(public name: string, public age: number) {}
|
||||
}
|
||||
|
||||
const user1 = create(User);
|
||||
const user2 = createWithArgs(User, "Alice", 30);
|
||||
|
||||
function inject<T>(
|
||||
Constructor: Constructable<T>,
|
||||
dependencies: Map<Constructable<any>, any>
|
||||
): T {
|
||||
const args = getDependencies(Constructor, dependencies);
|
||||
return new Constructor(...args);
|
||||
}
|
||||
```
|
||||
|
||||
## Mapped Type Transformations
|
||||
|
||||
Complex property transformations using mapped types:
|
||||
|
||||
```typescript
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
||||
};
|
||||
|
||||
interface Person {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
type PersonGetters = Getters<Person>;
|
||||
|
||||
type Setters<T> = {
|
||||
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
|
||||
};
|
||||
|
||||
type PersonSetters = Setters<Person>;
|
||||
|
||||
type ReadonlyKeys<T> = {
|
||||
[K in keyof T]-?: T[K] extends { readonly [key: string]: any }
|
||||
? K
|
||||
: never;
|
||||
}[keyof T];
|
||||
|
||||
type WritableKeys<T> = {
|
||||
[K in keyof T]-?: T[K] extends { readonly [key: string]: any }
|
||||
? never
|
||||
: K;
|
||||
}[keyof T];
|
||||
```
|
||||
|
||||
## Type-Safe Event Handlers
|
||||
|
||||
Generic event handler system with strict typing:
|
||||
|
||||
```typescript
|
||||
interface EventHandlers<T> {
|
||||
onCreate?: (item: T) => void;
|
||||
onUpdate?: (item: T, changes: Partial<T>) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
}
|
||||
|
||||
class Store<T extends { id: string }> {
|
||||
private items = new Map<string, T>();
|
||||
private handlers: EventHandlers<T> = {};
|
||||
|
||||
setHandlers(handlers: EventHandlers<T>): void {
|
||||
this.handlers = handlers;
|
||||
}
|
||||
|
||||
create(item: T): void {
|
||||
this.items.set(item.id, item);
|
||||
this.handlers.onCreate?.(item);
|
||||
}
|
||||
|
||||
update(id: string, changes: Partial<T>): void {
|
||||
const item = this.items.get(id);
|
||||
if (item) {
|
||||
const updated = { ...item, ...changes };
|
||||
this.items.set(id, updated);
|
||||
this.handlers.onUpdate?.(updated, changes);
|
||||
}
|
||||
}
|
||||
|
||||
delete(id: string): void {
|
||||
this.items.delete(id);
|
||||
this.handlers.onDelete?.(id);
|
||||
}
|
||||
}
|
||||
|
||||
interface Todo {
|
||||
id: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
const todoStore = new Store<Todo>();
|
||||
|
||||
todoStore.setHandlers({
|
||||
onCreate: (todo) => console.log("Created:", todo.title),
|
||||
onUpdate: (todo, changes) => console.log("Updated:", changes),
|
||||
onDelete: (id) => console.log("Deleted:", id)
|
||||
});
|
||||
```
|
||||
366
skills/using-runtime-checks/SKILL.md
Normal file
366
skills/using-runtime-checks/SKILL.md
Normal file
@@ -0,0 +1,366 @@
|
||||
---
|
||||
name: using-runtime-checks
|
||||
description: Teaches how to validate external data at runtime using Zod and other validation libraries in TypeScript. Use when working with APIs, JSON parsing, user input, or any external data source where runtime validation is needed.
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
<role>
|
||||
This skill teaches runtime validation patterns using libraries like Zod to bridge the gap between TypeScript's compile-time types and runtime realities. Critical for preventing runtime errors from unvalidated external data.
|
||||
</role>
|
||||
|
||||
<when-to-activate>
|
||||
This skill activates when:
|
||||
|
||||
- Working with API responses or external data
|
||||
- Parsing JSON from unknown sources
|
||||
- Handling user input or form data
|
||||
- Integrating with third-party services
|
||||
- User mentions validation, Zod, io-ts, runtime checks, or external data
|
||||
</when-to-activate>
|
||||
|
||||
<overview>
|
||||
**Critical Insight**: TypeScript types are erased at compile time. At runtime, you have no type safety for external data.
|
||||
|
||||
```typescript
|
||||
const data: User = await fetch("/api/user").then(r => r.json());
|
||||
```
|
||||
|
||||
This compiles, but if the API returns `{ username: string }` instead of `{ name: string }`, your code crashes at runtime.
|
||||
|
||||
**Solution**: Runtime validation libraries that:
|
||||
1. Validate data structure at runtime
|
||||
2. Provide TypeScript types automatically
|
||||
3. Generate helpful error messages
|
||||
|
||||
**Recommended Library**: Zod (modern, TypeScript-first, best DX)
|
||||
</overview>
|
||||
|
||||
<workflow>
|
||||
## Runtime Validation Flow
|
||||
|
||||
**Step 1: Define Schema**
|
||||
|
||||
Create a schema describing the expected shape:
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
age: z.number().int().positive()
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Extract TypeScript Type**
|
||||
|
||||
```typescript
|
||||
type User = z.infer<typeof UserSchema>;
|
||||
```
|
||||
|
||||
**Step 3: Validate at Runtime**
|
||||
|
||||
```typescript
|
||||
const data = await fetch("/api/user").then(r => r.json());
|
||||
|
||||
const result = UserSchema.safeParse(data);
|
||||
|
||||
if (result.success) {
|
||||
const user: User = result.data;
|
||||
console.log(user.name);
|
||||
} else {
|
||||
console.error("Validation failed:", result.error);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Handle Validation Errors**
|
||||
|
||||
```typescript
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues.map(issue => ({
|
||||
path: issue.path.join("."),
|
||||
message: issue.message
|
||||
}));
|
||||
throw new ValidationError("Invalid user data", issues);
|
||||
}
|
||||
```
|
||||
</workflow>
|
||||
|
||||
<examples>
|
||||
## Example 1: API Response Validation
|
||||
|
||||
**Setup**
|
||||
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
|
||||
const UserSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
age: z.number().int().min(0).max(150),
|
||||
role: z.enum(["admin", "user", "guest"]),
|
||||
createdAt: z.string().datetime()
|
||||
});
|
||||
|
||||
type User = z.infer<typeof UserSchema>;
|
||||
```
|
||||
|
||||
**Validation**
|
||||
|
||||
```typescript
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data: unknown = await response.json();
|
||||
|
||||
const result = UserSchema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`Invalid user data: ${result.error.message}`);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Form Input Validation
|
||||
|
||||
```typescript
|
||||
const LoginFormSchema = z.object({
|
||||
email: z.string().email("Invalid email address"),
|
||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
||||
rememberMe: z.boolean().optional()
|
||||
});
|
||||
|
||||
type LoginForm = z.infer<typeof LoginFormSchema>;
|
||||
|
||||
function validateLoginForm(formData: unknown): LoginForm {
|
||||
return LoginFormSchema.parse(formData);
|
||||
}
|
||||
|
||||
const form = {
|
||||
email: "user@example.com",
|
||||
password: "securepassword123"
|
||||
};
|
||||
|
||||
try {
|
||||
const validated = validateLoginForm(form);
|
||||
await login(validated);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
error.issues.forEach(issue => {
|
||||
console.error(`${issue.path.join(".")}: ${issue.message}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 3: Nested Object Validation
|
||||
|
||||
```typescript
|
||||
const AddressSchema = z.object({
|
||||
street: z.string(),
|
||||
city: z.string(),
|
||||
state: z.string().length(2),
|
||||
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/)
|
||||
});
|
||||
|
||||
const UserWithAddressSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
address: AddressSchema,
|
||||
billingAddress: AddressSchema.optional()
|
||||
});
|
||||
|
||||
type UserWithAddress = z.infer<typeof UserWithAddressSchema>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 4: Array Validation
|
||||
|
||||
```typescript
|
||||
const TagSchema = z.string().min(1).max(20);
|
||||
|
||||
const PostSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string().min(1).max(200),
|
||||
content: z.string(),
|
||||
tags: z.array(TagSchema).min(1).max(10),
|
||||
metadata: z.record(z.string(), z.unknown())
|
||||
});
|
||||
|
||||
type Post = z.infer<typeof PostSchema>;
|
||||
|
||||
async function fetchPosts(): Promise<Post[]> {
|
||||
const response = await fetch("/api/posts");
|
||||
const data: unknown = await response.json();
|
||||
|
||||
const PostsSchema = z.array(PostSchema);
|
||||
return PostsSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 5: Advanced Patterns
|
||||
|
||||
**Union Types**: Use discriminated unions for API responses with success/error states.
|
||||
|
||||
**Transforms**: Convert strings to dates or coerce query parameters to numbers.
|
||||
|
||||
**Refinements**: Add custom validation logic for complex business rules (e.g., password strength).
|
||||
|
||||
**Partial Schemas**: Create update schemas from full schemas using `.partial()`.
|
||||
|
||||
See `references/zod-patterns.md` for complete examples of unions, transforms, refinements, and partial schemas.
|
||||
</examples>
|
||||
|
||||
<progressive-disclosure>
|
||||
## Reference Files
|
||||
|
||||
For complete patterns see `references/`:
|
||||
|
||||
- `references/zod-patterns.md` - Complete Zod API reference
|
||||
- `references/error-handling.md` - Validation error handling strategies
|
||||
- `references/performance.md` - Validation performance optimization
|
||||
|
||||
For related skills:
|
||||
|
||||
- **Type Guards**: Use the using-type-guards skill for manual type narrowing
|
||||
- **Unknown vs Any**: Use the avoiding-any-types skill for why validation is needed
|
||||
- **External Data**: Use the validating-external-data skill for specific data source patterns
|
||||
|
||||
**Cross-Plugin References:**
|
||||
|
||||
- If constructing Zod schemas for runtime validation, use the validating-schema-basics skill for type-safe Zod v4 schema patterns
|
||||
- If handling validation errors, use the customizing-errors skill for error formatting and custom messages
|
||||
</progressive-disclosure>
|
||||
|
||||
<constraints>
|
||||
**MUST:**
|
||||
|
||||
- Validate all external data at runtime (APIs, JSON, user input)
|
||||
- Use `safeParse` for error handling, not `parse` (unless throwing is desired)
|
||||
- Handle validation errors gracefully with user-friendly messages
|
||||
- Validate before type assertions
|
||||
|
||||
**SHOULD:**
|
||||
|
||||
- Use Zod for new projects (best TypeScript integration)
|
||||
- Define schemas close to usage
|
||||
- Reuse schemas for related structures
|
||||
- Transform data during validation when beneficial
|
||||
|
||||
**NEVER:**
|
||||
|
||||
- Trust external data without validation
|
||||
- Use type assertions (`as Type`) on unvalidated data
|
||||
- Ignore validation errors
|
||||
- Validate in multiple places (validate at boundaries)
|
||||
- Skip validation for "trusted" sources (trust issues change)
|
||||
</constraints>
|
||||
|
||||
<installation>
|
||||
## Installing Zod
|
||||
|
||||
```bash
|
||||
npm install zod
|
||||
|
||||
pnpm add zod
|
||||
|
||||
yarn add zod
|
||||
```
|
||||
|
||||
TypeScript configuration:
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
```
|
||||
</installation>
|
||||
|
||||
<validation>
|
||||
## Validation Implementation Checklist
|
||||
|
||||
**Schema Definition**: Match expected structure, use appropriate validators, add custom refinements.
|
||||
|
||||
**Error Handling**: Use `safeParse`, log errors with context, provide user-friendly messages.
|
||||
|
||||
**Type Safety**: Derive types with `z.infer`, avoid manual assertions after validation.
|
||||
|
||||
**Performance**: Reuse schemas, validate at boundaries only, avoid redundant checks.
|
||||
|
||||
**Testing**: Test valid data, invalid data, and edge cases.
|
||||
|
||||
See `references/error-handling.md` for error handling patterns and `references/performance.md` for optimization techniques.
|
||||
</validation>
|
||||
|
||||
<alternatives>
|
||||
## Other Validation Libraries
|
||||
|
||||
**io-ts** (functional programming style)
|
||||
```typescript
|
||||
import * as t from "io-ts";
|
||||
|
||||
const UserCodec = t.type({
|
||||
id: t.string,
|
||||
name: t.string,
|
||||
email: t.string
|
||||
});
|
||||
|
||||
type User = t.TypeOf<typeof UserCodec>;
|
||||
```
|
||||
|
||||
**Yup** (common with Formik)
|
||||
```typescript
|
||||
import * as yup from "yup";
|
||||
|
||||
const userSchema = yup.object({
|
||||
name: yup.string().required(),
|
||||
email: yup.string().email().required()
|
||||
});
|
||||
```
|
||||
|
||||
**AJV** (JSON Schema)
|
||||
```typescript
|
||||
import Ajv from "ajv";
|
||||
|
||||
const ajv = new Ajv();
|
||||
const validate = ajv.compile({
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
email: { type: "string", format: "email" }
|
||||
},
|
||||
required: ["name", "email"]
|
||||
});
|
||||
```
|
||||
|
||||
**Recommendation**: Use Zod for new TypeScript projects. Best DX and type inference.
|
||||
</alternatives>
|
||||
|
||||
<common-patterns>
|
||||
## Common Patterns
|
||||
|
||||
**Validation Middleware**: Validate request bodies in Express/framework handlers before processing.
|
||||
|
||||
**Safe JSON Parse**: Combine JSON.parse with schema validation for type-safe parsing.
|
||||
|
||||
**Configuration Validation**: Validate environment variables and config at startup.
|
||||
|
||||
See `references/zod-patterns.md` for complete implementations and additional patterns.
|
||||
</common-patterns>
|
||||
175
skills/using-runtime-checks/references/error-handling.md
Normal file
175
skills/using-runtime-checks/references/error-handling.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Validation Error Handling
|
||||
|
||||
## Basic Error Handling
|
||||
|
||||
### Using safeParse
|
||||
|
||||
```typescript
|
||||
const result = UserSchema.safeParse(data);
|
||||
|
||||
if (result.success) {
|
||||
const user: User = result.data;
|
||||
console.log(user.name);
|
||||
} else {
|
||||
console.error("Validation failed:", result.error);
|
||||
}
|
||||
```
|
||||
|
||||
### Using parse (throwing)
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const user = UserSchema.parse(data);
|
||||
console.log(user.name);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error("Validation failed:", error.issues);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Processing Validation Issues
|
||||
|
||||
### Extracting Error Details
|
||||
|
||||
```typescript
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues.map(issue => ({
|
||||
path: issue.path.join("."),
|
||||
message: issue.message,
|
||||
code: issue.code
|
||||
}));
|
||||
|
||||
throw new ValidationError("Invalid user data", issues);
|
||||
}
|
||||
```
|
||||
|
||||
### User-Friendly Error Messages
|
||||
|
||||
```typescript
|
||||
function formatValidationError(error: z.ZodError): string {
|
||||
return error.issues
|
||||
.map(issue => {
|
||||
const field = issue.path.join(".");
|
||||
return `${field}: ${issue.message}`;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
try {
|
||||
const validated = validateLoginForm(form);
|
||||
await login(validated);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const message = formatValidationError(error);
|
||||
showError(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Recovery Strategies
|
||||
|
||||
### Fallback Values
|
||||
|
||||
```typescript
|
||||
const result = ConfigSchema.safeParse(data);
|
||||
|
||||
const config = result.success
|
||||
? result.data
|
||||
: getDefaultConfig();
|
||||
```
|
||||
|
||||
### Partial Success
|
||||
|
||||
```typescript
|
||||
function validateItemsWithPartialSuccess<T>(
|
||||
items: unknown[],
|
||||
schema: z.ZodType<T>
|
||||
): { valid: T[], invalid: { item: unknown, error: z.ZodError }[] } {
|
||||
const valid: T[] = [];
|
||||
const invalid: { item: unknown, error: z.ZodError }[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const result = schema.safeParse(item);
|
||||
if (result.success) {
|
||||
valid.push(result.data);
|
||||
} else {
|
||||
invalid.push({ item, error: result.error });
|
||||
}
|
||||
}
|
||||
|
||||
return { valid, invalid };
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Error Classes
|
||||
|
||||
```typescript
|
||||
class ValidationError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly issues: Array<{ path: string; message: string }>
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ValidationError";
|
||||
}
|
||||
}
|
||||
|
||||
function validateOrThrow<T>(data: unknown, schema: z.ZodType<T>): T {
|
||||
const result = schema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues.map(issue => ({
|
||||
path: issue.path.join("."),
|
||||
message: issue.message
|
||||
}));
|
||||
throw new ValidationError("Validation failed", issues);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
```
|
||||
|
||||
## Logging Validation Errors
|
||||
|
||||
```typescript
|
||||
function validateWithLogging<T>(
|
||||
data: unknown,
|
||||
schema: z.ZodType<T>,
|
||||
context: string
|
||||
): T | null {
|
||||
const result = schema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
logger.error({
|
||||
context,
|
||||
error: "Validation failed",
|
||||
issues: result.error.issues,
|
||||
data: sanitizeForLogging(data)
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
```
|
||||
|
||||
## API Error Responses
|
||||
|
||||
```typescript
|
||||
app.post("/api/users", (req, res) => {
|
||||
const result = UserSchema.safeParse(req.body);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid request body",
|
||||
details: result.error.issues.map(issue => ({
|
||||
field: issue.path.join("."),
|
||||
message: issue.message
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
const user = result.data;
|
||||
});
|
||||
```
|
||||
211
skills/using-runtime-checks/references/performance.md
Normal file
211
skills/using-runtime-checks/references/performance.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# Validation Performance Optimization
|
||||
|
||||
## Schema Reuse
|
||||
|
||||
**BAD: Creating schemas repeatedly**
|
||||
```typescript
|
||||
function validateUser(data: unknown) {
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
});
|
||||
return UserSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
**GOOD: Reusing schemas**
|
||||
```typescript
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
});
|
||||
|
||||
function validateUser(data: unknown) {
|
||||
return UserSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
## Boundary Validation
|
||||
|
||||
Validate once at system boundaries, not internally:
|
||||
|
||||
```typescript
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
const data: unknown = await response.json();
|
||||
|
||||
return UserSchema.parse(data);
|
||||
}
|
||||
|
||||
function processUser(user: User) {
|
||||
const fullName = formatName(user);
|
||||
return fullName;
|
||||
}
|
||||
```
|
||||
|
||||
## Lazy Schema Definition
|
||||
|
||||
For schemas with circular dependencies:
|
||||
|
||||
```typescript
|
||||
const CategorySchema: z.ZodType<Category> = z.lazy(() => z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
parent: CategorySchema.optional()
|
||||
}));
|
||||
```
|
||||
|
||||
## Selective Validation
|
||||
|
||||
Validate only what's needed:
|
||||
|
||||
```typescript
|
||||
const FullUserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
profile: z.object({
|
||||
bio: z.string(),
|
||||
avatar: z.string().url()
|
||||
}),
|
||||
settings: z.object({
|
||||
theme: z.enum(["light", "dark"]),
|
||||
notifications: z.boolean()
|
||||
})
|
||||
});
|
||||
|
||||
const BasicUserSchema = FullUserSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
email: true
|
||||
});
|
||||
```
|
||||
|
||||
## Batch Validation
|
||||
|
||||
For arrays, validate the array once rather than each item:
|
||||
|
||||
```typescript
|
||||
const UsersSchema = z.array(UserSchema);
|
||||
|
||||
async function fetchAllUsers(): Promise<User[]> {
|
||||
const response = await fetch("/api/users");
|
||||
const data: unknown = await response.json();
|
||||
|
||||
return UsersSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
## Early Validation
|
||||
|
||||
Fail fast on critical fields:
|
||||
|
||||
```typescript
|
||||
const CriticalFirstSchema = z.object({
|
||||
apiKey: z.string().uuid(),
|
||||
timestamp: z.number()
|
||||
}).strict();
|
||||
|
||||
const FullRequestSchema = CriticalFirstSchema.extend({
|
||||
payload: z.unknown()
|
||||
});
|
||||
```
|
||||
|
||||
## Caching Parsed Results
|
||||
|
||||
```typescript
|
||||
const schemaCache = new WeakMap<object, User>();
|
||||
|
||||
function getCachedUser(data: unknown): User {
|
||||
if (typeof data === "object" && data !== null) {
|
||||
const cached = schemaCache.get(data);
|
||||
if (cached) return cached;
|
||||
}
|
||||
|
||||
const user = UserSchema.parse(data);
|
||||
|
||||
if (typeof data === "object" && data !== null) {
|
||||
schemaCache.set(data, user);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
## Avoiding Redundant Validation
|
||||
|
||||
```typescript
|
||||
class UserService {
|
||||
private validated = new WeakSet<object>();
|
||||
|
||||
async processUser(data: unknown): Promise<void> {
|
||||
if (typeof data !== "object" || data === null) {
|
||||
throw new Error("Invalid data");
|
||||
}
|
||||
|
||||
if (!this.validated.has(data)) {
|
||||
UserSchema.parse(data);
|
||||
this.validated.add(data);
|
||||
}
|
||||
|
||||
const user = data as User;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
```typescript
|
||||
async function validateWithMetrics<T>(
|
||||
data: unknown,
|
||||
schema: z.ZodType<T>,
|
||||
name: string
|
||||
): Promise<T> {
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
const result = schema.parse(data);
|
||||
const duration = performance.now() - start;
|
||||
|
||||
metrics.record(`validation.${name}.success`, duration);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = performance.now() - start;
|
||||
metrics.record(`validation.${name}.failure`, duration);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Alternative Libraries for Performance
|
||||
|
||||
For maximum performance in hot paths:
|
||||
|
||||
**TypeBox** (compile-time optimized):
|
||||
```typescript
|
||||
import { Type, Static } from "@sinclair/typebox";
|
||||
import { Value } from "@sinclair/typebox/value";
|
||||
|
||||
const UserType = Type.Object({
|
||||
id: Type.String(),
|
||||
name: Type.String(),
|
||||
email: Type.String({ format: "email" })
|
||||
});
|
||||
|
||||
type User = Static<typeof UserType>;
|
||||
|
||||
const user = Value.Parse(UserType, data);
|
||||
```
|
||||
|
||||
**Valibot** (smaller bundle size):
|
||||
```typescript
|
||||
import * as v from "valibot";
|
||||
|
||||
const UserSchema = v.object({
|
||||
id: v.string(),
|
||||
name: v.string(),
|
||||
email: v.pipe(v.string(), v.email())
|
||||
});
|
||||
|
||||
type User = v.InferOutput<typeof UserSchema>;
|
||||
```
|
||||
241
skills/using-runtime-checks/references/zod-patterns.md
Normal file
241
skills/using-runtime-checks/references/zod-patterns.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Zod Patterns Reference
|
||||
|
||||
## Advanced Schema Composition
|
||||
|
||||
### Union Types
|
||||
|
||||
```typescript
|
||||
const SuccessResponseSchema = z.object({
|
||||
status: z.literal("success"),
|
||||
data: z.unknown()
|
||||
});
|
||||
|
||||
const ErrorResponseSchema = z.object({
|
||||
status: z.literal("error"),
|
||||
error: z.string(),
|
||||
code: z.number()
|
||||
});
|
||||
|
||||
const ApiResponseSchema = z.discriminatedUnion("status", [
|
||||
SuccessResponseSchema,
|
||||
ErrorResponseSchema
|
||||
]);
|
||||
|
||||
type ApiResponse = z.infer<typeof ApiResponseSchema>;
|
||||
```
|
||||
|
||||
### Transform and Coerce
|
||||
|
||||
```typescript
|
||||
const DateSchema = z.string().datetime().transform(str => new Date(str));
|
||||
|
||||
const UserWithDatesSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
createdAt: DateSchema,
|
||||
updatedAt: DateSchema
|
||||
});
|
||||
|
||||
type UserWithDates = z.infer<typeof UserWithDatesSchema>;
|
||||
|
||||
const CoerceNumberSchema = z.coerce.number();
|
||||
|
||||
const QueryParamsSchema = z.object({
|
||||
page: CoerceNumberSchema.int().positive().default(1),
|
||||
limit: CoerceNumberSchema.int().min(1).max(100).default(20),
|
||||
sort: z.enum(["asc", "desc"]).default("asc")
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Refinements
|
||||
|
||||
```typescript
|
||||
const PasswordSchema = z
|
||||
.string()
|
||||
.min(8)
|
||||
.refine(
|
||||
password => /[A-Z]/.test(password),
|
||||
"Password must contain at least one uppercase letter"
|
||||
)
|
||||
.refine(
|
||||
password => /[a-z]/.test(password),
|
||||
"Password must contain at least one lowercase letter"
|
||||
)
|
||||
.refine(
|
||||
password => /[0-9]/.test(password),
|
||||
"Password must contain at least one number"
|
||||
);
|
||||
|
||||
const SignupSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: PasswordSchema,
|
||||
confirmPassword: z.string()
|
||||
}).refine(
|
||||
data => data.password === data.confirmPassword,
|
||||
{
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"]
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Partial and Optional Schemas
|
||||
|
||||
```typescript
|
||||
const UpdateUserSchema = UserSchema.partial();
|
||||
|
||||
type UpdateUser = z.infer<typeof UpdateUserSchema>;
|
||||
|
||||
async function updateUser(id: string, updates: unknown): Promise<User> {
|
||||
const validated = UpdateUserSchema.parse(updates);
|
||||
|
||||
const response = await fetch(`/api/users/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(validated)
|
||||
});
|
||||
|
||||
return UserSchema.parse(await response.json());
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Objects
|
||||
|
||||
```typescript
|
||||
const AddressSchema = z.object({
|
||||
street: z.string(),
|
||||
city: z.string(),
|
||||
state: z.string().length(2),
|
||||
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/)
|
||||
});
|
||||
|
||||
const UserWithAddressSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
address: AddressSchema,
|
||||
billingAddress: AddressSchema.optional()
|
||||
});
|
||||
|
||||
type UserWithAddress = z.infer<typeof UserWithAddressSchema>;
|
||||
```
|
||||
|
||||
### Array Validation
|
||||
|
||||
```typescript
|
||||
const TagSchema = z.string().min(1).max(20);
|
||||
|
||||
const PostSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string().min(1).max(200),
|
||||
content: z.string(),
|
||||
tags: z.array(TagSchema).min(1).max(10),
|
||||
metadata: z.record(z.string(), z.unknown())
|
||||
});
|
||||
|
||||
type Post = z.infer<typeof PostSchema>;
|
||||
|
||||
async function fetchPosts(): Promise<Post[]> {
|
||||
const response = await fetch("/api/posts");
|
||||
const data: unknown = await response.json();
|
||||
|
||||
const PostsSchema = z.array(PostSchema);
|
||||
return PostsSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
## Generic Validation Helpers
|
||||
|
||||
```typescript
|
||||
async function apiCall<T>(
|
||||
endpoint: string,
|
||||
dataSchema: z.ZodType<T>
|
||||
): Promise<T> {
|
||||
const response = await fetch(endpoint);
|
||||
const rawData: unknown = await response.json();
|
||||
|
||||
const apiResponse = ApiResponseSchema.parse(rawData);
|
||||
|
||||
if (apiResponse.status === "error") {
|
||||
throw new Error(`API Error ${apiResponse.code}: ${apiResponse.error}`);
|
||||
}
|
||||
|
||||
return dataSchema.parse(apiResponse.data);
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Middleware
|
||||
|
||||
```typescript
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
function validateBody<T>(schema: z.ZodType<T>) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const result = schema.safeParse(req.body);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({
|
||||
error: "Validation failed",
|
||||
issues: result.error.issues
|
||||
});
|
||||
}
|
||||
|
||||
req.body = result.data;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
app.post("/users", validateBody(UserSchema), (req, res) => {
|
||||
const user: User = req.body;
|
||||
});
|
||||
```
|
||||
|
||||
## Safe JSON Parse
|
||||
|
||||
```typescript
|
||||
function safeJsonParse<T>(
|
||||
json: string,
|
||||
schema: z.ZodType<T>
|
||||
): T {
|
||||
try {
|
||||
const data: unknown = JSON.parse(json);
|
||||
return schema.parse(data);
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new Error("Invalid JSON");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Validation
|
||||
|
||||
```typescript
|
||||
const ConfigSchema = z.object({
|
||||
port: z.number().int().positive().default(3000),
|
||||
database: z.object({
|
||||
host: z.string(),
|
||||
port: z.number().int().positive(),
|
||||
name: z.string()
|
||||
}),
|
||||
redis: z.object({
|
||||
url: z.string().url()
|
||||
}).optional()
|
||||
});
|
||||
|
||||
type Config = z.infer<typeof ConfigSchema>;
|
||||
|
||||
function loadConfig(): Config {
|
||||
const data: unknown = process.env;
|
||||
|
||||
return ConfigSchema.parse({
|
||||
port: data.PORT,
|
||||
database: {
|
||||
host: data.DB_HOST,
|
||||
port: data.DB_PORT,
|
||||
name: data.DB_NAME
|
||||
},
|
||||
redis: data.REDIS_URL ? { url: data.REDIS_URL } : undefined
|
||||
});
|
||||
}
|
||||
```
|
||||
383
skills/using-type-guards/SKILL.md
Normal file
383
skills/using-type-guards/SKILL.md
Normal file
@@ -0,0 +1,383 @@
|
||||
---
|
||||
name: using-type-guards
|
||||
description: Teaches how to write custom type guards with type predicates and use built-in type narrowing in TypeScript. Use when working with unknown types, union types, validating external data, or implementing type-safe runtime checks.
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
<role>
|
||||
This skill teaches how to safely narrow types at runtime using type guards, enabling type-safe handling of dynamic data and union types. Critical for validating external data and preventing runtime errors.
|
||||
</role>
|
||||
|
||||
<when-to-activate>
|
||||
This skill activates when:
|
||||
|
||||
- Working with `unknown` or `any` types
|
||||
- Handling union types that need discrimination
|
||||
- Validating external data (APIs, user input, JSON)
|
||||
- Implementing runtime type checks
|
||||
- User mentions type guards, type narrowing, type predicates, or runtime validation
|
||||
</when-to-activate>
|
||||
|
||||
<overview>
|
||||
Type guards are runtime checks that inform TypeScript's type system about the actual type of a value. They bridge the gap between compile-time types and runtime values.
|
||||
|
||||
**Three Categories**:
|
||||
|
||||
1. **Built-in Guards**: `typeof`, `instanceof`, `in`, `Array.isArray()`
|
||||
2. **Type Predicates**: Custom functions returning `value is Type`
|
||||
3. **Assertion Functions**: Functions that throw if type check fails
|
||||
|
||||
**Key Pattern**: Runtime check → Type narrowing → Safe access
|
||||
</overview>
|
||||
|
||||
<workflow>
|
||||
## Type Guard Selection Flow
|
||||
|
||||
**Step 1: Identify the Type Challenge**
|
||||
|
||||
What do you need to narrow?
|
||||
- Primitive types → Use `typeof`
|
||||
- Class instances → Use `instanceof`
|
||||
- Object shapes → Use `in` or custom type predicate
|
||||
- Array types → Use `Array.isArray()` + element checks
|
||||
- Complex structures → Use validation library (Zod, io-ts)
|
||||
|
||||
**Step 2: Choose Guard Strategy**
|
||||
|
||||
**Built-in Guards** (primitives, classes, simple checks)
|
||||
```typescript
|
||||
typeof value === "string"
|
||||
value instanceof Error
|
||||
"property" in value
|
||||
Array.isArray(value)
|
||||
```
|
||||
|
||||
**Custom Type Predicates** (object shapes, complex logic)
|
||||
```typescript
|
||||
function isUser(value: unknown): value is User {
|
||||
return typeof value === "object" &&
|
||||
value !== null &&
|
||||
"id" in value;
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Libraries** (nested structures, multiple fields)
|
||||
```typescript
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
email: z.string().email()
|
||||
});
|
||||
```
|
||||
|
||||
**Step 3: Implement Guard**
|
||||
|
||||
1. Check for `null` and `undefined` first
|
||||
2. Check base type (object, array, primitive)
|
||||
3. Check structure (properties exist)
|
||||
4. Check property types
|
||||
5. Return type predicate or throw
|
||||
</workflow>
|
||||
|
||||
<examples>
|
||||
## Example 1: Built-in Type Guards
|
||||
|
||||
### typeof Guard
|
||||
|
||||
```typescript
|
||||
function processValue(value: unknown): string {
|
||||
if (typeof value === "string") {
|
||||
return value.toUpperCase();
|
||||
}
|
||||
|
||||
if (typeof value === "number") {
|
||||
return value.toFixed(2);
|
||||
}
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "yes" : "no";
|
||||
}
|
||||
|
||||
throw new Error("Unsupported type");
|
||||
}
|
||||
```
|
||||
|
||||
**typeof Guards Work For**:
|
||||
- `"string"`, `"number"`, `"boolean"`, `"symbol"`, `"undefined"`
|
||||
- `"function"`, `"bigint"`
|
||||
- `"object"` (but also matches `null` - check for `null` first!)
|
||||
|
||||
### instanceof Guard
|
||||
|
||||
```typescript
|
||||
function handleError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (error instanceof CustomError) {
|
||||
return `Custom error: ${error.code}`;
|
||||
}
|
||||
|
||||
return String(error);
|
||||
}
|
||||
```
|
||||
|
||||
**instanceof Works For**:
|
||||
- Class instances
|
||||
- Built-in classes (Error, Date, Map, Set, etc.)
|
||||
- DOM elements (HTMLElement, etc.)
|
||||
|
||||
### in Guard
|
||||
|
||||
```typescript
|
||||
type Circle = { kind: "circle"; radius: number };
|
||||
type Square = { kind: "square"; sideLength: number };
|
||||
type Shape = Circle | Square;
|
||||
|
||||
function getArea(shape: Shape): number {
|
||||
if ("radius" in shape) {
|
||||
return Math.PI * shape.radius ** 2;
|
||||
} else {
|
||||
return shape.sideLength ** 2;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**in Guard Best For**:
|
||||
- Discriminating union types
|
||||
- Checking optional properties
|
||||
- Object shape validation
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Custom Type Predicates
|
||||
|
||||
### Basic Type Predicate
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
function isUser(value: unknown): value is User {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"id" in value &&
|
||||
"name" in value &&
|
||||
"email" in value &&
|
||||
typeof (value as User).id === "string" &&
|
||||
typeof (value as User).name === "string" &&
|
||||
typeof (value as User).email === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function processUser(data: unknown): void {
|
||||
if (isUser(data)) {
|
||||
console.log(data.name);
|
||||
} else {
|
||||
throw new Error("Invalid user data");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Array Type Predicate
|
||||
|
||||
```typescript
|
||||
function isStringArray(value: unknown): value is string[] {
|
||||
return Array.isArray(value) && value.every(item => typeof item === "string");
|
||||
}
|
||||
|
||||
function processNames(data: unknown): string {
|
||||
if (isStringArray(data)) {
|
||||
return data.join(", ");
|
||||
}
|
||||
throw new Error("Expected array of strings");
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Type Predicate
|
||||
|
||||
For complex nested structures, compose guards from simpler guards. See `references/nested-validation.md` for detailed examples.
|
||||
|
||||
---
|
||||
|
||||
## Example 3: Discriminated Unions
|
||||
|
||||
### Using Literal Type Discrimination
|
||||
|
||||
```typescript
|
||||
type Success = {
|
||||
status: "success";
|
||||
data: string;
|
||||
};
|
||||
|
||||
type Failure = {
|
||||
status: "error";
|
||||
error: string;
|
||||
};
|
||||
|
||||
type Result = Success | Failure;
|
||||
|
||||
function handleResult(result: Result): void {
|
||||
if (result.status === "success") {
|
||||
console.log("Data:", result.data);
|
||||
} else {
|
||||
console.error("Error:", result.error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Runtime Validation of Discriminated Union
|
||||
|
||||
```typescript
|
||||
function isSuccess(value: unknown): value is Success {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"status" in value &&
|
||||
value.status === "success" &&
|
||||
"data" in value &&
|
||||
typeof (value as Success).data === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function isFailure(value: unknown): value is Failure {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"status" in value &&
|
||||
value.status === "error" &&
|
||||
"error" in value &&
|
||||
typeof (value as Failure).error === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function isResult(value: unknown): value is Result {
|
||||
return isSuccess(value) || isFailure(value);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 4: Assertion Functions
|
||||
|
||||
```typescript
|
||||
function assertIsString(value: unknown): asserts value is string {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(`Expected string, got ${typeof value}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertIsUser(value: unknown): asserts value is User {
|
||||
if (!isUser(value)) {
|
||||
throw new Error("Invalid user data");
|
||||
}
|
||||
}
|
||||
|
||||
function processData(data: unknown): void {
|
||||
assertIsUser(data);
|
||||
|
||||
console.log(data.name);
|
||||
}
|
||||
```
|
||||
|
||||
**Assertion Functions**:
|
||||
- Throw error if check fails
|
||||
- Narrow type for remainder of scope
|
||||
- Use `asserts value is Type` return type
|
||||
- Good for precondition checks
|
||||
</examples>
|
||||
|
||||
<progressive-disclosure>
|
||||
## Reference Files
|
||||
|
||||
**Local References** (in `references/` directory):
|
||||
- `advanced-patterns.md` - Optional properties, arrays, tuples, records, enums
|
||||
- `nested-validation.md` - Complex nested object validation patterns
|
||||
- `testing-guide.md` - Complete unit testing guide with examples
|
||||
|
||||
**Related Skills**:
|
||||
- **Runtime Validation Libraries**: Use the using-runtime-checks skill for Zod/io-ts patterns
|
||||
- **Unknown Type Handling**: Use the avoiding-any-types skill for when to use type guards
|
||||
- **Error Type Guards**: Error-specific type guard patterns can be found in error handling documentation
|
||||
</progressive-disclosure>
|
||||
|
||||
<constraints>
|
||||
**MUST:**
|
||||
|
||||
- Check for `null` and `undefined` before property access
|
||||
- Check object base type before using `in` operator
|
||||
- Use type predicates (`value is Type`) for reusable guards
|
||||
- Validate all required properties exist
|
||||
- Validate property types, not just existence
|
||||
|
||||
**SHOULD:**
|
||||
|
||||
- Compose simple guards into complex guards
|
||||
- Use discriminated unions for known variants
|
||||
- Prefer built-in guards over custom when possible
|
||||
- Name guard functions `isType` or `assertIsType`
|
||||
|
||||
**NEVER:**
|
||||
|
||||
- Access properties before type guard
|
||||
- Forget to check for `null` (typeof null === "object"!)
|
||||
- Use type assertions instead of type guards for external data
|
||||
- Assume property exists after checking with `in`
|
||||
- Skip validating nested object types
|
||||
</constraints>
|
||||
|
||||
<patterns>
|
||||
## Common Patterns
|
||||
|
||||
**Basic Patterns** (covered in examples above):
|
||||
- Built-in guards: typeof, instanceof, in, Array.isArray()
|
||||
- Custom type predicates for object shapes
|
||||
- Discriminated union narrowing
|
||||
- Assertion functions
|
||||
|
||||
**Advanced Patterns** (see `references/advanced-patterns.md`):
|
||||
- Optional property validation
|
||||
- Array element guards with generics
|
||||
- Tuple guards
|
||||
- Record guards
|
||||
- Enum guards
|
||||
</patterns>
|
||||
|
||||
<validation>
|
||||
## Type Guard Quality Checklist
|
||||
|
||||
1. **Null Safety**:
|
||||
- [ ] Checks for `null` before using `in` or property access
|
||||
- [ ] Checks for `undefined` for optional values
|
||||
|
||||
2. **Complete Validation**:
|
||||
- [ ] Validates all required properties exist
|
||||
- [ ] Validates property types, not just existence
|
||||
- [ ] Validates nested objects recursively
|
||||
|
||||
3. **Type Predicate**:
|
||||
- [ ] Returns `value is Type` for reusable guards
|
||||
- [ ] Or uses `asserts value is Type` for assertion functions
|
||||
|
||||
4. **Edge Cases**:
|
||||
- [ ] Handles arrays correctly
|
||||
- [ ] Handles empty objects
|
||||
- [ ] Handles inherited properties if relevant
|
||||
|
||||
5. **Composition**:
|
||||
- [ ] Complex guards built from simpler guards
|
||||
- [ ] Guards are unit testable
|
||||
</validation>
|
||||
|
||||
<testing-guards>
|
||||
## Unit Testing Type Guards
|
||||
|
||||
Test all edge cases: valid inputs, missing properties, wrong types, null, undefined, and non-objects.
|
||||
|
||||
See `references/testing-guide.md` for complete testing strategies and examples.
|
||||
</testing-guards>
|
||||
97
skills/using-type-guards/references/advanced-patterns.md
Normal file
97
skills/using-type-guards/references/advanced-patterns.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Advanced Type Guard Patterns
|
||||
|
||||
This reference provides detailed examples of advanced type guard patterns.
|
||||
|
||||
## Pattern 1: Optional Property Guard
|
||||
|
||||
```typescript
|
||||
interface Config {
|
||||
apiKey: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
function isConfig(value: unknown): value is Config {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const obj = value as Record<string, unknown>;
|
||||
|
||||
if (typeof obj.apiKey !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("timeout" in obj && typeof obj.timeout !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 2: Array Element Guard
|
||||
|
||||
```typescript
|
||||
function everyElementIs<T>(
|
||||
arr: unknown[],
|
||||
guard: (item: unknown) => item is T
|
||||
): arr is T[] {
|
||||
return arr.every(guard);
|
||||
}
|
||||
|
||||
const data: unknown = ["a", "b", "c"];
|
||||
|
||||
if (Array.isArray(data) && everyElementIs(data, (item): item is string => typeof item === "string")) {
|
||||
const lengths = data.map(str => str.length);
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 3: Tuple Guard
|
||||
|
||||
```typescript
|
||||
function isTuple<T, U>(
|
||||
value: unknown,
|
||||
guard1: (item: unknown) => item is T,
|
||||
guard2: (item: unknown) => item is U
|
||||
): value is [T, U] {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.length === 2 &&
|
||||
guard1(value[0]) &&
|
||||
guard2(value[1])
|
||||
);
|
||||
}
|
||||
|
||||
const isStringNumberPair = (value: unknown): value is [string, number] =>
|
||||
isTuple(
|
||||
value,
|
||||
(item): item is string => typeof item === "string",
|
||||
(item): item is number => typeof item === "number"
|
||||
);
|
||||
```
|
||||
|
||||
## Pattern 4: Record Guard
|
||||
|
||||
```typescript
|
||||
function isStringRecord(value: unknown): value is Record<string, string> {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Object.values(value).every(val => typeof val === "string");
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern 5: Enum Guard
|
||||
|
||||
```typescript
|
||||
enum Status {
|
||||
Active = "active",
|
||||
Inactive = "inactive",
|
||||
Pending = "pending"
|
||||
}
|
||||
|
||||
function isStatus(value: unknown): value is Status {
|
||||
return Object.values(Status).includes(value as Status);
|
||||
}
|
||||
```
|
||||
110
skills/using-type-guards/references/nested-validation.md
Normal file
110
skills/using-type-guards/references/nested-validation.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Nested Type Guard Validation
|
||||
|
||||
Examples of validating complex nested object structures.
|
||||
|
||||
## Basic Nested Validation
|
||||
|
||||
```typescript
|
||||
interface Address {
|
||||
street: string;
|
||||
city: string;
|
||||
zipCode: string;
|
||||
}
|
||||
|
||||
interface UserWithAddress {
|
||||
id: string;
|
||||
name: string;
|
||||
address: Address;
|
||||
}
|
||||
|
||||
function isAddress(value: unknown): value is Address {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"street" in value &&
|
||||
"city" in value &&
|
||||
"zipCode" in value &&
|
||||
typeof (value as Address).street === "string" &&
|
||||
typeof (value as Address).city === "string" &&
|
||||
typeof (value as Address).zipCode === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function isUserWithAddress(value: unknown): value is UserWithAddress {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"id" in value &&
|
||||
"name" in value &&
|
||||
"address" in value &&
|
||||
typeof (value as UserWithAddress).id === "string" &&
|
||||
typeof (value as UserWithAddress).name === "string" &&
|
||||
isAddress((value as UserWithAddress).address)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Composable Guards Pattern
|
||||
|
||||
Build complex guards by composing simpler ones:
|
||||
|
||||
```typescript
|
||||
function hasStringProperty(obj: unknown, key: string): boolean {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
key in obj &&
|
||||
typeof (obj as Record<string, unknown>)[key] === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function isUserWithAddress(value: unknown): value is UserWithAddress {
|
||||
return (
|
||||
hasStringProperty(value, "id") &&
|
||||
hasStringProperty(value, "name") &&
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"address" in value &&
|
||||
isAddress((value as UserWithAddress).address)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Deep Nesting Example
|
||||
|
||||
```typescript
|
||||
interface Company {
|
||||
name: string;
|
||||
address: Address;
|
||||
}
|
||||
|
||||
interface Employee {
|
||||
id: string;
|
||||
name: string;
|
||||
company: Company;
|
||||
}
|
||||
|
||||
function isCompany(value: unknown): value is Company {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"name" in value &&
|
||||
"address" in value &&
|
||||
typeof (value as Company).name === "string" &&
|
||||
isAddress((value as Company).address)
|
||||
);
|
||||
}
|
||||
|
||||
function isEmployee(value: unknown): value is Employee {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"id" in value &&
|
||||
"name" in value &&
|
||||
"company" in value &&
|
||||
typeof (value as Employee).id === "string" &&
|
||||
typeof (value as Employee).name === "string" &&
|
||||
isCompany((value as Employee).company)
|
||||
);
|
||||
}
|
||||
```
|
||||
97
skills/using-type-guards/references/testing-guide.md
Normal file
97
skills/using-type-guards/references/testing-guide.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Testing Type Guards
|
||||
|
||||
Complete guide to unit testing type guards with comprehensive test cases.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
When testing type guards, ensure coverage for:
|
||||
|
||||
1. Valid inputs (happy path)
|
||||
2. Missing required properties
|
||||
3. Wrong property types
|
||||
4. Null and undefined
|
||||
5. Non-object primitives
|
||||
6. Edge cases specific to your type
|
||||
|
||||
## Complete Test Suite Example
|
||||
|
||||
```typescript
|
||||
describe("isUser", () => {
|
||||
it("returns true for valid user", () => {
|
||||
expect(isUser({ id: "1", name: "Alice", email: "alice@example.com" })).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for missing property", () => {
|
||||
expect(isUser({ id: "1", name: "Alice" })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for wrong property type", () => {
|
||||
expect(isUser({ id: 1, name: "Alice", email: "alice@example.com" })).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for null", () => {
|
||||
expect(isUser(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for undefined", () => {
|
||||
expect(isUser(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for non-object", () => {
|
||||
expect(isUser("not an object")).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Array Guards
|
||||
|
||||
```typescript
|
||||
describe("isStringArray", () => {
|
||||
it("returns true for array of strings", () => {
|
||||
expect(isStringArray(["a", "b", "c"])).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for empty array", () => {
|
||||
expect(isStringArray([])).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for mixed types", () => {
|
||||
expect(isStringArray(["a", 1, "c"])).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for non-array", () => {
|
||||
expect(isStringArray("not array")).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Assertion Functions
|
||||
|
||||
```typescript
|
||||
describe("assertIsUser", () => {
|
||||
it("does not throw for valid user", () => {
|
||||
expect(() => assertIsUser({ id: "1", name: "Alice", email: "alice@example.com" })).not.toThrow();
|
||||
});
|
||||
|
||||
it("throws for invalid user", () => {
|
||||
expect(() => assertIsUser({ id: "1" })).toThrow("Invalid user data");
|
||||
});
|
||||
|
||||
it("throws for null", () => {
|
||||
expect(() => assertIsUser(null)).toThrow();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Test Coverage Checklist
|
||||
|
||||
- [ ] Valid inputs
|
||||
- [ ] Each required property missing
|
||||
- [ ] Each property with wrong type
|
||||
- [ ] Null input
|
||||
- [ ] Undefined input
|
||||
- [ ] Non-object primitives
|
||||
- [ ] Empty objects/arrays
|
||||
- [ ] Nested object validation
|
||||
- [ ] Optional properties (present and absent)
|
||||
- [ ] Edge cases for your domain
|
||||
736
skills/validating-external-data/SKILL.md
Normal file
736
skills/validating-external-data/SKILL.md
Normal file
@@ -0,0 +1,736 @@
|
||||
---
|
||||
name: validating-external-data
|
||||
description: Validating external data from APIs, JSON parsing, user input, and any untrusted sources in TypeScript applications
|
||||
---
|
||||
|
||||
# Validation: External Data
|
||||
|
||||
**Purpose:** Ensure runtime type safety by validating all data from external sources. TypeScript types are compile-time only and erased at runtime.
|
||||
|
||||
**When to use:** Any time you receive data from outside your TypeScript code - API responses, JSON files, user input, database queries, environment variables, file uploads, or any external system.
|
||||
|
||||
## Critical Understanding
|
||||
|
||||
### TypeScript Types Are Compile-Time Only
|
||||
|
||||
**This code has NO runtime safety:**
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
**Why it's unsafe:**
|
||||
- `response.json()` returns `any` (or `unknown` with strict settings)
|
||||
- Type assertion `as User` doesn't validate data
|
||||
- If API returns `{ id: 123, email: null, age: "twenty" }`, TypeScript accepts it
|
||||
- Runtime errors occur when code assumes correct types
|
||||
|
||||
**TypeScript types are documentation, not validation.**
|
||||
|
||||
### Runtime Validation is Mandatory
|
||||
|
||||
**Safe code validates at system boundaries:**
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
email: z.string().email(),
|
||||
age: z.number().int().positive(),
|
||||
});
|
||||
|
||||
type User = z.infer<typeof UserSchema>;
|
||||
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
const data = await response.json();
|
||||
|
||||
return UserSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
**Now:**
|
||||
- Data is validated at runtime
|
||||
- Throws error if data doesn't match schema
|
||||
- Type safety is guaranteed, not assumed
|
||||
- TypeScript types match runtime reality
|
||||
|
||||
## System Boundaries
|
||||
|
||||
**External data enters at these boundaries:**
|
||||
|
||||
1. **HTTP APIs** - fetch requests, axios, etc.
|
||||
2. **WebSocket messages** - real-time data streams
|
||||
3. **File system** - reading JSON, CSV, or other files
|
||||
4. **Database queries** - SQL, MongoDB, etc.
|
||||
5. **Environment variables** - `process.env`
|
||||
6. **User input** - forms, URL params, file uploads
|
||||
7. **Third-party libraries** - plugins, SDKs
|
||||
8. **Message queues** - RabbitMQ, Kafka, etc.
|
||||
|
||||
**Validate at every boundary. Never trust external data.**
|
||||
|
||||
## Validation with Zod
|
||||
|
||||
### Why Zod?
|
||||
|
||||
**Advantages:**
|
||||
- TypeScript-first design
|
||||
- Infer types from schemas (single source of truth)
|
||||
- Composable and reusable schemas
|
||||
- Excellent error messages
|
||||
- Zero dependencies
|
||||
- Full support for complex types
|
||||
|
||||
**Alternative libraries:**
|
||||
- `io-ts` - More functional, steeper learning curve
|
||||
- `yup` - Popular but not TypeScript-native
|
||||
- `joi` - Older, less TypeScript-friendly
|
||||
- `ajv` - JSON Schema validator, verbose
|
||||
|
||||
**Zod is the best choice for TypeScript projects.**
|
||||
|
||||
### Basic Zod Patterns
|
||||
|
||||
**Primitives:**
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const stringSchema = z.string();
|
||||
const numberSchema = z.number();
|
||||
const booleanSchema = z.boolean();
|
||||
const dateSchema = z.date();
|
||||
const nullSchema = z.null();
|
||||
const undefinedSchema = z.undefined();
|
||||
```
|
||||
|
||||
**Objects:**
|
||||
|
||||
```typescript
|
||||
const UserSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string().min(1).max(100),
|
||||
email: z.string().email(),
|
||||
age: z.number().int().positive().max(120),
|
||||
verified: z.boolean(),
|
||||
createdAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
type User = z.infer<typeof UserSchema>;
|
||||
```
|
||||
|
||||
**Arrays:**
|
||||
|
||||
```typescript
|
||||
const UsersSchema = z.array(UserSchema);
|
||||
|
||||
type Users = z.infer<typeof UsersSchema>;
|
||||
```
|
||||
|
||||
**Optional fields:**
|
||||
|
||||
```typescript
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
nickname: z.string().optional(),
|
||||
age: z.number().nullable(),
|
||||
});
|
||||
```
|
||||
|
||||
**Default values:**
|
||||
|
||||
```typescript
|
||||
const ConfigSchema = z.object({
|
||||
port: z.number().default(3000),
|
||||
host: z.string().default('localhost'),
|
||||
});
|
||||
```
|
||||
|
||||
## API Response Validation
|
||||
|
||||
### Basic Fetch Validation
|
||||
|
||||
```typescript
|
||||
const PostSchema = z.object({
|
||||
id: z.number(),
|
||||
title: z.string(),
|
||||
body: z.string(),
|
||||
userId: z.number(),
|
||||
});
|
||||
|
||||
type Post = z.infer<typeof PostSchema>;
|
||||
|
||||
async function getPost(id: number): Promise<Post> {
|
||||
const response = await fetch(
|
||||
`https://jsonplaceholder.typicode.com/posts/${id}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return PostSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
**What this does:**
|
||||
1. Fetches data from external API
|
||||
2. Checks HTTP status
|
||||
3. Parses JSON
|
||||
4. Validates structure matches schema
|
||||
5. Returns typed data or throws error
|
||||
|
||||
### Handling Validation Errors
|
||||
|
||||
```typescript
|
||||
async function getPost(id: number): Promise<Post | null> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://jsonplaceholder.typicode.com/posts/${id}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`HTTP error! status: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return PostSchema.parse(data);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error('Validation failed:', error.errors);
|
||||
|
||||
error.errors.forEach((err) => {
|
||||
console.error(`${err.path.join('.')}: ${err.message}`);
|
||||
});
|
||||
} else {
|
||||
console.error('Unexpected error:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**ZodError provides detailed information:**
|
||||
- `error.errors` - Array of validation errors
|
||||
- `err.path` - Field path that failed
|
||||
- `err.message` - Human-readable error message
|
||||
- `err.code` - Error type code
|
||||
|
||||
### Safe Parsing (No Throws)
|
||||
|
||||
```typescript
|
||||
async function getPost(id: number): Promise<Post | null> {
|
||||
const response = await fetch(
|
||||
`https://jsonplaceholder.typicode.com/posts/${id}`
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const result = PostSchema.safeParse(data);
|
||||
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
} else {
|
||||
console.error('Validation failed:', result.error.errors);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`safeParse` returns:**
|
||||
- `{ success: true, data: T }` on success
|
||||
- `{ success: false, error: ZodError }` on failure
|
||||
|
||||
**Use when:**
|
||||
- You want to handle errors without try/catch
|
||||
- Multiple validation attempts in a row
|
||||
- Building type-safe APIs
|
||||
|
||||
## Complex Data Structures
|
||||
|
||||
### Nested Objects
|
||||
|
||||
```typescript
|
||||
const AddressSchema = z.object({
|
||||
street: z.string(),
|
||||
city: z.string(),
|
||||
zipCode: z.string().regex(/^\d{5}$/),
|
||||
country: z.string().length(2),
|
||||
});
|
||||
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
address: AddressSchema,
|
||||
});
|
||||
```
|
||||
|
||||
### Discriminated Unions
|
||||
|
||||
```typescript
|
||||
const SuccessResponseSchema = z.object({
|
||||
status: z.literal('success'),
|
||||
data: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const ErrorResponseSchema = z.object({
|
||||
status: z.literal('error'),
|
||||
message: z.string(),
|
||||
code: z.number(),
|
||||
});
|
||||
|
||||
const APIResponseSchema = z.discriminatedUnion('status', [
|
||||
SuccessResponseSchema,
|
||||
ErrorResponseSchema,
|
||||
]);
|
||||
|
||||
type APIResponse = z.infer<typeof APIResponseSchema>;
|
||||
|
||||
function handleResponse(response: APIResponse) {
|
||||
if (response.status === 'success') {
|
||||
console.log(response.data.name);
|
||||
} else {
|
||||
console.error(response.message, response.code);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Recursive Schemas
|
||||
|
||||
```typescript
|
||||
type Category = {
|
||||
id: string;
|
||||
name: string;
|
||||
subcategories: Category[];
|
||||
};
|
||||
|
||||
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
subcategories: z.array(CategorySchema),
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### Transform and Refine
|
||||
|
||||
**Transform (convert data):**
|
||||
|
||||
```typescript
|
||||
const TimestampSchema = z.string().transform((str) => new Date(str));
|
||||
```
|
||||
|
||||
**Refine (custom validation):**
|
||||
|
||||
```typescript
|
||||
const PasswordSchema = z
|
||||
.string()
|
||||
.min(8)
|
||||
.refine(
|
||||
(password) => /[A-Z]/.test(password),
|
||||
{ message: 'Must contain uppercase letter' }
|
||||
)
|
||||
.refine(
|
||||
(password) => /[0-9]/.test(password),
|
||||
{ message: 'Must contain number' }
|
||||
);
|
||||
```
|
||||
|
||||
## JSON File Validation
|
||||
|
||||
```typescript
|
||||
import { readFile } from 'fs/promises';
|
||||
import { z } from 'zod';
|
||||
|
||||
const ConfigSchema = z.object({
|
||||
database: z.object({
|
||||
host: z.string(),
|
||||
port: z.number(),
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
}),
|
||||
server: z.object({
|
||||
port: z.number().default(3000),
|
||||
host: z.string().default('localhost'),
|
||||
}),
|
||||
});
|
||||
|
||||
type Config = z.infer<typeof ConfigSchema>;
|
||||
|
||||
async function loadConfig(path: string): Promise<Config> {
|
||||
const content = await readFile(path, 'utf-8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
return ConfigSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variable Validation
|
||||
|
||||
```typescript
|
||||
const EnvSchema = z.object({
|
||||
NODE_ENV: z.enum(['development', 'production', 'test']),
|
||||
PORT: z.coerce.number().default(3000),
|
||||
DATABASE_URL: z.string().url(),
|
||||
API_KEY: z.string().min(1),
|
||||
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
||||
});
|
||||
|
||||
type Env = z.infer<typeof EnvSchema>;
|
||||
|
||||
function validateEnv(): Env {
|
||||
const result = EnvSchema.safeParse(process.env);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Invalid environment variables:');
|
||||
console.error(result.error.errors);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export const env = validateEnv();
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { env } from './env';
|
||||
|
||||
const server = app.listen(env.PORT, () => {
|
||||
console.log(`Server running on port ${env.PORT}`);
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Type-safe environment access
|
||||
- Fails fast on startup if misconfigured
|
||||
- Self-documenting required environment variables
|
||||
- Coercion handles string-to-number conversion
|
||||
|
||||
## Database Query Validation
|
||||
|
||||
### Raw SQL Results
|
||||
|
||||
```typescript
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const pool = new Pool();
|
||||
|
||||
const UserRowSchema = z.object({
|
||||
id: z.string(),
|
||||
email: z.string(),
|
||||
created_at: z.string(),
|
||||
});
|
||||
|
||||
type UserRow = z.infer<typeof UserRowSchema>;
|
||||
|
||||
async function getUser(id: string): Promise<UserRow> {
|
||||
const result = await pool.query(
|
||||
'SELECT id, email, created_at FROM users WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
return UserRowSchema.parse(result.rows[0]);
|
||||
}
|
||||
```
|
||||
|
||||
**Database Safety Note:** Use parameterized queries (with `$1`, `$2` placeholders) to prevent SQL injection. For detailed patterns on safe query construction with Prisma, use the preventing-sql-injection skill from prisma-6 for database-specific sanitization patterns.
|
||||
|
||||
### MongoDB Results
|
||||
|
||||
```typescript
|
||||
import { MongoClient } from 'mongodb';
|
||||
|
||||
const UserDocumentSchema = z.object({
|
||||
_id: z.string(),
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
async function getUser(id: string) {
|
||||
const client = await MongoClient.connect('mongodb://localhost');
|
||||
const db = client.db('myapp');
|
||||
|
||||
const doc = await db.collection('users').findOne({ _id: id });
|
||||
|
||||
if (!doc) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
return UserDocumentSchema.parse(doc);
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket Message Validation
|
||||
|
||||
```typescript
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
const MessageSchema = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('chat'),
|
||||
message: z.string(),
|
||||
userId: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('typing'),
|
||||
userId: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
type Message = z.infer<typeof MessageSchema>;
|
||||
|
||||
const ws = new WebSocket('ws://localhost:8080');
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
const parsed = JSON.parse(data.toString());
|
||||
const message = MessageSchema.parse(parsed);
|
||||
|
||||
if (message.type === 'chat') {
|
||||
console.log(`${message.userId}: ${message.message}`);
|
||||
} else {
|
||||
console.log(`${message.userId} is typing...`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Invalid message received:', error);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Form Data Validation
|
||||
|
||||
### Express/Node.js
|
||||
|
||||
```typescript
|
||||
import express from 'express';
|
||||
import { z } from 'zod';
|
||||
|
||||
const CreateUserSchema = z.object({
|
||||
username: z.string().min(3).max(20),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(12),
|
||||
});
|
||||
|
||||
app.post('/register', async (req, res) => {
|
||||
const result = CreateUserSchema.safeParse(req.body);
|
||||
|
||||
if (!result.success) {
|
||||
res.status(400).json({
|
||||
error: 'Validation failed',
|
||||
details: result.error.errors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await createUser(result.data);
|
||||
|
||||
res.json({ user });
|
||||
});
|
||||
```
|
||||
|
||||
### React Forms
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import { useState } from 'react';
|
||||
|
||||
const LoginSchema = z.object({
|
||||
email: z.string().email('Invalid email'),
|
||||
password: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
});
|
||||
|
||||
function LoginForm() {
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const data = {
|
||||
email: formData.get('email'),
|
||||
password: formData.get('password'),
|
||||
};
|
||||
|
||||
const result = LoginSchema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
result.error.errors.forEach((err) => {
|
||||
const field = err.path[0] as string;
|
||||
fieldErrors[field] = err.message;
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
await login(result.data);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input name="email" type="email" />
|
||||
{errors.email && <span>{errors.email}</span>}
|
||||
|
||||
<input name="password" type="password" />
|
||||
{errors.password && <span>{errors.password}</span>}
|
||||
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Caching Schemas
|
||||
|
||||
**DON'T recreate schemas on every call:**
|
||||
|
||||
```typescript
|
||||
function validateUser(data: unknown) {
|
||||
const schema = z.object({ id: z.string() });
|
||||
return schema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
**DO create schemas once:**
|
||||
|
||||
```typescript
|
||||
const UserSchema = z.object({ id: z.string() });
|
||||
|
||||
function validateUser(data: unknown) {
|
||||
return UserSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
### Partial Validation
|
||||
|
||||
**Validate only needed fields:**
|
||||
|
||||
```typescript
|
||||
const FullUserSchema = z.object({
|
||||
id: z.string(),
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
address: z.object({
|
||||
street: z.string(),
|
||||
city: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const UserIdSchema = FullUserSchema.pick({ id: true });
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Mistake 1: Type Assertion Instead of Validation
|
||||
|
||||
```typescript
|
||||
const user = (await response.json()) as User;
|
||||
```
|
||||
|
||||
This provides ZERO runtime safety.
|
||||
|
||||
### Mistake 2: Validating After Using
|
||||
|
||||
```typescript
|
||||
const data = await response.json();
|
||||
console.log(data.user.email);
|
||||
|
||||
UserSchema.parse(data);
|
||||
```
|
||||
|
||||
Validation must come BEFORE use.
|
||||
|
||||
### Mistake 3: Catching and Ignoring Errors
|
||||
|
||||
```typescript
|
||||
try {
|
||||
return UserSchema.parse(data);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
```
|
||||
|
||||
Silently failing validation defeats its purpose.
|
||||
|
||||
## Best Practices Checklist
|
||||
|
||||
- [ ] Validate all external data at system boundaries
|
||||
- [ ] Use Zod for type-safe schemas
|
||||
- [ ] Infer TypeScript types from Zod schemas (`z.infer`)
|
||||
- [ ] Handle validation errors appropriately
|
||||
- [ ] Log validation failures for debugging
|
||||
- [ ] Never use type assertions on unvalidated data
|
||||
- [ ] Cache schema instances (don't recreate)
|
||||
- [ ] Validate environment variables on startup
|
||||
- [ ] Use `safeParse` for non-throwing validation
|
||||
- [ ] Provide user-friendly error messages
|
||||
|
||||
## Related Skills
|
||||
|
||||
**Zod v4 Features:**
|
||||
- If constructing Zod schemas for external data, use the validating-schema-basics skill for type-safe schema patterns and validation best practices
|
||||
- If validating email, URL, UUID formats, use the validating-string-formats skill for Zod v4 top-level format functions
|
||||
- If handling validation errors, use the customizing-errors skill for safeParse pattern, error formatting, and user-friendly messages
|
||||
|
||||
**Prisma 6 Database Validation:**
|
||||
- If validating Prisma query results as external data sources with type-safe patterns, use the ensuring-query-type-safety skill from prisma-6 for GetPayload type inference
|
||||
- If constructing database queries from external data, use the preventing-sql-injection skill from prisma-6 for database-specific sanitization patterns and safe parameterization
|
||||
|
||||
## Resources
|
||||
|
||||
- [Zod Documentation](https://zod.dev/)
|
||||
- [TypeScript Handbook: Type Assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions)
|
||||
- [io-ts](https://github.com/gcanti/io-ts) (alternative)
|
||||
|
||||
## Summary
|
||||
|
||||
**Key Principles:**
|
||||
|
||||
1. **TypeScript types are compile-time only** - No runtime enforcement
|
||||
2. **Validate at system boundaries** - APIs, files, databases, user input
|
||||
3. **Use Zod for runtime validation** - Type-safe, composable, excellent errors
|
||||
4. **Fail fast on invalid data** - Don't process bad data
|
||||
5. **Never trust external data** - Always validate, even from "trusted" sources
|
||||
|
||||
**Pattern:**
|
||||
|
||||
```typescript
|
||||
const Schema = z.object({});
|
||||
type Type = z.infer<typeof Schema>;
|
||||
|
||||
function processExternalData(data: unknown): Type {
|
||||
return Schema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
This ensures your TypeScript types match runtime reality, preventing the #1 cause of production bugs in TypeScript applications.
|
||||
456
skills/validating-type-assertions/SKILL.md
Normal file
456
skills/validating-type-assertions/SKILL.md
Normal file
@@ -0,0 +1,456 @@
|
||||
---
|
||||
name: validating-type-assertions
|
||||
description: Teaches when type assertions are safe versus dangerous in TypeScript. Use when considering using 'as' keyword, type casting, or when working with external data that might use assertions instead of proper validation.
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
<role>
|
||||
This skill teaches the difference between safe and dangerous type assertions, preventing the common anti-pattern of using `as Type` on unvalidated external data. Based on stress test findings where 50% of agents misused type assertions.
|
||||
</role>
|
||||
|
||||
<when-to-activate>
|
||||
This skill activates when:
|
||||
|
||||
- Code contains `as` keyword or angle bracket syntax `<Type>`
|
||||
- Working with type assertions or type casting
|
||||
- Converting between types
|
||||
- User mentions type assertions, casting, or "as" keyword
|
||||
- Code uses assertions on external data
|
||||
</when-to-activate>
|
||||
|
||||
<overview>
|
||||
**Critical Distinction**:
|
||||
|
||||
- **Type Guard**: Runtime check that narrows types safely
|
||||
- **Type Assertion**: Compile-time instruction telling TypeScript to trust you
|
||||
|
||||
**Type assertions are TypeScript compiler directives, not runtime operations.**
|
||||
|
||||
```typescript
|
||||
const data = JSON.parse(json) as User;
|
||||
```
|
||||
|
||||
This compiles fine but provides ZERO runtime safety. If JSON is malformed, your code crashes.
|
||||
|
||||
**Rule**: Type assertions are safe only when YOU control the data or have already validated it.
|
||||
</overview>
|
||||
|
||||
<workflow>
|
||||
## Assertion Safety Decision Tree
|
||||
|
||||
**Question 1: Where does this data come from?**
|
||||
|
||||
- **From your own code** (constants, constructors) → Assertion OK
|
||||
- **From external source** (API, user input, JSON) → NEVER assert, validate instead
|
||||
|
||||
**Question 2: Have you validated the data?**
|
||||
|
||||
- **Yes, with runtime validation** → Assertion OK after validation
|
||||
- **No validation** → NEVER assert
|
||||
|
||||
**Question 3: Is this a TypeScript limitation?**
|
||||
|
||||
- **Yes** (const assertions, narrowing limitations) → Assertion OK
|
||||
- **No** (trying to skip validation) → NEVER assert
|
||||
</workflow>
|
||||
|
||||
<examples>
|
||||
## Example 1: Dangerous Assertions (DO NOT DO)
|
||||
|
||||
**❌ Asserting external API data**
|
||||
|
||||
```typescript
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
const data = await response.json() as User;
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: If API returns different structure, runtime crash. TypeScript provides no protection.
|
||||
|
||||
**✅ Validate instead**
|
||||
|
||||
```typescript
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
const data: unknown = await response.json();
|
||||
|
||||
if (!isUser(data)) {
|
||||
throw new Error("Invalid user data from API");
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**❌ Asserting JSON.parse result**
|
||||
|
||||
```typescript
|
||||
const config = JSON.parse(configString) as Config;
|
||||
```
|
||||
|
||||
**Problem**: If JSON is malformed or wrong shape, crash at runtime.
|
||||
|
||||
**✅ Validate with Zod**
|
||||
|
||||
```typescript
|
||||
const data: unknown = JSON.parse(configString);
|
||||
const config = ConfigSchema.parse(data);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**❌ Asserting user input**
|
||||
|
||||
```typescript
|
||||
function handleSubmit(formData: FormData) {
|
||||
const user = {
|
||||
name: formData.get("name"),
|
||||
email: formData.get("email")
|
||||
} as User;
|
||||
|
||||
saveUser(user);
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: FormData can contain anything. No validation.
|
||||
|
||||
**✅ Validate form data**
|
||||
|
||||
```typescript
|
||||
function handleSubmit(formData: FormData) {
|
||||
const data = {
|
||||
name: formData.get("name"),
|
||||
email: formData.get("email")
|
||||
};
|
||||
|
||||
const user = UserSchema.parse(data);
|
||||
saveUser(user);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Safe Assertions (OK to use)
|
||||
|
||||
**✅ Const assertions**
|
||||
|
||||
```typescript
|
||||
const routes = [
|
||||
{ path: "/", component: "Home" },
|
||||
{ path: "/about", component: "About" }
|
||||
] as const;
|
||||
|
||||
type Route = typeof routes[number];
|
||||
```
|
||||
|
||||
**Safe because**: Data is hardcoded, not external.
|
||||
|
||||
---
|
||||
|
||||
**✅ After validation**
|
||||
|
||||
```typescript
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
const data: unknown = await response.json();
|
||||
|
||||
const result = UserSchema.safeParse(data);
|
||||
if (!result.success) {
|
||||
throw new Error("Invalid user data");
|
||||
}
|
||||
|
||||
return result.data as User;
|
||||
}
|
||||
```
|
||||
|
||||
**Safe because**: Data validated before assertion. (Though `result.data` already has correct type, so assertion is redundant.)
|
||||
|
||||
---
|
||||
|
||||
**✅ Constructor results**
|
||||
|
||||
```typescript
|
||||
class User {
|
||||
constructor(
|
||||
public id: string,
|
||||
public name: string
|
||||
) {}
|
||||
}
|
||||
|
||||
const users = [
|
||||
new User("1", "Alice"),
|
||||
new User("2", "Bob")
|
||||
] as User[];
|
||||
```
|
||||
|
||||
**Safe because**: You control construction, types are guaranteed.
|
||||
|
||||
---
|
||||
|
||||
**✅ Type narrowing limitations**
|
||||
|
||||
```typescript
|
||||
interface Circle { kind: "circle"; radius: number; }
|
||||
interface Square { kind: "square"; size: number; }
|
||||
type Shape = Circle | Square;
|
||||
|
||||
function getArea(shape: Shape): number {
|
||||
if (shape.kind === "circle") {
|
||||
return Math.PI * shape.radius ** 2;
|
||||
}
|
||||
return shape.size ** 2;
|
||||
}
|
||||
```
|
||||
|
||||
**Safe because**: TypeScript narrows to Square after checking for circle. Use `else` to avoid assertion.
|
||||
|
||||
---
|
||||
|
||||
**✅ Type widening prevention**
|
||||
|
||||
```typescript
|
||||
const config = {
|
||||
apiUrl: "https://api.example.com",
|
||||
timeout: 5000
|
||||
} as const;
|
||||
```
|
||||
|
||||
**Safe because**: Preventing literal types from widening to general types.
|
||||
|
||||
---
|
||||
|
||||
**✅ Unknown to specific after validation**
|
||||
|
||||
```typescript
|
||||
function processError(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
if (typeof error === "string") return error;
|
||||
return String(error);
|
||||
}
|
||||
```
|
||||
|
||||
**Safe because**: Type guards narrow before use without assertions.
|
||||
|
||||
---
|
||||
|
||||
## Example 3: Double Assertion Anti-Pattern
|
||||
|
||||
**❌ Double assertion to bypass safety**
|
||||
|
||||
```typescript
|
||||
const value = "not a number" as unknown as number;
|
||||
```
|
||||
|
||||
**Problem**: Intentionally bypassing type system. Defeats TypeScript's purpose.
|
||||
|
||||
**✅ Fix the types properly**
|
||||
|
||||
```typescript
|
||||
const value: unknown = "not a number";
|
||||
|
||||
if (typeof value === "number") {
|
||||
console.log(value.toFixed(2));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 4: Non-null Assertion
|
||||
|
||||
**❌ Non-null assertion on external data**
|
||||
|
||||
```typescript
|
||||
const user = await fetchUser(id);
|
||||
console.log(user!.name);
|
||||
```
|
||||
|
||||
**Problem**: If `fetchUser` can return `null`, this crashes.
|
||||
|
||||
**✅ Check explicitly**
|
||||
|
||||
```typescript
|
||||
const user = await fetchUser(id);
|
||||
|
||||
if (user) {
|
||||
console.log(user.name);
|
||||
} else {
|
||||
console.log("User not found");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**✅ Non-null assertion after explicit check**
|
||||
|
||||
```typescript
|
||||
const element = document.getElementById("root");
|
||||
|
||||
if (!element) {
|
||||
throw new Error("Root element not found");
|
||||
}
|
||||
|
||||
element.appendChild(child);
|
||||
```
|
||||
|
||||
**Safe because**: Checked for null and threw. TypeScript narrows automatically, no assertion needed.
|
||||
|
||||
---
|
||||
|
||||
## Example 5: Assertion Functions vs Assertions
|
||||
|
||||
**❌ Assertion to avoid validation**
|
||||
|
||||
```typescript
|
||||
function getUser(data: unknown): User {
|
||||
return data as User;
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Assertion function with validation**
|
||||
|
||||
```typescript
|
||||
function assertIsUser(data: unknown): asserts data is User {
|
||||
if (!isUser(data)) {
|
||||
throw new Error("Invalid user data");
|
||||
}
|
||||
}
|
||||
|
||||
function getUser(data: unknown): User {
|
||||
assertIsUser(data);
|
||||
return data;
|
||||
}
|
||||
```
|
||||
</examples>
|
||||
|
||||
<progressive-disclosure>
|
||||
## Reference Files
|
||||
|
||||
For related patterns:
|
||||
|
||||
- **Runtime Validation**: Use the using-runtime-checks skill for proper validation with Zod
|
||||
- **Type Guards**: Use the using-type-guards skill for safe type narrowing
|
||||
- **Unknown Type**: Use the avoiding-any-types skill for handling unknown data safely
|
||||
</progressive-disclosure>
|
||||
|
||||
<constraints>
|
||||
**MUST:**
|
||||
|
||||
- Validate external data with type guards or validation libraries
|
||||
- Use assertion functions (`asserts value is Type`) over direct assertions
|
||||
- Check for null/undefined before non-null assertions (`!`)
|
||||
- Document WHY assertion is safe when used
|
||||
|
||||
**SHOULD:**
|
||||
|
||||
- Prefer type guards over assertions
|
||||
- Use `as const` for literal type inference
|
||||
- Limit assertions to known-safe scenarios
|
||||
- Consider if assertion indicates missing validation
|
||||
|
||||
**NEVER:**
|
||||
|
||||
- Assert on external data without validation (APIs, JSON, user input)
|
||||
- Use double assertions (`as unknown as Type`)
|
||||
- Use non-null assertion (`!`) without prior check
|
||||
- Assert to silence compiler errors (fix the types instead)
|
||||
- Trust that data "will be correct"
|
||||
</constraints>
|
||||
|
||||
<safe-assertion-checklist>
|
||||
## When Assertions Are Safe
|
||||
|
||||
Type assertion is safe when ALL of these are true:
|
||||
|
||||
- [ ] Data source is internal/controlled (not external)
|
||||
- [ ] OR data has been validated with runtime checks
|
||||
- [ ] You understand what the assertion does (compiler directive, not runtime check)
|
||||
- [ ] Assertion is documented with reason
|
||||
- [ ] No double assertions being used
|
||||
|
||||
If ANY checkbox is false, use validation instead.
|
||||
</safe-assertion-checklist>
|
||||
|
||||
<common-patterns>
|
||||
## Pattern Library
|
||||
|
||||
### Pattern 1: Validated Assertion
|
||||
|
||||
```typescript
|
||||
function parseUser(data: unknown): User {
|
||||
const result = UserSchema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ValidationError("Invalid user", result.error);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Const Assertion for Config
|
||||
|
||||
```typescript
|
||||
const API_ENDPOINTS = {
|
||||
users: "/api/users",
|
||||
posts: "/api/posts",
|
||||
comments: "/api/comments"
|
||||
} as const;
|
||||
|
||||
type Endpoint = typeof API_ENDPOINTS[keyof typeof API_ENDPOINTS];
|
||||
```
|
||||
|
||||
### Pattern 3: Assertion Function
|
||||
|
||||
```typescript
|
||||
function assertNever(value: never): never {
|
||||
throw new Error(`Unexpected value: ${value}`);
|
||||
}
|
||||
|
||||
function handleShape(shape: Shape) {
|
||||
switch (shape.kind) {
|
||||
case "circle":
|
||||
return Math.PI * shape.radius ** 2;
|
||||
case "square":
|
||||
return shape.size ** 2;
|
||||
default:
|
||||
assertNever(shape);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Type Predicate Instead of Assertion
|
||||
|
||||
```typescript
|
||||
function isDefined<T>(value: T | null | undefined): value is T {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
const values = [1, null, 2, undefined, 3];
|
||||
const defined = values.filter(isDefined);
|
||||
```
|
||||
</common-patterns>
|
||||
|
||||
<migration>
|
||||
## Removing Unsafe Assertions
|
||||
|
||||
**Find assertions**: `grep -rn " as " src/` and `grep -rn "!" src/ | grep -v "!=="`
|
||||
|
||||
**Classify each**: External data → add validation; After validation → verify; Const assertion → keep; Bypassing types → fix types
|
||||
|
||||
**Replace pattern**:
|
||||
```typescript
|
||||
const data = JSON.parse(json) as User;
|
||||
```
|
||||
Becomes:
|
||||
```typescript
|
||||
const data: unknown = JSON.parse(json);
|
||||
const user = UserSchema.parse(data);
|
||||
```
|
||||
|
||||
**Enable strict mode**: Set `"strict": true` and `"noImplicitAny": true` in tsconfig.json
|
||||
</migration>
|
||||
Reference in New Issue
Block a user