Add NDK skill documentation and examples
- Introduced comprehensive documentation for the Nostr Development Kit (NDK) including an overview, quick reference, and troubleshooting guide. - Added detailed examples covering initialization, authentication, event publishing, querying, and user profile management. - Structured the documentation to facilitate quick lookups and deep learning, based on real-world usage patterns from the Plebeian Market application. - Created an index for examples to enhance usability and navigation. - Bumped version to 1.0.0 to reflect the addition of this new skill set.
This commit is contained in:
286
.claude/skills/ndk/INDEX.md
Normal file
286
.claude/skills/ndk/INDEX.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# NDK (Nostr Development Kit) Claude Skill
|
||||
|
||||
> **Comprehensive knowledge base for working with NDK in production applications**
|
||||
|
||||
This Claude skill provides deep expertise in the Nostr Development Kit based on real-world usage patterns from the Plebeian Market application.
|
||||
|
||||
## 📚 Documentation Structure
|
||||
|
||||
```
|
||||
.claude/skills/ndk/
|
||||
├── README.md # This file - Overview and getting started
|
||||
├── ndk-skill.md # Complete reference guide (18KB)
|
||||
├── quick-reference.md # Fast lookup for common tasks (7KB)
|
||||
├── troubleshooting.md # Common problems and solutions
|
||||
└── examples/ # Production code examples
|
||||
├── README.md
|
||||
├── 01-initialization.ts # NDK setup and connection
|
||||
├── 02-authentication.ts # NIP-07, NIP-46, private keys
|
||||
├── 03-publishing-events.ts # Creating and publishing events
|
||||
├── 04-querying-subscribing.ts # Fetching and real-time subs
|
||||
└── 05-users-profiles.ts # User and profile management
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### For Quick Lookups
|
||||
Start with **`quick-reference.md`** for:
|
||||
- Common code snippets
|
||||
- Quick syntax reminders
|
||||
- Frequently used patterns
|
||||
|
||||
### For Deep Learning
|
||||
Read **`ndk-skill.md`** for:
|
||||
- Complete API documentation
|
||||
- Best practices
|
||||
- Integration patterns
|
||||
- Performance optimization
|
||||
|
||||
### For Problem Solving
|
||||
Check **`troubleshooting.md`** for:
|
||||
- Common error solutions
|
||||
- Performance tips
|
||||
- Testing strategies
|
||||
- Debug techniques
|
||||
|
||||
### For Code Examples
|
||||
Browse **`examples/`** directory for:
|
||||
- Real production code
|
||||
- Full implementations
|
||||
- React integration patterns
|
||||
- Error handling examples
|
||||
|
||||
## 📖 Core Topics Covered
|
||||
|
||||
### 1. Initialization & Setup
|
||||
- Basic NDK initialization
|
||||
- Multiple instance patterns (main + zap relays)
|
||||
- Connection management with timeouts
|
||||
- Relay pool configuration
|
||||
- Connection status monitoring
|
||||
|
||||
### 2. Authentication
|
||||
- **NIP-07**: Browser extension signers (Alby, nos2x)
|
||||
- **NIP-46**: Remote signers (Bunker)
|
||||
- **Private Keys**: Direct key management
|
||||
- Auto-login with localStorage
|
||||
- Multi-account session management
|
||||
|
||||
### 3. Event Publishing
|
||||
- Basic text notes
|
||||
- Parameterized replaceable events (products, profiles)
|
||||
- Order and payment events
|
||||
- Batch publishing
|
||||
- Error handling patterns
|
||||
|
||||
### 4. Querying & Subscriptions
|
||||
- One-time fetches with `fetchEvents()`
|
||||
- Real-time subscriptions
|
||||
- Tag filtering patterns
|
||||
- Time-range queries
|
||||
- Event monitoring
|
||||
- React Query integration
|
||||
|
||||
### 5. User & Profile Management
|
||||
- Fetch profiles (npub, hex, NIP-05)
|
||||
- Update user profiles
|
||||
- Follow/unfollow operations
|
||||
- Batch profile loading
|
||||
- Profile caching strategies
|
||||
|
||||
### 6. Advanced Patterns
|
||||
- Store-based NDK management
|
||||
- Query + subscription combination
|
||||
- Event parsing utilities
|
||||
- Memory leak prevention
|
||||
- Performance optimization
|
||||
|
||||
## 🎯 Use Cases
|
||||
|
||||
### Building a Nostr Client
|
||||
```typescript
|
||||
// Initialize
|
||||
const { ndk, isConnected } = await initializeNDK({
|
||||
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
||||
timeoutMs: 10000
|
||||
})
|
||||
|
||||
// Authenticate
|
||||
const { user } = await loginWithExtension(ndk)
|
||||
|
||||
// Publish
|
||||
await publishBasicNote(ndk, 'Hello Nostr!')
|
||||
|
||||
// Subscribe
|
||||
const sub = subscribeToNotes(ndk, user.pubkey, (event) => {
|
||||
console.log('New note:', event.content)
|
||||
})
|
||||
```
|
||||
|
||||
### Building a Marketplace
|
||||
```typescript
|
||||
// Publish product
|
||||
await publishProduct(ndk, {
|
||||
slug: 'bitcoin-shirt',
|
||||
title: 'Bitcoin T-Shirt',
|
||||
price: 25,
|
||||
currency: 'USD',
|
||||
images: ['https://...']
|
||||
})
|
||||
|
||||
// Create order
|
||||
await createOrder(ndk, {
|
||||
orderId: uuidv4(),
|
||||
sellerPubkey: merchant.pubkey,
|
||||
productRef: '30402:pubkey:bitcoin-shirt',
|
||||
quantity: 1,
|
||||
totalAmount: '25.00'
|
||||
})
|
||||
|
||||
// Monitor payment
|
||||
monitorPaymentReceipt(ndk, orderId, invoiceId, (preimage) => {
|
||||
console.log('Payment confirmed!')
|
||||
})
|
||||
```
|
||||
|
||||
### React Integration
|
||||
```typescript
|
||||
function Feed() {
|
||||
const ndk = useNDK()
|
||||
const { user } = useAuth()
|
||||
|
||||
// Query with real-time updates
|
||||
const { data: notes } = useNotesWithSubscription(
|
||||
ndk,
|
||||
user.pubkey
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{notes?.map(note => (
|
||||
<NoteCard key={note.id} note={note} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 Common Patterns Quick Reference
|
||||
|
||||
### Safe NDK Access
|
||||
```typescript
|
||||
const ndk = ndkActions.getNDK()
|
||||
if (!ndk) throw new Error('NDK not initialized')
|
||||
```
|
||||
|
||||
### Subscription Cleanup
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const sub = ndk.subscribe(filter, { closeOnEose: false })
|
||||
sub.on('event', handleEvent)
|
||||
return () => sub.stop() // Critical!
|
||||
}, [ndk])
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```typescript
|
||||
try {
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
} catch (error) {
|
||||
console.error('Publishing failed:', error)
|
||||
throw new Error('Failed to publish. Check connection.')
|
||||
}
|
||||
```
|
||||
|
||||
### Tag Filtering
|
||||
```typescript
|
||||
// ✅ Correct (note the # prefix for tag filters)
|
||||
{ kinds: [16], '#order': [orderId] }
|
||||
|
||||
// ❌ Wrong
|
||||
{ kinds: [16], 'order': [orderId] }
|
||||
```
|
||||
|
||||
## 🛠 Development Tools
|
||||
|
||||
### VS Code Integration
|
||||
These skill files work with:
|
||||
- Cursor AI for code completion
|
||||
- Claude for code assistance
|
||||
- GitHub Copilot with context
|
||||
|
||||
### Debugging Tips
|
||||
```typescript
|
||||
// Check connection
|
||||
console.log('Connected relays:',
|
||||
Array.from(ndk.pool?.relays.values() || [])
|
||||
.filter(r => r.status === 1)
|
||||
.map(r => r.url)
|
||||
)
|
||||
|
||||
// Verify signer
|
||||
console.log('Signer:', ndk.signer)
|
||||
console.log('Active user:', ndk.activeUser)
|
||||
|
||||
// Event inspection
|
||||
console.log('Event:', {
|
||||
id: event.id,
|
||||
kind: event.kind,
|
||||
tags: event.tags,
|
||||
sig: event.sig
|
||||
})
|
||||
```
|
||||
|
||||
## 📊 Statistics
|
||||
|
||||
- **Total Documentation**: ~50KB
|
||||
- **Code Examples**: 5 complete modules
|
||||
- **Patterns Documented**: 50+
|
||||
- **Common Issues Covered**: 15+
|
||||
- **Based On**: Real production code
|
||||
|
||||
## 🔗 Additional Resources
|
||||
|
||||
### Official NDK Resources
|
||||
- **GitHub**: https://github.com/nostr-dev-kit/ndk
|
||||
- **Documentation**: https://ndk.fyi
|
||||
- **NPM**: `@nostr-dev-kit/ndk`
|
||||
|
||||
### Nostr Protocol
|
||||
- **NIPs**: https://github.com/nostr-protocol/nips
|
||||
- **Nostr**: https://nostr.com
|
||||
|
||||
### Related Tools
|
||||
- **TanStack Query**: React state management
|
||||
- **TanStack Router**: Type-safe routing
|
||||
- **Radix UI**: Accessible components
|
||||
|
||||
## 💡 Tips for Using This Skill
|
||||
|
||||
1. **Start Small**: Begin with quick-reference.md for syntax
|
||||
2. **Go Deep**: Read ndk-skill.md section by section
|
||||
3. **Copy Examples**: Use examples/ as templates
|
||||
4. **Debug Issues**: Check troubleshooting.md first
|
||||
5. **Stay Updated**: Patterns based on production usage
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
This skill is maintained based on the Plebeian Market codebase. To improve it:
|
||||
|
||||
1. Document new patterns you discover
|
||||
2. Add solutions to common problems
|
||||
3. Update examples with better approaches
|
||||
4. Keep synchronized with NDK updates
|
||||
|
||||
## 📝 Version Info
|
||||
|
||||
- **Skill Version**: 1.0.0
|
||||
- **NDK Version**: Latest (based on production usage)
|
||||
- **Last Updated**: November 2025
|
||||
- **Codebase**: Plebeian Market
|
||||
|
||||
---
|
||||
|
||||
**Ready to build with NDK?** Start with `quick-reference.md` or dive into `examples/01-initialization.ts`!
|
||||
|
||||
38
.claude/skills/ndk/README.md
Normal file
38
.claude/skills/ndk/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# NDK (Nostr Development Kit) Claude Skill
|
||||
|
||||
This skill provides comprehensive knowledge about working with the Nostr Development Kit (NDK) library.
|
||||
|
||||
## Files
|
||||
|
||||
- **ndk-skill.md** - Complete reference documentation with patterns from production usage
|
||||
- **quick-reference.md** - Quick lookup guide for common NDK tasks
|
||||
- **examples/** - Code examples extracted from the Plebeian Market codebase
|
||||
|
||||
## Usage
|
||||
|
||||
When working with NDK-related code, reference these documents to:
|
||||
- Understand initialization patterns
|
||||
- Learn authentication flows (NIP-07, NIP-46, private keys)
|
||||
- Implement event creation and publishing
|
||||
- Set up subscriptions for real-time updates
|
||||
- Query events with filters
|
||||
- Handle users and profiles
|
||||
- Integrate with TanStack Query
|
||||
|
||||
## Key Topics Covered
|
||||
|
||||
1. NDK Initialization & Configuration
|
||||
2. Authentication & Signers
|
||||
3. Event Creation & Publishing
|
||||
4. Querying Events
|
||||
5. Real-time Subscriptions
|
||||
6. User & Profile Management
|
||||
7. Tag Handling
|
||||
8. Replaceable Events
|
||||
9. Relay Management
|
||||
10. Integration with React/TanStack Query
|
||||
11. Error Handling & Best Practices
|
||||
12. Performance Optimization
|
||||
|
||||
All examples are based on real production code from the Plebeian Market application.
|
||||
|
||||
162
.claude/skills/ndk/examples/01-initialization.ts
Normal file
162
.claude/skills/ndk/examples/01-initialization.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* NDK Initialization Patterns
|
||||
*
|
||||
* Examples from: src/lib/stores/ndk.ts
|
||||
*/
|
||||
|
||||
import NDK from '@nostr-dev-kit/ndk'
|
||||
|
||||
// ============================================================
|
||||
// BASIC INITIALIZATION
|
||||
// ============================================================
|
||||
|
||||
const basicInit = () => {
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: ['wss://relay.damus.io', 'wss://relay.nostr.band']
|
||||
})
|
||||
|
||||
return ndk
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PRODUCTION PATTERN - WITH MULTIPLE NDK INSTANCES
|
||||
// ============================================================
|
||||
|
||||
const productionInit = (relays: string[], zapRelays: string[]) => {
|
||||
// Main NDK instance for general operations
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: relays
|
||||
})
|
||||
|
||||
// Separate NDK for zap operations (performance optimization)
|
||||
const zapNdk = new NDK({
|
||||
explicitRelayUrls: zapRelays
|
||||
})
|
||||
|
||||
return { ndk, zapNdk }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CONNECTION WITH TIMEOUT
|
||||
// ============================================================
|
||||
|
||||
const connectWithTimeout = async (
|
||||
ndk: NDK,
|
||||
timeoutMs: number = 10000
|
||||
): Promise<void> => {
|
||||
// Create connection promise
|
||||
const connectPromise = ndk.connect()
|
||||
|
||||
// Create timeout promise
|
||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Connection timeout')), timeoutMs)
|
||||
)
|
||||
|
||||
try {
|
||||
// Race between connection and timeout
|
||||
await Promise.race([connectPromise, timeoutPromise])
|
||||
console.log('✅ NDK connected successfully')
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Connection timeout') {
|
||||
console.error('❌ Connection timed out after', timeoutMs, 'ms')
|
||||
} else {
|
||||
console.error('❌ Connection failed:', error)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FULL INITIALIZATION FLOW
|
||||
// ============================================================
|
||||
|
||||
interface InitConfig {
|
||||
relays?: string[]
|
||||
zapRelays?: string[]
|
||||
timeoutMs?: number
|
||||
}
|
||||
|
||||
const defaultRelays = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.nostr.band',
|
||||
'wss://nos.lol'
|
||||
]
|
||||
|
||||
const defaultZapRelays = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://nostr.wine'
|
||||
]
|
||||
|
||||
const initializeNDK = async (config: InitConfig = {}) => {
|
||||
const {
|
||||
relays = defaultRelays,
|
||||
zapRelays = defaultZapRelays,
|
||||
timeoutMs = 10000
|
||||
} = config
|
||||
|
||||
// Initialize instances
|
||||
const ndk = new NDK({ explicitRelayUrls: relays })
|
||||
const zapNdk = new NDK({ explicitRelayUrls: zapRelays })
|
||||
|
||||
// Connect with timeout protection
|
||||
try {
|
||||
await connectWithTimeout(ndk, timeoutMs)
|
||||
await connectWithTimeout(zapNdk, timeoutMs)
|
||||
|
||||
return { ndk, zapNdk, isConnected: true }
|
||||
} catch (error) {
|
||||
return { ndk, zapNdk, isConnected: false, error }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CHECKING CONNECTION STATUS
|
||||
// ============================================================
|
||||
|
||||
const getConnectionStatus = (ndk: NDK) => {
|
||||
const connectedRelays = Array.from(ndk.pool?.relays.values() || [])
|
||||
.filter(relay => relay.status === 1)
|
||||
.map(relay => relay.url)
|
||||
|
||||
const isConnected = connectedRelays.length > 0
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
connectedRelays,
|
||||
totalRelays: ndk.pool?.relays.size || 0
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// USAGE EXAMPLE
|
||||
// ============================================================
|
||||
|
||||
async function main() {
|
||||
// Initialize
|
||||
const { ndk, zapNdk, isConnected } = await initializeNDK({
|
||||
relays: defaultRelays,
|
||||
zapRelays: defaultZapRelays,
|
||||
timeoutMs: 10000
|
||||
})
|
||||
|
||||
if (!isConnected) {
|
||||
console.error('Failed to connect to relays')
|
||||
return
|
||||
}
|
||||
|
||||
// Check status
|
||||
const status = getConnectionStatus(ndk)
|
||||
console.log('Connection status:', status)
|
||||
|
||||
// Ready to use
|
||||
console.log('NDK ready for operations')
|
||||
}
|
||||
|
||||
export {
|
||||
basicInit,
|
||||
productionInit,
|
||||
connectWithTimeout,
|
||||
initializeNDK,
|
||||
getConnectionStatus
|
||||
}
|
||||
|
||||
255
.claude/skills/ndk/examples/02-authentication.ts
Normal file
255
.claude/skills/ndk/examples/02-authentication.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* NDK Authentication Patterns
|
||||
*
|
||||
* Examples from: src/lib/stores/auth.ts
|
||||
*/
|
||||
|
||||
import NDK from '@nostr-dev-kit/ndk'
|
||||
import { NDKNip07Signer, NDKPrivateKeySigner, NDKNip46Signer } from '@nostr-dev-kit/ndk'
|
||||
|
||||
// ============================================================
|
||||
// NIP-07 - BROWSER EXTENSION SIGNER
|
||||
// ============================================================
|
||||
|
||||
const loginWithExtension = async (ndk: NDK) => {
|
||||
try {
|
||||
// Create NIP-07 signer (browser extension like Alby, nos2x)
|
||||
const signer = new NDKNip07Signer()
|
||||
|
||||
// Wait for signer to be ready
|
||||
await signer.blockUntilReady()
|
||||
|
||||
// Set signer on NDK instance
|
||||
ndk.signer = signer
|
||||
|
||||
// Get authenticated user
|
||||
const user = await signer.user()
|
||||
|
||||
console.log('✅ Logged in via extension:', user.npub)
|
||||
return { user, signer }
|
||||
} catch (error) {
|
||||
console.error('❌ Extension login failed:', error)
|
||||
throw new Error('Failed to login with browser extension. Is it installed?')
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PRIVATE KEY SIGNER
|
||||
// ============================================================
|
||||
|
||||
const loginWithPrivateKey = async (ndk: NDK, privateKeyHex: string) => {
|
||||
try {
|
||||
// Validate private key format (64 hex characters)
|
||||
if (!/^[0-9a-f]{64}$/.test(privateKeyHex)) {
|
||||
throw new Error('Invalid private key format')
|
||||
}
|
||||
|
||||
// Create private key signer
|
||||
const signer = new NDKPrivateKeySigner(privateKeyHex)
|
||||
|
||||
// Wait for signer to be ready
|
||||
await signer.blockUntilReady()
|
||||
|
||||
// Set signer on NDK instance
|
||||
ndk.signer = signer
|
||||
|
||||
// Get authenticated user
|
||||
const user = await signer.user()
|
||||
|
||||
console.log('✅ Logged in with private key:', user.npub)
|
||||
return { user, signer }
|
||||
} catch (error) {
|
||||
console.error('❌ Private key login failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NIP-46 - REMOTE SIGNER (BUNKER)
|
||||
// ============================================================
|
||||
|
||||
const loginWithNip46 = async (
|
||||
ndk: NDK,
|
||||
bunkerUrl: string,
|
||||
localPrivateKey?: string
|
||||
) => {
|
||||
try {
|
||||
// Create or use existing local signer
|
||||
const localSigner = localPrivateKey
|
||||
? new NDKPrivateKeySigner(localPrivateKey)
|
||||
: NDKPrivateKeySigner.generate()
|
||||
|
||||
// Create NIP-46 remote signer
|
||||
const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
|
||||
|
||||
// Wait for signer to be ready (may require user approval)
|
||||
await remoteSigner.blockUntilReady()
|
||||
|
||||
// Set signer on NDK instance
|
||||
ndk.signer = remoteSigner
|
||||
|
||||
// Get authenticated user
|
||||
const user = await remoteSigner.user()
|
||||
|
||||
console.log('✅ Logged in via NIP-46:', user.npub)
|
||||
|
||||
// Store local signer key for reconnection
|
||||
return {
|
||||
user,
|
||||
signer: remoteSigner,
|
||||
localSignerKey: localSigner.privateKey
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ NIP-46 login failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// AUTO-LOGIN FROM LOCAL STORAGE
|
||||
// ============================================================
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
AUTO_LOGIN: 'nostr:auto-login',
|
||||
LOCAL_SIGNER: 'nostr:local-signer',
|
||||
BUNKER_URL: 'nostr:bunker-url',
|
||||
ENCRYPTED_KEY: 'nostr:encrypted-key'
|
||||
}
|
||||
|
||||
const getAuthFromStorage = async (ndk: NDK) => {
|
||||
try {
|
||||
// Check if auto-login is enabled
|
||||
const autoLogin = localStorage.getItem(STORAGE_KEYS.AUTO_LOGIN)
|
||||
if (autoLogin !== 'true') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Try NIP-46 bunker connection
|
||||
const privateKey = localStorage.getItem(STORAGE_KEYS.LOCAL_SIGNER)
|
||||
const bunkerUrl = localStorage.getItem(STORAGE_KEYS.BUNKER_URL)
|
||||
|
||||
if (privateKey && bunkerUrl) {
|
||||
return await loginWithNip46(ndk, bunkerUrl, privateKey)
|
||||
}
|
||||
|
||||
// Try encrypted private key
|
||||
const encryptedKey = localStorage.getItem(STORAGE_KEYS.ENCRYPTED_KEY)
|
||||
if (encryptedKey) {
|
||||
// Would need decryption password from user
|
||||
return { needsPassword: true, encryptedKey }
|
||||
}
|
||||
|
||||
// Fallback to extension
|
||||
return await loginWithExtension(ndk)
|
||||
} catch (error) {
|
||||
console.error('Auto-login failed:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SAVE AUTH TO STORAGE
|
||||
// ============================================================
|
||||
|
||||
const saveAuthToStorage = (
|
||||
method: 'extension' | 'private-key' | 'nip46',
|
||||
data?: {
|
||||
privateKey?: string
|
||||
bunkerUrl?: string
|
||||
encryptedKey?: string
|
||||
}
|
||||
) => {
|
||||
// Enable auto-login
|
||||
localStorage.setItem(STORAGE_KEYS.AUTO_LOGIN, 'true')
|
||||
|
||||
if (method === 'nip46' && data?.privateKey && data?.bunkerUrl) {
|
||||
localStorage.setItem(STORAGE_KEYS.LOCAL_SIGNER, data.privateKey)
|
||||
localStorage.setItem(STORAGE_KEYS.BUNKER_URL, data.bunkerUrl)
|
||||
} else if (method === 'private-key' && data?.encryptedKey) {
|
||||
localStorage.setItem(STORAGE_KEYS.ENCRYPTED_KEY, data.encryptedKey)
|
||||
}
|
||||
// Extension doesn't need storage
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LOGOUT
|
||||
// ============================================================
|
||||
|
||||
const logout = (ndk: NDK) => {
|
||||
// Remove signer from NDK
|
||||
ndk.signer = undefined
|
||||
|
||||
// Clear all auth storage
|
||||
Object.values(STORAGE_KEYS).forEach(key => {
|
||||
localStorage.removeItem(key)
|
||||
})
|
||||
|
||||
console.log('✅ Logged out successfully')
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET CURRENT USER
|
||||
// ============================================================
|
||||
|
||||
const getCurrentUser = async (ndk: NDK) => {
|
||||
if (!ndk.signer) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await ndk.signer.user()
|
||||
return {
|
||||
pubkey: user.pubkey,
|
||||
npub: user.npub,
|
||||
profile: await user.fetchProfile()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get current user:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// USAGE EXAMPLE
|
||||
// ============================================================
|
||||
|
||||
async function authExample(ndk: NDK) {
|
||||
// Try auto-login first
|
||||
let auth = await getAuthFromStorage(ndk)
|
||||
|
||||
if (!auth) {
|
||||
// Manual login options
|
||||
console.log('Choose login method:')
|
||||
console.log('1. Browser Extension (NIP-07)')
|
||||
console.log('2. Private Key')
|
||||
console.log('3. Remote Signer (NIP-46)')
|
||||
|
||||
// Example: login with extension
|
||||
auth = await loginWithExtension(ndk)
|
||||
saveAuthToStorage('extension')
|
||||
}
|
||||
|
||||
if (auth && 'needsPassword' in auth) {
|
||||
// Handle encrypted key case
|
||||
console.log('Password required for encrypted key')
|
||||
return
|
||||
}
|
||||
|
||||
// Get current user info
|
||||
const currentUser = await getCurrentUser(ndk)
|
||||
console.log('Current user:', currentUser)
|
||||
|
||||
// Logout when done
|
||||
// logout(ndk)
|
||||
}
|
||||
|
||||
export {
|
||||
loginWithExtension,
|
||||
loginWithPrivateKey,
|
||||
loginWithNip46,
|
||||
getAuthFromStorage,
|
||||
saveAuthToStorage,
|
||||
logout,
|
||||
getCurrentUser
|
||||
}
|
||||
|
||||
376
.claude/skills/ndk/examples/03-publishing-events.ts
Normal file
376
.claude/skills/ndk/examples/03-publishing-events.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* NDK Event Publishing Patterns
|
||||
*
|
||||
* Examples from: src/publish/orders.tsx, scripts/gen_products.ts
|
||||
*/
|
||||
|
||||
import NDK, { NDKEvent, NDKTag } from '@nostr-dev-kit/ndk'
|
||||
|
||||
// ============================================================
|
||||
// BASIC EVENT PUBLISHING
|
||||
// ============================================================
|
||||
|
||||
const publishBasicNote = async (ndk: NDK, content: string) => {
|
||||
// Create event
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 1 // Text note
|
||||
event.content = content
|
||||
event.tags = []
|
||||
|
||||
// Sign and publish
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
|
||||
console.log('✅ Published note:', event.id)
|
||||
return event.id
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// EVENT WITH TAGS
|
||||
// ============================================================
|
||||
|
||||
const publishNoteWithTags = async (
|
||||
ndk: NDK,
|
||||
content: string,
|
||||
options: {
|
||||
mentions?: string[] // pubkeys to mention
|
||||
hashtags?: string[]
|
||||
replyTo?: string // event ID
|
||||
}
|
||||
) => {
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 1
|
||||
event.content = content
|
||||
event.tags = []
|
||||
|
||||
// Add mentions
|
||||
if (options.mentions) {
|
||||
options.mentions.forEach(pubkey => {
|
||||
event.tags.push(['p', pubkey])
|
||||
})
|
||||
}
|
||||
|
||||
// Add hashtags
|
||||
if (options.hashtags) {
|
||||
options.hashtags.forEach(tag => {
|
||||
event.tags.push(['t', tag])
|
||||
})
|
||||
}
|
||||
|
||||
// Add reply
|
||||
if (options.replyTo) {
|
||||
event.tags.push(['e', options.replyTo, '', 'reply'])
|
||||
}
|
||||
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
|
||||
return event.id
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PRODUCT LISTING (PARAMETERIZED REPLACEABLE EVENT)
|
||||
// ============================================================
|
||||
|
||||
interface ProductData {
|
||||
slug: string // Unique identifier
|
||||
title: string
|
||||
description: string
|
||||
price: number
|
||||
currency: string
|
||||
images: string[]
|
||||
shippingRefs?: string[]
|
||||
category?: string
|
||||
}
|
||||
|
||||
const publishProduct = async (ndk: NDK, product: ProductData) => {
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 30402 // Product listing kind
|
||||
event.content = product.description
|
||||
|
||||
// Build tags
|
||||
event.tags = [
|
||||
['d', product.slug], // Unique identifier (required for replaceable)
|
||||
['title', product.title],
|
||||
['price', product.price.toString(), product.currency],
|
||||
]
|
||||
|
||||
// Add images
|
||||
product.images.forEach(image => {
|
||||
event.tags.push(['image', image])
|
||||
})
|
||||
|
||||
// Add shipping options
|
||||
if (product.shippingRefs) {
|
||||
product.shippingRefs.forEach(ref => {
|
||||
event.tags.push(['shipping', ref])
|
||||
})
|
||||
}
|
||||
|
||||
// Add category
|
||||
if (product.category) {
|
||||
event.tags.push(['t', product.category])
|
||||
}
|
||||
|
||||
// Optional: set custom timestamp
|
||||
event.created_at = Math.floor(Date.now() / 1000)
|
||||
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
|
||||
console.log('✅ Published product:', product.title)
|
||||
return event.id
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ORDER CREATION EVENT
|
||||
// ============================================================
|
||||
|
||||
interface OrderData {
|
||||
orderId: string
|
||||
sellerPubkey: string
|
||||
productRef: string
|
||||
quantity: number
|
||||
totalAmount: string
|
||||
currency: string
|
||||
shippingRef?: string
|
||||
shippingAddress?: string
|
||||
email?: string
|
||||
phone?: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
const createOrder = async (ndk: NDK, order: OrderData) => {
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 16 // Order processing kind
|
||||
event.content = order.notes || ''
|
||||
|
||||
// Required tags per spec
|
||||
event.tags = [
|
||||
['p', order.sellerPubkey],
|
||||
['subject', `Order ${order.orderId.substring(0, 8)}`],
|
||||
['type', 'order-creation'],
|
||||
['order', order.orderId],
|
||||
['amount', order.totalAmount],
|
||||
['item', order.productRef, order.quantity.toString()],
|
||||
]
|
||||
|
||||
// Optional tags
|
||||
if (order.shippingRef) {
|
||||
event.tags.push(['shipping', order.shippingRef])
|
||||
}
|
||||
|
||||
if (order.shippingAddress) {
|
||||
event.tags.push(['address', order.shippingAddress])
|
||||
}
|
||||
|
||||
if (order.email) {
|
||||
event.tags.push(['email', order.email])
|
||||
}
|
||||
|
||||
if (order.phone) {
|
||||
event.tags.push(['phone', order.phone])
|
||||
}
|
||||
|
||||
try {
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
|
||||
console.log('✅ Order created:', order.orderId)
|
||||
return { success: true, eventId: event.id }
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create order:', error)
|
||||
return { success: false, error }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// STATUS UPDATE EVENT
|
||||
// ============================================================
|
||||
|
||||
const publishStatusUpdate = async (
|
||||
ndk: NDK,
|
||||
orderId: string,
|
||||
recipientPubkey: string,
|
||||
status: 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled',
|
||||
notes?: string
|
||||
) => {
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 16
|
||||
event.content = notes || `Order status updated to ${status}`
|
||||
event.tags = [
|
||||
['p', recipientPubkey],
|
||||
['subject', 'order-info'],
|
||||
['type', 'status-update'],
|
||||
['order', orderId],
|
||||
['status', status],
|
||||
]
|
||||
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
|
||||
return event.id
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BATCH PUBLISHING
|
||||
// ============================================================
|
||||
|
||||
const publishMultipleEvents = async (
|
||||
ndk: NDK,
|
||||
events: Array<{ kind: number; content: string; tags: NDKTag[] }>
|
||||
) => {
|
||||
const results = []
|
||||
|
||||
for (const eventData of events) {
|
||||
try {
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = eventData.kind
|
||||
event.content = eventData.content
|
||||
event.tags = eventData.tags
|
||||
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
|
||||
results.push({ success: true, eventId: event.id })
|
||||
} catch (error) {
|
||||
results.push({ success: false, error })
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PUBLISH WITH CUSTOM SIGNER
|
||||
// ============================================================
|
||||
|
||||
import { NDKSigner } from '@nostr-dev-kit/ndk'
|
||||
|
||||
const publishWithCustomSigner = async (
|
||||
ndk: NDK,
|
||||
signer: NDKSigner,
|
||||
eventData: { kind: number; content: string; tags: NDKTag[] }
|
||||
) => {
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = eventData.kind
|
||||
event.content = eventData.content
|
||||
event.tags = eventData.tags
|
||||
|
||||
// Sign with specific signer (not ndk.signer)
|
||||
await event.sign(signer)
|
||||
await event.publish()
|
||||
|
||||
return event.id
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ERROR HANDLING PATTERN
|
||||
// ============================================================
|
||||
|
||||
const publishWithErrorHandling = async (
|
||||
ndk: NDK,
|
||||
eventData: { kind: number; content: string; tags: NDKTag[] }
|
||||
) => {
|
||||
// Validate NDK
|
||||
if (!ndk) {
|
||||
throw new Error('NDK not initialized')
|
||||
}
|
||||
|
||||
// Validate signer
|
||||
if (!ndk.signer) {
|
||||
throw new Error('No active signer. Please login first.')
|
||||
}
|
||||
|
||||
try {
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = eventData.kind
|
||||
event.content = eventData.content
|
||||
event.tags = eventData.tags
|
||||
|
||||
// Sign
|
||||
await event.sign()
|
||||
|
||||
// Verify signature
|
||||
if (!event.sig) {
|
||||
throw new Error('Event signing failed')
|
||||
}
|
||||
|
||||
// Publish
|
||||
await event.publish()
|
||||
|
||||
// Verify event ID
|
||||
if (!event.id) {
|
||||
throw new Error('Event ID not generated')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
eventId: event.id,
|
||||
pubkey: event.pubkey
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Publishing failed:', error)
|
||||
|
||||
if (error instanceof Error) {
|
||||
// Handle specific error types
|
||||
if (error.message.includes('relay')) {
|
||||
throw new Error('Failed to publish to relays. Check connection.')
|
||||
}
|
||||
if (error.message.includes('sign')) {
|
||||
throw new Error('Failed to sign event. Check signer.')
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// USAGE EXAMPLE
|
||||
// ============================================================
|
||||
|
||||
async function publishingExample(ndk: NDK) {
|
||||
// Simple note
|
||||
await publishBasicNote(ndk, 'Hello Nostr!')
|
||||
|
||||
// Note with tags
|
||||
await publishNoteWithTags(ndk, 'Check out this product!', {
|
||||
hashtags: ['marketplace', 'nostr'],
|
||||
mentions: ['pubkey123...']
|
||||
})
|
||||
|
||||
// Product listing
|
||||
await publishProduct(ndk, {
|
||||
slug: 'bitcoin-tshirt',
|
||||
title: 'Bitcoin T-Shirt',
|
||||
description: 'High quality Bitcoin t-shirt',
|
||||
price: 25,
|
||||
currency: 'USD',
|
||||
images: ['https://example.com/image.jpg'],
|
||||
category: 'clothing'
|
||||
})
|
||||
|
||||
// Order
|
||||
await createOrder(ndk, {
|
||||
orderId: 'order-123',
|
||||
sellerPubkey: 'seller-pubkey',
|
||||
productRef: '30402:pubkey:bitcoin-tshirt',
|
||||
quantity: 1,
|
||||
totalAmount: '25.00',
|
||||
currency: 'USD',
|
||||
email: 'customer@example.com'
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
publishBasicNote,
|
||||
publishNoteWithTags,
|
||||
publishProduct,
|
||||
createOrder,
|
||||
publishStatusUpdate,
|
||||
publishMultipleEvents,
|
||||
publishWithCustomSigner,
|
||||
publishWithErrorHandling
|
||||
}
|
||||
|
||||
404
.claude/skills/ndk/examples/04-querying-subscribing.ts
Normal file
404
.claude/skills/ndk/examples/04-querying-subscribing.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
/**
|
||||
* NDK Query and Subscription Patterns
|
||||
*
|
||||
* Examples from: src/queries/orders.tsx, src/queries/payment.tsx
|
||||
*/
|
||||
|
||||
import NDK, { NDKEvent, NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk'
|
||||
|
||||
// ============================================================
|
||||
// BASIC FETCH (ONE-TIME QUERY)
|
||||
// ============================================================
|
||||
|
||||
const fetchNotes = async (ndk: NDK, authorPubkey: string, limit: number = 50) => {
|
||||
const filter: NDKFilter = {
|
||||
kinds: [1], // Text notes
|
||||
authors: [authorPubkey],
|
||||
limit
|
||||
}
|
||||
|
||||
// Fetch returns a Set
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
|
||||
// Convert to array and sort by timestamp
|
||||
const eventArray = Array.from(events).sort((a, b) =>
|
||||
(b.created_at || 0) - (a.created_at || 0)
|
||||
)
|
||||
|
||||
return eventArray
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FETCH WITH MULTIPLE FILTERS
|
||||
// ============================================================
|
||||
|
||||
const fetchProductsByMultipleAuthors = async (
|
||||
ndk: NDK,
|
||||
pubkeys: string[]
|
||||
) => {
|
||||
const filter: NDKFilter = {
|
||||
kinds: [30402], // Product listings
|
||||
authors: pubkeys,
|
||||
limit: 100
|
||||
}
|
||||
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
return Array.from(events)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FETCH WITH TAG FILTERS
|
||||
// ============================================================
|
||||
|
||||
const fetchOrderEvents = async (ndk: NDK, orderId: string) => {
|
||||
const filter: NDKFilter = {
|
||||
kinds: [16, 17], // Order and payment receipt
|
||||
'#order': [orderId], // Tag filter (note the # prefix)
|
||||
}
|
||||
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
return Array.from(events)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FETCH WITH TIME RANGE
|
||||
// ============================================================
|
||||
|
||||
const fetchRecentEvents = async (
|
||||
ndk: NDK,
|
||||
kind: number,
|
||||
hoursAgo: number = 24
|
||||
) => {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const since = now - (hoursAgo * 3600)
|
||||
|
||||
const filter: NDKFilter = {
|
||||
kinds: [kind],
|
||||
since,
|
||||
until: now,
|
||||
limit: 100
|
||||
}
|
||||
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
return Array.from(events)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FETCH BY EVENT ID
|
||||
// ============================================================
|
||||
|
||||
const fetchEventById = async (ndk: NDK, eventId: string) => {
|
||||
const filter: NDKFilter = {
|
||||
ids: [eventId]
|
||||
}
|
||||
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
|
||||
if (events.size === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return Array.from(events)[0]
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BASIC SUBSCRIPTION (REAL-TIME)
|
||||
// ============================================================
|
||||
|
||||
const subscribeToNotes = (
|
||||
ndk: NDK,
|
||||
authorPubkey: string,
|
||||
onEvent: (event: NDKEvent) => void
|
||||
): NDKSubscription => {
|
||||
const filter: NDKFilter = {
|
||||
kinds: [1],
|
||||
authors: [authorPubkey]
|
||||
}
|
||||
|
||||
const subscription = ndk.subscribe(filter, {
|
||||
closeOnEose: false // Keep open for real-time updates
|
||||
})
|
||||
|
||||
// Event handler
|
||||
subscription.on('event', (event: NDKEvent) => {
|
||||
onEvent(event)
|
||||
})
|
||||
|
||||
// EOSE (End of Stored Events) handler
|
||||
subscription.on('eose', () => {
|
||||
console.log('✅ Received all stored events')
|
||||
})
|
||||
|
||||
return subscription
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SUBSCRIPTION WITH CLEANUP
|
||||
// ============================================================
|
||||
|
||||
const createManagedSubscription = (
|
||||
ndk: NDK,
|
||||
filter: NDKFilter,
|
||||
handlers: {
|
||||
onEvent: (event: NDKEvent) => void
|
||||
onEose?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
) => {
|
||||
const subscription = ndk.subscribe(filter, { closeOnEose: false })
|
||||
|
||||
subscription.on('event', handlers.onEvent)
|
||||
|
||||
if (handlers.onEose) {
|
||||
subscription.on('eose', handlers.onEose)
|
||||
}
|
||||
|
||||
if (handlers.onClose) {
|
||||
subscription.on('close', handlers.onClose)
|
||||
}
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
subscription.stop()
|
||||
console.log('✅ Subscription stopped')
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MONITORING SPECIFIC EVENT
|
||||
// ============================================================
|
||||
|
||||
const monitorPaymentReceipt = (
|
||||
ndk: NDK,
|
||||
orderId: string,
|
||||
invoiceId: string,
|
||||
onPaymentReceived: (preimage: string) => void
|
||||
): NDKSubscription => {
|
||||
const sessionStart = Math.floor(Date.now() / 1000)
|
||||
|
||||
const filter: NDKFilter = {
|
||||
kinds: [17], // Payment receipt
|
||||
'#order': [orderId],
|
||||
'#payment-request': [invoiceId],
|
||||
since: sessionStart - 30 // 30 second buffer for clock skew
|
||||
}
|
||||
|
||||
const subscription = ndk.subscribe(filter, { closeOnEose: false })
|
||||
|
||||
subscription.on('event', (event: NDKEvent) => {
|
||||
// Verify event is recent
|
||||
if (event.created_at && event.created_at < sessionStart - 30) {
|
||||
console.log('⏰ Ignoring old receipt')
|
||||
return
|
||||
}
|
||||
|
||||
// Verify it's the correct invoice
|
||||
const paymentRequestTag = event.tags.find(tag => tag[0] === 'payment-request')
|
||||
if (paymentRequestTag?.[1] !== invoiceId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract preimage
|
||||
const paymentTag = event.tags.find(tag => tag[0] === 'payment')
|
||||
const preimage = paymentTag?.[3] || 'external-payment'
|
||||
|
||||
console.log('✅ Payment received!')
|
||||
subscription.stop()
|
||||
onPaymentReceived(preimage)
|
||||
})
|
||||
|
||||
return subscription
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REACT INTEGRATION PATTERN
|
||||
// ============================================================
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
function useOrderSubscription(ndk: NDK | null, orderId: string) {
|
||||
const [events, setEvents] = useState<NDKEvent[]>([])
|
||||
const [eosed, setEosed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!ndk || !orderId) return
|
||||
|
||||
const filter: NDKFilter = {
|
||||
kinds: [16, 17],
|
||||
'#order': [orderId]
|
||||
}
|
||||
|
||||
const subscription = ndk.subscribe(filter, { closeOnEose: false })
|
||||
|
||||
subscription.on('event', (event: NDKEvent) => {
|
||||
setEvents(prev => {
|
||||
// Avoid duplicates
|
||||
if (prev.some(e => e.id === event.id)) {
|
||||
return prev
|
||||
}
|
||||
return [...prev, event].sort((a, b) =>
|
||||
(a.created_at || 0) - (b.created_at || 0)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
subscription.on('eose', () => {
|
||||
setEosed(true)
|
||||
})
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
subscription.stop()
|
||||
}
|
||||
}, [ndk, orderId])
|
||||
|
||||
return { events, eosed }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REACT QUERY INTEGRATION
|
||||
// ============================================================
|
||||
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
// Query function
|
||||
const fetchProducts = async (ndk: NDK, pubkey: string) => {
|
||||
if (!ndk) throw new Error('NDK not initialized')
|
||||
|
||||
const filter: NDKFilter = {
|
||||
kinds: [30402],
|
||||
authors: [pubkey]
|
||||
}
|
||||
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
return Array.from(events)
|
||||
}
|
||||
|
||||
// Hook with subscription for real-time updates
|
||||
function useProductsWithSubscription(ndk: NDK | null, pubkey: string) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Initial query
|
||||
const query = useQuery({
|
||||
queryKey: ['products', pubkey],
|
||||
queryFn: () => fetchProducts(ndk!, pubkey),
|
||||
enabled: !!ndk && !!pubkey,
|
||||
staleTime: 30000
|
||||
})
|
||||
|
||||
// Real-time subscription
|
||||
useEffect(() => {
|
||||
if (!ndk || !pubkey) return
|
||||
|
||||
const filter: NDKFilter = {
|
||||
kinds: [30402],
|
||||
authors: [pubkey]
|
||||
}
|
||||
|
||||
const subscription = ndk.subscribe(filter, { closeOnEose: false })
|
||||
|
||||
subscription.on('event', () => {
|
||||
// Invalidate query to trigger refetch
|
||||
queryClient.invalidateQueries({ queryKey: ['products', pubkey] })
|
||||
})
|
||||
|
||||
return () => {
|
||||
subscription.stop()
|
||||
}
|
||||
}, [ndk, pubkey, queryClient])
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ADVANCED: WAITING FOR SPECIFIC EVENT
|
||||
// ============================================================
|
||||
|
||||
const waitForEvent = (
|
||||
ndk: NDK,
|
||||
filter: NDKFilter,
|
||||
condition: (event: NDKEvent) => boolean,
|
||||
timeoutMs: number = 30000
|
||||
): Promise<NDKEvent | null> => {
|
||||
return new Promise((resolve) => {
|
||||
const subscription = ndk.subscribe(filter, { closeOnEose: false })
|
||||
|
||||
// Timeout
|
||||
const timeout = setTimeout(() => {
|
||||
subscription.stop()
|
||||
resolve(null)
|
||||
}, timeoutMs)
|
||||
|
||||
// Event handler
|
||||
subscription.on('event', (event: NDKEvent) => {
|
||||
if (condition(event)) {
|
||||
clearTimeout(timeout)
|
||||
subscription.stop()
|
||||
resolve(event)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Usage example
|
||||
async function waitForPayment(ndk: NDK, orderId: string, invoiceId: string) {
|
||||
const paymentEvent = await waitForEvent(
|
||||
ndk,
|
||||
{
|
||||
kinds: [17],
|
||||
'#order': [orderId],
|
||||
since: Math.floor(Date.now() / 1000)
|
||||
},
|
||||
(event) => {
|
||||
const tag = event.tags.find(t => t[0] === 'payment-request')
|
||||
return tag?.[1] === invoiceId
|
||||
},
|
||||
60000 // 60 second timeout
|
||||
)
|
||||
|
||||
if (paymentEvent) {
|
||||
console.log('✅ Payment confirmed!')
|
||||
return paymentEvent
|
||||
} else {
|
||||
console.log('⏰ Payment timeout')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// USAGE EXAMPLES
|
||||
// ============================================================
|
||||
|
||||
async function queryExample(ndk: NDK) {
|
||||
// Fetch notes
|
||||
const notes = await fetchNotes(ndk, 'pubkey123', 50)
|
||||
console.log(`Found ${notes.length} notes`)
|
||||
|
||||
// Subscribe to new notes
|
||||
const cleanup = subscribeToNotes(ndk, 'pubkey123', (event) => {
|
||||
console.log('New note:', event.content)
|
||||
})
|
||||
|
||||
// Clean up after 60 seconds
|
||||
setTimeout(cleanup, 60000)
|
||||
|
||||
// Monitor payment
|
||||
monitorPaymentReceipt(ndk, 'order-123', 'invoice-456', (preimage) => {
|
||||
console.log('Payment received:', preimage)
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
fetchNotes,
|
||||
fetchProductsByMultipleAuthors,
|
||||
fetchOrderEvents,
|
||||
fetchRecentEvents,
|
||||
fetchEventById,
|
||||
subscribeToNotes,
|
||||
createManagedSubscription,
|
||||
monitorPaymentReceipt,
|
||||
useOrderSubscription,
|
||||
useProductsWithSubscription,
|
||||
waitForEvent
|
||||
}
|
||||
|
||||
423
.claude/skills/ndk/examples/05-users-profiles.ts
Normal file
423
.claude/skills/ndk/examples/05-users-profiles.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* NDK User and Profile Handling
|
||||
*
|
||||
* Examples from: src/queries/profiles.tsx, src/components/Profile.tsx
|
||||
*/
|
||||
|
||||
import NDK, { NDKUser, NDKUserProfile } from '@nostr-dev-kit/ndk'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
|
||||
// ============================================================
|
||||
// FETCH PROFILE BY NPUB
|
||||
// ============================================================
|
||||
|
||||
const fetchProfileByNpub = async (ndk: NDK, npub: string): Promise<NDKUserProfile | null> => {
|
||||
try {
|
||||
// Get user object from npub
|
||||
const user = ndk.getUser({ npub })
|
||||
|
||||
// Fetch profile from relays
|
||||
const profile = await user.fetchProfile()
|
||||
|
||||
return profile
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FETCH PROFILE BY HEX PUBKEY
|
||||
// ============================================================
|
||||
|
||||
const fetchProfileByPubkey = async (ndk: NDK, pubkey: string): Promise<NDKUserProfile | null> => {
|
||||
try {
|
||||
const user = ndk.getUser({ hexpubkey: pubkey })
|
||||
const profile = await user.fetchProfile()
|
||||
|
||||
return profile
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FETCH PROFILE BY NIP-05
|
||||
// ============================================================
|
||||
|
||||
const fetchProfileByNip05 = async (ndk: NDK, nip05: string): Promise<NDKUserProfile | null> => {
|
||||
try {
|
||||
// Resolve NIP-05 identifier to user
|
||||
const user = await ndk.getUserFromNip05(nip05)
|
||||
|
||||
if (!user) {
|
||||
console.log('User not found for NIP-05:', nip05)
|
||||
return null
|
||||
}
|
||||
|
||||
// Fetch profile
|
||||
const profile = await user.fetchProfile()
|
||||
|
||||
return profile
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile by NIP-05:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FETCH PROFILE BY ANY IDENTIFIER
|
||||
// ============================================================
|
||||
|
||||
const fetchProfileByIdentifier = async (
|
||||
ndk: NDK,
|
||||
identifier: string
|
||||
): Promise<{ profile: NDKUserProfile | null; user: NDKUser | null }> => {
|
||||
try {
|
||||
// Check if it's a NIP-05 (contains @)
|
||||
if (identifier.includes('@')) {
|
||||
const user = await ndk.getUserFromNip05(identifier)
|
||||
if (!user) return { profile: null, user: null }
|
||||
|
||||
const profile = await user.fetchProfile()
|
||||
return { profile, user }
|
||||
}
|
||||
|
||||
// Check if it's an npub
|
||||
if (identifier.startsWith('npub')) {
|
||||
const user = ndk.getUser({ npub: identifier })
|
||||
const profile = await user.fetchProfile()
|
||||
return { profile, user }
|
||||
}
|
||||
|
||||
// Assume it's a hex pubkey
|
||||
const user = ndk.getUser({ hexpubkey: identifier })
|
||||
const profile = await user.fetchProfile()
|
||||
return { profile, user }
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error)
|
||||
return { profile: null, user: null }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET CURRENT USER
|
||||
// ============================================================
|
||||
|
||||
const getCurrentUser = async (ndk: NDK): Promise<NDKUser | null> => {
|
||||
if (!ndk.signer) {
|
||||
console.log('No signer set')
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await ndk.signer.user()
|
||||
return user
|
||||
} catch (error) {
|
||||
console.error('Failed to get current user:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PROFILE DATA STRUCTURE
|
||||
// ============================================================
|
||||
|
||||
interface ProfileData {
|
||||
// Standard fields
|
||||
name?: string
|
||||
displayName?: string
|
||||
display_name?: string
|
||||
picture?: string
|
||||
image?: string
|
||||
banner?: string
|
||||
about?: string
|
||||
|
||||
// Contact
|
||||
nip05?: string
|
||||
lud06?: string // LNURL
|
||||
lud16?: string // Lightning address
|
||||
|
||||
// Social
|
||||
website?: string
|
||||
|
||||
// Raw data
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// EXTRACT PROFILE INFO
|
||||
// ============================================================
|
||||
|
||||
const extractProfileInfo = (profile: NDKUserProfile | null) => {
|
||||
if (!profile) {
|
||||
return {
|
||||
displayName: 'Anonymous',
|
||||
avatar: null,
|
||||
bio: null,
|
||||
lightningAddress: null,
|
||||
nip05: null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
displayName: profile.displayName || profile.display_name || profile.name || 'Anonymous',
|
||||
avatar: profile.picture || profile.image || null,
|
||||
banner: profile.banner || null,
|
||||
bio: profile.about || null,
|
||||
lightningAddress: profile.lud16 || profile.lud06 || null,
|
||||
nip05: profile.nip05 || null,
|
||||
website: profile.website || null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UPDATE PROFILE
|
||||
// ============================================================
|
||||
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
|
||||
const updateProfile = async (ndk: NDK, profileData: Partial<ProfileData>) => {
|
||||
if (!ndk.signer) {
|
||||
throw new Error('No signer available')
|
||||
}
|
||||
|
||||
// Get current profile
|
||||
const currentUser = await ndk.signer.user()
|
||||
const currentProfile = await currentUser.fetchProfile()
|
||||
|
||||
// Merge with new data
|
||||
const updatedProfile = {
|
||||
...currentProfile,
|
||||
...profileData
|
||||
}
|
||||
|
||||
// Create kind 0 (metadata) event
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 0
|
||||
event.content = JSON.stringify(updatedProfile)
|
||||
event.tags = []
|
||||
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
|
||||
console.log('✅ Profile updated')
|
||||
return event.id
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BATCH FETCH PROFILES
|
||||
// ============================================================
|
||||
|
||||
const fetchMultipleProfiles = async (
|
||||
ndk: NDK,
|
||||
pubkeys: string[]
|
||||
): Promise<Map<string, NDKUserProfile | null>> => {
|
||||
const profiles = new Map<string, NDKUserProfile | null>()
|
||||
|
||||
// Fetch all profiles in parallel
|
||||
await Promise.all(
|
||||
pubkeys.map(async (pubkey) => {
|
||||
try {
|
||||
const user = ndk.getUser({ hexpubkey: pubkey })
|
||||
const profile = await user.fetchProfile()
|
||||
profiles.set(pubkey, profile)
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch profile for ${pubkey}:`, error)
|
||||
profiles.set(pubkey, null)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return profiles
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CONVERT BETWEEN FORMATS
|
||||
// ============================================================
|
||||
|
||||
const convertPubkeyFormats = (identifier: string) => {
|
||||
try {
|
||||
// If it's npub, convert to hex
|
||||
if (identifier.startsWith('npub')) {
|
||||
const decoded = nip19.decode(identifier)
|
||||
if (decoded.type === 'npub') {
|
||||
return {
|
||||
hex: decoded.data as string,
|
||||
npub: identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it's hex, convert to npub
|
||||
if (/^[0-9a-f]{64}$/.test(identifier)) {
|
||||
return {
|
||||
hex: identifier,
|
||||
npub: nip19.npubEncode(identifier)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Invalid pubkey format')
|
||||
} catch (error) {
|
||||
console.error('Format conversion failed:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REACT HOOK FOR PROFILE
|
||||
// ============================================================
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
function useProfile(ndk: NDK | null, npub: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ['profile', npub],
|
||||
queryFn: async () => {
|
||||
if (!ndk || !npub) throw new Error('NDK or npub missing')
|
||||
return await fetchProfileByNpub(ndk, npub)
|
||||
},
|
||||
enabled: !!ndk && !!npub,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
cacheTime: 30 * 60 * 1000 // 30 minutes
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REACT COMPONENT EXAMPLE
|
||||
// ============================================================
|
||||
|
||||
interface ProfileDisplayProps {
|
||||
ndk: NDK
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
function ProfileDisplay({ ndk, pubkey }: ProfileDisplayProps) {
|
||||
const [profile, setProfile] = useState<NDKUserProfile | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const loadProfile = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const user = ndk.getUser({ hexpubkey: pubkey })
|
||||
const fetchedProfile = await user.fetchProfile()
|
||||
setProfile(fetchedProfile)
|
||||
} catch (error) {
|
||||
console.error('Failed to load profile:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadProfile()
|
||||
}, [ndk, pubkey])
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading profile...</div>
|
||||
}
|
||||
|
||||
const info = extractProfileInfo(profile)
|
||||
|
||||
return (
|
||||
<div className="profile">
|
||||
{info.avatar && <img src={info.avatar} alt={info.displayName} />}
|
||||
<h2>{info.displayName}</h2>
|
||||
{info.bio && <p>{info.bio}</p>}
|
||||
{info.nip05 && <span>✓ {info.nip05}</span>}
|
||||
{info.lightningAddress && <span>⚡ {info.lightningAddress}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FOLLOW/UNFOLLOW USER
|
||||
// ============================================================
|
||||
|
||||
const followUser = async (ndk: NDK, pubkeyToFollow: string) => {
|
||||
if (!ndk.signer) {
|
||||
throw new Error('No signer available')
|
||||
}
|
||||
|
||||
// Fetch current contact list (kind 3)
|
||||
const currentUser = await ndk.signer.user()
|
||||
const contactListFilter = {
|
||||
kinds: [3],
|
||||
authors: [currentUser.pubkey]
|
||||
}
|
||||
|
||||
const existingEvents = await ndk.fetchEvents(contactListFilter)
|
||||
const existingContactList = existingEvents.size > 0
|
||||
? Array.from(existingEvents)[0]
|
||||
: null
|
||||
|
||||
// Get existing p tags
|
||||
const existingPTags = existingContactList
|
||||
? existingContactList.tags.filter(tag => tag[0] === 'p')
|
||||
: []
|
||||
|
||||
// Check if already following
|
||||
const alreadyFollowing = existingPTags.some(tag => tag[1] === pubkeyToFollow)
|
||||
if (alreadyFollowing) {
|
||||
console.log('Already following this user')
|
||||
return
|
||||
}
|
||||
|
||||
// Create new contact list with added user
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 3
|
||||
event.content = existingContactList?.content || ''
|
||||
event.tags = [
|
||||
...existingPTags,
|
||||
['p', pubkeyToFollow]
|
||||
]
|
||||
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
|
||||
console.log('✅ Now following user')
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// USAGE EXAMPLE
|
||||
// ============================================================
|
||||
|
||||
async function profileExample(ndk: NDK) {
|
||||
// Fetch by different identifiers
|
||||
const profile1 = await fetchProfileByNpub(ndk, 'npub1...')
|
||||
const profile2 = await fetchProfileByNip05(ndk, 'user@domain.com')
|
||||
const profile3 = await fetchProfileByPubkey(ndk, 'hex pubkey...')
|
||||
|
||||
// Extract display info
|
||||
const info = extractProfileInfo(profile1)
|
||||
console.log('Display name:', info.displayName)
|
||||
console.log('Avatar:', info.avatar)
|
||||
|
||||
// Update own profile
|
||||
await updateProfile(ndk, {
|
||||
name: 'My Name',
|
||||
about: 'My bio',
|
||||
picture: 'https://example.com/avatar.jpg',
|
||||
lud16: 'me@getalby.com'
|
||||
})
|
||||
|
||||
// Follow someone
|
||||
await followUser(ndk, 'pubkey to follow')
|
||||
}
|
||||
|
||||
export {
|
||||
fetchProfileByNpub,
|
||||
fetchProfileByPubkey,
|
||||
fetchProfileByNip05,
|
||||
fetchProfileByIdentifier,
|
||||
getCurrentUser,
|
||||
extractProfileInfo,
|
||||
updateProfile,
|
||||
fetchMultipleProfiles,
|
||||
convertPubkeyFormats,
|
||||
useProfile,
|
||||
followUser
|
||||
}
|
||||
|
||||
94
.claude/skills/ndk/examples/README.md
Normal file
94
.claude/skills/ndk/examples/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# NDK Examples Index
|
||||
|
||||
Complete code examples extracted from the Plebeian Market production codebase.
|
||||
|
||||
## Available Examples
|
||||
|
||||
### 01-initialization.ts
|
||||
- Basic NDK initialization
|
||||
- Multiple NDK instances (main + zap relays)
|
||||
- Connection with timeout protection
|
||||
- Connection status checking
|
||||
- Full initialization flow with error handling
|
||||
|
||||
### 02-authentication.ts
|
||||
- NIP-07 browser extension login
|
||||
- Private key signer
|
||||
- NIP-46 remote signer (Bunker)
|
||||
- Auto-login from localStorage
|
||||
- Saving auth credentials
|
||||
- Logout functionality
|
||||
- Getting current user
|
||||
|
||||
### 03-publishing-events.ts
|
||||
- Basic note publishing
|
||||
- Events with tags (mentions, hashtags, replies)
|
||||
- Product listings (parameterized replaceable events)
|
||||
- Order creation events
|
||||
- Status update events
|
||||
- Batch publishing
|
||||
- Custom signer usage
|
||||
- Comprehensive error handling
|
||||
|
||||
### 04-querying-subscribing.ts
|
||||
- Basic fetch queries
|
||||
- Multiple author queries
|
||||
- Tag filtering
|
||||
- Time range filtering
|
||||
- Event ID lookup
|
||||
- Real-time subscriptions
|
||||
- Subscription cleanup patterns
|
||||
- React integration hooks
|
||||
- React Query integration
|
||||
- Waiting for specific events
|
||||
- Payment monitoring
|
||||
|
||||
### 05-users-profiles.ts
|
||||
- Fetch profile by npub
|
||||
- Fetch profile by hex pubkey
|
||||
- Fetch profile by NIP-05
|
||||
- Universal identifier lookup
|
||||
- Get current user
|
||||
- Extract profile information
|
||||
- Update user profile
|
||||
- Batch fetch multiple profiles
|
||||
- Convert between pubkey formats (hex/npub)
|
||||
- React hooks for profiles
|
||||
- Follow/unfollow users
|
||||
|
||||
## Usage
|
||||
|
||||
Each file contains:
|
||||
- Fully typed TypeScript code
|
||||
- JSDoc comments explaining the pattern
|
||||
- Error handling examples
|
||||
- Integration patterns with React/TanStack Query
|
||||
- Real-world usage examples
|
||||
|
||||
All examples are based on actual production code from the Plebeian Market application.
|
||||
|
||||
## Running Examples
|
||||
|
||||
```typescript
|
||||
import { initializeNDK } from './01-initialization'
|
||||
import { loginWithExtension } from './02-authentication'
|
||||
import { publishBasicNote } from './03-publishing-events'
|
||||
|
||||
// Initialize NDK
|
||||
const { ndk, isConnected } = await initializeNDK()
|
||||
|
||||
if (isConnected) {
|
||||
// Authenticate
|
||||
const { user } = await loginWithExtension(ndk)
|
||||
|
||||
// Publish
|
||||
await publishBasicNote(ndk, 'Hello Nostr!')
|
||||
}
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- See `../ndk-skill.md` for detailed documentation
|
||||
- See `../quick-reference.md` for quick lookup
|
||||
- Check the main codebase for more complex patterns
|
||||
|
||||
701
.claude/skills/ndk/ndk-skill.md
Normal file
701
.claude/skills/ndk/ndk-skill.md
Normal file
@@ -0,0 +1,701 @@
|
||||
# NDK (Nostr Development Kit) - Claude Skill Reference
|
||||
|
||||
## Overview
|
||||
|
||||
NDK is the primary Nostr development kit with outbox-model support, designed for building Nostr applications with TypeScript/JavaScript. This reference is based on analyzing production usage in the Plebeian Market codebase.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. NDK Initialization
|
||||
|
||||
**Basic Pattern:**
|
||||
```typescript
|
||||
import NDK from '@nostr-dev-kit/ndk'
|
||||
|
||||
// Simple initialization
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: ['wss://relay.damus.io', 'wss://relay.nostr.band']
|
||||
})
|
||||
|
||||
await ndk.connect()
|
||||
```
|
||||
|
||||
**Store-based Pattern (Production):**
|
||||
```typescript
|
||||
// From src/lib/stores/ndk.ts
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: relays || defaultRelaysUrls,
|
||||
})
|
||||
|
||||
// Separate NDK for zaps on specialized relays
|
||||
const zapNdk = new NDK({
|
||||
explicitRelayUrls: ZAP_RELAYS,
|
||||
})
|
||||
|
||||
// Connect with timeout protection
|
||||
const connectPromise = ndk.connect()
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Connection timeout')), timeoutMs)
|
||||
)
|
||||
await Promise.race([connectPromise, timeoutPromise])
|
||||
```
|
||||
|
||||
### 2. Authentication & Signers
|
||||
|
||||
NDK supports multiple signer types for different authentication methods:
|
||||
|
||||
#### NIP-07 (Browser Extension)
|
||||
```typescript
|
||||
import { NDKNip07Signer } from '@nostr-dev-kit/ndk'
|
||||
|
||||
const signer = new NDKNip07Signer()
|
||||
await signer.blockUntilReady()
|
||||
ndk.signer = signer
|
||||
|
||||
const user = await signer.user()
|
||||
```
|
||||
|
||||
#### Private Key Signer
|
||||
```typescript
|
||||
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'
|
||||
|
||||
const signer = new NDKPrivateKeySigner(privateKeyHex)
|
||||
await signer.blockUntilReady()
|
||||
ndk.signer = signer
|
||||
|
||||
const user = await signer.user()
|
||||
```
|
||||
|
||||
#### NIP-46 (Remote Signer / Bunker)
|
||||
```typescript
|
||||
import { NDKNip46Signer } from '@nostr-dev-kit/ndk'
|
||||
|
||||
const localSigner = new NDKPrivateKeySigner(localPrivateKey)
|
||||
const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
|
||||
await remoteSigner.blockUntilReady()
|
||||
ndk.signer = remoteSigner
|
||||
|
||||
const user = await remoteSigner.user()
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Always call `blockUntilReady()` before using a signer
|
||||
- Store signer reference in your state management
|
||||
- Set `ndk.signer` to enable signing operations
|
||||
- Use `await signer.user()` to get the authenticated user
|
||||
|
||||
### 3. Event Creation & Publishing
|
||||
|
||||
#### Basic Event Pattern
|
||||
```typescript
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
|
||||
// Create event
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 1 // Kind 1 = text note
|
||||
event.content = "Hello Nostr!"
|
||||
event.tags = [
|
||||
['t', 'nostr'],
|
||||
['p', recipientPubkey]
|
||||
]
|
||||
|
||||
// Sign and publish
|
||||
await event.sign() // Uses ndk.signer automatically
|
||||
await event.publish()
|
||||
|
||||
// Get event ID after signing
|
||||
console.log(event.id)
|
||||
```
|
||||
|
||||
#### Production Pattern with Error Handling
|
||||
```typescript
|
||||
// From src/publish/orders.tsx
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = ORDER_PROCESS_KIND
|
||||
event.content = orderNotes || ''
|
||||
event.tags = [
|
||||
['p', sellerPubkey],
|
||||
['subject', `Order for ${productName}`],
|
||||
['type', 'order-creation'],
|
||||
['order', orderId],
|
||||
['amount', totalAmount],
|
||||
['item', productRef, quantity.toString()],
|
||||
]
|
||||
|
||||
// Optional tags
|
||||
if (shippingRef) {
|
||||
event.tags.push(['shipping', shippingRef])
|
||||
}
|
||||
|
||||
try {
|
||||
await event.sign(signer) // Can pass explicit signer
|
||||
await event.publish()
|
||||
return event.id
|
||||
} catch (error) {
|
||||
console.error('Failed to publish event:', error)
|
||||
throw error
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Create event with `new NDKEvent(ndk)`
|
||||
- Set `kind`, `content`, and `tags` properties
|
||||
- Optional: Set `created_at` timestamp (defaults to now)
|
||||
- Call `await event.sign()` before publishing
|
||||
- Call `await event.publish()` to broadcast to relays
|
||||
- Access `event.id` after signing for the event hash
|
||||
|
||||
### 4. Querying Events with Filters
|
||||
|
||||
#### fetchEvents() - One-time Fetch
|
||||
```typescript
|
||||
import { NDKFilter } from '@nostr-dev-kit/ndk'
|
||||
|
||||
// Simple filter
|
||||
const filter: NDKFilter = {
|
||||
kinds: [30402], // Product listings
|
||||
authors: [merchantPubkey],
|
||||
limit: 50
|
||||
}
|
||||
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
// Returns Set<NDKEvent>
|
||||
|
||||
// Convert to array and process
|
||||
const eventArray = Array.from(events)
|
||||
const sortedEvents = eventArray.sort((a, b) =>
|
||||
(b.created_at || 0) - (a.created_at || 0)
|
||||
)
|
||||
```
|
||||
|
||||
#### Advanced Filters
|
||||
```typescript
|
||||
// Multiple kinds
|
||||
const filter: NDKFilter = {
|
||||
kinds: [16, 17], // Orders and payment receipts
|
||||
'#order': [orderId], // Tag filter (# prefix)
|
||||
since: Math.floor(Date.now() / 1000) - 86400, // Last 24 hours
|
||||
limit: 100
|
||||
}
|
||||
|
||||
// Event ID lookup
|
||||
const filter: NDKFilter = {
|
||||
ids: [eventIdHex],
|
||||
}
|
||||
|
||||
// Tag filtering
|
||||
const filter: NDKFilter = {
|
||||
kinds: [1],
|
||||
'#p': [pubkey], // Events mentioning pubkey
|
||||
'#t': ['nostr'], // Events with hashtag 'nostr'
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Subscriptions (Real-time)
|
||||
|
||||
#### Basic Subscription
|
||||
```typescript
|
||||
// From src/queries/blacklist.tsx
|
||||
const filter = {
|
||||
kinds: [10000],
|
||||
authors: [appPubkey],
|
||||
}
|
||||
|
||||
const subscription = ndk.subscribe(filter, {
|
||||
closeOnEose: false, // Keep open for real-time updates
|
||||
})
|
||||
|
||||
subscription.on('event', (event: NDKEvent) => {
|
||||
console.log('New event received:', event)
|
||||
// Process event
|
||||
})
|
||||
|
||||
subscription.on('eose', () => {
|
||||
console.log('End of stored events')
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
subscription.stop()
|
||||
```
|
||||
|
||||
#### Production Pattern with React Query
|
||||
```typescript
|
||||
// From src/queries/orders.tsx
|
||||
useEffect(() => {
|
||||
if (!orderId || !ndk) return
|
||||
|
||||
const filter = {
|
||||
kinds: [ORDER_PROCESS_KIND, PAYMENT_RECEIPT_KIND],
|
||||
'#order': [orderId],
|
||||
}
|
||||
|
||||
const subscription = ndk.subscribe(filter, {
|
||||
closeOnEose: false,
|
||||
})
|
||||
|
||||
subscription.on('event', (newEvent) => {
|
||||
// Invalidate React Query cache to trigger refetch
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orderKeys.details(orderId)
|
||||
})
|
||||
})
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
subscription.stop()
|
||||
}
|
||||
}, [orderId, ndk, queryClient])
|
||||
```
|
||||
|
||||
#### Monitoring Specific Events
|
||||
```typescript
|
||||
// From src/queries/payment.tsx - Payment receipt monitoring
|
||||
const receiptFilter = {
|
||||
kinds: [17], // Payment receipts
|
||||
'#order': [orderId],
|
||||
'#payment-request': [invoiceId],
|
||||
since: sessionStartTime - 30, // Clock skew buffer
|
||||
}
|
||||
|
||||
const subscription = ndk.subscribe(receiptFilter, {
|
||||
closeOnEose: false,
|
||||
})
|
||||
|
||||
subscription.on('event', (receiptEvent: NDKEvent) => {
|
||||
// Verify this is the correct invoice
|
||||
const paymentRequestTag = receiptEvent.tags.find(
|
||||
tag => tag[0] === 'payment-request'
|
||||
)
|
||||
|
||||
if (paymentRequestTag?.[1] === invoiceId) {
|
||||
const paymentTag = receiptEvent.tags.find(tag => tag[0] === 'payment')
|
||||
const preimage = paymentTag?.[3] || 'external-payment'
|
||||
|
||||
// Stop subscription after finding payment
|
||||
subscription.stop()
|
||||
handlePaymentReceived(preimage)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Key Subscription Patterns:**
|
||||
- Use `closeOnEose: false` for real-time monitoring
|
||||
- Use `closeOnEose: true` for one-time historical fetch
|
||||
- Always call `subscription.stop()` in cleanup
|
||||
- Listen to both `'event'` and `'eose'` events
|
||||
- Filter events in the handler for specific conditions
|
||||
- Integrate with React Query for reactive UI updates
|
||||
|
||||
### 6. User & Profile Handling
|
||||
|
||||
#### Fetching User Profiles
|
||||
```typescript
|
||||
// From src/queries/profiles.tsx
|
||||
|
||||
// By npub
|
||||
const user = ndk.getUser({ npub })
|
||||
const profile = await user.fetchProfile()
|
||||
// Returns NDKUserProfile with name, picture, about, etc.
|
||||
|
||||
// By hex pubkey
|
||||
const user = ndk.getUser({ hexpubkey: pubkey })
|
||||
const profile = await user.fetchProfile()
|
||||
|
||||
// By NIP-05 identifier
|
||||
const user = await ndk.getUserFromNip05('user@domain.com')
|
||||
if (user) {
|
||||
const profile = await user.fetchProfile()
|
||||
}
|
||||
|
||||
// Profile fields
|
||||
const name = profile?.name || profile?.displayName
|
||||
const avatar = profile?.picture || profile?.image
|
||||
const bio = profile?.about
|
||||
const nip05 = profile?.nip05
|
||||
const lud16 = profile?.lud16 // Lightning address
|
||||
```
|
||||
|
||||
#### Getting Current User
|
||||
```typescript
|
||||
// Active user (authenticated)
|
||||
const user = ndk.activeUser
|
||||
|
||||
// From signer
|
||||
const user = await ndk.signer?.user()
|
||||
|
||||
// User properties
|
||||
const pubkey = user.pubkey // Hex format
|
||||
const npub = user.npub // NIP-19 encoded
|
||||
```
|
||||
|
||||
### 7. NDK Event Object
|
||||
|
||||
#### Essential Properties
|
||||
```typescript
|
||||
interface NDKEvent {
|
||||
id: string // Event hash (after signing)
|
||||
kind: number // Event kind
|
||||
content: string // Event content
|
||||
tags: NDKTag[] // Array of tag arrays
|
||||
created_at?: number // Unix timestamp
|
||||
pubkey?: string // Author pubkey (after signing)
|
||||
sig?: string // Signature (after signing)
|
||||
|
||||
// Methods
|
||||
sign(signer?: NDKSigner): Promise<void>
|
||||
publish(): Promise<void>
|
||||
tagValue(tagName: string): string | undefined
|
||||
}
|
||||
|
||||
type NDKTag = string[] // e.g., ['p', pubkey, relay, petname]
|
||||
```
|
||||
|
||||
#### Tag Helpers
|
||||
```typescript
|
||||
// Get first value of a tag
|
||||
const orderId = event.tagValue('order')
|
||||
const recipientPubkey = event.tagValue('p')
|
||||
|
||||
// Find specific tag
|
||||
const paymentTag = event.tags.find(tag => tag[0] === 'payment')
|
||||
const preimage = paymentTag?.[3]
|
||||
|
||||
// Get all tags of a type
|
||||
const pTags = event.tags.filter(tag => tag[0] === 'p')
|
||||
const allPubkeys = pTags.map(tag => tag[1])
|
||||
|
||||
// Common tag patterns
|
||||
event.tags.push(['p', pubkey]) // Mention
|
||||
event.tags.push(['e', eventId]) // Reference event
|
||||
event.tags.push(['t', 'nostr']) // Hashtag
|
||||
event.tags.push(['d', identifier]) // Replaceable event ID
|
||||
event.tags.push(['a', '30402:pubkey:d-tag']) // Addressable event reference
|
||||
```
|
||||
|
||||
### 8. Parameterized Replaceable Events (NIP-33)
|
||||
|
||||
Used for products, collections, profiles that need updates:
|
||||
|
||||
```typescript
|
||||
// Product listing (kind 30402)
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 30402
|
||||
event.content = JSON.stringify(productDetails)
|
||||
event.tags = [
|
||||
['d', productSlug], // Unique identifier
|
||||
['title', productName],
|
||||
['price', price, currency],
|
||||
['image', imageUrl],
|
||||
['shipping', shippingRef],
|
||||
]
|
||||
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
|
||||
// Querying replaceable events
|
||||
const filter = {
|
||||
kinds: [30402],
|
||||
authors: [merchantPubkey],
|
||||
'#d': [productSlug], // Specific product
|
||||
}
|
||||
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
// Returns only the latest version due to replaceable nature
|
||||
```
|
||||
|
||||
### 9. Relay Management
|
||||
|
||||
#### Getting Relay Status
|
||||
```typescript
|
||||
// From src/lib/stores/ndk.ts
|
||||
const connectedRelays = Array.from(ndk.pool?.relays.values() || [])
|
||||
.filter(relay => relay.status === 1) // 1 = connected
|
||||
.map(relay => relay.url)
|
||||
|
||||
const outboxRelays = Array.from(ndk.outboxPool?.relays.values() || [])
|
||||
```
|
||||
|
||||
#### Adding Relays
|
||||
```typescript
|
||||
// Add explicit relays
|
||||
ndk.addExplicitRelay('wss://relay.example.com')
|
||||
|
||||
// Multiple relays
|
||||
const relays = ['wss://relay1.com', 'wss://relay2.com']
|
||||
relays.forEach(url => ndk.addExplicitRelay(url))
|
||||
```
|
||||
|
||||
### 10. Common Patterns & Best Practices
|
||||
|
||||
#### Null Safety
|
||||
```typescript
|
||||
// Always check NDK initialization
|
||||
const ndk = ndkActions.getNDK()
|
||||
if (!ndk) throw new Error('NDK not initialized')
|
||||
|
||||
// Check signer before operations requiring auth
|
||||
const signer = ndk.signer
|
||||
if (!signer) throw new Error('No active signer')
|
||||
|
||||
// Check user authentication
|
||||
const user = ndk.activeUser
|
||||
if (!user) throw new Error('Not authenticated')
|
||||
```
|
||||
|
||||
#### Error Handling
|
||||
```typescript
|
||||
try {
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
if (events.size === 0) {
|
||||
return null // No results found
|
||||
}
|
||||
return Array.from(events)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch events:', error)
|
||||
throw new Error('Could not fetch data from relays')
|
||||
}
|
||||
```
|
||||
|
||||
#### Connection Lifecycle
|
||||
```typescript
|
||||
// Initialize once at app startup
|
||||
const ndk = new NDK({ explicitRelayUrls: relays })
|
||||
|
||||
// Connect with timeout
|
||||
await Promise.race([
|
||||
ndk.connect(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), 10000)
|
||||
)
|
||||
])
|
||||
|
||||
// Check connection status
|
||||
const isConnected = ndk.pool?.connectedRelays().length > 0
|
||||
|
||||
// Reconnect if needed
|
||||
if (!isConnected) {
|
||||
await ndk.connect()
|
||||
}
|
||||
```
|
||||
|
||||
#### Subscription Cleanup
|
||||
```typescript
|
||||
// In React components
|
||||
useEffect(() => {
|
||||
if (!ndk) return
|
||||
|
||||
const sub = ndk.subscribe(filter, { closeOnEose: false })
|
||||
|
||||
sub.on('event', handleEvent)
|
||||
sub.on('eose', handleEose)
|
||||
|
||||
// Critical: cleanup on unmount
|
||||
return () => {
|
||||
sub.stop()
|
||||
}
|
||||
}, [dependencies])
|
||||
```
|
||||
|
||||
#### Event Validation
|
||||
```typescript
|
||||
// Check required fields before processing
|
||||
if (!event.pubkey) {
|
||||
console.error('Event missing pubkey')
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.created_at) {
|
||||
console.error('Event missing timestamp')
|
||||
return
|
||||
}
|
||||
|
||||
// Verify event age
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const eventAge = now - (event.created_at || 0)
|
||||
if (eventAge > 86400) { // Older than 24 hours
|
||||
console.log('Event is old, skipping')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate specific tags exist
|
||||
const orderId = event.tagValue('order')
|
||||
if (!orderId) {
|
||||
console.error('Order event missing order ID')
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 11. Common Event Kinds
|
||||
|
||||
```typescript
|
||||
// NIP-01: Basic Events
|
||||
const KIND_METADATA = 0 // User profile
|
||||
const KIND_TEXT_NOTE = 1 // Short text note
|
||||
const KIND_RECOMMEND_RELAY = 2 // Relay recommendation
|
||||
|
||||
// NIP-04: Encrypted Direct Messages
|
||||
const KIND_ENCRYPTED_DM = 4
|
||||
|
||||
// NIP-25: Reactions
|
||||
const KIND_REACTION = 7
|
||||
|
||||
// NIP-51: Lists
|
||||
const KIND_MUTE_LIST = 10000
|
||||
const KIND_PIN_LIST = 10001
|
||||
const KIND_RELAY_LIST = 10002
|
||||
|
||||
// NIP-57: Lightning Zaps
|
||||
const KIND_ZAP_REQUEST = 9734
|
||||
const KIND_ZAP_RECEIPT = 9735
|
||||
|
||||
// Marketplace (Plebeian/Gamma spec)
|
||||
const ORDER_PROCESS_KIND = 16 // Order processing
|
||||
const PAYMENT_RECEIPT_KIND = 17 // Payment receipts
|
||||
const DIRECT_MESSAGE_KIND = 14 // Direct messages
|
||||
const ORDER_GENERAL_KIND = 27 // General order events
|
||||
const SHIPPING_KIND = 30405 // Shipping options
|
||||
const PRODUCT_KIND = 30402 // Product listings
|
||||
const COLLECTION_KIND = 30401 // Product collections
|
||||
const REVIEW_KIND = 30407 // Product reviews
|
||||
|
||||
// Application Handlers
|
||||
const APP_HANDLER_KIND = 31990 // NIP-89 app handlers
|
||||
```
|
||||
|
||||
## Integration with TanStack Query
|
||||
|
||||
NDK works excellently with TanStack Query for reactive data fetching:
|
||||
|
||||
### Query Functions
|
||||
```typescript
|
||||
// From src/queries/products.tsx
|
||||
export const fetchProductsByPubkey = async (pubkey: string) => {
|
||||
const ndk = ndkActions.getNDK()
|
||||
if (!ndk) throw new Error('NDK not initialized')
|
||||
|
||||
const filter: NDKFilter = {
|
||||
kinds: [30402],
|
||||
authors: [pubkey],
|
||||
}
|
||||
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
return Array.from(events).map(parseProductEvent)
|
||||
}
|
||||
|
||||
export const useProductsByPubkey = (pubkey: string) => {
|
||||
return useQuery({
|
||||
queryKey: productKeys.byAuthor(pubkey),
|
||||
queryFn: () => fetchProductsByPubkey(pubkey),
|
||||
enabled: !!pubkey,
|
||||
staleTime: 30000,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Combining Queries with Subscriptions
|
||||
```typescript
|
||||
// Query for initial data
|
||||
const { data: order, refetch } = useQuery({
|
||||
queryKey: orderKeys.details(orderId),
|
||||
queryFn: () => fetchOrderById(orderId),
|
||||
enabled: !!orderId,
|
||||
})
|
||||
|
||||
// Subscription for real-time updates
|
||||
useEffect(() => {
|
||||
if (!orderId || !ndk) return
|
||||
|
||||
const sub = ndk.subscribe(
|
||||
{ kinds: [16, 17], '#order': [orderId] },
|
||||
{ closeOnEose: false }
|
||||
)
|
||||
|
||||
sub.on('event', () => {
|
||||
// Invalidate query to trigger refetch
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orderKeys.details(orderId)
|
||||
})
|
||||
})
|
||||
|
||||
return () => sub.stop()
|
||||
}, [orderId, ndk, queryClient])
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Events Not Received
|
||||
- Check relay connections: `ndk.pool?.connectedRelays()`
|
||||
- Verify filter syntax (especially tag filters with `#` prefix)
|
||||
- Check event timestamps match filter's `since`/`until`
|
||||
- Ensure `closeOnEose: false` for real-time subscriptions
|
||||
|
||||
### Signing Errors
|
||||
- Verify signer is initialized: `await signer.blockUntilReady()`
|
||||
- Check signer is set: `ndk.signer !== undefined`
|
||||
- For NIP-07, ensure browser extension is installed and enabled
|
||||
- For NIP-46, verify bunker URL and local signer are correct
|
||||
|
||||
### Connection Timeouts
|
||||
- Implement connection timeout pattern shown above
|
||||
- Try connecting to fewer, more reliable relays initially
|
||||
- Use fallback relays in production
|
||||
|
||||
### Duplicate Events
|
||||
- NDK deduplicates by event ID automatically
|
||||
- For subscriptions, track processed event IDs if needed
|
||||
- Use replaceable events (kinds 10000-19999, 30000-39999) when appropriate
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Batching Queries
|
||||
```typescript
|
||||
// Instead of multiple fetchEvents calls
|
||||
const [products, orders, profiles] = await Promise.all([
|
||||
ndk.fetchEvents(productFilter),
|
||||
ndk.fetchEvents(orderFilter),
|
||||
ndk.fetchEvents(profileFilter),
|
||||
])
|
||||
```
|
||||
|
||||
### Limiting Results
|
||||
```typescript
|
||||
const filter = {
|
||||
kinds: [1],
|
||||
authors: [pubkey],
|
||||
limit: 50, // Limit results
|
||||
since: recentTimestamp, // Only recent events
|
||||
}
|
||||
```
|
||||
|
||||
### Caching with React Query
|
||||
```typescript
|
||||
export const useProfile = (npub: string) => {
|
||||
return useQuery({
|
||||
queryKey: profileKeys.byNpub(npub),
|
||||
queryFn: () => fetchProfileByNpub(npub),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
cacheTime: 30 * 60 * 1000, // 30 minutes
|
||||
enabled: !!npub,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- **NDK GitHub**: https://github.com/nostr-dev-kit/ndk
|
||||
- **NDK Documentation**: https://ndk.fyi
|
||||
- **Nostr NIPs**: https://github.com/nostr-protocol/nips
|
||||
- **Production Example**: Plebeian Market codebase
|
||||
|
||||
## Key Files in This Codebase
|
||||
|
||||
- `src/lib/stores/ndk.ts` - NDK store and initialization
|
||||
- `src/lib/stores/auth.ts` - Authentication with NDK signers
|
||||
- `src/queries/*.tsx` - Query patterns with NDK
|
||||
- `src/publish/*.tsx` - Event publishing patterns
|
||||
- `scripts/gen_*.ts` - Event creation examples
|
||||
|
||||
---
|
||||
|
||||
*This reference is based on NDK version used in production and real-world patterns from the Plebeian Market application.*
|
||||
|
||||
351
.claude/skills/ndk/quick-reference.md
Normal file
351
.claude/skills/ndk/quick-reference.md
Normal file
@@ -0,0 +1,351 @@
|
||||
# NDK Quick Reference
|
||||
|
||||
Fast lookup guide for common NDK tasks.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import NDK from '@nostr-dev-kit/ndk'
|
||||
|
||||
const ndk = new NDK({ explicitRelayUrls: ['wss://relay.damus.io'] })
|
||||
await ndk.connect()
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### Browser Extension (NIP-07)
|
||||
```typescript
|
||||
import { NDKNip07Signer } from '@nostr-dev-kit/ndk'
|
||||
const signer = new NDKNip07Signer()
|
||||
await signer.blockUntilReady()
|
||||
ndk.signer = signer
|
||||
```
|
||||
|
||||
### Private Key
|
||||
```typescript
|
||||
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'
|
||||
const signer = new NDKPrivateKeySigner(privateKeyHex)
|
||||
await signer.blockUntilReady()
|
||||
ndk.signer = signer
|
||||
```
|
||||
|
||||
### Remote Signer (NIP-46)
|
||||
```typescript
|
||||
import { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'
|
||||
const localSigner = new NDKPrivateKeySigner()
|
||||
const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
|
||||
await remoteSigner.blockUntilReady()
|
||||
ndk.signer = remoteSigner
|
||||
```
|
||||
|
||||
## Publish Event
|
||||
|
||||
```typescript
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 1
|
||||
event.content = "Hello Nostr!"
|
||||
event.tags = [['t', 'nostr']]
|
||||
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
```
|
||||
|
||||
## Query Events (One-time)
|
||||
|
||||
```typescript
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [1],
|
||||
authors: [pubkey],
|
||||
limit: 50
|
||||
})
|
||||
|
||||
// Convert Set to Array
|
||||
const eventArray = Array.from(events)
|
||||
```
|
||||
|
||||
## Subscribe (Real-time)
|
||||
|
||||
```typescript
|
||||
const sub = ndk.subscribe(
|
||||
{ kinds: [1], authors: [pubkey] },
|
||||
{ closeOnEose: false }
|
||||
)
|
||||
|
||||
sub.on('event', (event) => {
|
||||
console.log('New event:', event.content)
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
sub.stop()
|
||||
```
|
||||
|
||||
## Get User Profile
|
||||
|
||||
```typescript
|
||||
// By npub
|
||||
const user = ndk.getUser({ npub })
|
||||
const profile = await user.fetchProfile()
|
||||
|
||||
// By hex pubkey
|
||||
const user = ndk.getUser({ hexpubkey: pubkey })
|
||||
const profile = await user.fetchProfile()
|
||||
|
||||
// By NIP-05
|
||||
const user = await ndk.getUserFromNip05('user@domain.com')
|
||||
const profile = await user?.fetchProfile()
|
||||
```
|
||||
|
||||
## Common Filters
|
||||
|
||||
```typescript
|
||||
// By author
|
||||
{ kinds: [1], authors: [pubkey] }
|
||||
|
||||
// By tag
|
||||
{ kinds: [1], '#p': [pubkey] }
|
||||
{ kinds: [30402], '#d': [productSlug] }
|
||||
|
||||
// By time
|
||||
{
|
||||
kinds: [1],
|
||||
since: Math.floor(Date.now() / 1000) - 86400, // Last 24h
|
||||
until: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
// By event ID
|
||||
{ ids: [eventId] }
|
||||
|
||||
// Multiple conditions
|
||||
{
|
||||
kinds: [16, 17],
|
||||
'#order': [orderId],
|
||||
since: timestamp,
|
||||
limit: 100
|
||||
}
|
||||
```
|
||||
|
||||
## Tag Helpers
|
||||
|
||||
```typescript
|
||||
// Get first tag value
|
||||
const orderId = event.tagValue('order')
|
||||
|
||||
// Find specific tag
|
||||
const tag = event.tags.find(t => t[0] === 'payment')
|
||||
const value = tag?.[1]
|
||||
|
||||
// Get all of one type
|
||||
const pTags = event.tags.filter(t => t[0] === 'p')
|
||||
|
||||
// Common tag formats
|
||||
['p', pubkey] // Mention
|
||||
['e', eventId] // Event reference
|
||||
['t', 'nostr'] // Hashtag
|
||||
['d', identifier] // Replaceable ID
|
||||
['a', '30402:pubkey:d-tag'] // Addressable reference
|
||||
```
|
||||
|
||||
## Error Handling Pattern
|
||||
|
||||
```typescript
|
||||
const ndk = ndkActions.getNDK()
|
||||
if (!ndk) throw new Error('NDK not initialized')
|
||||
|
||||
const signer = ndk.signer
|
||||
if (!signer) throw new Error('No active signer')
|
||||
|
||||
try {
|
||||
await event.publish()
|
||||
} catch (error) {
|
||||
console.error('Publish failed:', error)
|
||||
throw error
|
||||
}
|
||||
```
|
||||
|
||||
## React Integration
|
||||
|
||||
```typescript
|
||||
// Query function
|
||||
export const fetchProducts = async (pubkey: string) => {
|
||||
const ndk = ndkActions.getNDK()
|
||||
if (!ndk) throw new Error('NDK not initialized')
|
||||
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [30402],
|
||||
authors: [pubkey]
|
||||
})
|
||||
|
||||
return Array.from(events)
|
||||
}
|
||||
|
||||
// React Query hook
|
||||
export const useProducts = (pubkey: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['products', pubkey],
|
||||
queryFn: () => fetchProducts(pubkey),
|
||||
enabled: !!pubkey,
|
||||
})
|
||||
}
|
||||
|
||||
// Subscription in useEffect
|
||||
useEffect(() => {
|
||||
if (!ndk || !orderId) return
|
||||
|
||||
const sub = ndk.subscribe(
|
||||
{ kinds: [16], '#order': [orderId] },
|
||||
{ closeOnEose: false }
|
||||
)
|
||||
|
||||
sub.on('event', () => {
|
||||
queryClient.invalidateQueries(['order', orderId])
|
||||
})
|
||||
|
||||
return () => sub.stop()
|
||||
}, [ndk, orderId, queryClient])
|
||||
```
|
||||
|
||||
## Common Event Kinds
|
||||
|
||||
```typescript
|
||||
0 // Metadata (profile)
|
||||
1 // Text note
|
||||
4 // Encrypted DM (NIP-04)
|
||||
7 // Reaction
|
||||
9735 // Zap receipt
|
||||
10000 // Mute list
|
||||
10002 // Relay list
|
||||
30402 // Product listing (Marketplace)
|
||||
31990 // App handler (NIP-89)
|
||||
```
|
||||
|
||||
## Relay Management
|
||||
|
||||
```typescript
|
||||
// Check connection
|
||||
const connected = ndk.pool?.connectedRelays().length > 0
|
||||
|
||||
// Get connected relays
|
||||
const relays = Array.from(ndk.pool?.relays.values() || [])
|
||||
.filter(r => r.status === 1)
|
||||
|
||||
// Add relay
|
||||
ndk.addExplicitRelay('wss://relay.example.com')
|
||||
```
|
||||
|
||||
## Connection with Timeout
|
||||
|
||||
```typescript
|
||||
const connectWithTimeout = async (timeoutMs = 10000) => {
|
||||
const connectPromise = ndk.connect()
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), timeoutMs)
|
||||
)
|
||||
|
||||
await Promise.race([connectPromise, timeoutPromise])
|
||||
}
|
||||
```
|
||||
|
||||
## Current User
|
||||
|
||||
```typescript
|
||||
// Active user
|
||||
const user = ndk.activeUser
|
||||
|
||||
// From signer
|
||||
const user = await ndk.signer?.user()
|
||||
|
||||
// User info
|
||||
const pubkey = user.pubkey // hex
|
||||
const npub = user.npub // NIP-19
|
||||
```
|
||||
|
||||
## Parameterized Replaceable Events
|
||||
|
||||
```typescript
|
||||
// Create
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 30402
|
||||
event.content = JSON.stringify(data)
|
||||
event.tags = [
|
||||
['d', uniqueIdentifier], // Required for replaceable
|
||||
['title', 'Product Name'],
|
||||
]
|
||||
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
|
||||
// Query (returns latest only)
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [30402],
|
||||
authors: [pubkey],
|
||||
'#d': [identifier]
|
||||
})
|
||||
```
|
||||
|
||||
## Validation Checks
|
||||
|
||||
```typescript
|
||||
// Event age check
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const age = now - (event.created_at || 0)
|
||||
if (age > 86400) console.log('Event older than 24h')
|
||||
|
||||
// Required fields
|
||||
if (!event.pubkey || !event.created_at || !event.sig) {
|
||||
throw new Error('Invalid event')
|
||||
}
|
||||
|
||||
// Tag existence
|
||||
const orderId = event.tagValue('order')
|
||||
if (!orderId) throw new Error('Missing order tag')
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
```typescript
|
||||
// Batch queries
|
||||
const [products, orders] = await Promise.all([
|
||||
ndk.fetchEvents(productFilter),
|
||||
ndk.fetchEvents(orderFilter)
|
||||
])
|
||||
|
||||
// Limit results
|
||||
const filter = {
|
||||
kinds: [1],
|
||||
limit: 50,
|
||||
since: recentTimestamp
|
||||
}
|
||||
|
||||
// Cache with React Query
|
||||
const { data } = useQuery({
|
||||
queryKey: ['profile', npub],
|
||||
queryFn: () => fetchProfile(npub),
|
||||
staleTime: 5 * 60 * 1000, // 5 min
|
||||
})
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```typescript
|
||||
// Check NDK state
|
||||
console.log('Connected:', ndk.pool?.connectedRelays())
|
||||
console.log('Signer:', ndk.signer)
|
||||
console.log('Active user:', ndk.activeUser)
|
||||
|
||||
// Event inspection
|
||||
console.log('Event ID:', event.id)
|
||||
console.log('Tags:', event.tags)
|
||||
console.log('Content:', event.content)
|
||||
console.log('Author:', event.pubkey)
|
||||
|
||||
// Subscription events
|
||||
sub.on('event', e => console.log('Event:', e))
|
||||
sub.on('eose', () => console.log('End of stored events'))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For detailed explanations and advanced patterns, see `ndk-skill.md`.
|
||||
|
||||
530
.claude/skills/ndk/troubleshooting.md
Normal file
530
.claude/skills/ndk/troubleshooting.md
Normal file
@@ -0,0 +1,530 @@
|
||||
# NDK Common Patterns & Troubleshooting
|
||||
|
||||
Quick reference for common patterns and solutions to frequent NDK issues.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Store-Based NDK Management
|
||||
|
||||
```typescript
|
||||
// Store pattern (recommended for React apps)
|
||||
import { Store } from '@tanstack/store'
|
||||
|
||||
interface NDKState {
|
||||
ndk: NDK | null
|
||||
isConnected: boolean
|
||||
signer?: NDKSigner
|
||||
}
|
||||
|
||||
const ndkStore = new Store<NDKState>({
|
||||
ndk: null,
|
||||
isConnected: false
|
||||
})
|
||||
|
||||
export const ndkActions = {
|
||||
initialize: () => {
|
||||
const ndk = new NDK({ explicitRelayUrls: relays })
|
||||
ndkStore.setState({ ndk })
|
||||
return ndk
|
||||
},
|
||||
|
||||
getNDK: () => ndkStore.state.ndk,
|
||||
|
||||
setSigner: (signer: NDKSigner) => {
|
||||
const ndk = ndkStore.state.ndk
|
||||
if (ndk) {
|
||||
ndk.signer = signer
|
||||
ndkStore.setState({ signer })
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Query + Subscription Pattern
|
||||
|
||||
```typescript
|
||||
// Initial data load + real-time updates
|
||||
function useOrdersWithRealtime(orderId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
const ndk = ndkActions.getNDK()
|
||||
|
||||
// Fetch initial data
|
||||
const query = useQuery({
|
||||
queryKey: ['orders', orderId],
|
||||
queryFn: () => fetchOrders(orderId),
|
||||
})
|
||||
|
||||
// Subscribe to updates
|
||||
useEffect(() => {
|
||||
if (!ndk || !orderId) return
|
||||
|
||||
const sub = ndk.subscribe(
|
||||
{ kinds: [16], '#order': [orderId] },
|
||||
{ closeOnEose: false }
|
||||
)
|
||||
|
||||
sub.on('event', () => {
|
||||
queryClient.invalidateQueries(['orders', orderId])
|
||||
})
|
||||
|
||||
return () => sub.stop()
|
||||
}, [ndk, orderId])
|
||||
|
||||
return query
|
||||
}
|
||||
```
|
||||
|
||||
### Event Parsing Pattern
|
||||
|
||||
```typescript
|
||||
// Parse event tags into structured data
|
||||
function parseProductEvent(event: NDKEvent) {
|
||||
const getTag = (name: string) =>
|
||||
event.tags.find(t => t[0] === name)?.[1]
|
||||
|
||||
const getAllTags = (name: string) =>
|
||||
event.tags.filter(t => t[0] === name).map(t => t[1])
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
slug: getTag('d'),
|
||||
title: getTag('title'),
|
||||
price: parseFloat(getTag('price') || '0'),
|
||||
currency: event.tags.find(t => t[0] === 'price')?.[2] || 'USD',
|
||||
images: getAllTags('image'),
|
||||
shipping: getAllTags('shipping'),
|
||||
description: event.content,
|
||||
createdAt: event.created_at,
|
||||
author: event.pubkey
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Relay Pool Pattern
|
||||
|
||||
```typescript
|
||||
// Separate NDK instances for different purposes
|
||||
const mainNdk = new NDK({
|
||||
explicitRelayUrls: ['wss://relay.damus.io', 'wss://nos.lol']
|
||||
})
|
||||
|
||||
const zapNdk = new NDK({
|
||||
explicitRelayUrls: ['wss://relay.damus.io'] // Zap-optimized relays
|
||||
})
|
||||
|
||||
const blossomNdk = new NDK({
|
||||
explicitRelayUrls: ['wss://blossom.server.com'] // Media server
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
mainNdk.connect(),
|
||||
zapNdk.connect(),
|
||||
blossomNdk.connect()
|
||||
])
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Events Not Received
|
||||
|
||||
**Symptoms:** Subscription doesn't receive events, fetchEvents returns empty Set
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check relay connection:
|
||||
```typescript
|
||||
const status = ndk.pool?.connectedRelays()
|
||||
console.log('Connected relays:', status?.length)
|
||||
if (status?.length === 0) {
|
||||
await ndk.connect()
|
||||
}
|
||||
```
|
||||
|
||||
2. Verify filter syntax (especially tags):
|
||||
```typescript
|
||||
// ❌ Wrong
|
||||
{ kinds: [16], 'order': [orderId] }
|
||||
|
||||
// ✅ Correct (note the # prefix for tags)
|
||||
{ kinds: [16], '#order': [orderId] }
|
||||
```
|
||||
|
||||
3. Check timestamps:
|
||||
```typescript
|
||||
// Events might be too old/new
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const filter = {
|
||||
kinds: [1],
|
||||
since: now - 86400, // Last 24 hours
|
||||
until: now
|
||||
}
|
||||
```
|
||||
|
||||
4. Ensure closeOnEose is correct:
|
||||
```typescript
|
||||
// For real-time updates
|
||||
ndk.subscribe(filter, { closeOnEose: false })
|
||||
|
||||
// For one-time historical fetch
|
||||
ndk.subscribe(filter, { closeOnEose: true })
|
||||
```
|
||||
|
||||
### Problem: "NDK not initialized"
|
||||
|
||||
**Symptoms:** `ndk` is null/undefined
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Initialize before use:
|
||||
```typescript
|
||||
// In app entry point
|
||||
const ndk = new NDK({ explicitRelayUrls: relays })
|
||||
await ndk.connect()
|
||||
```
|
||||
|
||||
2. Add null checks:
|
||||
```typescript
|
||||
const ndk = ndkActions.getNDK()
|
||||
if (!ndk) throw new Error('NDK not initialized')
|
||||
```
|
||||
|
||||
3. Use initialization guard:
|
||||
```typescript
|
||||
const ensureNDK = () => {
|
||||
let ndk = ndkActions.getNDK()
|
||||
if (!ndk) {
|
||||
ndk = ndkActions.initialize()
|
||||
}
|
||||
return ndk
|
||||
}
|
||||
```
|
||||
|
||||
### Problem: "No active signer" / Cannot Sign Events
|
||||
|
||||
**Symptoms:** Event signing fails, publishing throws error
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check signer is set:
|
||||
```typescript
|
||||
if (!ndk.signer) {
|
||||
throw new Error('Please login first')
|
||||
}
|
||||
```
|
||||
|
||||
2. Ensure blockUntilReady called:
|
||||
```typescript
|
||||
const signer = new NDKNip07Signer()
|
||||
await signer.blockUntilReady() // ← Critical!
|
||||
ndk.signer = signer
|
||||
```
|
||||
|
||||
3. Handle NIP-07 unavailable:
|
||||
```typescript
|
||||
try {
|
||||
const signer = new NDKNip07Signer()
|
||||
await signer.blockUntilReady()
|
||||
ndk.signer = signer
|
||||
} catch (error) {
|
||||
console.error('Browser extension not available')
|
||||
// Fallback to other auth method
|
||||
}
|
||||
```
|
||||
|
||||
### Problem: Duplicate Events in Subscriptions
|
||||
|
||||
**Symptoms:** Same event received multiple times
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Track processed event IDs:
|
||||
```typescript
|
||||
const processedIds = new Set<string>()
|
||||
|
||||
sub.on('event', (event) => {
|
||||
if (processedIds.has(event.id)) return
|
||||
processedIds.add(event.id)
|
||||
handleEvent(event)
|
||||
})
|
||||
```
|
||||
|
||||
2. Use Map for event storage:
|
||||
```typescript
|
||||
const [events, setEvents] = useState<Map<string, NDKEvent>>(new Map())
|
||||
|
||||
sub.on('event', (event) => {
|
||||
setEvents(prev => new Map(prev).set(event.id, event))
|
||||
})
|
||||
```
|
||||
|
||||
### Problem: Connection Timeout
|
||||
|
||||
**Symptoms:** connect() hangs, never resolves
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Use timeout wrapper:
|
||||
```typescript
|
||||
const connectWithTimeout = async (ndk: NDK, ms = 10000) => {
|
||||
await Promise.race([
|
||||
ndk.connect(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), ms)
|
||||
)
|
||||
])
|
||||
}
|
||||
```
|
||||
|
||||
2. Try fewer relays:
|
||||
```typescript
|
||||
// Start with reliable relays only
|
||||
const reliableRelays = ['wss://relay.damus.io']
|
||||
const ndk = new NDK({ explicitRelayUrls: reliableRelays })
|
||||
```
|
||||
|
||||
3. Add connection retry:
|
||||
```typescript
|
||||
const connectWithRetry = async (ndk: NDK, maxRetries = 3) => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
await connectWithTimeout(ndk, 10000)
|
||||
return
|
||||
} catch (error) {
|
||||
console.log(`Retry ${i + 1}/${maxRetries}`)
|
||||
if (i === maxRetries - 1) throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Problem: Subscription Memory Leak
|
||||
|
||||
**Symptoms:** App gets slower, memory usage increases
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Always stop subscriptions:
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const sub = ndk.subscribe(filter, { closeOnEose: false })
|
||||
|
||||
// ← CRITICAL: cleanup
|
||||
return () => {
|
||||
sub.stop()
|
||||
}
|
||||
}, [dependencies])
|
||||
```
|
||||
|
||||
2. Track active subscriptions:
|
||||
```typescript
|
||||
const activeSubscriptions = new Set<NDKSubscription>()
|
||||
|
||||
const createSub = (filter: NDKFilter) => {
|
||||
const sub = ndk.subscribe(filter, { closeOnEose: false })
|
||||
activeSubscriptions.add(sub)
|
||||
return sub
|
||||
}
|
||||
|
||||
const stopAllSubs = () => {
|
||||
activeSubscriptions.forEach(sub => sub.stop())
|
||||
activeSubscriptions.clear()
|
||||
}
|
||||
```
|
||||
|
||||
### Problem: Profile Not Found
|
||||
|
||||
**Symptoms:** fetchProfile() returns null/undefined
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check different relays:
|
||||
```typescript
|
||||
// Add more relay URLs
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: [
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.nostr.band',
|
||||
'wss://nos.lol'
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
2. Verify pubkey format:
|
||||
```typescript
|
||||
// Ensure correct format
|
||||
if (pubkey.startsWith('npub')) {
|
||||
const user = ndk.getUser({ npub: pubkey })
|
||||
} else if (/^[0-9a-f]{64}$/.test(pubkey)) {
|
||||
const user = ndk.getUser({ hexpubkey: pubkey })
|
||||
}
|
||||
```
|
||||
|
||||
3. Handle missing profiles gracefully:
|
||||
```typescript
|
||||
const profile = await user.fetchProfile()
|
||||
const displayName = profile?.name || profile?.displayName || 'Anonymous'
|
||||
const avatar = profile?.picture || '/default-avatar.png'
|
||||
```
|
||||
|
||||
### Problem: Events Published But Not Visible
|
||||
|
||||
**Symptoms:** publish() succeeds but event not found in queries
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify event was signed:
|
||||
```typescript
|
||||
await event.sign()
|
||||
console.log('Event ID:', event.id) // Should be set
|
||||
console.log('Signature:', event.sig) // Should exist
|
||||
```
|
||||
|
||||
2. Check relay acceptance:
|
||||
```typescript
|
||||
const relays = await event.publish()
|
||||
console.log('Published to relays:', relays)
|
||||
```
|
||||
|
||||
3. Query immediately after publish:
|
||||
```typescript
|
||||
await event.publish()
|
||||
|
||||
// Wait a moment for relay propagation
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
const found = await ndk.fetchEvents({ ids: [event.id] })
|
||||
console.log('Event found:', found.size > 0)
|
||||
```
|
||||
|
||||
### Problem: NIP-46 Connection Fails
|
||||
|
||||
**Symptoms:** Remote signer connection times out or fails
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify bunker URL format:
|
||||
```typescript
|
||||
// Correct format: bunker://<remote-pubkey>?relay=wss://...
|
||||
const isValidBunkerUrl = (url: string) => {
|
||||
return url.startsWith('bunker://') && url.includes('?relay=')
|
||||
}
|
||||
```
|
||||
|
||||
2. Ensure local signer is ready:
|
||||
```typescript
|
||||
const localSigner = new NDKPrivateKeySigner(privateKey)
|
||||
await localSigner.blockUntilReady()
|
||||
|
||||
const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
|
||||
await remoteSigner.blockUntilReady()
|
||||
```
|
||||
|
||||
3. Store credentials for reconnection:
|
||||
```typescript
|
||||
// Save for future sessions
|
||||
localStorage.setItem('local-signer-key', localSigner.privateKey)
|
||||
localStorage.setItem('bunker-url', bunkerUrl)
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### Optimize Queries
|
||||
|
||||
```typescript
|
||||
// ❌ Slow: Multiple sequential queries
|
||||
const products = await ndk.fetchEvents({ kinds: [30402], authors: [pk1] })
|
||||
const orders = await ndk.fetchEvents({ kinds: [16], authors: [pk1] })
|
||||
const profiles = await ndk.fetchEvents({ kinds: [0], authors: [pk1] })
|
||||
|
||||
// ✅ Fast: Parallel queries
|
||||
const [products, orders, profiles] = await Promise.all([
|
||||
ndk.fetchEvents({ kinds: [30402], authors: [pk1] }),
|
||||
ndk.fetchEvents({ kinds: [16], authors: [pk1] }),
|
||||
ndk.fetchEvents({ kinds: [0], authors: [pk1] })
|
||||
])
|
||||
```
|
||||
|
||||
### Cache Profile Lookups
|
||||
|
||||
```typescript
|
||||
const profileCache = new Map<string, NDKUserProfile>()
|
||||
|
||||
const getCachedProfile = async (ndk: NDK, pubkey: string) => {
|
||||
if (profileCache.has(pubkey)) {
|
||||
return profileCache.get(pubkey)!
|
||||
}
|
||||
|
||||
const user = ndk.getUser({ hexpubkey: pubkey })
|
||||
const profile = await user.fetchProfile()
|
||||
if (profile) {
|
||||
profileCache.set(pubkey, profile)
|
||||
}
|
||||
|
||||
return profile
|
||||
}
|
||||
```
|
||||
|
||||
### Limit Result Sets
|
||||
|
||||
```typescript
|
||||
// Always use limit to prevent over-fetching
|
||||
const filter: NDKFilter = {
|
||||
kinds: [1],
|
||||
authors: [pubkey],
|
||||
limit: 50 // ← Important!
|
||||
}
|
||||
```
|
||||
|
||||
### Debounce Subscription Updates
|
||||
|
||||
```typescript
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
const debouncedUpdate = debounce((event: NDKEvent) => {
|
||||
handleEvent(event)
|
||||
}, 300)
|
||||
|
||||
sub.on('event', debouncedUpdate)
|
||||
```
|
||||
|
||||
## Testing Tips
|
||||
|
||||
### Mock NDK in Tests
|
||||
|
||||
```typescript
|
||||
const mockNDK = {
|
||||
fetchEvents: vi.fn().mockResolvedValue(new Set()),
|
||||
subscribe: vi.fn().mockReturnValue({
|
||||
on: vi.fn(),
|
||||
stop: vi.fn()
|
||||
}),
|
||||
signer: {
|
||||
user: vi.fn().mockResolvedValue({ pubkey: 'test-pubkey' })
|
||||
}
|
||||
} as unknown as NDK
|
||||
```
|
||||
|
||||
### Test Event Creation
|
||||
|
||||
```typescript
|
||||
const createTestEvent = (overrides?: Partial<NDKEvent>): NDKEvent => {
|
||||
return {
|
||||
id: 'test-id',
|
||||
kind: 1,
|
||||
content: 'test content',
|
||||
tags: [],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: 'test-pubkey',
|
||||
sig: 'test-sig',
|
||||
...overrides
|
||||
} as NDKEvent
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For more detailed information, see:
|
||||
- `ndk-skill.md` - Complete reference
|
||||
- `quick-reference.md` - Quick lookup
|
||||
- `examples/` - Code examples
|
||||
|
||||
119
.claude/skills/react/README.md
Normal file
119
.claude/skills/react/README.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# React 19 Skill
|
||||
|
||||
A comprehensive Claude skill for working with React 19, including hooks, components, server components, and modern React architecture.
|
||||
|
||||
## Contents
|
||||
|
||||
### Main Skill File
|
||||
- **SKILL.md** - Main skill document with React 19 fundamentals, hooks, components, and best practices
|
||||
|
||||
### References
|
||||
- **hooks-quick-reference.md** - Quick reference for all React hooks with examples
|
||||
- **server-components.md** - Complete guide to React Server Components and Server Functions
|
||||
- **performance.md** - Performance optimization strategies and techniques
|
||||
|
||||
### Examples
|
||||
- **practical-patterns.tsx** - Real-world React patterns and solutions
|
||||
|
||||
## What This Skill Covers
|
||||
|
||||
### Core Topics
|
||||
- React 19 features and improvements
|
||||
- All built-in hooks (useState, useEffect, useTransition, useOptimistic, etc.)
|
||||
- Component patterns and composition
|
||||
- Server Components and Server Functions
|
||||
- React Compiler and automatic optimization
|
||||
- Performance optimization techniques
|
||||
- Form handling and validation
|
||||
- Error boundaries and error handling
|
||||
- Context and global state management
|
||||
- Code splitting and lazy loading
|
||||
|
||||
### Best Practices
|
||||
- Component design principles
|
||||
- State management strategies
|
||||
- Performance optimization
|
||||
- Error handling patterns
|
||||
- TypeScript integration
|
||||
- Testing considerations
|
||||
- Accessibility guidelines
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Building React 19 applications
|
||||
- Working with React hooks
|
||||
- Implementing server components
|
||||
- Optimizing React performance
|
||||
- Troubleshooting React-specific issues
|
||||
- Understanding concurrent features
|
||||
- Working with forms and user input
|
||||
- Implementing complex UI patterns
|
||||
|
||||
## Quick Start Examples
|
||||
|
||||
### Basic Component
|
||||
```typescript
|
||||
interface ButtonProps {
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const Button = ({ label, onClick }: ButtonProps) => {
|
||||
return <button onClick={onClick}>{label}</button>
|
||||
}
|
||||
```
|
||||
|
||||
### Using Hooks
|
||||
```typescript
|
||||
const Counter = () => {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`Count is: ${count}`)
|
||||
}, [count])
|
||||
|
||||
return (
|
||||
<button onClick={() => setCount(c => c + 1)}>
|
||||
Count: {count}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Server Component
|
||||
```typescript
|
||||
const Page = async () => {
|
||||
const data = await fetchData()
|
||||
return <div>{data}</div>
|
||||
}
|
||||
```
|
||||
|
||||
### Server Function
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
export async function createUser(formData: FormData) {
|
||||
const name = formData.get('name')
|
||||
return await db.user.create({ data: { name } })
|
||||
}
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
- **typescript** - TypeScript patterns for React
|
||||
- **ndk** - Nostr integration with React
|
||||
- **skill-creator** - Creating reusable component libraries
|
||||
|
||||
## Resources
|
||||
|
||||
- [React Documentation](https://react.dev)
|
||||
- [React API Reference](https://react.dev/reference/react)
|
||||
- [React Hooks Reference](https://react.dev/reference/react/hooks)
|
||||
- [React Server Components](https://react.dev/reference/rsc)
|
||||
- [React Compiler](https://react.dev/reference/react-compiler)
|
||||
|
||||
## Version
|
||||
|
||||
This skill is based on React 19.2 and includes the latest features and APIs.
|
||||
|
||||
1026
.claude/skills/react/SKILL.md
Normal file
1026
.claude/skills/react/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
878
.claude/skills/react/examples/practical-patterns.tsx
Normal file
878
.claude/skills/react/examples/practical-patterns.tsx
Normal file
@@ -0,0 +1,878 @@
|
||||
# React Practical Examples
|
||||
|
||||
This file contains real-world examples of React patterns and solutions.
|
||||
|
||||
## Example 1: Custom Hook for Data Fetching
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface FetchState<T> {
|
||||
data: T | null
|
||||
loading: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
const useFetch = <T,>(url: string) => {
|
||||
const [state, setState] = useState<FetchState<T>>({
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const controller = new AbortController()
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }))
|
||||
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!cancelled) {
|
||||
setState({ data, loading: false, error: null })
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled && error.name !== 'AbortError') {
|
||||
setState({
|
||||
data: null,
|
||||
loading: false,
|
||||
error: error as Error
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
controller.abort()
|
||||
}
|
||||
}, [url])
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// Usage
|
||||
const UserProfile = ({ userId }: { userId: string }) => {
|
||||
const { data, loading, error } = useFetch<User>(`/api/users/${userId}`)
|
||||
|
||||
if (loading) return <Spinner />
|
||||
if (error) return <ErrorMessage error={error} />
|
||||
if (!data) return null
|
||||
|
||||
return <UserCard user={data} />
|
||||
}
|
||||
```
|
||||
|
||||
## Example 2: Form with Validation
|
||||
|
||||
```typescript
|
||||
import { useState, useCallback } from 'react'
|
||||
import { z } from 'zod'
|
||||
|
||||
const userSchema = z.object({
|
||||
name: z.string().min(2, 'Name must be at least 2 characters'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
age: z.number().min(18, 'Must be 18 or older')
|
||||
})
|
||||
|
||||
type UserForm = z.infer<typeof userSchema>
|
||||
type FormErrors = Partial<Record<keyof UserForm, string>>
|
||||
|
||||
const UserForm = () => {
|
||||
const [formData, setFormData] = useState<UserForm>({
|
||||
name: '',
|
||||
email: '',
|
||||
age: 0
|
||||
})
|
||||
const [errors, setErrors] = useState<FormErrors>({})
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const handleChange = useCallback((
|
||||
field: keyof UserForm,
|
||||
value: string | number
|
||||
) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }))
|
||||
// Clear error when user starts typing
|
||||
setErrors(prev => ({ ...prev, [field]: undefined }))
|
||||
}, [])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
// Validate
|
||||
const result = userSchema.safeParse(formData)
|
||||
if (!result.success) {
|
||||
const fieldErrors: FormErrors = {}
|
||||
result.error.errors.forEach(err => {
|
||||
const field = err.path[0] as keyof UserForm
|
||||
fieldErrors[field] = err.message
|
||||
})
|
||||
setErrors(fieldErrors)
|
||||
return
|
||||
}
|
||||
|
||||
// Submit
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await submitUser(result.data)
|
||||
// Success handling
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={e => handleChange('name', e.target.value)}
|
||||
/>
|
||||
{errors.name && <span className="error">{errors.name}</span>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={e => handleChange('email', e.target.value)}
|
||||
/>
|
||||
{errors.email && <span className="error">{errors.email}</span>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="age">Age</label>
|
||||
<input
|
||||
id="age"
|
||||
type="number"
|
||||
value={formData.age || ''}
|
||||
onChange={e => handleChange('age', Number(e.target.value))}
|
||||
/>
|
||||
{errors.age && <span className="error">{errors.age}</span>}
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Example 3: Modal with Portal
|
||||
|
||||
```typescript
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
children: React.ReactNode
|
||||
title?: string
|
||||
}
|
||||
|
||||
const Modal = ({ isOpen, onClose, children, title }: ModalProps) => {
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
// Prevent body scroll
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
document.body.style.overflow = 'unset'
|
||||
}
|
||||
}, [isOpen, onClose])
|
||||
|
||||
// Close on backdrop click
|
||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||
if (e.target === modalRef.current) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
ref={modalRef}
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
{title && <h2 className="text-xl font-bold">{title}</h2>}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
|
||||
// Usage
|
||||
const App = () => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setIsOpen(true)}>Open Modal</button>
|
||||
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)} title="My Modal">
|
||||
<p>Modal content goes here</p>
|
||||
<button onClick={() => setIsOpen(false)}>Close</button>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Example 4: Infinite Scroll
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
|
||||
interface InfiniteScrollProps<T> {
|
||||
fetchData: (page: number) => Promise<T[]>
|
||||
renderItem: (item: T, index: number) => React.ReactNode
|
||||
loader?: React.ReactNode
|
||||
endMessage?: React.ReactNode
|
||||
}
|
||||
|
||||
const InfiniteScroll = <T extends { id: string | number },>({
|
||||
fetchData,
|
||||
renderItem,
|
||||
loader = <div>Loading...</div>,
|
||||
endMessage = <div>No more items</div>
|
||||
}: InfiniteScrollProps<T>) => {
|
||||
const [items, setItems] = useState<T[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const observerRef = useRef<IntersectionObserver | null>(null)
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (loading || !hasMore) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const newItems = await fetchData(page)
|
||||
|
||||
if (newItems.length === 0) {
|
||||
setHasMore(false)
|
||||
} else {
|
||||
setItems(prev => [...prev, ...newItems])
|
||||
setPage(prev => prev + 1)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load items:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, loading, hasMore, fetchData])
|
||||
|
||||
// Set up intersection observer
|
||||
useEffect(() => {
|
||||
observerRef.current = new IntersectionObserver(
|
||||
entries => {
|
||||
if (entries[0].isIntersecting) {
|
||||
loadMore()
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
)
|
||||
|
||||
const currentRef = loadMoreRef.current
|
||||
if (currentRef) {
|
||||
observerRef.current.observe(currentRef)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (observerRef.current && currentRef) {
|
||||
observerRef.current.unobserve(currentRef)
|
||||
}
|
||||
}
|
||||
}, [loadMore])
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
loadMore()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id}>
|
||||
{renderItem(item, index)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div ref={loadMoreRef}>
|
||||
{loading && loader}
|
||||
{!loading && !hasMore && endMessage}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Usage
|
||||
const PostsList = () => {
|
||||
const fetchPosts = async (page: number) => {
|
||||
const response = await fetch(`/api/posts?page=${page}`)
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return (
|
||||
<InfiniteScroll<Post>
|
||||
fetchData={fetchPosts}
|
||||
renderItem={(post) => <PostCard post={post} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Example 5: Dark Mode Toggle
|
||||
|
||||
```typescript
|
||||
import { createContext, useContext, useState, useEffect } from 'react'
|
||||
|
||||
type Theme = 'light' | 'dark'
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | null>(null)
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext)
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within ThemeProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
// Check localStorage and system preference
|
||||
const saved = localStorage.getItem('theme') as Theme | null
|
||||
if (saved) return saved
|
||||
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark'
|
||||
}
|
||||
|
||||
return 'light'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Update DOM and localStorage
|
||||
const root = document.documentElement
|
||||
root.classList.remove('light', 'dark')
|
||||
root.classList.add(theme)
|
||||
localStorage.setItem('theme', theme)
|
||||
}, [theme])
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prev => prev === 'light' ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Usage
|
||||
const ThemeToggle = () => {
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<button onClick={toggleTheme} aria-label="Toggle theme">
|
||||
{theme === 'light' ? '🌙' : '☀️'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Example 6: Debounced Search
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
|
||||
const useDebounce = <T,>(value: T, delay: number): T => {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
const SearchPage = () => {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState<Product[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const debouncedQuery = useDebounce(query, 500)
|
||||
|
||||
useEffect(() => {
|
||||
if (!debouncedQuery) {
|
||||
setResults([])
|
||||
return
|
||||
}
|
||||
|
||||
const searchProducts = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch(`/api/search?q=${debouncedQuery}`)
|
||||
const data = await response.json()
|
||||
setResults(data)
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
searchProducts()
|
||||
}, [debouncedQuery])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
placeholder="Search products..."
|
||||
/>
|
||||
|
||||
{loading && <Spinner />}
|
||||
|
||||
{!loading && results.length > 0 && (
|
||||
<div>
|
||||
{results.map(product => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && query && results.length === 0 && (
|
||||
<p>No results found for "{query}"</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Example 7: Tabs Component
|
||||
|
||||
```typescript
|
||||
import { createContext, useContext, useState, useId } from 'react'
|
||||
|
||||
interface TabsContextType {
|
||||
activeTab: string
|
||||
setActiveTab: (id: string) => void
|
||||
tabsId: string
|
||||
}
|
||||
|
||||
const TabsContext = createContext<TabsContextType | null>(null)
|
||||
|
||||
const useTabs = () => {
|
||||
const context = useContext(TabsContext)
|
||||
if (!context) throw new Error('Tabs compound components must be used within Tabs')
|
||||
return context
|
||||
}
|
||||
|
||||
interface TabsProps {
|
||||
children: React.ReactNode
|
||||
defaultValue: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Tabs = ({ children, defaultValue, className }: TabsProps) => {
|
||||
const [activeTab, setActiveTab] = useState(defaultValue)
|
||||
const tabsId = useId()
|
||||
|
||||
return (
|
||||
<TabsContext.Provider value={{ activeTab, setActiveTab, tabsId }}>
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
</TabsContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const TabsList = ({ children, className }: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) => (
|
||||
<div role="tablist" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
interface TabsTriggerProps {
|
||||
value: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TabsTrigger = ({ value, children, className }: TabsTriggerProps) => {
|
||||
const { activeTab, setActiveTab, tabsId } = useTabs()
|
||||
const isActive = activeTab === value
|
||||
|
||||
return (
|
||||
<button
|
||||
role="tab"
|
||||
id={`${tabsId}-tab-${value}`}
|
||||
aria-controls={`${tabsId}-panel-${value}`}
|
||||
aria-selected={isActive}
|
||||
onClick={() => setActiveTab(value)}
|
||||
className={`${className} ${isActive ? 'active' : ''}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
interface TabsContentProps {
|
||||
value: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TabsContent = ({ value, children, className }: TabsContentProps) => {
|
||||
const { activeTab, tabsId } = useTabs()
|
||||
|
||||
if (activeTab !== value) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
id={`${tabsId}-panel-${value}`}
|
||||
aria-labelledby={`${tabsId}-tab-${value}`}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Export compound component
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
|
||||
// Usage
|
||||
const App = () => (
|
||||
<Tabs defaultValue="profile">
|
||||
<TabsList>
|
||||
<TabsTrigger value="profile">Profile</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="profile">
|
||||
<h2>Profile Content</h2>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings">
|
||||
<h2>Settings Content</h2>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="notifications">
|
||||
<h2>Notifications Content</h2>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
```
|
||||
|
||||
## Example 8: Error Boundary
|
||||
|
||||
```typescript
|
||||
import { Component, ErrorInfo, ReactNode } from 'react'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
fallback?: (error: Error, reset: () => void) => ReactNode
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { hasError: false, error: null }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error('ErrorBoundary caught:', error, errorInfo)
|
||||
this.props.onError?.(error, errorInfo)
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
this.setState({ hasError: false, error: null })
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError && this.state.error) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback(this.state.error, this.reset)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="error-boundary">
|
||||
<h2>Something went wrong</h2>
|
||||
<details>
|
||||
<summary>Error details</summary>
|
||||
<pre>{this.state.error.message}</pre>
|
||||
</details>
|
||||
<button onClick={this.reset}>Try again</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const App = () => (
|
||||
<ErrorBoundary
|
||||
fallback={(error, reset) => (
|
||||
<div>
|
||||
<h1>Oops! Something went wrong</h1>
|
||||
<p>{error.message}</p>
|
||||
<button onClick={reset}>Retry</button>
|
||||
</div>
|
||||
)}
|
||||
onError={(error, errorInfo) => {
|
||||
// Send to error tracking service
|
||||
console.error('Error logged:', error, errorInfo)
|
||||
}}
|
||||
>
|
||||
<YourApp />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
```
|
||||
|
||||
## Example 9: Custom Hook for Local Storage
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
|
||||
const useLocalStorage = <T,>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): [T, (value: T | ((val: T) => T)) => void, () => void] => {
|
||||
// Get initial value from localStorage
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key)
|
||||
return item ? JSON.parse(item) : initialValue
|
||||
} catch (error) {
|
||||
console.error(`Error loading ${key} from localStorage:`, error)
|
||||
return initialValue
|
||||
}
|
||||
})
|
||||
|
||||
// Update localStorage when value changes
|
||||
const setValue = useCallback((value: T | ((val: T) => T)) => {
|
||||
try {
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value
|
||||
setStoredValue(valueToStore)
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore))
|
||||
|
||||
// Dispatch storage event for other tabs
|
||||
window.dispatchEvent(new Event('storage'))
|
||||
} catch (error) {
|
||||
console.error(`Error saving ${key} to localStorage:`, error)
|
||||
}
|
||||
}, [key, storedValue])
|
||||
|
||||
// Remove from localStorage
|
||||
const removeValue = useCallback(() => {
|
||||
try {
|
||||
window.localStorage.removeItem(key)
|
||||
setStoredValue(initialValue)
|
||||
} catch (error) {
|
||||
console.error(`Error removing ${key} from localStorage:`, error)
|
||||
}
|
||||
}, [key, initialValue])
|
||||
|
||||
// Listen for changes in other tabs
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === key && e.newValue) {
|
||||
setStoredValue(JSON.parse(e.newValue))
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('storage', handleStorageChange)
|
||||
return () => window.removeEventListener('storage', handleStorageChange)
|
||||
}, [key])
|
||||
|
||||
return [storedValue, setValue, removeValue]
|
||||
}
|
||||
|
||||
// Usage
|
||||
const UserPreferences = () => {
|
||||
const [preferences, setPreferences, clearPreferences] = useLocalStorage('user-prefs', {
|
||||
theme: 'light',
|
||||
language: 'en',
|
||||
notifications: true
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={preferences.notifications}
|
||||
onChange={e => setPreferences({
|
||||
...preferences,
|
||||
notifications: e.target.checked
|
||||
})}
|
||||
/>
|
||||
Enable notifications
|
||||
</label>
|
||||
|
||||
<button onClick={clearPreferences}>
|
||||
Reset to defaults
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Example 10: Optimistic Updates with useOptimistic
|
||||
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { useOptimistic } from 'react'
|
||||
import { likePost, unlikePost } from './actions'
|
||||
|
||||
interface Post {
|
||||
id: string
|
||||
content: string
|
||||
likes: number
|
||||
isLiked: boolean
|
||||
}
|
||||
|
||||
const PostCard = ({ post }: { post: Post }) => {
|
||||
const [optimisticPost, addOptimistic] = useOptimistic(
|
||||
post,
|
||||
(currentPost, update: Partial<Post>) => ({
|
||||
...currentPost,
|
||||
...update
|
||||
})
|
||||
)
|
||||
|
||||
const handleLike = async () => {
|
||||
// Optimistically update UI
|
||||
addOptimistic({
|
||||
likes: optimisticPost.likes + 1,
|
||||
isLiked: true
|
||||
})
|
||||
|
||||
try {
|
||||
// Send server request
|
||||
await likePost(post.id)
|
||||
} catch (error) {
|
||||
// Server will send correct state via revalidation
|
||||
console.error('Failed to like post:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUnlike = async () => {
|
||||
addOptimistic({
|
||||
likes: optimisticPost.likes - 1,
|
||||
isLiked: false
|
||||
})
|
||||
|
||||
try {
|
||||
await unlikePost(post.id)
|
||||
} catch (error) {
|
||||
console.error('Failed to unlike post:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="post-card">
|
||||
<p>{optimisticPost.content}</p>
|
||||
<button
|
||||
onClick={optimisticPost.isLiked ? handleUnlike : handleLike}
|
||||
className={optimisticPost.isLiked ? 'liked' : ''}
|
||||
>
|
||||
❤️ {optimisticPost.likes}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
These examples demonstrate:
|
||||
- Custom hooks for reusable logic
|
||||
- Form handling with validation
|
||||
- Portal usage for modals
|
||||
- Infinite scroll with Intersection Observer
|
||||
- Context for global state
|
||||
- Debouncing for performance
|
||||
- Compound components pattern
|
||||
- Error boundaries
|
||||
- LocalStorage integration
|
||||
- Optimistic updates (React 19)
|
||||
|
||||
291
.claude/skills/react/references/hooks-quick-reference.md
Normal file
291
.claude/skills/react/references/hooks-quick-reference.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# React Hooks Quick Reference
|
||||
|
||||
## State Hooks
|
||||
|
||||
### useState
|
||||
```typescript
|
||||
const [state, setState] = useState<Type>(initialValue)
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
// Functional update
|
||||
setCount(prev => prev + 1)
|
||||
|
||||
// Lazy initialization
|
||||
const [state, setState] = useState(() => expensiveComputation())
|
||||
```
|
||||
|
||||
### useReducer
|
||||
```typescript
|
||||
type State = { count: number }
|
||||
type Action = { type: 'increment' } | { type: 'decrement' }
|
||||
|
||||
const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'increment': return { count: state.count + 1 }
|
||||
case 'decrement': return { count: state.count - 1 }
|
||||
}
|
||||
}
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, { count: 0 })
|
||||
dispatch({ type: 'increment' })
|
||||
```
|
||||
|
||||
### useActionState (React 19)
|
||||
```typescript
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
async (previousState, formData: FormData) => {
|
||||
// Server action
|
||||
return await processForm(formData)
|
||||
},
|
||||
initialState
|
||||
)
|
||||
|
||||
<form action={formAction}>
|
||||
<button disabled={isPending}>Submit</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
## Effect Hooks
|
||||
|
||||
### useEffect
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
// Side effect
|
||||
const subscription = api.subscribe()
|
||||
|
||||
// Cleanup
|
||||
return () => subscription.unsubscribe()
|
||||
}, [dependencies])
|
||||
```
|
||||
|
||||
**Timing**: After render & paint
|
||||
**Use for**: Data fetching, subscriptions, DOM mutations
|
||||
|
||||
### useLayoutEffect
|
||||
```typescript
|
||||
useLayoutEffect(() => {
|
||||
// Runs before paint
|
||||
const height = ref.current.offsetHeight
|
||||
setHeight(height)
|
||||
}, [])
|
||||
```
|
||||
|
||||
**Timing**: After render, before paint
|
||||
**Use for**: DOM measurements, preventing flicker
|
||||
|
||||
### useInsertionEffect
|
||||
```typescript
|
||||
useInsertionEffect(() => {
|
||||
// Insert styles before any DOM reads
|
||||
const style = document.createElement('style')
|
||||
style.textContent = css
|
||||
document.head.appendChild(style)
|
||||
return () => document.head.removeChild(style)
|
||||
}, [css])
|
||||
```
|
||||
|
||||
**Timing**: Before any DOM mutations
|
||||
**Use for**: CSS-in-JS libraries
|
||||
|
||||
## Performance Hooks
|
||||
|
||||
### useMemo
|
||||
```typescript
|
||||
const memoizedValue = useMemo(() => {
|
||||
return expensiveComputation(a, b)
|
||||
}, [a, b])
|
||||
```
|
||||
|
||||
**Use for**: Expensive calculations, stable object references
|
||||
|
||||
### useCallback
|
||||
```typescript
|
||||
const memoizedCallback = useCallback(() => {
|
||||
doSomething(a, b)
|
||||
}, [a, b])
|
||||
```
|
||||
|
||||
**Use for**: Passing callbacks to optimized components
|
||||
|
||||
## Ref Hooks
|
||||
|
||||
### useRef
|
||||
```typescript
|
||||
// DOM reference
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
ref.current?.focus()
|
||||
|
||||
// Mutable value (doesn't trigger re-render)
|
||||
const countRef = useRef(0)
|
||||
countRef.current += 1
|
||||
```
|
||||
|
||||
### useImperativeHandle
|
||||
```typescript
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => inputRef.current?.focus(),
|
||||
clear: () => inputRef.current && (inputRef.current.value = '')
|
||||
}), [])
|
||||
```
|
||||
|
||||
## Context Hook
|
||||
|
||||
### useContext
|
||||
```typescript
|
||||
const value = useContext(MyContext)
|
||||
```
|
||||
|
||||
Must be used within a Provider.
|
||||
|
||||
## Transition Hooks
|
||||
|
||||
### useTransition
|
||||
```typescript
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
startTransition(() => {
|
||||
setState(newValue) // Non-urgent update
|
||||
})
|
||||
```
|
||||
|
||||
### useDeferredValue
|
||||
```typescript
|
||||
const [input, setInput] = useState('')
|
||||
const deferredInput = useDeferredValue(input)
|
||||
|
||||
// Use deferredInput for expensive operations
|
||||
const results = useMemo(() => search(deferredInput), [deferredInput])
|
||||
```
|
||||
|
||||
## Optimistic Updates (React 19)
|
||||
|
||||
### useOptimistic
|
||||
```typescript
|
||||
const [optimisticState, addOptimistic] = useOptimistic(
|
||||
actualState,
|
||||
(currentState, optimisticValue) => {
|
||||
return [...currentState, optimisticValue]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Other Hooks
|
||||
|
||||
### useId
|
||||
```typescript
|
||||
const id = useId()
|
||||
<label htmlFor={id}>Name</label>
|
||||
<input id={id} />
|
||||
```
|
||||
|
||||
### useSyncExternalStore
|
||||
```typescript
|
||||
const state = useSyncExternalStore(
|
||||
subscribe,
|
||||
getSnapshot,
|
||||
getServerSnapshot
|
||||
)
|
||||
```
|
||||
|
||||
### useDebugValue
|
||||
```typescript
|
||||
useDebugValue(isOnline ? 'Online' : 'Offline')
|
||||
```
|
||||
|
||||
### use (React 19)
|
||||
```typescript
|
||||
// Read context or promise
|
||||
const value = use(MyContext)
|
||||
const data = use(fetchPromise) // Must be in Suspense
|
||||
```
|
||||
|
||||
## Form Hooks (React DOM)
|
||||
|
||||
### useFormStatus
|
||||
```typescript
|
||||
import { useFormStatus } from 'react-dom'
|
||||
|
||||
const { pending, data, method, action } = useFormStatus()
|
||||
```
|
||||
|
||||
## Hook Rules
|
||||
|
||||
1. **Only call at top level** - Not in loops, conditions, or nested functions
|
||||
2. **Only call from React functions** - Components or custom hooks
|
||||
3. **Custom hooks start with "use"** - Naming convention
|
||||
4. **Same hooks in same order** - Every render must call same hooks
|
||||
|
||||
## Dependencies Best Practices
|
||||
|
||||
1. **Include all used values** - Variables, props, state from component scope
|
||||
2. **Use ESLint plugin** - `eslint-plugin-react-hooks` enforces rules
|
||||
3. **Functions as dependencies** - Wrap with useCallback or define outside component
|
||||
4. **Object/array dependencies** - Use useMemo for stable references
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Fetching Data
|
||||
```typescript
|
||||
const [data, setData] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
|
||||
fetch('/api/data', { signal: controller.signal })
|
||||
.then(res => res.json())
|
||||
.then(setData)
|
||||
.catch(setError)
|
||||
.finally(() => setLoading(false))
|
||||
|
||||
return () => controller.abort()
|
||||
}, [])
|
||||
```
|
||||
|
||||
### Debouncing
|
||||
```typescript
|
||||
const [value, setValue] = useState('')
|
||||
const [debouncedValue, setDebouncedValue] = useState(value)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, 500)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [value])
|
||||
```
|
||||
|
||||
### Previous Value
|
||||
```typescript
|
||||
const usePrevious = <T,>(value: T): T | undefined => {
|
||||
const ref = useRef<T>()
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
})
|
||||
return ref.current
|
||||
}
|
||||
```
|
||||
|
||||
### Interval
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
setCount(c => c + 1)
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
```
|
||||
|
||||
### Event Listeners
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const handleResize = () => setWidth(window.innerWidth)
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
```
|
||||
|
||||
658
.claude/skills/react/references/performance.md
Normal file
658
.claude/skills/react/references/performance.md
Normal file
@@ -0,0 +1,658 @@
|
||||
# React Performance Optimization Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide covers performance optimization strategies for React 19 applications.
|
||||
|
||||
## Measurement & Profiling
|
||||
|
||||
### React DevTools Profiler
|
||||
|
||||
Record performance data:
|
||||
1. Open React DevTools
|
||||
2. Go to Profiler tab
|
||||
3. Click record button
|
||||
4. Interact with app
|
||||
5. Stop recording
|
||||
6. Analyze flame graph and ranked chart
|
||||
|
||||
### Profiler Component
|
||||
|
||||
```typescript
|
||||
import { Profiler } from 'react'
|
||||
|
||||
const App = () => {
|
||||
const onRender = (
|
||||
id: string,
|
||||
phase: 'mount' | 'update',
|
||||
actualDuration: number,
|
||||
baseDuration: number,
|
||||
startTime: number,
|
||||
commitTime: number
|
||||
) => {
|
||||
console.log({
|
||||
component: id,
|
||||
phase,
|
||||
actualDuration, // Time spent rendering this update
|
||||
baseDuration // Estimated time without memoization
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Profiler id="App" onRender={onRender}>
|
||||
<YourApp />
|
||||
</Profiler>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
```typescript
|
||||
// Custom performance tracking
|
||||
const startTime = performance.now()
|
||||
// ... do work
|
||||
const endTime = performance.now()
|
||||
console.log(`Operation took ${endTime - startTime}ms`)
|
||||
|
||||
// React rendering metrics
|
||||
import { unstable_trace as trace } from 'react'
|
||||
|
||||
trace('expensive-operation', async () => {
|
||||
await performExpensiveOperation()
|
||||
})
|
||||
```
|
||||
|
||||
## Memoization Strategies
|
||||
|
||||
### React.memo
|
||||
|
||||
Prevent unnecessary re-renders:
|
||||
|
||||
```typescript
|
||||
// Basic memoization
|
||||
const ExpensiveComponent = memo(({ data }: Props) => {
|
||||
return <div>{processData(data)}</div>
|
||||
})
|
||||
|
||||
// Custom comparison
|
||||
const MemoizedComponent = memo(
|
||||
({ user }: Props) => <UserCard user={user} />,
|
||||
(prevProps, nextProps) => {
|
||||
// Return true if props are equal (skip render)
|
||||
return prevProps.user.id === nextProps.user.id
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Component renders often with same props
|
||||
- Rendering is expensive
|
||||
- Component receives complex prop objects
|
||||
|
||||
**When NOT to use:**
|
||||
- Props change frequently
|
||||
- Component is already fast
|
||||
- Premature optimization
|
||||
|
||||
### useMemo
|
||||
|
||||
Memoize computed values:
|
||||
|
||||
```typescript
|
||||
const SortedList = ({ items, filter }: Props) => {
|
||||
// Without memoization - runs every render
|
||||
const filteredItems = items.filter(item => item.type === filter)
|
||||
const sortedItems = filteredItems.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// With memoization - only runs when dependencies change
|
||||
const sortedFilteredItems = useMemo(() => {
|
||||
const filtered = items.filter(item => item.type === filter)
|
||||
return filtered.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}, [items, filter])
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{sortedFilteredItems.map(item => (
|
||||
<li key={item.id}>{item.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Expensive calculations (sorting, filtering large arrays)
|
||||
- Creating stable object references
|
||||
- Computed values used as dependencies
|
||||
|
||||
### useCallback
|
||||
|
||||
Memoize callback functions:
|
||||
|
||||
```typescript
|
||||
const Parent = () => {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
// Without useCallback - new function every render
|
||||
const handleClick = () => {
|
||||
setCount(c => c + 1)
|
||||
}
|
||||
|
||||
// With useCallback - stable function reference
|
||||
const handleClickMemo = useCallback(() => {
|
||||
setCount(c => c + 1)
|
||||
}, [])
|
||||
|
||||
return <MemoizedChild onClick={handleClickMemo} />
|
||||
}
|
||||
|
||||
const MemoizedChild = memo(({ onClick }: Props) => {
|
||||
return <button onClick={onClick}>Click</button>
|
||||
})
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Passing callbacks to memoized components
|
||||
- Callback is used in dependency array
|
||||
- Callback is expensive to create
|
||||
|
||||
## React Compiler (Automatic Optimization)
|
||||
|
||||
### Enable React Compiler
|
||||
|
||||
React 19 can automatically optimize without manual memoization:
|
||||
|
||||
```javascript
|
||||
// babel.config.js
|
||||
module.exports = {
|
||||
plugins: [
|
||||
['react-compiler', {
|
||||
compilationMode: 'all', // Optimize all components
|
||||
}]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compilation Modes
|
||||
|
||||
```javascript
|
||||
{
|
||||
compilationMode: 'annotation', // Only components with "use memo"
|
||||
compilationMode: 'all', // All components (recommended)
|
||||
compilationMode: 'infer' // Based on component complexity
|
||||
}
|
||||
```
|
||||
|
||||
### Directives
|
||||
|
||||
```typescript
|
||||
// Force memoization
|
||||
'use memo'
|
||||
const Component = ({ data }: Props) => {
|
||||
return <div>{data}</div>
|
||||
}
|
||||
|
||||
// Prevent memoization
|
||||
'use no memo'
|
||||
const SimpleComponent = ({ text }: Props) => {
|
||||
return <span>{text}</span>
|
||||
}
|
||||
```
|
||||
|
||||
## State Management Optimization
|
||||
|
||||
### State Colocation
|
||||
|
||||
Keep state as close as possible to where it's used:
|
||||
|
||||
```typescript
|
||||
// Bad - state too high
|
||||
const App = () => {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Content />
|
||||
<Modal show={showModal} onClose={() => setShowModal(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Good - state colocated
|
||||
const App = () => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Content />
|
||||
<ModalContainer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ModalContainer = () => {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
return <Modal show={showModal} onClose={() => setShowModal(false)} />
|
||||
}
|
||||
```
|
||||
|
||||
### Split Context
|
||||
|
||||
Avoid unnecessary re-renders by splitting context:
|
||||
|
||||
```typescript
|
||||
// Bad - single context causes all consumers to re-render
|
||||
const AppContext = createContext({ user, theme, settings })
|
||||
|
||||
// Good - split into separate contexts
|
||||
const UserContext = createContext(user)
|
||||
const ThemeContext = createContext(theme)
|
||||
const SettingsContext = createContext(settings)
|
||||
```
|
||||
|
||||
### Context with useMemo
|
||||
|
||||
```typescript
|
||||
const ThemeProvider = ({ children }: Props) => {
|
||||
const [theme, setTheme] = useState('light')
|
||||
|
||||
// Memoize context value to prevent unnecessary re-renders
|
||||
const value = useMemo(() => ({
|
||||
theme,
|
||||
setTheme
|
||||
}), [theme])
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Code Splitting & Lazy Loading
|
||||
|
||||
### React.lazy
|
||||
|
||||
Split components into separate bundles:
|
||||
|
||||
```typescript
|
||||
import { lazy, Suspense } from 'react'
|
||||
|
||||
// Lazy load components
|
||||
const Dashboard = lazy(() => import('./Dashboard'))
|
||||
const Settings = lazy(() => import('./Settings'))
|
||||
const Profile = lazy(() => import('./Profile'))
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Route-based Splitting
|
||||
|
||||
```typescript
|
||||
// App.tsx
|
||||
const routes = [
|
||||
{ path: '/', component: lazy(() => import('./pages/Home')) },
|
||||
{ path: '/about', component: lazy(() => import('./pages/About')) },
|
||||
{ path: '/products', component: lazy(() => import('./pages/Products')) },
|
||||
]
|
||||
|
||||
const App = () => (
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Routes>
|
||||
{routes.map(({ path, component: Component }) => (
|
||||
<Route key={path} path={path} element={<Component />} />
|
||||
))}
|
||||
</Routes>
|
||||
</Suspense>
|
||||
)
|
||||
```
|
||||
|
||||
### Component-based Splitting
|
||||
|
||||
```typescript
|
||||
// Split expensive components
|
||||
const HeavyChart = lazy(() => import('./HeavyChart'))
|
||||
|
||||
const Dashboard = () => {
|
||||
const [showChart, setShowChart] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setShowChart(true)}>
|
||||
Load Chart
|
||||
</button>
|
||||
{showChart && (
|
||||
<Suspense fallback={<ChartSkeleton />}>
|
||||
<HeavyChart />
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## List Rendering Optimization
|
||||
|
||||
### Keys
|
||||
|
||||
Always use stable, unique keys:
|
||||
|
||||
```typescript
|
||||
// Bad - index as key (causes issues on reorder/insert)
|
||||
{items.map((item, index) => (
|
||||
<Item key={index} data={item} />
|
||||
))}
|
||||
|
||||
// Good - unique ID as key
|
||||
{items.map(item => (
|
||||
<Item key={item.id} data={item} />
|
||||
))}
|
||||
|
||||
// For static lists without IDs
|
||||
{items.map(item => (
|
||||
<Item key={`${item.name}-${item.category}`} data={item} />
|
||||
))}
|
||||
```
|
||||
|
||||
### Virtualization
|
||||
|
||||
For long lists, render only visible items:
|
||||
|
||||
```typescript
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
|
||||
const VirtualList = ({ items }: { items: Item[] }) => {
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: items.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 50, // Estimated item height
|
||||
overscan: 5 // Render 5 extra items above/below viewport
|
||||
})
|
||||
|
||||
return (
|
||||
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualizer.getTotalSize()}px`,
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map(virtualItem => (
|
||||
<div
|
||||
key={virtualItem.key}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: `${virtualItem.size}px`,
|
||||
transform: `translateY(${virtualItem.start}px)`
|
||||
}}
|
||||
>
|
||||
<Item data={items[virtualItem.index]} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
```typescript
|
||||
const PaginatedList = ({ items }: Props) => {
|
||||
const [page, setPage] = useState(1)
|
||||
const itemsPerPage = 20
|
||||
|
||||
const paginatedItems = useMemo(() => {
|
||||
const start = (page - 1) * itemsPerPage
|
||||
const end = start + itemsPerPage
|
||||
return items.slice(start, end)
|
||||
}, [items, page, itemsPerPage])
|
||||
|
||||
return (
|
||||
<>
|
||||
{paginatedItems.map(item => (
|
||||
<Item key={item.id} data={item} />
|
||||
))}
|
||||
<Pagination
|
||||
page={page}
|
||||
total={Math.ceil(items.length / itemsPerPage)}
|
||||
onChange={setPage}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Transitions & Concurrent Features
|
||||
|
||||
### useTransition
|
||||
|
||||
Keep UI responsive during expensive updates:
|
||||
|
||||
```typescript
|
||||
const SearchPage = () => {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState([])
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setQuery(value) // Urgent - update input immediately
|
||||
|
||||
// Non-urgent - can be interrupted
|
||||
startTransition(() => {
|
||||
const filtered = expensiveFilter(items, value)
|
||||
setResults(filtered)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<input value={query} onChange={e => handleSearch(e.target.value)} />
|
||||
{isPending && <Spinner />}
|
||||
<ResultsList results={results} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useDeferredValue
|
||||
|
||||
Defer non-urgent renders:
|
||||
|
||||
```typescript
|
||||
const SearchPage = () => {
|
||||
const [query, setQuery] = useState('')
|
||||
const deferredQuery = useDeferredValue(query)
|
||||
|
||||
// Input updates immediately
|
||||
// Results update with deferred value (can be interrupted)
|
||||
const results = useMemo(() => {
|
||||
return expensiveFilter(items, deferredQuery)
|
||||
}, [deferredQuery])
|
||||
|
||||
return (
|
||||
<>
|
||||
<input value={query} onChange={e => setQuery(e.target.value)} />
|
||||
<ResultsList results={results} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Image & Asset Optimization
|
||||
|
||||
### Lazy Load Images
|
||||
|
||||
```typescript
|
||||
const LazyImage = ({ src, alt }: Props) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{!isLoaded && <ImageSkeleton />}
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
loading="lazy" // Native lazy loading
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
className={isLoaded ? 'opacity-100' : 'opacity-0'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js Image Component
|
||||
|
||||
```typescript
|
||||
import Image from 'next/image'
|
||||
|
||||
const OptimizedImage = () => (
|
||||
<Image
|
||||
src="/hero.jpg"
|
||||
alt="Hero"
|
||||
width={800}
|
||||
height={600}
|
||||
priority // Load immediately for above-fold images
|
||||
placeholder="blur"
|
||||
blurDataURL="data:image/jpeg;base64,..."
|
||||
/>
|
||||
)
|
||||
```
|
||||
|
||||
## Bundle Size Optimization
|
||||
|
||||
### Tree Shaking
|
||||
|
||||
Import only what you need:
|
||||
|
||||
```typescript
|
||||
// Bad - imports entire library
|
||||
import _ from 'lodash'
|
||||
|
||||
// Good - import only needed functions
|
||||
import debounce from 'lodash/debounce'
|
||||
import throttle from 'lodash/throttle'
|
||||
|
||||
// Even better - use native methods when possible
|
||||
const debounce = (fn, delay) => {
|
||||
let timeoutId
|
||||
return (...args) => {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => fn(...args), delay)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Analyze Bundle
|
||||
|
||||
```bash
|
||||
# Next.js
|
||||
ANALYZE=true npm run build
|
||||
|
||||
# Create React App
|
||||
npm install --save-dev webpack-bundle-analyzer
|
||||
```
|
||||
|
||||
### Dynamic Imports
|
||||
|
||||
```typescript
|
||||
// Load library only when needed
|
||||
const handleExport = async () => {
|
||||
const { jsPDF } = await import('jspdf')
|
||||
const doc = new jsPDF()
|
||||
doc.save('report.pdf')
|
||||
}
|
||||
```
|
||||
|
||||
## Common Performance Pitfalls
|
||||
|
||||
### 1. Inline Object Creation
|
||||
|
||||
```typescript
|
||||
// Bad - new object every render
|
||||
<Component style={{ margin: 10 }} />
|
||||
|
||||
// Good - stable reference
|
||||
const style = { margin: 10 }
|
||||
<Component style={style} />
|
||||
|
||||
// Or use useMemo
|
||||
const style = useMemo(() => ({ margin: 10 }), [])
|
||||
```
|
||||
|
||||
### 2. Inline Functions
|
||||
|
||||
```typescript
|
||||
// Bad - new function every render (if child is memoized)
|
||||
<MemoizedChild onClick={() => handleClick(id)} />
|
||||
|
||||
// Good
|
||||
const handleClickMemo = useCallback(() => handleClick(id), [id])
|
||||
<MemoizedChild onClick={handleClickMemo} />
|
||||
```
|
||||
|
||||
### 3. Spreading Props
|
||||
|
||||
```typescript
|
||||
// Bad - causes re-renders even when props unchanged
|
||||
<Component {...props} />
|
||||
|
||||
// Good - pass only needed props
|
||||
<Component value={props.value} onChange={props.onChange} />
|
||||
```
|
||||
|
||||
### 4. Large Context
|
||||
|
||||
```typescript
|
||||
// Bad - everything re-renders on any state change
|
||||
const AppContext = createContext({ user, theme, cart, settings, ... })
|
||||
|
||||
// Good - split into focused contexts
|
||||
const UserContext = createContext(user)
|
||||
const ThemeContext = createContext(theme)
|
||||
const CartContext = createContext(cart)
|
||||
```
|
||||
|
||||
## Performance Checklist
|
||||
|
||||
- [ ] Measure before optimizing (use Profiler)
|
||||
- [ ] Use React DevTools to identify slow components
|
||||
- [ ] Implement code splitting for large routes
|
||||
- [ ] Lazy load below-the-fold content
|
||||
- [ ] Virtualize long lists
|
||||
- [ ] Memoize expensive calculations
|
||||
- [ ] Split large contexts
|
||||
- [ ] Colocate state close to usage
|
||||
- [ ] Use transitions for non-urgent updates
|
||||
- [ ] Optimize images and assets
|
||||
- [ ] Analyze and minimize bundle size
|
||||
- [ ] Remove console.logs in production
|
||||
- [ ] Use production build for testing
|
||||
- [ ] Monitor real-world performance metrics
|
||||
|
||||
## References
|
||||
|
||||
- React Performance: https://react.dev/learn/render-and-commit
|
||||
- React Profiler: https://react.dev/reference/react/Profiler
|
||||
- React Compiler: https://react.dev/reference/react-compiler
|
||||
- Web Vitals: https://web.dev/vitals/
|
||||
|
||||
656
.claude/skills/react/references/server-components.md
Normal file
656
.claude/skills/react/references/server-components.md
Normal file
@@ -0,0 +1,656 @@
|
||||
# React Server Components & Server Functions
|
||||
|
||||
## Overview
|
||||
|
||||
React Server Components (RSC) allow components to render on the server, improving performance and enabling direct data access. Server Functions allow client components to call server-side functions.
|
||||
|
||||
## Server Components
|
||||
|
||||
### What are Server Components?
|
||||
|
||||
Components that run **only on the server**:
|
||||
- Can access databases directly
|
||||
- Zero bundle size (code stays on server)
|
||||
- Better performance (less JavaScript to client)
|
||||
- Automatic code splitting
|
||||
|
||||
### Creating Server Components
|
||||
|
||||
```typescript
|
||||
// app/products/page.tsx
|
||||
// Server Component by default in App Router
|
||||
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
const ProductsPage = async () => {
|
||||
// Direct database access
|
||||
const products = await db.product.findMany({
|
||||
where: { active: true },
|
||||
include: { category: true }
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Products</h1>
|
||||
{products.map(product => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductsPage
|
||||
```
|
||||
|
||||
### Server Component Rules
|
||||
|
||||
**Can do:**
|
||||
- Access databases and APIs directly
|
||||
- Use server-only modules (fs, path, etc.)
|
||||
- Keep secrets secure (API keys, tokens)
|
||||
- Reduce client bundle size
|
||||
- Use async/await at top level
|
||||
|
||||
**Cannot do:**
|
||||
- Use hooks (useState, useEffect, etc.)
|
||||
- Use browser APIs (window, document)
|
||||
- Attach event handlers (onClick, etc.)
|
||||
- Use Context
|
||||
|
||||
### Mixing Server and Client Components
|
||||
|
||||
```typescript
|
||||
// Server Component (default)
|
||||
const Page = async () => {
|
||||
const data = await fetchData()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ServerComponent data={data} />
|
||||
{/* Client component for interactivity */}
|
||||
<ClientComponent initialData={data} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Client Component
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
const ClientComponent = ({ initialData }) => {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<button onClick={() => setCount(c => c + 1)}>
|
||||
{count}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Server Component Patterns
|
||||
|
||||
#### Data Fetching
|
||||
```typescript
|
||||
// app/user/[id]/page.tsx
|
||||
interface PageProps {
|
||||
params: { id: string }
|
||||
}
|
||||
|
||||
const UserPage = async ({ params }: PageProps) => {
|
||||
const user = await db.user.findUnique({
|
||||
where: { id: params.id }
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
notFound() // Next.js 404
|
||||
}
|
||||
|
||||
return <UserProfile user={user} />
|
||||
}
|
||||
```
|
||||
|
||||
#### Parallel Data Fetching
|
||||
```typescript
|
||||
const DashboardPage = async () => {
|
||||
// Fetch in parallel
|
||||
const [user, orders, stats] = await Promise.all([
|
||||
fetchUser(),
|
||||
fetchOrders(),
|
||||
fetchStats()
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserHeader user={user} />
|
||||
<OrdersList orders={orders} />
|
||||
<StatsWidget stats={stats} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Streaming with Suspense
|
||||
```typescript
|
||||
const Page = () => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Suspense fallback={<ProductsSkeleton />}>
|
||||
<Products />
|
||||
</Suspense>
|
||||
<Suspense fallback={<ReviewsSkeleton />}>
|
||||
<Reviews />
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Products = async () => {
|
||||
const products = await fetchProducts() // Slow query
|
||||
return <ProductsList products={products} />
|
||||
}
|
||||
```
|
||||
|
||||
## Server Functions (Server Actions)
|
||||
|
||||
### What are Server Functions?
|
||||
|
||||
Functions that run on the server but can be called from client components:
|
||||
- Marked with `'use server'` directive
|
||||
- Can mutate data
|
||||
- Integrated with forms
|
||||
- Type-safe with TypeScript
|
||||
|
||||
### Creating Server Functions
|
||||
|
||||
#### File-level directive
|
||||
```typescript
|
||||
// app/actions.ts
|
||||
'use server'
|
||||
|
||||
import { db } from '@/lib/db'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export async function createProduct(formData: FormData) {
|
||||
const name = formData.get('name') as string
|
||||
const price = Number(formData.get('price'))
|
||||
|
||||
const product = await db.product.create({
|
||||
data: { name, price }
|
||||
})
|
||||
|
||||
revalidatePath('/products')
|
||||
return product
|
||||
}
|
||||
|
||||
export async function deleteProduct(id: string) {
|
||||
await db.product.delete({ where: { id } })
|
||||
revalidatePath('/products')
|
||||
}
|
||||
```
|
||||
|
||||
#### Function-level directive
|
||||
```typescript
|
||||
// Inside a Server Component
|
||||
const MyComponent = async () => {
|
||||
async function handleSubmit(formData: FormData) {
|
||||
'use server'
|
||||
const email = formData.get('email') as string
|
||||
await saveEmail(email)
|
||||
}
|
||||
|
||||
return <form action={handleSubmit}>...</form>
|
||||
}
|
||||
```
|
||||
|
||||
### Using Server Functions
|
||||
|
||||
#### With Forms
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { createProduct } from './actions'
|
||||
|
||||
const ProductForm = () => {
|
||||
return (
|
||||
<form action={createProduct}>
|
||||
<input name="name" required />
|
||||
<input name="price" type="number" required />
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### With useActionState
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { useActionState } from 'react'
|
||||
import { createProduct } from './actions'
|
||||
|
||||
type FormState = {
|
||||
message: string
|
||||
success: boolean
|
||||
} | null
|
||||
|
||||
const ProductForm = () => {
|
||||
const [state, formAction, isPending] = useActionState<FormState>(
|
||||
async (previousState, formData: FormData) => {
|
||||
try {
|
||||
await createProduct(formData)
|
||||
return { message: 'Product created!', success: true }
|
||||
} catch (error) {
|
||||
return { message: 'Failed to create product', success: false }
|
||||
}
|
||||
},
|
||||
null
|
||||
)
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input name="name" required />
|
||||
<input name="price" type="number" required />
|
||||
<button disabled={isPending}>
|
||||
{isPending ? 'Creating...' : 'Create'}
|
||||
</button>
|
||||
{state?.message && (
|
||||
<p className={state.success ? 'text-green-600' : 'text-red-600'}>
|
||||
{state.message}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Programmatic Invocation
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { deleteProduct } from './actions'
|
||||
|
||||
const DeleteButton = ({ productId }: { productId: string }) => {
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsPending(true)
|
||||
try {
|
||||
await deleteProduct(productId)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsPending(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleDelete} disabled={isPending}>
|
||||
{isPending ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Server Function Patterns
|
||||
|
||||
#### Validation with Zod
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
import { z } from 'zod'
|
||||
|
||||
const ProductSchema = z.object({
|
||||
name: z.string().min(3),
|
||||
price: z.number().positive(),
|
||||
description: z.string().optional()
|
||||
})
|
||||
|
||||
export async function createProduct(formData: FormData) {
|
||||
const rawData = {
|
||||
name: formData.get('name'),
|
||||
price: Number(formData.get('price')),
|
||||
description: formData.get('description')
|
||||
}
|
||||
|
||||
// Validate
|
||||
const result = ProductSchema.safeParse(rawData)
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: result.error.flatten().fieldErrors
|
||||
}
|
||||
}
|
||||
|
||||
// Create product
|
||||
const product = await db.product.create({
|
||||
data: result.data
|
||||
})
|
||||
|
||||
revalidatePath('/products')
|
||||
return { success: true, product }
|
||||
}
|
||||
```
|
||||
|
||||
#### Authentication Check
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
import { auth } from '@/lib/auth'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export async function createOrder(formData: FormData) {
|
||||
const session = await auth()
|
||||
|
||||
if (!session?.user) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
const order = await db.order.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
// ... other fields
|
||||
}
|
||||
})
|
||||
|
||||
return order
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Handling
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
export async function updateProfile(formData: FormData) {
|
||||
try {
|
||||
const userId = await getCurrentUserId()
|
||||
|
||||
const profile = await db.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
name: formData.get('name') as string,
|
||||
bio: formData.get('bio') as string
|
||||
}
|
||||
})
|
||||
|
||||
revalidatePath('/profile')
|
||||
return { success: true, profile }
|
||||
} catch (error) {
|
||||
console.error('Failed to update profile:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to update profile. Please try again.'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Optimistic Updates
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { useOptimistic } from 'react'
|
||||
import { likePost } from './actions'
|
||||
|
||||
const Post = ({ post }: { post: Post }) => {
|
||||
const [optimisticLikes, addOptimisticLike] = useOptimistic(
|
||||
post.likes,
|
||||
(currentLikes) => currentLikes + 1
|
||||
)
|
||||
|
||||
const handleLike = async () => {
|
||||
addOptimisticLike(null)
|
||||
await likePost(post.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{post.content}</p>
|
||||
<button onClick={handleLike}>
|
||||
❤️ {optimisticLikes}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Data Mutations & Revalidation
|
||||
|
||||
### revalidatePath
|
||||
Invalidate cached data for a path:
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
await db.post.create({ data: {...} })
|
||||
|
||||
// Revalidate the posts page
|
||||
revalidatePath('/posts')
|
||||
|
||||
// Revalidate with layout
|
||||
revalidatePath('/posts', 'layout')
|
||||
}
|
||||
```
|
||||
|
||||
### revalidateTag
|
||||
Invalidate cached data by tag:
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export async function updateProduct(id: string, data: ProductData) {
|
||||
await db.product.update({ where: { id }, data })
|
||||
|
||||
// Revalidate all queries tagged with 'products'
|
||||
revalidateTag('products')
|
||||
}
|
||||
```
|
||||
|
||||
### redirect
|
||||
Redirect after mutation:
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const post = await db.post.create({ data: {...} })
|
||||
|
||||
// Redirect to the new post
|
||||
redirect(`/posts/${post.id}`)
|
||||
}
|
||||
```
|
||||
|
||||
## Caching with Server Components
|
||||
|
||||
### cache Function
|
||||
Deduplicate requests within a render:
|
||||
|
||||
```typescript
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getUser = cache(async (id: string) => {
|
||||
return await db.user.findUnique({ where: { id } })
|
||||
})
|
||||
|
||||
// Called multiple times but only fetches once per render
|
||||
const Page = async () => {
|
||||
const user1 = await getUser('123')
|
||||
const user2 = await getUser('123') // Uses cached result
|
||||
|
||||
return <div>...</div>
|
||||
}
|
||||
```
|
||||
|
||||
### Next.js fetch Caching
|
||||
```typescript
|
||||
// Cached by default
|
||||
const data = await fetch('https://api.example.com/data')
|
||||
|
||||
// Revalidate every 60 seconds
|
||||
const data = await fetch('https://api.example.com/data', {
|
||||
next: { revalidate: 60 }
|
||||
})
|
||||
|
||||
// Never cache
|
||||
const data = await fetch('https://api.example.com/data', {
|
||||
cache: 'no-store'
|
||||
})
|
||||
|
||||
// Tag for revalidation
|
||||
const data = await fetch('https://api.example.com/data', {
|
||||
next: { tags: ['products'] }
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Component Placement
|
||||
- Keep interactive components client-side
|
||||
- Use server components for data fetching
|
||||
- Place 'use client' as deep as possible in tree
|
||||
|
||||
### 2. Data Fetching
|
||||
- Fetch in parallel when possible
|
||||
- Use Suspense for streaming
|
||||
- Cache expensive operations
|
||||
|
||||
### 3. Server Functions
|
||||
- Validate all inputs
|
||||
- Check authentication/authorization
|
||||
- Handle errors gracefully
|
||||
- Return serializable data only
|
||||
|
||||
### 4. Performance
|
||||
- Minimize client JavaScript
|
||||
- Use streaming for slow queries
|
||||
- Implement proper caching
|
||||
- Optimize database queries
|
||||
|
||||
### 5. Security
|
||||
- Never expose secrets to client
|
||||
- Validate server function inputs
|
||||
- Use environment variables
|
||||
- Implement rate limiting
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Layout with Dynamic Data
|
||||
```typescript
|
||||
// app/layout.tsx
|
||||
const RootLayout = async ({ children }: { children: React.ReactNode }) => {
|
||||
const user = await getCurrentUser()
|
||||
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<Header user={user} />
|
||||
{children}
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Loading States
|
||||
```typescript
|
||||
// app/products/loading.tsx
|
||||
export default function Loading() {
|
||||
return <ProductsSkeleton />
|
||||
}
|
||||
|
||||
// app/products/page.tsx
|
||||
const ProductsPage = async () => {
|
||||
const products = await fetchProducts()
|
||||
return <ProductsList products={products} />
|
||||
}
|
||||
```
|
||||
|
||||
### Error Boundaries
|
||||
```typescript
|
||||
// app/products/error.tsx
|
||||
'use client'
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset
|
||||
}: {
|
||||
error: Error
|
||||
reset: () => void
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Something went wrong!</h2>
|
||||
<p>{error.message}</p>
|
||||
<button onClick={reset}>Try again</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Search with Server Functions
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { searchProducts } from './actions'
|
||||
import { useDeferredValue, useState, useEffect } from 'react'
|
||||
|
||||
const SearchPage = () => {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState([])
|
||||
const deferredQuery = useDeferredValue(query)
|
||||
|
||||
useEffect(() => {
|
||||
if (deferredQuery) {
|
||||
searchProducts(deferredQuery).then(setResults)
|
||||
}
|
||||
}, [deferredQuery])
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
/>
|
||||
<ResultsList results={results} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"Cannot use hooks in Server Component"**
|
||||
- Add 'use client' directive
|
||||
- Move state logic to client component
|
||||
|
||||
2. **"Functions cannot be passed to Client Components"**
|
||||
- Use Server Functions instead
|
||||
- Pass data, not functions
|
||||
|
||||
3. **Hydration mismatches**
|
||||
- Ensure server and client render same HTML
|
||||
- Use useEffect for browser-only code
|
||||
|
||||
4. **Slow initial load**
|
||||
- Implement Suspense boundaries
|
||||
- Use streaming rendering
|
||||
- Optimize database queries
|
||||
|
||||
## References
|
||||
|
||||
- React Server Components: https://react.dev/reference/rsc/server-components
|
||||
- Server Functions: https://react.dev/reference/rsc/server-functions
|
||||
- Next.js App Router: https://nextjs.org/docs/app
|
||||
|
||||
133
.claude/skills/typescript/README.md
Normal file
133
.claude/skills/typescript/README.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# TypeScript Claude Skill
|
||||
|
||||
Comprehensive TypeScript skill for type-safe development with modern JavaScript/TypeScript applications.
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides in-depth knowledge about TypeScript's type system, patterns, best practices, and integration with popular frameworks like React. It covers everything from basic types to advanced type manipulation techniques.
|
||||
|
||||
## Files
|
||||
|
||||
### Core Documentation
|
||||
- **SKILL.md** - Main skill file with workflows and when to use this skill
|
||||
- **quick-reference.md** - Quick lookup guide for common TypeScript syntax and patterns
|
||||
|
||||
### Reference Materials
|
||||
- **references/type-system.md** - Comprehensive guide to TypeScript's type system
|
||||
- **references/utility-types.md** - Complete reference for built-in and custom utility types
|
||||
- **references/common-patterns.md** - Real-world TypeScript patterns and idioms
|
||||
|
||||
### Examples
|
||||
- **examples/type-system-basics.ts** - Fundamental TypeScript concepts
|
||||
- **examples/advanced-types.ts** - Generics, conditional types, mapped types
|
||||
- **examples/react-patterns.ts** - Type-safe React components and hooks
|
||||
- **examples/README.md** - Guide to using the examples
|
||||
|
||||
## Usage
|
||||
|
||||
### When to Use This Skill
|
||||
|
||||
Reference this skill when:
|
||||
- Writing or refactoring TypeScript code
|
||||
- Designing type-safe APIs and interfaces
|
||||
- Working with advanced type system features
|
||||
- Configuring TypeScript projects
|
||||
- Troubleshooting type errors
|
||||
- Implementing type-safe patterns with libraries
|
||||
- Converting JavaScript to TypeScript
|
||||
|
||||
### Quick Start
|
||||
|
||||
For quick lookups, start with `quick-reference.md` which provides concise syntax and patterns.
|
||||
|
||||
For learning or deep dives:
|
||||
1. **Fundamentals**: Start with `references/type-system.md`
|
||||
2. **Utilities**: Learn about transformations in `references/utility-types.md`
|
||||
3. **Patterns**: Study real-world patterns in `references/common-patterns.md`
|
||||
4. **Practice**: Explore code examples in `examples/`
|
||||
|
||||
## Key Topics Covered
|
||||
|
||||
### Type System
|
||||
- Primitive types and special types
|
||||
- Object types (interfaces, type aliases)
|
||||
- Union and intersection types
|
||||
- Literal types and template literal types
|
||||
- Type inference and narrowing
|
||||
- Generic types with constraints
|
||||
- Conditional types and mapped types
|
||||
- Recursive types
|
||||
|
||||
### Advanced Features
|
||||
- Type guards and type predicates
|
||||
- Assertion functions
|
||||
- Branded types for nominal typing
|
||||
- Key remapping and filtering
|
||||
- Distributive conditional types
|
||||
- Type-level programming
|
||||
|
||||
### Utility Types
|
||||
- Built-in utilities (Partial, Pick, Omit, etc.)
|
||||
- Custom utility type patterns
|
||||
- Deep transformations
|
||||
- Type composition
|
||||
|
||||
### React Integration
|
||||
- Component props typing
|
||||
- Generic components
|
||||
- Hooks with TypeScript
|
||||
- Context with type safety
|
||||
- Event handlers
|
||||
- Ref typing
|
||||
|
||||
### Best Practices
|
||||
- Type safety patterns
|
||||
- Error handling
|
||||
- Code organization
|
||||
- Integration with Zod for runtime validation
|
||||
- Named return variables (Go-style)
|
||||
- Discriminated unions for state management
|
||||
|
||||
## Integration with Project Stack
|
||||
|
||||
This skill is designed to work seamlessly with:
|
||||
- **React 19**: Type-safe component development
|
||||
- **TanStack Ecosystem**: Typed queries, routing, forms, and stores
|
||||
- **Zod**: Runtime validation with type inference
|
||||
- **Radix UI**: Component prop typing
|
||||
- **Tailwind CSS**: Type-safe className composition
|
||||
|
||||
## Examples
|
||||
|
||||
All examples are self-contained and demonstrate practical patterns:
|
||||
- Based on real-world usage
|
||||
- Follow project best practices
|
||||
- Include comprehensive comments
|
||||
- Can be run with `ts-node`
|
||||
- Ready to adapt to your needs
|
||||
|
||||
## Configuration
|
||||
|
||||
The skill includes guidance on TypeScript configuration with recommended settings for:
|
||||
- Strict type checking
|
||||
- Module resolution
|
||||
- JSX support
|
||||
- Path aliases
|
||||
- Declaration files
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new patterns or examples:
|
||||
1. Follow existing file structure
|
||||
2. Include comprehensive comments
|
||||
3. Demonstrate real-world usage
|
||||
4. Add to appropriate reference file
|
||||
5. Update this README if needed
|
||||
|
||||
## Resources
|
||||
|
||||
- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/)
|
||||
- [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/)
|
||||
- [Type Challenges](https://github.com/type-challenges/type-challenges)
|
||||
- [TSConfig Reference](https://www.typescriptlang.org/tsconfig)
|
||||
|
||||
359
.claude/skills/typescript/SKILL.md
Normal file
359
.claude/skills/typescript/SKILL.md
Normal file
@@ -0,0 +1,359 @@
|
||||
---
|
||||
name: typescript
|
||||
description: This skill should be used when working with TypeScript code, including type definitions, type inference, generics, utility types, and TypeScript configuration. Provides comprehensive knowledge of TypeScript patterns, best practices, and advanced type system features.
|
||||
---
|
||||
|
||||
# TypeScript Skill
|
||||
|
||||
This skill provides comprehensive knowledge and patterns for working with TypeScript effectively in modern applications.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Writing or refactoring TypeScript code
|
||||
- Designing type-safe APIs and interfaces
|
||||
- Working with advanced type system features (generics, conditional types, mapped types)
|
||||
- Configuring TypeScript projects (tsconfig.json)
|
||||
- Troubleshooting type errors
|
||||
- Implementing type-safe patterns with libraries (React, TanStack, etc.)
|
||||
- Converting JavaScript code to TypeScript
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Type System Fundamentals
|
||||
|
||||
TypeScript provides static typing for JavaScript with a powerful type system that includes:
|
||||
- Primitive types (string, number, boolean, null, undefined, symbol, bigint)
|
||||
- Object types (interfaces, type aliases, classes)
|
||||
- Array and tuple types
|
||||
- Union and intersection types
|
||||
- Literal types and template literal types
|
||||
- Type inference and type narrowing
|
||||
- Generic types with constraints
|
||||
- Conditional types and mapped types
|
||||
|
||||
### Type Inference
|
||||
|
||||
Leverage TypeScript's type inference to write less verbose code:
|
||||
- Let TypeScript infer return types when obvious
|
||||
- Use type inference for variable declarations
|
||||
- Rely on generic type inference in function calls
|
||||
- Use `as const` for immutable literal types
|
||||
|
||||
### Type Safety Patterns
|
||||
|
||||
Implement type-safe patterns:
|
||||
- Use discriminated unions for state management
|
||||
- Implement type guards for runtime type checking
|
||||
- Use branded types for nominal typing
|
||||
- Leverage conditional types for API design
|
||||
- Use template literal types for string manipulation
|
||||
|
||||
## Key Workflows
|
||||
|
||||
### 1. Designing Type-Safe APIs
|
||||
|
||||
When designing APIs, follow these patterns:
|
||||
|
||||
**Interface vs Type Alias:**
|
||||
- Use `interface` for object shapes that may be extended
|
||||
- Use `type` for unions, intersections, and complex type operations
|
||||
- Use `type` with mapped types and conditional types
|
||||
|
||||
**Generic Constraints:**
|
||||
```typescript
|
||||
// Use extends for generic constraints
|
||||
function getValue<T extends { id: string }>(item: T): string {
|
||||
return item.id
|
||||
}
|
||||
```
|
||||
|
||||
**Discriminated Unions:**
|
||||
```typescript
|
||||
// Use for type-safe state machines
|
||||
type State =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: Data }
|
||||
| { status: 'error'; error: Error }
|
||||
```
|
||||
|
||||
### 2. Working with Utility Types
|
||||
|
||||
Use built-in utility types for common transformations:
|
||||
- `Partial<T>` - Make all properties optional
|
||||
- `Required<T>` - Make all properties required
|
||||
- `Readonly<T>` - Make all properties readonly
|
||||
- `Pick<T, K>` - Select specific properties
|
||||
- `Omit<T, K>` - Exclude specific properties
|
||||
- `Record<K, T>` - Create object type with specific keys
|
||||
- `Exclude<T, U>` - Exclude types from union
|
||||
- `Extract<T, U>` - Extract types from union
|
||||
- `NonNullable<T>` - Remove null/undefined
|
||||
- `ReturnType<T>` - Get function return type
|
||||
- `Parameters<T>` - Get function parameter types
|
||||
- `Awaited<T>` - Unwrap Promise type
|
||||
|
||||
### 3. Advanced Type Patterns
|
||||
|
||||
**Mapped Types:**
|
||||
```typescript
|
||||
// Transform object types
|
||||
type Nullable<T> = {
|
||||
[K in keyof T]: T[K] | null
|
||||
}
|
||||
|
||||
type ReadonlyDeep<T> = {
|
||||
readonly [K in keyof T]: T[K] extends object
|
||||
? ReadonlyDeep<T[K]>
|
||||
: T[K]
|
||||
}
|
||||
```
|
||||
|
||||
**Conditional Types:**
|
||||
```typescript
|
||||
// Type-level logic
|
||||
type IsArray<T> = T extends Array<any> ? true : false
|
||||
|
||||
type Flatten<T> = T extends Array<infer U> ? U : T
|
||||
```
|
||||
|
||||
**Template Literal Types:**
|
||||
```typescript
|
||||
// String manipulation at type level
|
||||
type EventName<T extends string> = `on${Capitalize<T>}`
|
||||
type Route = `/api/${'users' | 'posts'}/${string}`
|
||||
```
|
||||
|
||||
### 4. Type Narrowing
|
||||
|
||||
Use type guards and narrowing techniques:
|
||||
|
||||
**typeof guards:**
|
||||
```typescript
|
||||
if (typeof value === 'string') {
|
||||
// value is string here
|
||||
}
|
||||
```
|
||||
|
||||
**instanceof guards:**
|
||||
```typescript
|
||||
if (error instanceof Error) {
|
||||
// error is Error here
|
||||
}
|
||||
```
|
||||
|
||||
**Custom type guards:**
|
||||
```typescript
|
||||
function isUser(value: unknown): value is User {
|
||||
return typeof value === 'object' && value !== null && 'id' in value
|
||||
}
|
||||
```
|
||||
|
||||
**Discriminated unions:**
|
||||
```typescript
|
||||
function handle(state: State) {
|
||||
switch (state.status) {
|
||||
case 'idle':
|
||||
// state is { status: 'idle' }
|
||||
break
|
||||
case 'success':
|
||||
// state is { status: 'success'; data: Data }
|
||||
console.log(state.data)
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Working with External Libraries
|
||||
|
||||
**Typing Third-Party Libraries:**
|
||||
- Install type definitions: `npm install --save-dev @types/package-name`
|
||||
- Create custom declarations in `.d.ts` files when types unavailable
|
||||
- Use module augmentation to extend existing type definitions
|
||||
|
||||
**Declaration Files:**
|
||||
```typescript
|
||||
// globals.d.ts
|
||||
declare global {
|
||||
interface Window {
|
||||
myCustomProperty: string
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
```
|
||||
|
||||
### 6. TypeScript Configuration
|
||||
|
||||
Configure `tsconfig.json` for strict type checking:
|
||||
|
||||
**Essential Strict Options:**
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Prefer Type Inference Over Explicit Types
|
||||
Let TypeScript infer types when they're obvious from context.
|
||||
|
||||
### 2. Use Strict Mode
|
||||
Enable strict type checking to catch more errors at compile time.
|
||||
|
||||
### 3. Avoid `any` Type
|
||||
Use `unknown` for truly unknown types, then narrow with type guards.
|
||||
|
||||
### 4. Use Const Assertions
|
||||
Use `as const` for immutable values and narrow literal types.
|
||||
|
||||
### 5. Leverage Discriminated Unions
|
||||
Use for state machines and variant types for better type safety.
|
||||
|
||||
### 6. Create Reusable Generic Types
|
||||
Extract common type patterns into reusable generics.
|
||||
|
||||
### 7. Use Branded Types for Nominal Typing
|
||||
Create distinct types for values with same structure but different meaning.
|
||||
|
||||
### 8. Document Complex Types
|
||||
Add JSDoc comments to explain non-obvious type decisions.
|
||||
|
||||
### 9. Use Type-Only Imports
|
||||
Use `import type` for type-only imports to aid tree-shaking.
|
||||
|
||||
### 10. Handle Errors with Type Guards
|
||||
Use type guards to safely work with error objects.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### React Component Props
|
||||
```typescript
|
||||
// Use interface for component props
|
||||
interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
onClick?: () => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Button({ variant = 'primary', size = 'md', onClick, children }: ButtonProps) {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
### API Response Types
|
||||
```typescript
|
||||
// Use discriminated unions for API responses
|
||||
type ApiResponse<T> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: string }
|
||||
|
||||
// Helper for safe API calls
|
||||
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Store/State Types
|
||||
```typescript
|
||||
// Use interfaces for state objects
|
||||
interface AppState {
|
||||
user: User | null
|
||||
isAuthenticated: boolean
|
||||
theme: 'light' | 'dark'
|
||||
}
|
||||
|
||||
// Use type for actions (discriminated union)
|
||||
type AppAction =
|
||||
| { type: 'LOGIN'; payload: User }
|
||||
| { type: 'LOGOUT' }
|
||||
| { type: 'SET_THEME'; payload: 'light' | 'dark' }
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
For detailed information on specific topics, refer to:
|
||||
- `references/type-system.md` - Deep dive into TypeScript's type system
|
||||
- `references/utility-types.md` - Complete guide to built-in utility types
|
||||
- `references/advanced-types.md` - Advanced type patterns and techniques
|
||||
- `references/tsconfig-reference.md` - Comprehensive tsconfig.json reference
|
||||
- `references/common-patterns.md` - Common TypeScript patterns and idioms
|
||||
- `examples/` - Practical code examples
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Type Errors
|
||||
|
||||
**Type 'X' is not assignable to type 'Y':**
|
||||
- Check if types are compatible
|
||||
- Use type assertions when you know better than the compiler
|
||||
- Consider using union types or widening the target type
|
||||
|
||||
**Object is possibly 'null' or 'undefined':**
|
||||
- Use optional chaining: `object?.property`
|
||||
- Use nullish coalescing: `value ?? defaultValue`
|
||||
- Add type guards or null checks
|
||||
|
||||
**Type 'any' implicitly has...**
|
||||
- Enable strict mode and fix type definitions
|
||||
- Add explicit type annotations
|
||||
- Use `unknown` instead of `any` when appropriate
|
||||
|
||||
**Cannot find module or its type declarations:**
|
||||
- Install type definitions: `@types/package-name`
|
||||
- Create custom `.d.ts` declaration file
|
||||
- Add to `types` array in tsconfig.json
|
||||
|
||||
## Integration with Project Stack
|
||||
|
||||
### React 19
|
||||
Use TypeScript with React 19 features:
|
||||
- Type component props with interfaces
|
||||
- Use generic types for hooks
|
||||
- Type context providers properly
|
||||
- Use `React.FC` sparingly (prefer explicit typing)
|
||||
|
||||
### TanStack Ecosystem
|
||||
Type TanStack libraries properly:
|
||||
- TanStack Query: Type query keys and data
|
||||
- TanStack Router: Use typed route definitions
|
||||
- TanStack Form: Type form values and validation
|
||||
- TanStack Store: Type state and actions
|
||||
|
||||
### Zod Integration
|
||||
Combine Zod with TypeScript:
|
||||
- Use `z.infer<typeof schema>` to extract types from schemas
|
||||
- Let Zod handle runtime validation
|
||||
- Use TypeScript for compile-time type checking
|
||||
|
||||
## Resources
|
||||
|
||||
The TypeScript documentation provides comprehensive information:
|
||||
- Handbook: https://www.typescriptlang.org/docs/handbook/
|
||||
- Type manipulation: https://www.typescriptlang.org/docs/handbook/2/types-from-types.html
|
||||
- Utility types: https://www.typescriptlang.org/docs/handbook/utility-types.html
|
||||
- TSConfig reference: https://www.typescriptlang.org/tsconfig
|
||||
|
||||
45
.claude/skills/typescript/examples/README.md
Normal file
45
.claude/skills/typescript/examples/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# TypeScript Examples
|
||||
|
||||
This directory contains practical TypeScript examples demonstrating various patterns and features.
|
||||
|
||||
## Examples
|
||||
|
||||
1. **type-system-basics.ts** - Fundamental TypeScript types and features
|
||||
2. **advanced-types.ts** - Generics, conditional types, and mapped types
|
||||
3. **react-patterns.ts** - Type-safe React components and hooks
|
||||
4. **api-patterns.ts** - API response handling with type safety
|
||||
5. **validation.ts** - Runtime validation with Zod and TypeScript
|
||||
|
||||
## How to Use
|
||||
|
||||
Each example file is self-contained and demonstrates specific TypeScript concepts. They're based on real-world patterns used in the Plebeian Market application and follow best practices for:
|
||||
|
||||
- Type safety
|
||||
- Error handling
|
||||
- Code organization
|
||||
- Reusability
|
||||
- Maintainability
|
||||
|
||||
## Running Examples
|
||||
|
||||
These examples are TypeScript files that can be:
|
||||
- Copied into your project
|
||||
- Used as reference for patterns
|
||||
- Modified for your specific needs
|
||||
- Run with `ts-node` for testing
|
||||
|
||||
```bash
|
||||
# Run an example
|
||||
npx ts-node examples/type-system-basics.ts
|
||||
```
|
||||
|
||||
## Learning Path
|
||||
|
||||
1. Start with `type-system-basics.ts` to understand fundamentals
|
||||
2. Move to `advanced-types.ts` for complex type patterns
|
||||
3. Explore `react-patterns.ts` for component typing
|
||||
4. Study `api-patterns.ts` for type-safe API handling
|
||||
5. Review `validation.ts` for runtime safety
|
||||
|
||||
Each example builds on previous concepts, so following this order is recommended for learners.
|
||||
|
||||
478
.claude/skills/typescript/examples/advanced-types.ts
Normal file
478
.claude/skills/typescript/examples/advanced-types.ts
Normal file
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* Advanced TypeScript Types
|
||||
*
|
||||
* This file demonstrates advanced TypeScript features including:
|
||||
* - Generics with constraints
|
||||
* - Conditional types
|
||||
* - Mapped types
|
||||
* - Template literal types
|
||||
* - Recursive types
|
||||
* - Utility type implementations
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Generics Basics
|
||||
// ============================================================================
|
||||
|
||||
// Generic function
|
||||
function identity<T>(value: T): T {
|
||||
return value
|
||||
}
|
||||
|
||||
const stringValue = identity('hello') // Type: string
|
||||
const numberValue = identity(42) // Type: number
|
||||
|
||||
// Generic interface
|
||||
interface Box<T> {
|
||||
value: T
|
||||
}
|
||||
|
||||
const stringBox: Box<string> = { value: 'hello' }
|
||||
const numberBox: Box<number> = { value: 42 }
|
||||
|
||||
// Generic class
|
||||
class Stack<T> {
|
||||
private items: T[] = []
|
||||
|
||||
push(item: T): void {
|
||||
this.items.push(item)
|
||||
}
|
||||
|
||||
pop(): T | undefined {
|
||||
return this.items.pop()
|
||||
}
|
||||
|
||||
peek(): T | undefined {
|
||||
return this.items[this.items.length - 1]
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.items.length === 0
|
||||
}
|
||||
}
|
||||
|
||||
const numberStack = new Stack<number>()
|
||||
numberStack.push(1)
|
||||
numberStack.push(2)
|
||||
numberStack.pop() // Type: number | undefined
|
||||
|
||||
// ============================================================================
|
||||
// Generic Constraints
|
||||
// ============================================================================
|
||||
|
||||
// Constrain to specific type
|
||||
interface HasLength {
|
||||
length: number
|
||||
}
|
||||
|
||||
function logLength<T extends HasLength>(item: T): void {
|
||||
console.log(item.length)
|
||||
}
|
||||
|
||||
logLength('string') // OK
|
||||
logLength([1, 2, 3]) // OK
|
||||
logLength({ length: 10 }) // OK
|
||||
// logLength(42) // Error: number doesn't have length
|
||||
|
||||
// Constrain to object keys
|
||||
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
||||
return obj[key]
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
age: number
|
||||
}
|
||||
|
||||
const user: User = { id: '1', name: 'Alice', age: 30 }
|
||||
const userName = getProperty(user, 'name') // Type: string
|
||||
// const invalid = getProperty(user, 'invalid') // Error
|
||||
|
||||
// Multiple type parameters with constraints
|
||||
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
|
||||
return { ...obj1, ...obj2 }
|
||||
}
|
||||
|
||||
const merged = merge({ a: 1 }, { b: 2 }) // Type: { a: number } & { b: number }
|
||||
|
||||
// ============================================================================
|
||||
// Conditional Types
|
||||
// ============================================================================
|
||||
|
||||
// Basic conditional type
|
||||
type IsString<T> = T extends string ? true : false
|
||||
|
||||
type A = IsString<string> // true
|
||||
type B = IsString<number> // false
|
||||
|
||||
// Nested conditional types
|
||||
type TypeName<T> = T extends string
|
||||
? 'string'
|
||||
: T extends number
|
||||
? 'number'
|
||||
: T extends boolean
|
||||
? 'boolean'
|
||||
: T extends undefined
|
||||
? 'undefined'
|
||||
: T extends Function
|
||||
? 'function'
|
||||
: 'object'
|
||||
|
||||
type T1 = TypeName<string> // "string"
|
||||
type T2 = TypeName<number> // "number"
|
||||
type T3 = TypeName<() => void> // "function"
|
||||
|
||||
// Distributive conditional types
|
||||
type ToArray<T> = T extends any ? T[] : never
|
||||
|
||||
type StrArrOrNumArr = ToArray<string | number> // string[] | number[]
|
||||
|
||||
// infer keyword
|
||||
type Flatten<T> = T extends Array<infer U> ? U : T
|
||||
|
||||
type Str = Flatten<string[]> // string
|
||||
type Num = Flatten<number> // number
|
||||
|
||||
// Return type extraction
|
||||
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
|
||||
|
||||
function exampleFn(): string {
|
||||
return 'hello'
|
||||
}
|
||||
|
||||
type ExampleReturn = MyReturnType<typeof exampleFn> // string
|
||||
|
||||
// Parameters extraction
|
||||
type MyParameters<T> = T extends (...args: infer P) => any ? P : never
|
||||
|
||||
function createUser(name: string, age: number): User {
|
||||
return { id: '1', name, age }
|
||||
}
|
||||
|
||||
type CreateUserParams = MyParameters<typeof createUser> // [string, number]
|
||||
|
||||
// ============================================================================
|
||||
// Mapped Types
|
||||
// ============================================================================
|
||||
|
||||
// Make all properties optional
|
||||
type MyPartial<T> = {
|
||||
[K in keyof T]?: T[K]
|
||||
}
|
||||
|
||||
interface Person {
|
||||
name: string
|
||||
age: number
|
||||
email: string
|
||||
}
|
||||
|
||||
type PartialPerson = MyPartial<Person>
|
||||
// {
|
||||
// name?: string
|
||||
// age?: number
|
||||
// email?: string
|
||||
// }
|
||||
|
||||
// Make all properties required
|
||||
type MyRequired<T> = {
|
||||
[K in keyof T]-?: T[K]
|
||||
}
|
||||
|
||||
// Make all properties readonly
|
||||
type MyReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K]
|
||||
}
|
||||
|
||||
// Pick specific properties
|
||||
type MyPick<T, K extends keyof T> = {
|
||||
[P in K]: T[P]
|
||||
}
|
||||
|
||||
type UserProfile = MyPick<User, 'id' | 'name'>
|
||||
// { id: string; name: string }
|
||||
|
||||
// Omit specific properties
|
||||
type MyOmit<T, K extends keyof T> = {
|
||||
[P in keyof T as P extends K ? never : P]: T[P]
|
||||
}
|
||||
|
||||
type UserWithoutAge = MyOmit<User, 'age'>
|
||||
// { id: string; name: string }
|
||||
|
||||
// Transform property types
|
||||
type Nullable<T> = {
|
||||
[K in keyof T]: T[K] | null
|
||||
}
|
||||
|
||||
type NullablePerson = Nullable<Person>
|
||||
// {
|
||||
// name: string | null
|
||||
// age: number | null
|
||||
// email: string | null
|
||||
// }
|
||||
|
||||
// ============================================================================
|
||||
// Key Remapping
|
||||
// ============================================================================
|
||||
|
||||
// Add prefix to keys
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
|
||||
}
|
||||
|
||||
type PersonGetters = Getters<Person>
|
||||
// {
|
||||
// getName: () => string
|
||||
// getAge: () => number
|
||||
// getEmail: () => string
|
||||
// }
|
||||
|
||||
// Filter keys by type
|
||||
type PickByType<T, U> = {
|
||||
[K in keyof T as T[K] extends U ? K : never]: T[K]
|
||||
}
|
||||
|
||||
interface Model {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
price: number
|
||||
}
|
||||
|
||||
type StringFields = PickByType<Model, string>
|
||||
// { name: string; description: string }
|
||||
|
||||
// Remove specific key
|
||||
type RemoveKindField<T> = {
|
||||
[K in keyof T as Exclude<K, 'kind'>]: T[K]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Template Literal Types
|
||||
// ============================================================================
|
||||
|
||||
// Event name generation
|
||||
type EventName<T extends string> = `on${Capitalize<T>}`
|
||||
|
||||
type ClickEvent = EventName<'click'> // "onClick"
|
||||
type SubmitEvent = EventName<'submit'> // "onSubmit"
|
||||
|
||||
// Combining literals
|
||||
type Color = 'red' | 'green' | 'blue'
|
||||
type Shade = 'light' | 'dark'
|
||||
type ColorShade = `${Shade}-${Color}`
|
||||
// "light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue"
|
||||
|
||||
// CSS properties
|
||||
type CSSProperty = 'margin' | 'padding'
|
||||
type Side = 'top' | 'right' | 'bottom' | 'left'
|
||||
type CSSPropertyWithSide = `${CSSProperty}-${Side}`
|
||||
// "margin-top" | "margin-right" | ... | "padding-left"
|
||||
|
||||
// Route generation
|
||||
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
type Endpoint = '/users' | '/products' | '/orders'
|
||||
type ApiRoute = `${HttpMethod} ${Endpoint}`
|
||||
// "GET /users" | "POST /users" | ... | "DELETE /orders"
|
||||
|
||||
// ============================================================================
|
||||
// Recursive Types
|
||||
// ============================================================================
|
||||
|
||||
// JSON value type
|
||||
type JSONValue = string | number | boolean | null | JSONObject | JSONArray
|
||||
|
||||
interface JSONObject {
|
||||
[key: string]: JSONValue
|
||||
}
|
||||
|
||||
interface JSONArray extends Array<JSONValue> {}
|
||||
|
||||
// Tree structure
|
||||
interface TreeNode<T> {
|
||||
value: T
|
||||
children?: TreeNode<T>[]
|
||||
}
|
||||
|
||||
const tree: TreeNode<number> = {
|
||||
value: 1,
|
||||
children: [
|
||||
{ value: 2, children: [{ value: 4 }, { value: 5 }] },
|
||||
{ value: 3, children: [{ value: 6 }] },
|
||||
],
|
||||
}
|
||||
|
||||
// Deep readonly
|
||||
type DeepReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
|
||||
}
|
||||
|
||||
interface NestedConfig {
|
||||
api: {
|
||||
url: string
|
||||
timeout: number
|
||||
}
|
||||
features: {
|
||||
darkMode: boolean
|
||||
}
|
||||
}
|
||||
|
||||
type ImmutableConfig = DeepReadonly<NestedConfig>
|
||||
// All properties at all levels are readonly
|
||||
|
||||
// Deep partial
|
||||
type DeepPartial<T> = {
|
||||
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Advanced Utility Types
|
||||
// ============================================================================
|
||||
|
||||
// Exclude types from union
|
||||
type MyExclude<T, U> = T extends U ? never : T
|
||||
|
||||
type T4 = MyExclude<'a' | 'b' | 'c', 'a'> // "b" | "c"
|
||||
|
||||
// Extract types from union
|
||||
type MyExtract<T, U> = T extends U ? T : never
|
||||
|
||||
type T5 = MyExtract<'a' | 'b' | 'c', 'a' | 'f'> // "a"
|
||||
|
||||
// NonNullable
|
||||
type MyNonNullable<T> = T extends null | undefined ? never : T
|
||||
|
||||
type T6 = MyNonNullable<string | null | undefined> // string
|
||||
|
||||
// Record
|
||||
type MyRecord<K extends keyof any, T> = {
|
||||
[P in K]: T
|
||||
}
|
||||
|
||||
type PageInfo = MyRecord<string, number>
|
||||
|
||||
// Awaited
|
||||
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T
|
||||
|
||||
type T7 = MyAwaited<Promise<string>> // string
|
||||
type T8 = MyAwaited<Promise<Promise<number>>> // number
|
||||
|
||||
// ============================================================================
|
||||
// Branded Types
|
||||
// ============================================================================
|
||||
|
||||
type Brand<K, T> = K & { __brand: T }
|
||||
|
||||
type USD = Brand<number, 'USD'>
|
||||
type EUR = Brand<number, 'EUR'>
|
||||
type UserId = Brand<string, 'UserId'>
|
||||
type ProductId = Brand<string, 'ProductId'>
|
||||
|
||||
function makeUSD(amount: number): USD {
|
||||
return amount as USD
|
||||
}
|
||||
|
||||
function makeUserId(id: string): UserId {
|
||||
return id as UserId
|
||||
}
|
||||
|
||||
const usd = makeUSD(100)
|
||||
const userId = makeUserId('user-123')
|
||||
|
||||
// Type-safe operations
|
||||
function addMoney(a: USD, b: USD): USD {
|
||||
return (a + b) as USD
|
||||
}
|
||||
|
||||
// Prevents mixing different branded types
|
||||
// const total = addMoney(usd, eur) // Error
|
||||
|
||||
// ============================================================================
|
||||
// Union to Intersection
|
||||
// ============================================================================
|
||||
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
||||
k: infer I,
|
||||
) => void
|
||||
? I
|
||||
: never
|
||||
|
||||
type Union = { a: string } | { b: number }
|
||||
type Intersection = UnionToIntersection<Union>
|
||||
// { a: string } & { b: number }
|
||||
|
||||
// ============================================================================
|
||||
// Advanced Generic Patterns
|
||||
// ============================================================================
|
||||
|
||||
// Constraining multiple related types
|
||||
function merge<
|
||||
T extends Record<string, any>,
|
||||
U extends Record<string, any>,
|
||||
K extends keyof T & keyof U,
|
||||
>(obj1: T, obj2: U, conflictKeys: K[]): T & U {
|
||||
const result = { ...obj1, ...obj2 }
|
||||
conflictKeys.forEach((key) => {
|
||||
// Handle conflicts
|
||||
})
|
||||
return result as T & U
|
||||
}
|
||||
|
||||
// Builder pattern with fluent API
|
||||
class QueryBuilder<T, Selected extends keyof T = never> {
|
||||
private selectFields: Set<keyof T> = new Set()
|
||||
|
||||
select<K extends keyof T>(
|
||||
...fields: K[]
|
||||
): QueryBuilder<T, Selected | K> {
|
||||
fields.forEach((field) => this.selectFields.add(field))
|
||||
return this as any
|
||||
}
|
||||
|
||||
execute(): Pick<T, Selected> {
|
||||
// Execute query
|
||||
return {} as Pick<T, Selected>
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
description: string
|
||||
}
|
||||
|
||||
const result = new QueryBuilder<Product>()
|
||||
.select('id', 'name')
|
||||
.select('price')
|
||||
.execute()
|
||||
// Type: { id: string; name: string; price: number }
|
||||
|
||||
// ============================================================================
|
||||
// Exports
|
||||
// ============================================================================
|
||||
|
||||
export type {
|
||||
Box,
|
||||
HasLength,
|
||||
IsString,
|
||||
Flatten,
|
||||
MyPartial,
|
||||
MyRequired,
|
||||
MyReadonly,
|
||||
Nullable,
|
||||
DeepReadonly,
|
||||
DeepPartial,
|
||||
Brand,
|
||||
USD,
|
||||
EUR,
|
||||
UserId,
|
||||
ProductId,
|
||||
JSONValue,
|
||||
TreeNode,
|
||||
}
|
||||
|
||||
export { Stack, identity, getProperty, merge, makeUSD, makeUserId }
|
||||
|
||||
555
.claude/skills/typescript/examples/react-patterns.ts
Normal file
555
.claude/skills/typescript/examples/react-patterns.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
/**
|
||||
* TypeScript React Patterns
|
||||
*
|
||||
* This file demonstrates type-safe React patterns including:
|
||||
* - Component props typing
|
||||
* - Hooks with TypeScript
|
||||
* - Context with type safety
|
||||
* - Generic components
|
||||
* - Event handlers
|
||||
* - Ref types
|
||||
*/
|
||||
|
||||
import { createContext, useContext, useEffect, useReducer, useRef, useState } from 'react'
|
||||
import type { ReactNode, InputHTMLAttributes, FormEvent, ChangeEvent } from 'react'
|
||||
|
||||
// ============================================================================
|
||||
// Component Props Patterns
|
||||
// ============================================================================
|
||||
|
||||
// Basic component with props
|
||||
interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary' | 'tertiary'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
disabled?: boolean
|
||||
onClick?: () => void
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function Button({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
onClick,
|
||||
children,
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={`btn-${variant} btn-${size}`}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// Props extending HTML attributes
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string
|
||||
error?: string
|
||||
helperText?: string
|
||||
}
|
||||
|
||||
export function Input({ label, error, helperText, ...inputProps }: InputProps) {
|
||||
return (
|
||||
<div className="input-wrapper">
|
||||
{label && <label>{label}</label>}
|
||||
<input className={error ? 'input-error' : ''} {...inputProps} />
|
||||
{error && <span className="error">{error}</span>}
|
||||
{helperText && <span className="helper">{helperText}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Generic component
|
||||
interface ListProps<T> {
|
||||
items: T[]
|
||||
renderItem: (item: T, index: number) => ReactNode
|
||||
keyExtractor: (item: T, index: number) => string
|
||||
emptyMessage?: string
|
||||
}
|
||||
|
||||
export function List<T>({
|
||||
items,
|
||||
renderItem,
|
||||
keyExtractor,
|
||||
emptyMessage = 'No items',
|
||||
}: ListProps<T>) {
|
||||
if (items.length === 0) {
|
||||
return <div>{emptyMessage}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={keyExtractor(item, index)}>{renderItem(item, index)}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
// Component with children render prop
|
||||
interface ContainerProps {
|
||||
isLoading: boolean
|
||||
error: Error | null
|
||||
children: (props: { retry: () => void }) => ReactNode
|
||||
}
|
||||
|
||||
export function Container({ isLoading, error, children }: ContainerProps) {
|
||||
const retry = () => {
|
||||
// Retry logic
|
||||
}
|
||||
|
||||
if (isLoading) return <div>Loading...</div>
|
||||
if (error) return <div>Error: {error.message}</div>
|
||||
|
||||
return <>{children({ retry })}</>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hooks Patterns
|
||||
// ============================================================================
|
||||
|
||||
// useState with explicit type
|
||||
function useCounter(initialValue: number = 0) {
|
||||
const [count, setCount] = useState<number>(initialValue)
|
||||
|
||||
const increment = () => setCount((c) => c + 1)
|
||||
const decrement = () => setCount((c) => c - 1)
|
||||
const reset = () => setCount(initialValue)
|
||||
|
||||
return { count, increment, decrement, reset }
|
||||
}
|
||||
|
||||
// useState with union type
|
||||
type LoadingState = 'idle' | 'loading' | 'success' | 'error'
|
||||
|
||||
function useLoadingState() {
|
||||
const [state, setState] = useState<LoadingState>('idle')
|
||||
|
||||
const startLoading = () => setState('loading')
|
||||
const setSuccess = () => setState('success')
|
||||
const setError = () => setState('error')
|
||||
const reset = () => setState('idle')
|
||||
|
||||
return { state, startLoading, setSuccess, setError, reset }
|
||||
}
|
||||
|
||||
// Custom hook with options
|
||||
interface UseFetchOptions<T> {
|
||||
initialData?: T
|
||||
onSuccess?: (data: T) => void
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
interface UseFetchReturn<T> {
|
||||
data: T | undefined
|
||||
loading: boolean
|
||||
error: Error | null
|
||||
refetch: () => Promise<void>
|
||||
}
|
||||
|
||||
function useFetch<T>(url: string, options?: UseFetchOptions<T>): UseFetchReturn<T> {
|
||||
const [data, setData] = useState<T | undefined>(options?.initialData)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
const json = await response.json()
|
||||
setData(json)
|
||||
options?.onSuccess?.(json)
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err))
|
||||
setError(error)
|
||||
options?.onError?.(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [url])
|
||||
|
||||
return { data, loading, error, refetch: fetchData }
|
||||
}
|
||||
|
||||
// useReducer with discriminated unions
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
type FetchState<T> =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: T }
|
||||
| { status: 'error'; error: Error }
|
||||
|
||||
type FetchAction<T> =
|
||||
| { type: 'FETCH_START' }
|
||||
| { type: 'FETCH_SUCCESS'; payload: T }
|
||||
| { type: 'FETCH_ERROR'; error: Error }
|
||||
| { type: 'RESET' }
|
||||
|
||||
function fetchReducer<T>(state: FetchState<T>, action: FetchAction<T>): FetchState<T> {
|
||||
switch (action.type) {
|
||||
case 'FETCH_START':
|
||||
return { status: 'loading' }
|
||||
case 'FETCH_SUCCESS':
|
||||
return { status: 'success', data: action.payload }
|
||||
case 'FETCH_ERROR':
|
||||
return { status: 'error', error: action.error }
|
||||
case 'RESET':
|
||||
return { status: 'idle' }
|
||||
}
|
||||
}
|
||||
|
||||
function useFetchWithReducer<T>(url: string) {
|
||||
const [state, dispatch] = useReducer(fetchReducer<T>, { status: 'idle' })
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false
|
||||
|
||||
const fetchData = async () => {
|
||||
dispatch({ type: 'FETCH_START' })
|
||||
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
|
||||
if (!isCancelled) {
|
||||
dispatch({ type: 'FETCH_SUCCESS', payload: data })
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isCancelled) {
|
||||
dispatch({
|
||||
type: 'FETCH_ERROR',
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
|
||||
return () => {
|
||||
isCancelled = true
|
||||
}
|
||||
}, [url])
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Context Patterns
|
||||
// ============================================================================
|
||||
|
||||
// Type-safe context
|
||||
interface AuthContextType {
|
||||
user: User | null
|
||||
isAuthenticated: boolean
|
||||
login: (email: string, password: string) => Promise<void>
|
||||
logout: () => void
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
// Login logic
|
||||
const userData = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password }),
|
||||
}).then((r) => r.json())
|
||||
|
||||
setUser(userData)
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
setUser(null)
|
||||
}
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
isAuthenticated: user !== null,
|
||||
login,
|
||||
logout,
|
||||
}
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||
}
|
||||
|
||||
// Custom hook with error handling
|
||||
export function useAuth(): AuthContextType {
|
||||
const context = useContext(AuthContext)
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within AuthProvider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Event Handler Patterns
|
||||
// ============================================================================
|
||||
|
||||
interface FormData {
|
||||
name: string
|
||||
email: string
|
||||
message: string
|
||||
}
|
||||
|
||||
function ContactForm() {
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: '',
|
||||
email: '',
|
||||
message: '',
|
||||
})
|
||||
|
||||
// Type-safe change handler
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}))
|
||||
}
|
||||
|
||||
// Type-safe submit handler
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
console.log('Submitting:', formData)
|
||||
}
|
||||
|
||||
// Specific field handler
|
||||
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder="Name"
|
||||
/>
|
||||
<input
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
placeholder="Email"
|
||||
/>
|
||||
<textarea
|
||||
name="message"
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
placeholder="Message"
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Ref Patterns
|
||||
// ============================================================================
|
||||
|
||||
function FocusInput() {
|
||||
// useRef with DOM element
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const focusInput = () => {
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input ref={inputRef} />
|
||||
<button onClick={focusInput}>Focus Input</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Timer() {
|
||||
// useRef for mutable value
|
||||
const countRef = useRef<number>(0)
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const startTimer = () => {
|
||||
intervalRef.current = setInterval(() => {
|
||||
countRef.current += 1
|
||||
console.log(countRef.current)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const stopTimer = () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={startTimer}>Start</button>
|
||||
<button onClick={stopTimer}>Stop</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Generic Component Patterns
|
||||
// ============================================================================
|
||||
|
||||
// Select component with generic options
|
||||
interface SelectProps<T> {
|
||||
options: T[]
|
||||
value: T
|
||||
onChange: (value: T) => void
|
||||
getLabel: (option: T) => string
|
||||
getValue: (option: T) => string
|
||||
}
|
||||
|
||||
export function Select<T>({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
getLabel,
|
||||
getValue,
|
||||
}: SelectProps<T>) {
|
||||
return (
|
||||
<select
|
||||
value={getValue(value)}
|
||||
onChange={(e) => {
|
||||
const selectedValue = e.target.value
|
||||
const option = options.find((opt) => getValue(opt) === selectedValue)
|
||||
if (option) {
|
||||
onChange(option)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={getValue(option)} value={getValue(option)}>
|
||||
{getLabel(option)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
// Data table component
|
||||
interface Column<T> {
|
||||
key: keyof T
|
||||
header: string
|
||||
render?: (value: T[keyof T], row: T) => ReactNode
|
||||
}
|
||||
|
||||
interface TableProps<T> {
|
||||
data: T[]
|
||||
columns: Column<T>[]
|
||||
keyExtractor: (row: T) => string
|
||||
}
|
||||
|
||||
export function Table<T>({ data, columns, keyExtractor }: TableProps<T>) {
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((col) => (
|
||||
<th key={String(col.key)}>{col.header}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row) => (
|
||||
<tr key={keyExtractor(row)}>
|
||||
{columns.map((col) => (
|
||||
<td key={String(col.key)}>
|
||||
{col.render ? col.render(row[col.key], row) : String(row[col.key])}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Higher-Order Component Pattern
|
||||
// ============================================================================
|
||||
|
||||
interface WithLoadingProps {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
function withLoading<P extends object>(
|
||||
Component: React.ComponentType<P>,
|
||||
): React.FC<P & WithLoadingProps> {
|
||||
return ({ isLoading, ...props }: WithLoadingProps & P) => {
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
return <Component {...(props as P)} />
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
interface UserListProps {
|
||||
users: User[]
|
||||
}
|
||||
|
||||
const UserList: React.FC<UserListProps> = ({ users }) => (
|
||||
<ul>
|
||||
{users.map((user) => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
|
||||
const UserListWithLoading = withLoading(UserList)
|
||||
|
||||
// ============================================================================
|
||||
// Exports
|
||||
// ============================================================================
|
||||
|
||||
export {
|
||||
useCounter,
|
||||
useLoadingState,
|
||||
useFetch,
|
||||
useFetchWithReducer,
|
||||
ContactForm,
|
||||
FocusInput,
|
||||
Timer,
|
||||
}
|
||||
|
||||
export type {
|
||||
ButtonProps,
|
||||
InputProps,
|
||||
ListProps,
|
||||
UseFetchOptions,
|
||||
UseFetchReturn,
|
||||
FetchState,
|
||||
FetchAction,
|
||||
AuthContextType,
|
||||
SelectProps,
|
||||
Column,
|
||||
TableProps,
|
||||
}
|
||||
|
||||
361
.claude/skills/typescript/examples/type-system-basics.ts
Normal file
361
.claude/skills/typescript/examples/type-system-basics.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* TypeScript Type System Basics
|
||||
*
|
||||
* This file demonstrates fundamental TypeScript concepts including:
|
||||
* - Primitive types
|
||||
* - Object types (interfaces, type aliases)
|
||||
* - Union and intersection types
|
||||
* - Type inference and narrowing
|
||||
* - Function types
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Primitive Types
|
||||
// ============================================================================
|
||||
|
||||
const message: string = 'Hello, TypeScript!'
|
||||
const count: number = 42
|
||||
const isActive: boolean = true
|
||||
const nothing: null = null
|
||||
const notDefined: undefined = undefined
|
||||
|
||||
// ============================================================================
|
||||
// Object Types
|
||||
// ============================================================================
|
||||
|
||||
// Interface definition
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
age?: number // Optional property
|
||||
readonly createdAt: Date // Readonly property
|
||||
}
|
||||
|
||||
// Type alias definition
|
||||
type Product = {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
category: string
|
||||
}
|
||||
|
||||
// Creating objects
|
||||
const user: User = {
|
||||
id: '1',
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
createdAt: new Date(),
|
||||
}
|
||||
|
||||
const product: Product = {
|
||||
id: 'p1',
|
||||
name: 'Laptop',
|
||||
price: 999,
|
||||
category: 'electronics',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Union Types
|
||||
// ============================================================================
|
||||
|
||||
type Status = 'idle' | 'loading' | 'success' | 'error'
|
||||
type ID = string | number
|
||||
|
||||
function formatId(id: ID): string {
|
||||
if (typeof id === 'string') {
|
||||
return id.toUpperCase()
|
||||
}
|
||||
return id.toString()
|
||||
}
|
||||
|
||||
// Discriminated unions
|
||||
type ApiResponse =
|
||||
| { success: true; data: User }
|
||||
| { success: false; error: string }
|
||||
|
||||
function handleResponse(response: ApiResponse) {
|
||||
if (response.success) {
|
||||
// TypeScript knows response.data exists here
|
||||
console.log(response.data.name)
|
||||
} else {
|
||||
// TypeScript knows response.error exists here
|
||||
console.error(response.error)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Intersection Types
|
||||
// ============================================================================
|
||||
|
||||
type Timestamped = {
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
type TimestampedUser = User & Timestamped
|
||||
|
||||
const timestampedUser: TimestampedUser = {
|
||||
id: '1',
|
||||
name: 'Bob',
|
||||
email: 'bob@example.com',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Array Types
|
||||
// ============================================================================
|
||||
|
||||
const numbers: number[] = [1, 2, 3, 4, 5]
|
||||
const strings: Array<string> = ['a', 'b', 'c']
|
||||
const users: User[] = [user, timestampedUser]
|
||||
|
||||
// Readonly arrays
|
||||
const immutableNumbers: readonly number[] = [1, 2, 3]
|
||||
// immutableNumbers.push(4) // Error: push does not exist on readonly array
|
||||
|
||||
// ============================================================================
|
||||
// Tuple Types
|
||||
// ============================================================================
|
||||
|
||||
type Point = [number, number]
|
||||
type NamedPoint = [x: number, y: number, z?: number]
|
||||
|
||||
const point: Point = [10, 20]
|
||||
const namedPoint: NamedPoint = [10, 20, 30]
|
||||
|
||||
// ============================================================================
|
||||
// Function Types
|
||||
// ============================================================================
|
||||
|
||||
// Function declaration
|
||||
function add(a: number, b: number): number {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// Arrow function
|
||||
const subtract = (a: number, b: number): number => a - b
|
||||
|
||||
// Function type alias
|
||||
type MathOperation = (a: number, b: number) => number
|
||||
|
||||
const multiply: MathOperation = (a, b) => a * b
|
||||
|
||||
// Optional parameters
|
||||
function greet(name: string, greeting?: string): string {
|
||||
return `${greeting ?? 'Hello'}, ${name}!`
|
||||
}
|
||||
|
||||
// Default parameters
|
||||
function createUser(name: string, role: string = 'user'): User {
|
||||
return {
|
||||
id: Math.random().toString(),
|
||||
name,
|
||||
email: `${name.toLowerCase()}@example.com`,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
// Rest parameters
|
||||
function sum(...numbers: number[]): number {
|
||||
return numbers.reduce((acc, n) => acc + n, 0)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Type Inference
|
||||
// ============================================================================
|
||||
|
||||
// Type is inferred as string
|
||||
let inferredString = 'hello'
|
||||
|
||||
// Type is inferred as number
|
||||
let inferredNumber = 42
|
||||
|
||||
// Type is inferred as { name: string; age: number }
|
||||
let inferredObject = {
|
||||
name: 'Alice',
|
||||
age: 30,
|
||||
}
|
||||
|
||||
// Return type is inferred as number
|
||||
function inferredReturn(a: number, b: number) {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Type Narrowing
|
||||
// ============================================================================
|
||||
|
||||
// typeof guard
|
||||
function processValue(value: string | number) {
|
||||
if (typeof value === 'string') {
|
||||
// value is string here
|
||||
return value.toUpperCase()
|
||||
}
|
||||
// value is number here
|
||||
return value.toFixed(2)
|
||||
}
|
||||
|
||||
// Truthiness narrowing
|
||||
function printName(name: string | null | undefined) {
|
||||
if (name) {
|
||||
// name is string here
|
||||
console.log(name.toUpperCase())
|
||||
}
|
||||
}
|
||||
|
||||
// Equality narrowing
|
||||
function example(x: string | number, y: string | boolean) {
|
||||
if (x === y) {
|
||||
// x and y are both string here
|
||||
console.log(x.toUpperCase(), y.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
// in operator narrowing
|
||||
type Fish = { swim: () => void }
|
||||
type Bird = { fly: () => void }
|
||||
|
||||
function move(animal: Fish | Bird) {
|
||||
if ('swim' in animal) {
|
||||
// animal is Fish here
|
||||
animal.swim()
|
||||
} else {
|
||||
// animal is Bird here
|
||||
animal.fly()
|
||||
}
|
||||
}
|
||||
|
||||
// instanceof narrowing
|
||||
function processError(error: Error | string) {
|
||||
if (error instanceof Error) {
|
||||
// error is Error here
|
||||
console.error(error.message)
|
||||
} else {
|
||||
// error is string here
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Type Predicates (Custom Type Guards)
|
||||
// ============================================================================
|
||||
|
||||
function isUser(value: unknown): value is User {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'id' in value &&
|
||||
'name' in value &&
|
||||
'email' in value
|
||||
)
|
||||
}
|
||||
|
||||
function processData(data: unknown) {
|
||||
if (isUser(data)) {
|
||||
// data is User here
|
||||
console.log(data.name)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Const Assertions
|
||||
// ============================================================================
|
||||
|
||||
// Without const assertion
|
||||
const mutableConfig = {
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
}
|
||||
// mutableConfig.host = 'example.com' // OK
|
||||
|
||||
// With const assertion
|
||||
const immutableConfig = {
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
} as const
|
||||
// immutableConfig.host = 'example.com' // Error: cannot assign to readonly property
|
||||
|
||||
// Array with const assertion
|
||||
const directions = ['north', 'south', 'east', 'west'] as const
|
||||
// Type: readonly ["north", "south", "east", "west"]
|
||||
|
||||
// ============================================================================
|
||||
// Literal Types
|
||||
// ============================================================================
|
||||
|
||||
type Direction = 'north' | 'south' | 'east' | 'west'
|
||||
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6
|
||||
|
||||
function move(direction: Direction, steps: number) {
|
||||
console.log(`Moving ${direction} by ${steps} steps`)
|
||||
}
|
||||
|
||||
move('north', 10) // OK
|
||||
// move('up', 10) // Error: "up" is not assignable to Direction
|
||||
|
||||
// ============================================================================
|
||||
// Index Signatures
|
||||
// ============================================================================
|
||||
|
||||
interface StringMap {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
const translations: StringMap = {
|
||||
hello: 'Hola',
|
||||
goodbye: 'Adiós',
|
||||
thanks: 'Gracias',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Functions
|
||||
// ============================================================================
|
||||
|
||||
// Type-safe object keys
|
||||
function getObjectKeys<T extends object>(obj: T): Array<keyof T> {
|
||||
return Object.keys(obj) as Array<keyof T>
|
||||
}
|
||||
|
||||
// Type-safe property access
|
||||
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
||||
return obj[key]
|
||||
}
|
||||
|
||||
const userName = getProperty(user, 'name') // Type: string
|
||||
const userAge = getProperty(user, 'age') // Type: number | undefined
|
||||
|
||||
// ============================================================================
|
||||
// Named Return Values (Go-style)
|
||||
// ============================================================================
|
||||
|
||||
function parseJSON(json: string): { data: unknown | null; err: Error | null } {
|
||||
let data: unknown | null = null
|
||||
let err: Error | null = null
|
||||
|
||||
try {
|
||||
data = JSON.parse(json)
|
||||
} catch (error) {
|
||||
err = error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
return { data, err }
|
||||
}
|
||||
|
||||
// Usage
|
||||
const { data, err } = parseJSON('{"name": "Alice"}')
|
||||
if (err) {
|
||||
console.error('Failed to parse JSON:', err.message)
|
||||
} else {
|
||||
console.log('Parsed data:', data)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Exports
|
||||
// ============================================================================
|
||||
|
||||
export type { User, Product, Status, ID, ApiResponse, TimestampedUser }
|
||||
export { formatId, handleResponse, processValue, isUser, getProperty, parseJSON }
|
||||
|
||||
395
.claude/skills/typescript/quick-reference.md
Normal file
395
.claude/skills/typescript/quick-reference.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# TypeScript Quick Reference
|
||||
|
||||
Quick lookup guide for common TypeScript patterns and syntax.
|
||||
|
||||
## Basic Types
|
||||
|
||||
```typescript
|
||||
// Primitives
|
||||
string, number, boolean, null, undefined, symbol, bigint
|
||||
|
||||
// Special types
|
||||
any // Avoid - disables type checking
|
||||
unknown // Type-safe alternative to any
|
||||
void // No return value
|
||||
never // Never returns
|
||||
|
||||
// Arrays
|
||||
number[]
|
||||
Array<string>
|
||||
readonly number[]
|
||||
|
||||
// Tuples
|
||||
[string, number]
|
||||
[x: number, y: number]
|
||||
|
||||
// Objects
|
||||
{ name: string; age: number }
|
||||
Record<string, number>
|
||||
```
|
||||
|
||||
## Type Declarations
|
||||
|
||||
```typescript
|
||||
// Interface
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
age?: number // Optional
|
||||
readonly createdAt: Date // Readonly
|
||||
}
|
||||
|
||||
// Type alias
|
||||
type Status = 'idle' | 'loading' | 'success' | 'error'
|
||||
type ID = string | number
|
||||
type Point = { x: number; y: number }
|
||||
|
||||
// Function type
|
||||
type Callback = (data: string) => void
|
||||
type MathOp = (a: number, b: number) => number
|
||||
```
|
||||
|
||||
## Union & Intersection
|
||||
|
||||
```typescript
|
||||
// Union (OR)
|
||||
string | number
|
||||
type Result = Success | Error
|
||||
|
||||
// Intersection (AND)
|
||||
A & B
|
||||
type Combined = User & Timestamped
|
||||
|
||||
// Discriminated union
|
||||
type State =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: Data }
|
||||
| { status: 'error'; error: Error }
|
||||
```
|
||||
|
||||
## Generics
|
||||
|
||||
```typescript
|
||||
// Generic function
|
||||
function identity<T>(value: T): T
|
||||
|
||||
// Generic interface
|
||||
interface Box<T> { value: T }
|
||||
|
||||
// Generic with constraint
|
||||
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]
|
||||
|
||||
// Multiple type parameters
|
||||
function merge<T, U>(a: T, b: U): T & U
|
||||
|
||||
// Default type parameter
|
||||
interface Response<T = unknown> { data: T }
|
||||
```
|
||||
|
||||
## Utility Types
|
||||
|
||||
```typescript
|
||||
Partial<T> // Make all optional
|
||||
Required<T> // Make all required
|
||||
Readonly<T> // Make all readonly
|
||||
Pick<T, K> // Select properties
|
||||
Omit<T, K> // Exclude properties
|
||||
Record<K, T> // Object with specific keys
|
||||
Exclude<T, U> // Remove from union
|
||||
Extract<T, U> // Extract from union
|
||||
NonNullable<T> // Remove null/undefined
|
||||
ReturnType<T> // Get function return type
|
||||
Parameters<T> // Get function parameters
|
||||
Awaited<T> // Unwrap Promise
|
||||
```
|
||||
|
||||
## Type Guards
|
||||
|
||||
```typescript
|
||||
// typeof
|
||||
if (typeof value === 'string') { }
|
||||
|
||||
// instanceof
|
||||
if (error instanceof Error) { }
|
||||
|
||||
// in operator
|
||||
if ('property' in object) { }
|
||||
|
||||
// Custom type guard
|
||||
function isUser(value: unknown): value is User {
|
||||
return typeof value === 'object' && value !== null && 'id' in value
|
||||
}
|
||||
|
||||
// Assertion function
|
||||
function assertIsString(value: unknown): asserts value is string {
|
||||
if (typeof value !== 'string') throw new Error()
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Types
|
||||
|
||||
```typescript
|
||||
// Conditional types
|
||||
type IsString<T> = T extends string ? true : false
|
||||
|
||||
// Mapped types
|
||||
type Nullable<T> = { [K in keyof T]: T[K] | null }
|
||||
|
||||
// Template literal types
|
||||
type EventName<T extends string> = `on${Capitalize<T>}`
|
||||
|
||||
// Key remapping
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
|
||||
}
|
||||
|
||||
// infer keyword
|
||||
type Flatten<T> = T extends Array<infer U> ? U : T
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
```typescript
|
||||
// Function declaration
|
||||
function add(a: number, b: number): number { return a + b }
|
||||
|
||||
// Arrow function
|
||||
const subtract = (a: number, b: number): number => a - b
|
||||
|
||||
// Optional parameters
|
||||
function greet(name: string, greeting?: string): string { }
|
||||
|
||||
// Default parameters
|
||||
function create(name: string, role = 'user'): User { }
|
||||
|
||||
// Rest parameters
|
||||
function sum(...numbers: number[]): number { }
|
||||
|
||||
// Overloads
|
||||
function format(value: string): string
|
||||
function format(value: number): string
|
||||
function format(value: string | number): string { }
|
||||
```
|
||||
|
||||
## Classes
|
||||
|
||||
```typescript
|
||||
class User {
|
||||
// Properties
|
||||
private id: string
|
||||
public name: string
|
||||
protected age: number
|
||||
readonly createdAt: Date
|
||||
|
||||
// Constructor
|
||||
constructor(name: string) {
|
||||
this.name = name
|
||||
this.createdAt = new Date()
|
||||
}
|
||||
|
||||
// Methods
|
||||
greet(): string {
|
||||
return `Hello, ${this.name}`
|
||||
}
|
||||
|
||||
// Static
|
||||
static create(name: string): User {
|
||||
return new User(name)
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
get displayName(): string {
|
||||
return this.name.toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
// Inheritance
|
||||
class Admin extends User {
|
||||
constructor(name: string, public permissions: string[]) {
|
||||
super(name)
|
||||
}
|
||||
}
|
||||
|
||||
// Abstract class
|
||||
abstract class Animal {
|
||||
abstract makeSound(): void
|
||||
}
|
||||
```
|
||||
|
||||
## React Patterns
|
||||
|
||||
```typescript
|
||||
// Component props
|
||||
interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary'
|
||||
onClick?: () => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Button({ variant = 'primary', onClick, children }: ButtonProps) { }
|
||||
|
||||
// Generic component
|
||||
interface ListProps<T> {
|
||||
items: T[]
|
||||
renderItem: (item: T) => React.ReactNode
|
||||
}
|
||||
|
||||
export function List<T>({ items, renderItem }: ListProps<T>) { }
|
||||
|
||||
// Hooks
|
||||
const [state, setState] = useState<string>('')
|
||||
const [data, setData] = useState<User | null>(null)
|
||||
|
||||
// Context
|
||||
interface AuthContextType {
|
||||
user: User | null
|
||||
login: () => Promise<void>
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
export function useAuth(): AuthContextType {
|
||||
const context = useContext(AuthContext)
|
||||
if (!context) throw new Error('useAuth must be used within AuthProvider')
|
||||
return context
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Result Type
|
||||
```typescript
|
||||
type Result<T, E = Error> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: E }
|
||||
```
|
||||
|
||||
### Option Type
|
||||
```typescript
|
||||
type Option<T> = Some<T> | None
|
||||
interface Some<T> { _tag: 'Some'; value: T }
|
||||
interface None { _tag: 'None' }
|
||||
```
|
||||
|
||||
### Branded Types
|
||||
```typescript
|
||||
type Brand<K, T> = K & { __brand: T }
|
||||
type UserId = Brand<string, 'UserId'>
|
||||
```
|
||||
|
||||
### Named Returns (Go-style)
|
||||
```typescript
|
||||
function parseJSON(json: string): { data: unknown | null; err: Error | null } {
|
||||
let data: unknown | null = null
|
||||
let err: Error | null = null
|
||||
|
||||
try {
|
||||
data = JSON.parse(json)
|
||||
} catch (error) {
|
||||
err = error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
return { data, err }
|
||||
}
|
||||
```
|
||||
|
||||
## Type Assertions
|
||||
|
||||
```typescript
|
||||
// as syntax (preferred)
|
||||
const value = input as string
|
||||
|
||||
// Angle bracket syntax (not in JSX)
|
||||
const value = <string>input
|
||||
|
||||
// as const
|
||||
const config = { host: 'localhost' } as const
|
||||
|
||||
// Non-null assertion (use sparingly)
|
||||
const element = document.getElementById('app')!
|
||||
```
|
||||
|
||||
## Type Narrowing
|
||||
|
||||
```typescript
|
||||
// Control flow
|
||||
if (value !== null) {
|
||||
// value is non-null here
|
||||
}
|
||||
|
||||
// Switch with discriminated unions
|
||||
switch (state.status) {
|
||||
case 'success':
|
||||
console.log(state.data) // TypeScript knows data exists
|
||||
break
|
||||
case 'error':
|
||||
console.log(state.error) // TypeScript knows error exists
|
||||
break
|
||||
}
|
||||
|
||||
// Optional chaining
|
||||
user?.profile?.name
|
||||
|
||||
// Nullish coalescing
|
||||
const name = user?.name ?? 'Anonymous'
|
||||
```
|
||||
|
||||
## Module Syntax
|
||||
|
||||
```typescript
|
||||
// Named exports
|
||||
export function helper() { }
|
||||
export const CONFIG = { }
|
||||
|
||||
// Default export
|
||||
export default class App { }
|
||||
|
||||
// Type-only imports/exports
|
||||
import type { User } from './types'
|
||||
export type { User }
|
||||
|
||||
// Namespace imports
|
||||
import * as utils from './utils'
|
||||
```
|
||||
|
||||
## TSConfig Essentials
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Errors & Fixes
|
||||
|
||||
| Error | Fix |
|
||||
|-------|-----|
|
||||
| Type 'X' is not assignable to type 'Y' | Check type compatibility, use type assertion if needed |
|
||||
| Object is possibly 'null' | Use optional chaining `?.` or null check |
|
||||
| Cannot find module | Install `@types/package-name` |
|
||||
| Implicit any | Add type annotation or enable strict mode |
|
||||
| Property does not exist | Check object shape, use type guard |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Enable `strict` mode in tsconfig.json
|
||||
2. Avoid `any`, use `unknown` instead
|
||||
3. Use discriminated unions for state
|
||||
4. Leverage type inference
|
||||
5. Use `const` assertions for immutable data
|
||||
6. Create custom type guards for runtime safety
|
||||
7. Use utility types instead of recreating
|
||||
8. Document complex types with JSDoc
|
||||
9. Prefer interfaces for objects, types for unions
|
||||
10. Use branded types for domain-specific primitives
|
||||
|
||||
756
.claude/skills/typescript/references/common-patterns.md
Normal file
756
.claude/skills/typescript/references/common-patterns.md
Normal file
@@ -0,0 +1,756 @@
|
||||
# TypeScript Common Patterns Reference
|
||||
|
||||
This document contains commonly used TypeScript patterns and idioms from real-world applications.
|
||||
|
||||
## React Patterns
|
||||
|
||||
### Component Props
|
||||
|
||||
```typescript
|
||||
// Basic props with children
|
||||
interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary' | 'tertiary'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
disabled?: boolean
|
||||
onClick?: () => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Button({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
onClick,
|
||||
children,
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button className={`btn-${variant} btn-${size}`} disabled={disabled} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// Props extending HTML attributes
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export function Input({ label, error, ...inputProps }: InputProps) {
|
||||
return (
|
||||
<div>
|
||||
{label && <label>{label}</label>}
|
||||
<input {...inputProps} />
|
||||
{error && <span>{error}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Generic component props
|
||||
interface ListProps<T> {
|
||||
items: T[]
|
||||
renderItem: (item: T) => React.ReactNode
|
||||
keyExtractor: (item: T) => string
|
||||
}
|
||||
|
||||
export function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
|
||||
return (
|
||||
<ul>
|
||||
{items.map((item) => (
|
||||
<li key={keyExtractor(item)}>{renderItem(item)}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Hooks
|
||||
|
||||
```typescript
|
||||
// Custom hook with return type
|
||||
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key)
|
||||
return item ? JSON.parse(item) : initialValue
|
||||
} catch (error) {
|
||||
return initialValue
|
||||
}
|
||||
})
|
||||
|
||||
const setValue = (value: T) => {
|
||||
setStoredValue(value)
|
||||
window.localStorage.setItem(key, JSON.stringify(value))
|
||||
}
|
||||
|
||||
return [storedValue, setValue]
|
||||
}
|
||||
|
||||
// Hook with options object
|
||||
interface UseFetchOptions<T> {
|
||||
initialData?: T
|
||||
onSuccess?: (data: T) => void
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
function useFetch<T>(url: string, options?: UseFetchOptions<T>) {
|
||||
const [data, setData] = useState<T | undefined>(options?.initialData)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const json = await response.json()
|
||||
if (!isCancelled) {
|
||||
setData(json)
|
||||
options?.onSuccess?.(json)
|
||||
}
|
||||
} catch (err) {
|
||||
if (!isCancelled) {
|
||||
const error = err instanceof Error ? err : new Error(String(err))
|
||||
setError(error)
|
||||
options?.onError?.(error)
|
||||
}
|
||||
} finally {
|
||||
if (!isCancelled) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
|
||||
return () => {
|
||||
isCancelled = true
|
||||
}
|
||||
}, [url])
|
||||
|
||||
return { data, loading, error }
|
||||
}
|
||||
```
|
||||
|
||||
### Context
|
||||
|
||||
```typescript
|
||||
// Type-safe context
|
||||
interface AuthContextType {
|
||||
user: User | null
|
||||
login: (email: string, password: string) => Promise<void>
|
||||
logout: () => void
|
||||
isAuthenticated: boolean
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
// Login logic
|
||||
const user = await api.login(email, password)
|
||||
setUser(user)
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
setUser(null)
|
||||
}
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
isAuthenticated: user !== null,
|
||||
}
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||||
}
|
||||
|
||||
// Custom hook with proper error handling
|
||||
export function useAuth(): AuthContextType {
|
||||
const context = useContext(AuthContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within AuthProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
```
|
||||
|
||||
## API Response Patterns
|
||||
|
||||
### Result Type Pattern
|
||||
|
||||
```typescript
|
||||
// Discriminated union for API responses
|
||||
type Result<T, E = Error> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: E }
|
||||
|
||||
// Helper functions
|
||||
function success<T>(data: T): Result<T> {
|
||||
return { success: true, data }
|
||||
}
|
||||
|
||||
function failure<E = Error>(error: E): Result<never, E> {
|
||||
return { success: false, error }
|
||||
}
|
||||
|
||||
// Usage
|
||||
async function fetchUser(id: string): Promise<Result<User>> {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${id}`)
|
||||
if (!response.ok) {
|
||||
return failure(new Error(`HTTP ${response.status}`))
|
||||
}
|
||||
const data = await response.json()
|
||||
return success(data)
|
||||
} catch (error) {
|
||||
return failure(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
}
|
||||
|
||||
// Consuming the result
|
||||
const result = await fetchUser('123')
|
||||
if (result.success) {
|
||||
console.log(result.data.name) // Type-safe access
|
||||
} else {
|
||||
console.error(result.error.message) // Type-safe error handling
|
||||
}
|
||||
```
|
||||
|
||||
### Option Type Pattern
|
||||
|
||||
```typescript
|
||||
// Option/Maybe type for nullable values
|
||||
type Option<T> = Some<T> | None
|
||||
|
||||
interface Some<T> {
|
||||
readonly _tag: 'Some'
|
||||
readonly value: T
|
||||
}
|
||||
|
||||
interface None {
|
||||
readonly _tag: 'None'
|
||||
}
|
||||
|
||||
// Constructors
|
||||
function some<T>(value: T): Option<T> {
|
||||
return { _tag: 'Some', value }
|
||||
}
|
||||
|
||||
function none(): Option<never> {
|
||||
return { _tag: 'None' }
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function isSome<T>(option: Option<T>): option is Some<T> {
|
||||
return option._tag === 'Some'
|
||||
}
|
||||
|
||||
function isNone<T>(option: Option<T>): option is None {
|
||||
return option._tag === 'None'
|
||||
}
|
||||
|
||||
function map<T, U>(option: Option<T>, fn: (value: T) => U): Option<U> {
|
||||
return isSome(option) ? some(fn(option.value)) : none()
|
||||
}
|
||||
|
||||
function getOrElse<T>(option: Option<T>, defaultValue: T): T {
|
||||
return isSome(option) ? option.value : defaultValue
|
||||
}
|
||||
|
||||
// Usage
|
||||
function findUser(id: string): Option<User> {
|
||||
const user = users.find((u) => u.id === id)
|
||||
return user ? some(user) : none()
|
||||
}
|
||||
|
||||
const user = findUser('123')
|
||||
const userName = getOrElse(map(user, (u) => u.name), 'Unknown')
|
||||
```
|
||||
|
||||
## State Management Patterns
|
||||
|
||||
### Discriminated Union for State
|
||||
|
||||
```typescript
|
||||
// State machine using discriminated unions
|
||||
type FetchState<T> =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: T }
|
||||
| { status: 'error'; error: Error }
|
||||
|
||||
// Reducer pattern
|
||||
type FetchAction<T> =
|
||||
| { type: 'FETCH_START' }
|
||||
| { type: 'FETCH_SUCCESS'; payload: T }
|
||||
| { type: 'FETCH_ERROR'; error: Error }
|
||||
| { type: 'RESET' }
|
||||
|
||||
function fetchReducer<T>(state: FetchState<T>, action: FetchAction<T>): FetchState<T> {
|
||||
switch (action.type) {
|
||||
case 'FETCH_START':
|
||||
return { status: 'loading' }
|
||||
case 'FETCH_SUCCESS':
|
||||
return { status: 'success', data: action.payload }
|
||||
case 'FETCH_ERROR':
|
||||
return { status: 'error', error: action.error }
|
||||
case 'RESET':
|
||||
return { status: 'idle' }
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in component
|
||||
function UserProfile({ userId }: { userId: string }) {
|
||||
const [state, dispatch] = useReducer(fetchReducer<User>, { status: 'idle' })
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'FETCH_START' })
|
||||
fetchUser(userId)
|
||||
.then((user) => dispatch({ type: 'FETCH_SUCCESS', payload: user }))
|
||||
.catch((error) => dispatch({ type: 'FETCH_ERROR', error }))
|
||||
}, [userId])
|
||||
|
||||
switch (state.status) {
|
||||
case 'idle':
|
||||
return <div>Ready to load</div>
|
||||
case 'loading':
|
||||
return <div>Loading...</div>
|
||||
case 'success':
|
||||
return <div>{state.data.name}</div>
|
||||
case 'error':
|
||||
return <div>Error: {state.error.message}</div>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Store Pattern
|
||||
|
||||
```typescript
|
||||
// Type-safe store implementation
|
||||
interface Store<T> {
|
||||
getState: () => T
|
||||
setState: (partial: Partial<T>) => void
|
||||
subscribe: (listener: (state: T) => void) => () => void
|
||||
}
|
||||
|
||||
function createStore<T>(initialState: T): Store<T> {
|
||||
let state = initialState
|
||||
const listeners = new Set<(state: T) => void>()
|
||||
|
||||
return {
|
||||
getState: () => state,
|
||||
setState: (partial) => {
|
||||
state = { ...state, ...partial }
|
||||
listeners.forEach((listener) => listener(state))
|
||||
},
|
||||
subscribe: (listener) => {
|
||||
listeners.add(listener)
|
||||
return () => listeners.delete(listener)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
interface AppState {
|
||||
user: User | null
|
||||
theme: 'light' | 'dark'
|
||||
}
|
||||
|
||||
const store = createStore<AppState>({
|
||||
user: null,
|
||||
theme: 'light',
|
||||
})
|
||||
|
||||
// React hook integration
|
||||
function useStore<T, U>(store: Store<T>, selector: (state: T) => U): U {
|
||||
const [value, setValue] = useState(() => selector(store.getState()))
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = store.subscribe((state) => {
|
||||
setValue(selector(state))
|
||||
})
|
||||
return unsubscribe
|
||||
}, [store, selector])
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// Usage in component
|
||||
function ThemeToggle() {
|
||||
const theme = useStore(store, (state) => state.theme)
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => store.setState({ theme: theme === 'light' ? 'dark' : 'light' })}
|
||||
>
|
||||
Toggle Theme
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Form Patterns
|
||||
|
||||
### Form State Management
|
||||
|
||||
```typescript
|
||||
// Generic form state
|
||||
interface FormState<T> {
|
||||
values: T
|
||||
errors: Partial<Record<keyof T, string>>
|
||||
touched: Partial<Record<keyof T, boolean>>
|
||||
isSubmitting: boolean
|
||||
}
|
||||
|
||||
// Form hook
|
||||
function useForm<T extends Record<string, any>>(
|
||||
initialValues: T,
|
||||
validate: (values: T) => Partial<Record<keyof T, string>>,
|
||||
) {
|
||||
const [state, setState] = useState<FormState<T>>({
|
||||
values: initialValues,
|
||||
errors: {},
|
||||
touched: {},
|
||||
isSubmitting: false,
|
||||
})
|
||||
|
||||
const handleChange = <K extends keyof T>(field: K, value: T[K]) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
values: { ...prev.values, [field]: value },
|
||||
errors: { ...prev.errors, [field]: undefined },
|
||||
}))
|
||||
}
|
||||
|
||||
const handleBlur = <K extends keyof T>(field: K) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
touched: { ...prev.touched, [field]: true },
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSubmit = async (onSubmit: (values: T) => Promise<void>) => {
|
||||
const errors = validate(state.values)
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
errors,
|
||||
touched: Object.keys(state.values).reduce(
|
||||
(acc, key) => ({ ...acc, [key]: true }),
|
||||
{},
|
||||
),
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
setState((prev) => ({ ...prev, isSubmitting: true }))
|
||||
try {
|
||||
await onSubmit(state.values)
|
||||
} finally {
|
||||
setState((prev) => ({ ...prev, isSubmitting: false }))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
values: state.values,
|
||||
errors: state.errors,
|
||||
touched: state.touched,
|
||||
isSubmitting: state.isSubmitting,
|
||||
handleChange,
|
||||
handleBlur,
|
||||
handleSubmit,
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
interface LoginFormValues {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
function LoginForm() {
|
||||
const form = useForm<LoginFormValues>(
|
||||
{ email: '', password: '' },
|
||||
(values) => {
|
||||
const errors: Partial<Record<keyof LoginFormValues, string>> = {}
|
||||
if (!values.email) {
|
||||
errors.email = 'Email is required'
|
||||
}
|
||||
if (!values.password) {
|
||||
errors.password = 'Password is required'
|
||||
}
|
||||
return errors
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
form.handleSubmit(async (values) => {
|
||||
await login(values.email, values.password)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<input
|
||||
value={form.values.email}
|
||||
onChange={(e) => form.handleChange('email', e.target.value)}
|
||||
onBlur={() => form.handleBlur('email')}
|
||||
/>
|
||||
{form.touched.email && form.errors.email && <span>{form.errors.email}</span>}
|
||||
|
||||
<input
|
||||
type="password"
|
||||
value={form.values.password}
|
||||
onChange={(e) => form.handleChange('password', e.target.value)}
|
||||
onBlur={() => form.handleBlur('password')}
|
||||
/>
|
||||
{form.touched.password && form.errors.password && (
|
||||
<span>{form.errors.password}</span>
|
||||
)}
|
||||
|
||||
<button type="submit" disabled={form.isSubmitting}>
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Patterns
|
||||
|
||||
### Zod Integration
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
// Schema definition
|
||||
const userSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string().min(1).max(100),
|
||||
email: z.string().email(),
|
||||
age: z.number().int().min(0).max(120),
|
||||
role: z.enum(['admin', 'user', 'guest']),
|
||||
})
|
||||
|
||||
// Extract type from schema
|
||||
type User = z.infer<typeof userSchema>
|
||||
|
||||
// Validation function
|
||||
function validateUser(data: unknown): Result<User> {
|
||||
const result = userSchema.safeParse(data)
|
||||
if (result.success) {
|
||||
return { success: true, data: result.data }
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: new Error(result.error.errors.map((e) => e.message).join(', ')),
|
||||
}
|
||||
}
|
||||
|
||||
// API integration
|
||||
async function createUser(data: unknown): Promise<Result<User>> {
|
||||
const validation = validateUser(data)
|
||||
if (!validation.success) {
|
||||
return validation
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(validation.data),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return failure(new Error(`HTTP ${response.status}`))
|
||||
}
|
||||
|
||||
const user = await response.json()
|
||||
return success(user)
|
||||
} catch (error) {
|
||||
return failure(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Builder Pattern
|
||||
|
||||
```typescript
|
||||
// Fluent builder pattern
|
||||
class QueryBuilder<T> {
|
||||
private filters: Array<(item: T) => boolean> = []
|
||||
private sortFn?: (a: T, b: T) => number
|
||||
private limitValue?: number
|
||||
|
||||
where(predicate: (item: T) => boolean): this {
|
||||
this.filters.push(predicate)
|
||||
return this
|
||||
}
|
||||
|
||||
sortBy(compareFn: (a: T, b: T) => number): this {
|
||||
this.sortFn = compareFn
|
||||
return this
|
||||
}
|
||||
|
||||
limit(count: number): this {
|
||||
this.limitValue = count
|
||||
return this
|
||||
}
|
||||
|
||||
execute(data: T[]): T[] {
|
||||
let result = data
|
||||
|
||||
// Apply filters
|
||||
this.filters.forEach((filter) => {
|
||||
result = result.filter(filter)
|
||||
})
|
||||
|
||||
// Apply sorting
|
||||
if (this.sortFn) {
|
||||
result = result.sort(this.sortFn)
|
||||
}
|
||||
|
||||
// Apply limit
|
||||
if (this.limitValue !== undefined) {
|
||||
result = result.slice(0, this.limitValue)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
category: string
|
||||
}
|
||||
|
||||
const products: Product[] = [
|
||||
/* ... */
|
||||
]
|
||||
|
||||
const query = new QueryBuilder<Product>()
|
||||
.where((p) => p.category === 'electronics')
|
||||
.where((p) => p.price < 1000)
|
||||
.sortBy((a, b) => a.price - b.price)
|
||||
.limit(10)
|
||||
.execute(products)
|
||||
```
|
||||
|
||||
## Factory Pattern
|
||||
|
||||
```typescript
|
||||
// Abstract factory pattern with TypeScript
|
||||
interface Button {
|
||||
render: () => string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
interface ButtonFactory {
|
||||
createButton: (label: string, onClick: () => void) => Button
|
||||
}
|
||||
|
||||
class PrimaryButton implements Button {
|
||||
constructor(private label: string, private clickHandler: () => void) {}
|
||||
|
||||
render() {
|
||||
return `<button class="primary">${this.label}</button>`
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.clickHandler()
|
||||
}
|
||||
}
|
||||
|
||||
class SecondaryButton implements Button {
|
||||
constructor(private label: string, private clickHandler: () => void) {}
|
||||
|
||||
render() {
|
||||
return `<button class="secondary">${this.label}</button>`
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this.clickHandler()
|
||||
}
|
||||
}
|
||||
|
||||
class PrimaryButtonFactory implements ButtonFactory {
|
||||
createButton(label: string, onClick: () => void): Button {
|
||||
return new PrimaryButton(label, onClick)
|
||||
}
|
||||
}
|
||||
|
||||
class SecondaryButtonFactory implements ButtonFactory {
|
||||
createButton(label: string, onClick: () => void): Button {
|
||||
return new SecondaryButton(label, onClick)
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
function createUI(factory: ButtonFactory) {
|
||||
const button = factory.createButton('Click me', () => console.log('Clicked!'))
|
||||
return button.render()
|
||||
}
|
||||
```
|
||||
|
||||
## Named Return Variables Pattern
|
||||
|
||||
```typescript
|
||||
// Following Go-style named returns
|
||||
function parseUser(data: unknown): { user: User | null; err: Error | null } {
|
||||
let user: User | null = null
|
||||
let err: Error | null = null
|
||||
|
||||
try {
|
||||
user = userSchema.parse(data)
|
||||
} catch (error) {
|
||||
err = error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
return { user, err }
|
||||
}
|
||||
|
||||
// With explicit naming
|
||||
function fetchData(url: string): {
|
||||
data: unknown | null
|
||||
status: number
|
||||
err: Error | null
|
||||
} {
|
||||
let data: unknown | null = null
|
||||
let status = 0
|
||||
let err: Error | null = null
|
||||
|
||||
try {
|
||||
const response = fetch(url)
|
||||
// Process response
|
||||
} catch (error) {
|
||||
err = error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
return { data, status, err }
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use discriminated unions** for type-safe state management
|
||||
2. **Leverage generic types** for reusable components and hooks
|
||||
3. **Extract types from Zod schemas** for runtime + compile-time safety
|
||||
4. **Use Result/Option types** for explicit error handling
|
||||
5. **Create builder patterns** for complex object construction
|
||||
6. **Use factory patterns** for flexible object creation
|
||||
7. **Type context properly** to catch usage errors at compile time
|
||||
8. **Prefer const assertions** for immutable configurations
|
||||
9. **Use branded types** for domain-specific primitives
|
||||
10. **Document patterns** with JSDoc for team knowledge sharing
|
||||
|
||||
804
.claude/skills/typescript/references/type-system.md
Normal file
804
.claude/skills/typescript/references/type-system.md
Normal file
@@ -0,0 +1,804 @@
|
||||
# TypeScript Type System Reference
|
||||
|
||||
## Overview
|
||||
|
||||
TypeScript's type system is structural (duck-typed) rather than nominal. Two types are compatible if their structure matches, regardless of their names.
|
||||
|
||||
## Primitive Types
|
||||
|
||||
### Basic Primitives
|
||||
|
||||
```typescript
|
||||
let str: string = 'hello'
|
||||
let num: number = 42
|
||||
let bool: boolean = true
|
||||
let nul: null = null
|
||||
let undef: undefined = undefined
|
||||
let sym: symbol = Symbol('key')
|
||||
let big: bigint = 100n
|
||||
```
|
||||
|
||||
### Special Types
|
||||
|
||||
**any** - Disables type checking (avoid when possible):
|
||||
```typescript
|
||||
let anything: any = 'string'
|
||||
anything = 42 // OK
|
||||
anything.nonExistent() // OK at compile time, error at runtime
|
||||
```
|
||||
|
||||
**unknown** - Type-safe alternative to any (requires type checking):
|
||||
```typescript
|
||||
let value: unknown = 'string'
|
||||
// value.toUpperCase() // Error: must narrow type first
|
||||
|
||||
if (typeof value === 'string') {
|
||||
value.toUpperCase() // OK after narrowing
|
||||
}
|
||||
```
|
||||
|
||||
**void** - Absence of a value (function return type):
|
||||
```typescript
|
||||
function log(message: string): void {
|
||||
console.log(message)
|
||||
}
|
||||
```
|
||||
|
||||
**never** - Value that never occurs (exhaustive checks, infinite loops):
|
||||
```typescript
|
||||
function throwError(message: string): never {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
function exhaustiveCheck(value: never): never {
|
||||
throw new Error(`Unhandled case: ${value}`)
|
||||
}
|
||||
```
|
||||
|
||||
## Object Types
|
||||
|
||||
### Interfaces
|
||||
|
||||
```typescript
|
||||
// Basic interface
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
// Optional properties
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
description?: string // Optional
|
||||
}
|
||||
|
||||
// Readonly properties
|
||||
interface Config {
|
||||
readonly apiUrl: string
|
||||
readonly timeout: number
|
||||
}
|
||||
|
||||
// Index signatures
|
||||
interface Dictionary {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
// Method signatures
|
||||
interface Calculator {
|
||||
add(a: number, b: number): number
|
||||
subtract(a: number, b: number): number
|
||||
}
|
||||
|
||||
// Extending interfaces
|
||||
interface Employee extends User {
|
||||
role: string
|
||||
department: string
|
||||
}
|
||||
|
||||
// Multiple inheritance
|
||||
interface Admin extends User, Employee {
|
||||
permissions: string[]
|
||||
}
|
||||
```
|
||||
|
||||
### Type Aliases
|
||||
|
||||
```typescript
|
||||
// Basic type alias
|
||||
type ID = string | number
|
||||
|
||||
// Object type
|
||||
type Point = {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
// Union type
|
||||
type Status = 'idle' | 'loading' | 'success' | 'error'
|
||||
|
||||
// Intersection type
|
||||
type Timestamped = {
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
type TimestampedUser = User & Timestamped
|
||||
|
||||
// Function type
|
||||
type Callback = (data: string) => void
|
||||
|
||||
// Generic type alias
|
||||
type Result<T> = { success: true; data: T } | { success: false; error: string }
|
||||
```
|
||||
|
||||
### Interface vs Type Alias
|
||||
|
||||
**Use interface when:**
|
||||
- Defining object shapes
|
||||
- Need declaration merging
|
||||
- Building public API types that others might extend
|
||||
|
||||
**Use type when:**
|
||||
- Creating unions or intersections
|
||||
- Working with mapped types
|
||||
- Need conditional types
|
||||
- Defining primitive aliases
|
||||
|
||||
## Array and Tuple Types
|
||||
|
||||
### Arrays
|
||||
|
||||
```typescript
|
||||
// Array syntax
|
||||
let numbers: number[] = [1, 2, 3]
|
||||
let strings: Array<string> = ['a', 'b', 'c']
|
||||
|
||||
// Readonly arrays
|
||||
let immutable: readonly number[] = [1, 2, 3]
|
||||
let alsoImmutable: ReadonlyArray<string> = ['a', 'b']
|
||||
```
|
||||
|
||||
### Tuples
|
||||
|
||||
```typescript
|
||||
// Fixed-length, mixed-type arrays
|
||||
type Point = [number, number]
|
||||
type NamedPoint = [x: number, y: number]
|
||||
|
||||
// Optional elements
|
||||
type OptionalTuple = [string, number?]
|
||||
|
||||
// Rest elements
|
||||
type StringNumberBooleans = [string, number, ...boolean[]]
|
||||
|
||||
// Readonly tuples
|
||||
type ReadonlyPair = readonly [string, number]
|
||||
```
|
||||
|
||||
## Union and Intersection Types
|
||||
|
||||
### Union Types
|
||||
|
||||
```typescript
|
||||
// Value can be one of several types
|
||||
type StringOrNumber = string | number
|
||||
|
||||
function format(value: StringOrNumber): string {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
}
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
// Discriminated unions
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Intersection Types
|
||||
|
||||
```typescript
|
||||
// Combine multiple types
|
||||
type Draggable = {
|
||||
drag: () => void
|
||||
}
|
||||
|
||||
type Resizable = {
|
||||
resize: () => void
|
||||
}
|
||||
|
||||
type UIWidget = Draggable & Resizable
|
||||
|
||||
const widget: UIWidget = {
|
||||
drag: () => console.log('dragging'),
|
||||
resize: () => console.log('resizing'),
|
||||
}
|
||||
```
|
||||
|
||||
## Literal Types
|
||||
|
||||
### String Literal Types
|
||||
|
||||
```typescript
|
||||
type Direction = 'north' | 'south' | 'east' | 'west'
|
||||
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
|
||||
function move(direction: Direction) {
|
||||
// direction can only be one of the four values
|
||||
}
|
||||
```
|
||||
|
||||
### Number Literal Types
|
||||
|
||||
```typescript
|
||||
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6
|
||||
type PowerOfTwo = 1 | 2 | 4 | 8 | 16 | 32
|
||||
```
|
||||
|
||||
### Boolean Literal Types
|
||||
|
||||
```typescript
|
||||
type Yes = true
|
||||
type No = false
|
||||
```
|
||||
|
||||
### Template Literal Types
|
||||
|
||||
```typescript
|
||||
// String manipulation at type level
|
||||
type EventName<T extends string> = `on${Capitalize<T>}`
|
||||
type ClickEvent = EventName<'click'> // "onClick"
|
||||
|
||||
// Combining literals
|
||||
type Color = 'red' | 'blue' | 'green'
|
||||
type Shade = 'light' | 'dark'
|
||||
type ColorShade = `${Shade}-${Color}` // "light-red" | "light-blue" | ...
|
||||
|
||||
// Extract patterns
|
||||
type EmailLocaleIDs = 'welcome_email' | 'email_heading'
|
||||
type FooterLocaleIDs = 'footer_title' | 'footer_sendoff'
|
||||
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`
|
||||
```
|
||||
|
||||
## Type Inference
|
||||
|
||||
### Automatic Inference
|
||||
|
||||
```typescript
|
||||
// Type inferred as string
|
||||
let message = 'hello'
|
||||
|
||||
// Type inferred as number[]
|
||||
let numbers = [1, 2, 3]
|
||||
|
||||
// Type inferred as { name: string; age: number }
|
||||
let person = {
|
||||
name: 'Alice',
|
||||
age: 30,
|
||||
}
|
||||
|
||||
// Return type inferred
|
||||
function add(a: number, b: number) {
|
||||
return a + b // Returns number
|
||||
}
|
||||
```
|
||||
|
||||
### Const Assertions
|
||||
|
||||
```typescript
|
||||
// Without const assertion
|
||||
let colors1 = ['red', 'green', 'blue'] // Type: string[]
|
||||
|
||||
// With const assertion
|
||||
let colors2 = ['red', 'green', 'blue'] as const // Type: readonly ["red", "green", "blue"]
|
||||
|
||||
// Object with const assertion
|
||||
const config = {
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
} as const // All properties become readonly with literal types
|
||||
```
|
||||
|
||||
### Type Inference in Generics
|
||||
|
||||
```typescript
|
||||
// Generic type inference from usage
|
||||
function identity<T>(value: T): T {
|
||||
return value
|
||||
}
|
||||
|
||||
let str = identity('hello') // T inferred as string
|
||||
let num = identity(42) // T inferred as number
|
||||
|
||||
// Multiple type parameters
|
||||
function pair<T, U>(first: T, second: U): [T, U] {
|
||||
return [first, second]
|
||||
}
|
||||
|
||||
let p = pair('hello', 42) // [string, number]
|
||||
```
|
||||
|
||||
## Type Narrowing
|
||||
|
||||
### typeof Guards
|
||||
|
||||
```typescript
|
||||
function padLeft(value: string, padding: string | number) {
|
||||
if (typeof padding === 'number') {
|
||||
// padding is number here
|
||||
return ' '.repeat(padding) + value
|
||||
}
|
||||
// padding is string here
|
||||
return padding + value
|
||||
}
|
||||
```
|
||||
|
||||
### instanceof Guards
|
||||
|
||||
```typescript
|
||||
class Dog {
|
||||
bark() {
|
||||
console.log('Woof!')
|
||||
}
|
||||
}
|
||||
|
||||
class Cat {
|
||||
meow() {
|
||||
console.log('Meow!')
|
||||
}
|
||||
}
|
||||
|
||||
function makeSound(animal: Dog | Cat) {
|
||||
if (animal instanceof Dog) {
|
||||
animal.bark()
|
||||
} else {
|
||||
animal.meow()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### in Operator
|
||||
|
||||
```typescript
|
||||
type Fish = { swim: () => void }
|
||||
type Bird = { fly: () => void }
|
||||
|
||||
function move(animal: Fish | Bird) {
|
||||
if ('swim' in animal) {
|
||||
animal.swim()
|
||||
} else {
|
||||
animal.fly()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Equality Narrowing
|
||||
|
||||
```typescript
|
||||
function example(x: string | number, y: string | boolean) {
|
||||
if (x === y) {
|
||||
// x and y are both string here
|
||||
x.toUpperCase()
|
||||
y.toLowerCase()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Control Flow Analysis
|
||||
|
||||
```typescript
|
||||
function example(value: string | null) {
|
||||
if (value === null) {
|
||||
return
|
||||
}
|
||||
// value is string here (null eliminated)
|
||||
console.log(value.toUpperCase())
|
||||
}
|
||||
```
|
||||
|
||||
### Type Predicates (Custom Type Guards)
|
||||
|
||||
```typescript
|
||||
function isString(value: unknown): value is string {
|
||||
return typeof value === 'string'
|
||||
}
|
||||
|
||||
function example(value: unknown) {
|
||||
if (isString(value)) {
|
||||
// value is string here
|
||||
console.log(value.toUpperCase())
|
||||
}
|
||||
}
|
||||
|
||||
// More complex example
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
function isUser(value: unknown): value is User {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'id' in value &&
|
||||
'name' in value &&
|
||||
typeof (value as User).id === 'string' &&
|
||||
typeof (value as User).name === 'string'
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Assertion Functions
|
||||
|
||||
```typescript
|
||||
function assert(condition: unknown, message?: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message || 'Assertion failed')
|
||||
}
|
||||
}
|
||||
|
||||
function assertIsString(value: unknown): asserts value is string {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('Value must be a string')
|
||||
}
|
||||
}
|
||||
|
||||
function example(value: unknown) {
|
||||
assertIsString(value)
|
||||
// value is string here
|
||||
console.log(value.toUpperCase())
|
||||
}
|
||||
```
|
||||
|
||||
## Generic Types
|
||||
|
||||
### Basic Generics
|
||||
|
||||
```typescript
|
||||
// Generic function
|
||||
function first<T>(items: T[]): T | undefined {
|
||||
return items[0]
|
||||
}
|
||||
|
||||
// Generic interface
|
||||
interface Box<T> {
|
||||
value: T
|
||||
}
|
||||
|
||||
// Generic type alias
|
||||
type Result<T> = { success: true; data: T } | { success: false; error: string }
|
||||
|
||||
// Generic class
|
||||
class Stack<T> {
|
||||
private items: T[] = []
|
||||
|
||||
push(item: T) {
|
||||
this.items.push(item)
|
||||
}
|
||||
|
||||
pop(): T | undefined {
|
||||
return this.items.pop()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generic Constraints
|
||||
|
||||
```typescript
|
||||
// Constrain to specific type
|
||||
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
||||
return obj[key]
|
||||
}
|
||||
|
||||
// Constrain to interface
|
||||
interface HasLength {
|
||||
length: number
|
||||
}
|
||||
|
||||
function logLength<T extends HasLength>(item: T): void {
|
||||
console.log(item.length)
|
||||
}
|
||||
|
||||
logLength('string') // OK
|
||||
logLength([1, 2, 3]) // OK
|
||||
logLength({ length: 10 }) // OK
|
||||
// logLength(42) // Error: number doesn't have length
|
||||
```
|
||||
|
||||
### Default Generic Parameters
|
||||
|
||||
```typescript
|
||||
interface Response<T = unknown> {
|
||||
data: T
|
||||
status: number
|
||||
}
|
||||
|
||||
// Uses default
|
||||
let response1: Response = { data: 'anything', status: 200 }
|
||||
|
||||
// Explicitly typed
|
||||
let response2: Response<User> = { data: user, status: 200 }
|
||||
```
|
||||
|
||||
### Generic Utility Functions
|
||||
|
||||
```typescript
|
||||
// Pick specific properties
|
||||
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
|
||||
const result = {} as Pick<T, K>
|
||||
keys.forEach((key) => {
|
||||
result[key] = obj[key]
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Map array
|
||||
function map<T, U>(items: T[], fn: (item: T) => U): U[] {
|
||||
return items.map(fn)
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Type Features
|
||||
|
||||
### Conditional Types
|
||||
|
||||
```typescript
|
||||
// Basic conditional type
|
||||
type IsString<T> = T extends string ? true : false
|
||||
|
||||
type A = IsString<string> // true
|
||||
type B = IsString<number> // false
|
||||
|
||||
// Distributive conditional types
|
||||
type ToArray<T> = T extends any ? T[] : never
|
||||
|
||||
type StrArrOrNumArr = ToArray<string | number> // string[] | number[]
|
||||
|
||||
// Infer keyword
|
||||
type Flatten<T> = T extends Array<infer U> ? U : T
|
||||
|
||||
type Str = Flatten<string[]> // string
|
||||
type Num = Flatten<number> // number
|
||||
|
||||
// ReturnType implementation
|
||||
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
|
||||
```
|
||||
|
||||
### Mapped Types
|
||||
|
||||
```typescript
|
||||
// Make all properties optional
|
||||
type Partial<T> = {
|
||||
[K in keyof T]?: T[K]
|
||||
}
|
||||
|
||||
// Make all properties required
|
||||
type Required<T> = {
|
||||
[K in keyof T]-?: T[K]
|
||||
}
|
||||
|
||||
// Make all properties readonly
|
||||
type Readonly<T> = {
|
||||
readonly [K in keyof T]: T[K]
|
||||
}
|
||||
|
||||
// Transform keys
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
|
||||
}
|
||||
|
||||
interface Person {
|
||||
name: string
|
||||
age: number
|
||||
}
|
||||
|
||||
type PersonGetters = Getters<Person>
|
||||
// {
|
||||
// getName: () => string
|
||||
// getAge: () => number
|
||||
// }
|
||||
```
|
||||
|
||||
### Key Remapping
|
||||
|
||||
```typescript
|
||||
// Filter keys
|
||||
type RemoveKindField<T> = {
|
||||
[K in keyof T as Exclude<K, 'kind'>]: T[K]
|
||||
}
|
||||
|
||||
// Conditional key inclusion
|
||||
type PickByType<T, U> = {
|
||||
[K in keyof T as T[K] extends U ? K : never]: T[K]
|
||||
}
|
||||
|
||||
interface Model {
|
||||
id: number
|
||||
name: string
|
||||
age: number
|
||||
email: string
|
||||
}
|
||||
|
||||
type StringFields = PickByType<Model, string> // { name: string, email: string }
|
||||
```
|
||||
|
||||
### Recursive Types
|
||||
|
||||
```typescript
|
||||
// JSON value type
|
||||
type JSONValue = string | number | boolean | null | JSONObject | JSONArray
|
||||
|
||||
interface JSONObject {
|
||||
[key: string]: JSONValue
|
||||
}
|
||||
|
||||
interface JSONArray extends Array<JSONValue> {}
|
||||
|
||||
// Tree structure
|
||||
interface TreeNode<T> {
|
||||
value: T
|
||||
children?: TreeNode<T>[]
|
||||
}
|
||||
|
||||
// Deep readonly
|
||||
type DeepReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
|
||||
}
|
||||
```
|
||||
|
||||
## Type Compatibility
|
||||
|
||||
### Structural Typing
|
||||
|
||||
```typescript
|
||||
interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface Named {
|
||||
name: string
|
||||
}
|
||||
|
||||
// Compatible if structure matches
|
||||
let point: Point = { x: 0, y: 0 }
|
||||
let namedPoint = { x: 0, y: 0, name: 'origin' }
|
||||
|
||||
point = namedPoint // OK: namedPoint has x and y
|
||||
```
|
||||
|
||||
### Variance
|
||||
|
||||
**Covariance** (return types):
|
||||
```typescript
|
||||
interface Animal {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface Dog extends Animal {
|
||||
breed: string
|
||||
}
|
||||
|
||||
let getDog: () => Dog
|
||||
let getAnimal: () => Animal
|
||||
|
||||
getAnimal = getDog // OK: Dog is assignable to Animal
|
||||
```
|
||||
|
||||
**Contravariance** (parameter types):
|
||||
```typescript
|
||||
let handleAnimal: (animal: Animal) => void
|
||||
let handleDog: (dog: Dog) => void
|
||||
|
||||
handleDog = handleAnimal // OK: can pass Dog to function expecting Animal
|
||||
```
|
||||
|
||||
## Index Types
|
||||
|
||||
### Index Signatures
|
||||
|
||||
```typescript
|
||||
// String index
|
||||
interface StringMap {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
// Number index
|
||||
interface NumberArray {
|
||||
[index: number]: number
|
||||
}
|
||||
|
||||
// Combine with named properties
|
||||
interface MixedInterface {
|
||||
length: number
|
||||
[index: number]: string
|
||||
}
|
||||
```
|
||||
|
||||
### keyof Operator
|
||||
|
||||
```typescript
|
||||
interface Person {
|
||||
name: string
|
||||
age: number
|
||||
}
|
||||
|
||||
type PersonKeys = keyof Person // "name" | "age"
|
||||
|
||||
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
||||
return obj[key]
|
||||
}
|
||||
```
|
||||
|
||||
### Indexed Access Types
|
||||
|
||||
```typescript
|
||||
interface Person {
|
||||
name: string
|
||||
age: number
|
||||
address: {
|
||||
street: string
|
||||
city: string
|
||||
}
|
||||
}
|
||||
|
||||
type Name = Person['name'] // string
|
||||
type Age = Person['age'] // number
|
||||
type Address = Person['address'] // { street: string; city: string }
|
||||
type AddressCity = Person['address']['city'] // string
|
||||
|
||||
// Access multiple keys
|
||||
type NameOrAge = Person['name' | 'age'] // string | number
|
||||
```
|
||||
|
||||
## Branded Types
|
||||
|
||||
```typescript
|
||||
// Create nominal types from structural types
|
||||
type Brand<K, T> = K & { __brand: T }
|
||||
|
||||
type USD = Brand<number, 'USD'>
|
||||
type EUR = Brand<number, 'EUR'>
|
||||
|
||||
function makeUSD(amount: number): USD {
|
||||
return amount as USD
|
||||
}
|
||||
|
||||
function makeEUR(amount: number): EUR {
|
||||
return amount as EUR
|
||||
}
|
||||
|
||||
let usd = makeUSD(100)
|
||||
let eur = makeEUR(100)
|
||||
|
||||
// usd = eur // Error: different brands
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Prefer type inference** - Let TypeScript infer types when obvious
|
||||
2. **Use strict null checks** - Enable strictNullChecks for better safety
|
||||
3. **Avoid `any`** - Use `unknown` and narrow with type guards
|
||||
4. **Use discriminated unions** - Better than loose unions for state
|
||||
5. **Leverage const assertions** - Get narrow literal types
|
||||
6. **Use branded types** - When structural typing isn't enough
|
||||
7. **Document complex types** - Add JSDoc comments
|
||||
8. **Extract reusable types** - DRY principle applies to types too
|
||||
9. **Use utility types** - Leverage built-in transformation types
|
||||
10. **Test your types** - Use type assertions to verify type correctness
|
||||
|
||||
666
.claude/skills/typescript/references/utility-types.md
Normal file
666
.claude/skills/typescript/references/utility-types.md
Normal file
@@ -0,0 +1,666 @@
|
||||
# TypeScript Utility Types Reference
|
||||
|
||||
TypeScript provides several built-in utility types that help transform and manipulate types. These are implemented using advanced type features like mapped types and conditional types.
|
||||
|
||||
## Property Modifiers
|
||||
|
||||
### Partial\<T\>
|
||||
|
||||
Makes all properties in `T` optional.
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
age: number
|
||||
}
|
||||
|
||||
type PartialUser = Partial<User>
|
||||
// {
|
||||
// id?: string
|
||||
// name?: string
|
||||
// email?: string
|
||||
// age?: number
|
||||
// }
|
||||
|
||||
// Useful for update operations
|
||||
function updateUser(id: string, updates: Partial<User>) {
|
||||
// Only update provided fields
|
||||
}
|
||||
|
||||
updateUser('123', { name: 'Alice' }) // OK
|
||||
updateUser('123', { name: 'Alice', age: 30 }) // OK
|
||||
```
|
||||
|
||||
### Required\<T\>
|
||||
|
||||
Makes all properties in `T` required (removes optionality).
|
||||
|
||||
```typescript
|
||||
interface Config {
|
||||
host?: string
|
||||
port?: number
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
type RequiredConfig = Required<Config>
|
||||
// {
|
||||
// host: string
|
||||
// port: number
|
||||
// timeout: number
|
||||
// }
|
||||
|
||||
function initServer(config: RequiredConfig) {
|
||||
// All properties are guaranteed to exist
|
||||
console.log(config.host, config.port, config.timeout)
|
||||
}
|
||||
```
|
||||
|
||||
### Readonly\<T\>
|
||||
|
||||
Makes all properties in `T` readonly.
|
||||
|
||||
```typescript
|
||||
interface MutablePoint {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
type ImmutablePoint = Readonly<MutablePoint>
|
||||
// {
|
||||
// readonly x: number
|
||||
// readonly y: number
|
||||
// }
|
||||
|
||||
const point: ImmutablePoint = { x: 0, y: 0 }
|
||||
// point.x = 10 // Error: Cannot assign to 'x' because it is a read-only property
|
||||
```
|
||||
|
||||
### Mutable\<T\> (Custom)
|
||||
|
||||
Removes readonly modifiers (not built-in, but useful pattern).
|
||||
|
||||
```typescript
|
||||
type Mutable<T> = {
|
||||
-readonly [K in keyof T]: T[K]
|
||||
}
|
||||
|
||||
interface ReadonlyPerson {
|
||||
readonly name: string
|
||||
readonly age: number
|
||||
}
|
||||
|
||||
type MutablePerson = Mutable<ReadonlyPerson>
|
||||
// {
|
||||
// name: string
|
||||
// age: number
|
||||
// }
|
||||
```
|
||||
|
||||
## Property Selection
|
||||
|
||||
### Pick\<T, K\>
|
||||
|
||||
Creates a type by picking specific properties from `T`.
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
type UserProfile = Pick<User, 'id' | 'name' | 'email'>
|
||||
// {
|
||||
// id: string
|
||||
// name: string
|
||||
// email: string
|
||||
// }
|
||||
|
||||
// Useful for API responses
|
||||
function getUserProfile(id: string): UserProfile {
|
||||
// Return only safe properties
|
||||
}
|
||||
```
|
||||
|
||||
### Omit\<T, K\>
|
||||
|
||||
Creates a type by omitting specific properties from `T`.
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
type UserWithoutPassword = Omit<User, 'password'>
|
||||
// {
|
||||
// id: string
|
||||
// name: string
|
||||
// email: string
|
||||
// }
|
||||
|
||||
// Useful for public user data
|
||||
function publishUser(user: User): UserWithoutPassword {
|
||||
const { password, ...publicData } = user
|
||||
return publicData
|
||||
}
|
||||
```
|
||||
|
||||
## Union Type Utilities
|
||||
|
||||
### Exclude\<T, U\>
|
||||
|
||||
Excludes types from `T` that are assignable to `U`.
|
||||
|
||||
```typescript
|
||||
type T1 = Exclude<'a' | 'b' | 'c', 'a'> // "b" | "c"
|
||||
type T2 = Exclude<string | number | boolean, boolean> // string | number
|
||||
|
||||
type EventType = 'click' | 'scroll' | 'mousemove' | 'keypress'
|
||||
type UIEvent = Exclude<EventType, 'scroll'> // "click" | "mousemove" | "keypress"
|
||||
```
|
||||
|
||||
### Extract\<T, U\>
|
||||
|
||||
Extracts types from `T` that are assignable to `U`.
|
||||
|
||||
```typescript
|
||||
type T1 = Extract<'a' | 'b' | 'c', 'a' | 'f'> // "a"
|
||||
type T2 = Extract<string | number | boolean, boolean> // boolean
|
||||
|
||||
type Shape = 'circle' | 'square' | 'triangle' | 'rectangle'
|
||||
type RoundedShape = Extract<Shape, 'circle'> // "circle"
|
||||
```
|
||||
|
||||
### NonNullable\<T\>
|
||||
|
||||
Excludes `null` and `undefined` from `T`.
|
||||
|
||||
```typescript
|
||||
type T1 = NonNullable<string | null | undefined> // string
|
||||
type T2 = NonNullable<string | number | null> // string | number
|
||||
|
||||
function processValue(value: string | null | undefined) {
|
||||
if (value !== null && value !== undefined) {
|
||||
const nonNull: NonNullable<typeof value> = value
|
||||
// nonNull is guaranteed to be string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Object Construction
|
||||
|
||||
### Record\<K, T\>
|
||||
|
||||
Constructs an object type with keys of type `K` and values of type `T`.
|
||||
|
||||
```typescript
|
||||
type PageInfo = Record<string, number>
|
||||
// { [key: string]: number }
|
||||
|
||||
const pages: PageInfo = {
|
||||
home: 1,
|
||||
about: 2,
|
||||
contact: 3,
|
||||
}
|
||||
|
||||
// Useful for mapped objects
|
||||
type UserRole = 'admin' | 'user' | 'guest'
|
||||
type RolePermissions = Record<UserRole, string[]>
|
||||
|
||||
const permissions: RolePermissions = {
|
||||
admin: ['read', 'write', 'delete'],
|
||||
user: ['read', 'write'],
|
||||
guest: ['read'],
|
||||
}
|
||||
|
||||
// With specific keys
|
||||
type ThemeColors = Record<'primary' | 'secondary' | 'accent', string>
|
||||
|
||||
const colors: ThemeColors = {
|
||||
primary: '#007bff',
|
||||
secondary: '#6c757d',
|
||||
accent: '#28a745',
|
||||
}
|
||||
```
|
||||
|
||||
## Function Utilities
|
||||
|
||||
### Parameters\<T\>
|
||||
|
||||
Extracts the parameter types of a function type as a tuple.
|
||||
|
||||
```typescript
|
||||
function createUser(name: string, age: number, email: string) {
|
||||
// ...
|
||||
}
|
||||
|
||||
type CreateUserParams = Parameters<typeof createUser>
|
||||
// [name: string, age: number, email: string]
|
||||
|
||||
// Useful for higher-order functions
|
||||
function withLogging<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
...args: Parameters<T>
|
||||
): ReturnType<T> {
|
||||
console.log('Calling with:', args)
|
||||
return fn(...args)
|
||||
}
|
||||
```
|
||||
|
||||
### ConstructorParameters\<T\>
|
||||
|
||||
Extracts the parameter types of a constructor function type.
|
||||
|
||||
```typescript
|
||||
class User {
|
||||
constructor(public name: string, public age: number) {}
|
||||
}
|
||||
|
||||
type UserConstructorParams = ConstructorParameters<typeof User>
|
||||
// [name: string, age: number]
|
||||
|
||||
function createUser(...args: UserConstructorParams): User {
|
||||
return new User(...args)
|
||||
}
|
||||
```
|
||||
|
||||
### ReturnType\<T\>
|
||||
|
||||
Extracts the return type of a function type.
|
||||
|
||||
```typescript
|
||||
function createUser() {
|
||||
return {
|
||||
id: '123',
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
}
|
||||
}
|
||||
|
||||
type User = ReturnType<typeof createUser>
|
||||
// {
|
||||
// id: string
|
||||
// name: string
|
||||
// email: string
|
||||
// }
|
||||
|
||||
// Useful with async functions
|
||||
async function fetchData() {
|
||||
return { success: true, data: [1, 2, 3] }
|
||||
}
|
||||
|
||||
type FetchResult = ReturnType<typeof fetchData>
|
||||
// Promise<{ success: boolean; data: number[] }>
|
||||
|
||||
type UnwrappedResult = Awaited<FetchResult>
|
||||
// { success: boolean; data: number[] }
|
||||
```
|
||||
|
||||
### InstanceType\<T\>
|
||||
|
||||
Extracts the instance type of a constructor function type.
|
||||
|
||||
```typescript
|
||||
class User {
|
||||
name: string
|
||||
constructor(name: string) {
|
||||
this.name = name
|
||||
}
|
||||
}
|
||||
|
||||
type UserInstance = InstanceType<typeof User>
|
||||
// User
|
||||
|
||||
function processUser(user: UserInstance) {
|
||||
console.log(user.name)
|
||||
}
|
||||
```
|
||||
|
||||
### ThisParameterType\<T\>
|
||||
|
||||
Extracts the type of the `this` parameter for a function type.
|
||||
|
||||
```typescript
|
||||
function toHex(this: Number) {
|
||||
return this.toString(16)
|
||||
}
|
||||
|
||||
type ThisType = ThisParameterType<typeof toHex> // Number
|
||||
```
|
||||
|
||||
### OmitThisParameter\<T\>
|
||||
|
||||
Removes the `this` parameter from a function type.
|
||||
|
||||
```typescript
|
||||
function toHex(this: Number) {
|
||||
return this.toString(16)
|
||||
}
|
||||
|
||||
type PlainFunction = OmitThisParameter<typeof toHex>
|
||||
// () => string
|
||||
```
|
||||
|
||||
## String Manipulation
|
||||
|
||||
### Uppercase\<S\>
|
||||
|
||||
Converts string literal type to uppercase.
|
||||
|
||||
```typescript
|
||||
type Greeting = 'hello'
|
||||
type LoudGreeting = Uppercase<Greeting> // "HELLO"
|
||||
|
||||
// Useful for constants
|
||||
type HttpMethod = 'get' | 'post' | 'put' | 'delete'
|
||||
type HttpMethodUppercase = Uppercase<HttpMethod>
|
||||
// "GET" | "POST" | "PUT" | "DELETE"
|
||||
```
|
||||
|
||||
### Lowercase\<S\>
|
||||
|
||||
Converts string literal type to lowercase.
|
||||
|
||||
```typescript
|
||||
type Greeting = 'HELLO'
|
||||
type QuietGreeting = Lowercase<Greeting> // "hello"
|
||||
```
|
||||
|
||||
### Capitalize\<S\>
|
||||
|
||||
Capitalizes the first letter of a string literal type.
|
||||
|
||||
```typescript
|
||||
type Event = 'click' | 'scroll' | 'mousemove'
|
||||
type EventHandler = `on${Capitalize<Event>}`
|
||||
// "onClick" | "onScroll" | "onMousemove"
|
||||
```
|
||||
|
||||
### Uncapitalize\<S\>
|
||||
|
||||
Uncapitalizes the first letter of a string literal type.
|
||||
|
||||
```typescript
|
||||
type Greeting = 'Hello'
|
||||
type LowerGreeting = Uncapitalize<Greeting> // "hello"
|
||||
```
|
||||
|
||||
## Async Utilities
|
||||
|
||||
### Awaited\<T\>
|
||||
|
||||
Unwraps the type of a Promise (recursively).
|
||||
|
||||
```typescript
|
||||
type T1 = Awaited<Promise<string>> // string
|
||||
type T2 = Awaited<Promise<Promise<number>>> // number
|
||||
type T3 = Awaited<boolean | Promise<string>> // boolean | string
|
||||
|
||||
// Useful with async functions
|
||||
async function fetchUser() {
|
||||
return { id: '123', name: 'Alice' }
|
||||
}
|
||||
|
||||
type User = Awaited<ReturnType<typeof fetchUser>>
|
||||
// { id: string; name: string }
|
||||
```
|
||||
|
||||
## Custom Utility Types
|
||||
|
||||
### DeepPartial\<T\>
|
||||
|
||||
Makes all properties and nested properties optional.
|
||||
|
||||
```typescript
|
||||
type DeepPartial<T> = {
|
||||
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
profile: {
|
||||
name: string
|
||||
address: {
|
||||
street: string
|
||||
city: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PartialUser = DeepPartial<User>
|
||||
// All properties at all levels are optional
|
||||
```
|
||||
|
||||
### DeepReadonly\<T\>
|
||||
|
||||
Makes all properties and nested properties readonly.
|
||||
|
||||
```typescript
|
||||
type DeepReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
profile: {
|
||||
name: string
|
||||
address: {
|
||||
street: string
|
||||
city: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ImmutableUser = DeepReadonly<User>
|
||||
// All properties at all levels are readonly
|
||||
```
|
||||
|
||||
### PartialBy\<T, K\>
|
||||
|
||||
Makes specific properties optional.
|
||||
|
||||
```typescript
|
||||
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
age: number
|
||||
}
|
||||
|
||||
type UserWithOptionalEmail = PartialBy<User, 'email' | 'age'>
|
||||
// {
|
||||
// id: string
|
||||
// name: string
|
||||
// email?: string
|
||||
// age?: number
|
||||
// }
|
||||
```
|
||||
|
||||
### RequiredBy\<T, K\>
|
||||
|
||||
Makes specific properties required.
|
||||
|
||||
```typescript
|
||||
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
|
||||
|
||||
interface User {
|
||||
id?: string
|
||||
name?: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
type UserWithRequiredId = RequiredBy<User, 'id'>
|
||||
// {
|
||||
// id: string
|
||||
// name?: string
|
||||
// email?: string
|
||||
// }
|
||||
```
|
||||
|
||||
### PickByType\<T, U\>
|
||||
|
||||
Picks properties by their value type.
|
||||
|
||||
```typescript
|
||||
type PickByType<T, U> = {
|
||||
[K in keyof T as T[K] extends U ? K : never]: T[K]
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
age: number
|
||||
active: boolean
|
||||
}
|
||||
|
||||
type StringProperties = PickByType<User, string>
|
||||
// { id: string; name: string }
|
||||
|
||||
type NumberProperties = PickByType<User, number>
|
||||
// { age: number }
|
||||
```
|
||||
|
||||
### OmitByType\<T, U\>
|
||||
|
||||
Omits properties by their value type.
|
||||
|
||||
```typescript
|
||||
type OmitByType<T, U> = {
|
||||
[K in keyof T as T[K] extends U ? never : K]: T[K]
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
age: number
|
||||
active: boolean
|
||||
}
|
||||
|
||||
type NonStringProperties = OmitByType<User, string>
|
||||
// { age: number; active: boolean }
|
||||
```
|
||||
|
||||
### Prettify\<T\>
|
||||
|
||||
Flattens intersections for better IDE tooltips.
|
||||
|
||||
```typescript
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K]
|
||||
} & {}
|
||||
|
||||
type A = { a: string }
|
||||
type B = { b: number }
|
||||
type C = A & B
|
||||
|
||||
type PrettyC = Prettify<C>
|
||||
// Displays as: { a: string; b: number }
|
||||
// Instead of: A & B
|
||||
```
|
||||
|
||||
### ValueOf\<T\>
|
||||
|
||||
Gets the union of all value types.
|
||||
|
||||
```typescript
|
||||
type ValueOf<T> = T[keyof T]
|
||||
|
||||
interface Colors {
|
||||
red: '#ff0000'
|
||||
green: '#00ff00'
|
||||
blue: '#0000ff'
|
||||
}
|
||||
|
||||
type ColorValue = ValueOf<Colors>
|
||||
// "#ff0000" | "#00ff00" | "#0000ff"
|
||||
```
|
||||
|
||||
### Nullable\<T\>
|
||||
|
||||
Makes type nullable.
|
||||
|
||||
```typescript
|
||||
type Nullable<T> = T | null
|
||||
|
||||
type NullableString = Nullable<string> // string | null
|
||||
```
|
||||
|
||||
### Maybe\<T\>
|
||||
|
||||
Makes type nullable or undefined.
|
||||
|
||||
```typescript
|
||||
type Maybe<T> = T | null | undefined
|
||||
|
||||
type MaybeString = Maybe<string> // string | null | undefined
|
||||
```
|
||||
|
||||
### UnionToIntersection\<U\>
|
||||
|
||||
Converts union to intersection (advanced).
|
||||
|
||||
```typescript
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
||||
k: infer I,
|
||||
) => void
|
||||
? I
|
||||
: never
|
||||
|
||||
type Union = { a: string } | { b: number }
|
||||
type Intersection = UnionToIntersection<Union>
|
||||
// { a: string } & { b: number }
|
||||
```
|
||||
|
||||
## Combining Utility Types
|
||||
|
||||
Utility types can be composed for powerful transformations:
|
||||
|
||||
```typescript
|
||||
// Make specific properties optional and readonly
|
||||
type PartialReadonly<T, K extends keyof T> = Readonly<Pick<T, K>> &
|
||||
Partial<Omit<T, K>>
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
type SafeUser = PartialReadonly<User, 'id' | 'name'>
|
||||
// {
|
||||
// readonly id: string
|
||||
// readonly name: string
|
||||
// email?: string
|
||||
// password?: string
|
||||
// }
|
||||
|
||||
// Pick and make readonly
|
||||
type ReadonlyPick<T, K extends keyof T> = Readonly<Pick<T, K>>
|
||||
|
||||
// Omit and make required
|
||||
type RequiredOmit<T, K extends keyof T> = Required<Omit<T, K>>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use built-in utilities first** - They're well-tested and optimized
|
||||
2. **Compose utilities** - Combine utilities for complex transformations
|
||||
3. **Create custom utilities** - For patterns you use frequently
|
||||
4. **Name utilities clearly** - Make intent obvious from the name
|
||||
5. **Document complex utilities** - Add JSDoc for non-obvious transformations
|
||||
6. **Test utility types** - Use type assertions to verify behavior
|
||||
7. **Avoid over-engineering** - Don't create utilities for one-off uses
|
||||
8. **Consider readability** - Sometimes explicit types are clearer
|
||||
9. **Use Prettify** - For better IDE tooltips with intersections
|
||||
10. **Leverage keyof** - For type-safe property selection
|
||||
|
||||
Reference in New Issue
Block a user