@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/auth-implementation-patterns",
|
||||
"installedAt": "2026-04-07T00:45:24.777Z"
|
||||
}
|
||||
638
.claude/skills/auth-implementation-patterns/SKILL.md
Normal file
638
.claude/skills/auth-implementation-patterns/SKILL.md
Normal file
@@ -0,0 +1,638 @@
|
||||
---
|
||||
name: auth-implementation-patterns
|
||||
description: Master authentication and authorization patterns including JWT, OAuth2, session management, and RBAC to build secure, scalable access control systems. Use when implementing auth systems, securing APIs, or debugging security issues.
|
||||
---
|
||||
|
||||
# Authentication & Authorization Implementation Patterns
|
||||
|
||||
Build secure, scalable authentication and authorization systems using industry-standard patterns and modern best practices.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Implementing user authentication systems
|
||||
- Securing REST or GraphQL APIs
|
||||
- Adding OAuth2/social login
|
||||
- Implementing role-based access control (RBAC)
|
||||
- Designing session management
|
||||
- Migrating authentication systems
|
||||
- Debugging auth issues
|
||||
- Implementing SSO or multi-tenancy
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Authentication vs Authorization
|
||||
|
||||
**Authentication (AuthN)**: Who are you?
|
||||
|
||||
- Verifying identity (username/password, OAuth, biometrics)
|
||||
- Issuing credentials (sessions, tokens)
|
||||
- Managing login/logout
|
||||
|
||||
**Authorization (AuthZ)**: What can you do?
|
||||
|
||||
- Permission checking
|
||||
- Role-based access control (RBAC)
|
||||
- Resource ownership validation
|
||||
- Policy enforcement
|
||||
|
||||
### 2. Authentication Strategies
|
||||
|
||||
**Session-Based:**
|
||||
|
||||
- Server stores session state
|
||||
- Session ID in cookie
|
||||
- Traditional, simple, stateful
|
||||
|
||||
**Token-Based (JWT):**
|
||||
|
||||
- Stateless, self-contained
|
||||
- Scales horizontally
|
||||
- Can store claims
|
||||
|
||||
**OAuth2/OpenID Connect:**
|
||||
|
||||
- Delegate authentication
|
||||
- Social login (Google, GitHub)
|
||||
- Enterprise SSO
|
||||
|
||||
## JWT Authentication
|
||||
|
||||
### Pattern 1: JWT Implementation
|
||||
|
||||
```typescript
|
||||
// JWT structure: header.payload.signature
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
interface JWTPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
// Generate JWT
|
||||
function generateTokens(userId: string, email: string, role: string) {
|
||||
const accessToken = jwt.sign(
|
||||
{ userId, email, role },
|
||||
process.env.JWT_SECRET!,
|
||||
{ expiresIn: "15m" }, // Short-lived
|
||||
);
|
||||
|
||||
const refreshToken = jwt.sign(
|
||||
{ userId },
|
||||
process.env.JWT_REFRESH_SECRET!,
|
||||
{ expiresIn: "7d" }, // Long-lived
|
||||
);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
// Verify JWT
|
||||
function verifyToken(token: string): JWTPayload {
|
||||
try {
|
||||
return jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
throw new Error("Token expired");
|
||||
}
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware
|
||||
function authenticate(req: Request, res: Response, next: NextFunction) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "No token provided" });
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
try {
|
||||
const payload = verifyToken(token);
|
||||
req.user = payload; // Attach user to request
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: "Invalid token" });
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
app.get("/api/profile", authenticate, (req, res) => {
|
||||
res.json({ user: req.user });
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Refresh Token Flow
|
||||
|
||||
```typescript
|
||||
interface StoredRefreshToken {
|
||||
token: string;
|
||||
userId: string;
|
||||
expiresAt: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
class RefreshTokenService {
|
||||
// Store refresh token in database
|
||||
async storeRefreshToken(userId: string, refreshToken: string) {
|
||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||||
await db.refreshTokens.create({
|
||||
token: await hash(refreshToken), // Hash before storing
|
||||
userId,
|
||||
expiresAt,
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh access token
|
||||
async refreshAccessToken(refreshToken: string) {
|
||||
// Verify refresh token
|
||||
let payload;
|
||||
try {
|
||||
payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as {
|
||||
userId: string;
|
||||
};
|
||||
} catch {
|
||||
throw new Error("Invalid refresh token");
|
||||
}
|
||||
|
||||
// Check if token exists in database
|
||||
const storedToken = await db.refreshTokens.findOne({
|
||||
where: {
|
||||
token: await hash(refreshToken),
|
||||
userId: payload.userId,
|
||||
expiresAt: { $gt: new Date() },
|
||||
},
|
||||
});
|
||||
|
||||
if (!storedToken) {
|
||||
throw new Error("Refresh token not found or expired");
|
||||
}
|
||||
|
||||
// Get user
|
||||
const user = await db.users.findById(payload.userId);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
// Generate new access token
|
||||
const accessToken = jwt.sign(
|
||||
{ userId: user.id, email: user.email, role: user.role },
|
||||
process.env.JWT_SECRET!,
|
||||
{ expiresIn: "15m" },
|
||||
);
|
||||
|
||||
return { accessToken };
|
||||
}
|
||||
|
||||
// Revoke refresh token (logout)
|
||||
async revokeRefreshToken(refreshToken: string) {
|
||||
await db.refreshTokens.deleteOne({
|
||||
token: await hash(refreshToken),
|
||||
});
|
||||
}
|
||||
|
||||
// Revoke all user tokens (logout all devices)
|
||||
async revokeAllUserTokens(userId: string) {
|
||||
await db.refreshTokens.deleteMany({ userId });
|
||||
}
|
||||
}
|
||||
|
||||
// API endpoints
|
||||
app.post("/api/auth/refresh", async (req, res) => {
|
||||
const { refreshToken } = req.body;
|
||||
try {
|
||||
const { accessToken } =
|
||||
await refreshTokenService.refreshAccessToken(refreshToken);
|
||||
res.json({ accessToken });
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: "Invalid refresh token" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/auth/logout", authenticate, async (req, res) => {
|
||||
const { refreshToken } = req.body;
|
||||
await refreshTokenService.revokeRefreshToken(refreshToken);
|
||||
res.json({ message: "Logged out successfully" });
|
||||
});
|
||||
```
|
||||
|
||||
## Session-Based Authentication
|
||||
|
||||
### Pattern 1: Express Session
|
||||
|
||||
```typescript
|
||||
import session from "express-session";
|
||||
import RedisStore from "connect-redis";
|
||||
import { createClient } from "redis";
|
||||
|
||||
// Setup Redis for session storage
|
||||
const redisClient = createClient({
|
||||
url: process.env.REDIS_URL,
|
||||
});
|
||||
await redisClient.connect();
|
||||
|
||||
app.use(
|
||||
session({
|
||||
store: new RedisStore({ client: redisClient }),
|
||||
secret: process.env.SESSION_SECRET!,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === "production", // HTTPS only
|
||||
httpOnly: true, // No JavaScript access
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
sameSite: "strict", // CSRF protection
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Login
|
||||
app.post("/api/auth/login", async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const user = await db.users.findOne({ email });
|
||||
if (!user || !(await verifyPassword(password, user.passwordHash))) {
|
||||
return res.status(401).json({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
// Store user in session
|
||||
req.session.userId = user.id;
|
||||
req.session.role = user.role;
|
||||
|
||||
res.json({ user: { id: user.id, email: user.email, role: user.role } });
|
||||
});
|
||||
|
||||
// Session middleware
|
||||
function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.session.userId) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// Protected route
|
||||
app.get("/api/profile", requireAuth, async (req, res) => {
|
||||
const user = await db.users.findById(req.session.userId);
|
||||
res.json({ user });
|
||||
});
|
||||
|
||||
// Logout
|
||||
app.post("/api/auth/logout", (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: "Logout failed" });
|
||||
}
|
||||
res.clearCookie("connect.sid");
|
||||
res.json({ message: "Logged out successfully" });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## OAuth2 / Social Login
|
||||
|
||||
### Pattern 1: OAuth2 with Passport.js
|
||||
|
||||
```typescript
|
||||
import passport from "passport";
|
||||
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
|
||||
import { Strategy as GitHubStrategy } from "passport-github2";
|
||||
|
||||
// Google OAuth
|
||||
passport.use(
|
||||
new GoogleStrategy(
|
||||
{
|
||||
clientID: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||
callbackURL: "/api/auth/google/callback",
|
||||
},
|
||||
async (accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
// Find or create user
|
||||
let user = await db.users.findOne({
|
||||
googleId: profile.id,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = await db.users.create({
|
||||
googleId: profile.id,
|
||||
email: profile.emails?.[0]?.value,
|
||||
name: profile.displayName,
|
||||
avatar: profile.photos?.[0]?.value,
|
||||
});
|
||||
}
|
||||
|
||||
return done(null, user);
|
||||
} catch (error) {
|
||||
return done(error, undefined);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Routes
|
||||
app.get(
|
||||
"/api/auth/google",
|
||||
passport.authenticate("google", {
|
||||
scope: ["profile", "email"],
|
||||
}),
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/auth/google/callback",
|
||||
passport.authenticate("google", { session: false }),
|
||||
(req, res) => {
|
||||
// Generate JWT
|
||||
const tokens = generateTokens(req.user.id, req.user.email, req.user.role);
|
||||
// Redirect to frontend with token
|
||||
res.redirect(
|
||||
`${process.env.FRONTEND_URL}/auth/callback?token=${tokens.accessToken}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Authorization Patterns
|
||||
|
||||
### Pattern 1: Role-Based Access Control (RBAC)
|
||||
|
||||
```typescript
|
||||
enum Role {
|
||||
USER = "user",
|
||||
MODERATOR = "moderator",
|
||||
ADMIN = "admin",
|
||||
}
|
||||
|
||||
const roleHierarchy: Record<Role, Role[]> = {
|
||||
[Role.ADMIN]: [Role.ADMIN, Role.MODERATOR, Role.USER],
|
||||
[Role.MODERATOR]: [Role.MODERATOR, Role.USER],
|
||||
[Role.USER]: [Role.USER],
|
||||
};
|
||||
|
||||
function hasRole(userRole: Role, requiredRole: Role): boolean {
|
||||
return roleHierarchy[userRole].includes(requiredRole);
|
||||
}
|
||||
|
||||
// Middleware
|
||||
function requireRole(...roles: Role[]) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
|
||||
if (!roles.some((role) => hasRole(req.user.role, role))) {
|
||||
return res.status(403).json({ error: "Insufficient permissions" });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
app.delete(
|
||||
"/api/users/:id",
|
||||
authenticate,
|
||||
requireRole(Role.ADMIN),
|
||||
async (req, res) => {
|
||||
// Only admins can delete users
|
||||
await db.users.delete(req.params.id);
|
||||
res.json({ message: "User deleted" });
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Pattern 2: Permission-Based Access Control
|
||||
|
||||
```typescript
|
||||
enum Permission {
|
||||
READ_USERS = "read:users",
|
||||
WRITE_USERS = "write:users",
|
||||
DELETE_USERS = "delete:users",
|
||||
READ_POSTS = "read:posts",
|
||||
WRITE_POSTS = "write:posts",
|
||||
}
|
||||
|
||||
const rolePermissions: Record<Role, Permission[]> = {
|
||||
[Role.USER]: [Permission.READ_POSTS, Permission.WRITE_POSTS],
|
||||
[Role.MODERATOR]: [
|
||||
Permission.READ_POSTS,
|
||||
Permission.WRITE_POSTS,
|
||||
Permission.READ_USERS,
|
||||
],
|
||||
[Role.ADMIN]: Object.values(Permission),
|
||||
};
|
||||
|
||||
function hasPermission(userRole: Role, permission: Permission): boolean {
|
||||
return rolePermissions[userRole]?.includes(permission) ?? false;
|
||||
}
|
||||
|
||||
function requirePermission(...permissions: Permission[]) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
|
||||
const hasAllPermissions = permissions.every((permission) =>
|
||||
hasPermission(req.user.role, permission),
|
||||
);
|
||||
|
||||
if (!hasAllPermissions) {
|
||||
return res.status(403).json({ error: "Insufficient permissions" });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
app.get(
|
||||
"/api/users",
|
||||
authenticate,
|
||||
requirePermission(Permission.READ_USERS),
|
||||
async (req, res) => {
|
||||
const users = await db.users.findAll();
|
||||
res.json({ users });
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Pattern 3: Resource Ownership
|
||||
|
||||
```typescript
|
||||
// Check if user owns resource
|
||||
async function requireOwnership(
|
||||
resourceType: "post" | "comment",
|
||||
resourceIdParam: string = "id",
|
||||
) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
|
||||
const resourceId = req.params[resourceIdParam];
|
||||
|
||||
// Admins can access anything
|
||||
if (req.user.role === Role.ADMIN) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
let resource;
|
||||
if (resourceType === "post") {
|
||||
resource = await db.posts.findById(resourceId);
|
||||
} else if (resourceType === "comment") {
|
||||
resource = await db.comments.findById(resourceId);
|
||||
}
|
||||
|
||||
if (!resource) {
|
||||
return res.status(404).json({ error: "Resource not found" });
|
||||
}
|
||||
|
||||
if (resource.userId !== req.user.userId) {
|
||||
return res.status(403).json({ error: "Not authorized" });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
app.put(
|
||||
"/api/posts/:id",
|
||||
authenticate,
|
||||
requireOwnership("post"),
|
||||
async (req, res) => {
|
||||
// User can only update their own posts
|
||||
const post = await db.posts.update(req.params.id, req.body);
|
||||
res.json({ post });
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Pattern 1: Password Security
|
||||
|
||||
```typescript
|
||||
import bcrypt from "bcrypt";
|
||||
import { z } from "zod";
|
||||
|
||||
// Password validation schema
|
||||
const passwordSchema = z
|
||||
.string()
|
||||
.min(12, "Password must be at least 12 characters")
|
||||
.regex(/[A-Z]/, "Password must contain uppercase letter")
|
||||
.regex(/[a-z]/, "Password must contain lowercase letter")
|
||||
.regex(/[0-9]/, "Password must contain number")
|
||||
.regex(/[^A-Za-z0-9]/, "Password must contain special character");
|
||||
|
||||
// Hash password
|
||||
async function hashPassword(password: string): Promise<string> {
|
||||
const saltRounds = 12; // 2^12 iterations
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
|
||||
// Verify password
|
||||
async function verifyPassword(
|
||||
password: string,
|
||||
hash: string,
|
||||
): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
// Registration with password validation
|
||||
app.post("/api/auth/register", async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Validate password
|
||||
passwordSchema.parse(password);
|
||||
|
||||
// Check if user exists
|
||||
const existingUser = await db.users.findOne({ email });
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: "Email already registered" });
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
// Create user
|
||||
const user = await db.users.create({
|
||||
email,
|
||||
passwordHash,
|
||||
});
|
||||
|
||||
// Generate tokens
|
||||
const tokens = generateTokens(user.id, user.email, user.role);
|
||||
|
||||
res.status(201).json({
|
||||
user: { id: user.id, email: user.email },
|
||||
...tokens,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ error: error.errors[0].message });
|
||||
}
|
||||
res.status(500).json({ error: "Registration failed" });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Rate Limiting
|
||||
|
||||
```typescript
|
||||
import rateLimit from "express-rate-limit";
|
||||
import RedisStore from "rate-limit-redis";
|
||||
|
||||
// Login rate limiter
|
||||
const loginLimiter = rateLimit({
|
||||
store: new RedisStore({ client: redisClient }),
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // 5 attempts
|
||||
message: "Too many login attempts, please try again later",
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
// API rate limiter
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 100, // 100 requests per minute
|
||||
standardHeaders: true,
|
||||
});
|
||||
|
||||
// Apply to routes
|
||||
app.post("/api/auth/login", loginLimiter, async (req, res) => {
|
||||
// Login logic
|
||||
});
|
||||
|
||||
app.use("/api/", apiLimiter);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Never Store Plain Passwords**: Always hash with bcrypt/argon2
|
||||
2. **Use HTTPS**: Encrypt data in transit
|
||||
3. **Short-Lived Access Tokens**: 15-30 minutes max
|
||||
4. **Secure Cookies**: httpOnly, secure, sameSite flags
|
||||
5. **Validate All Input**: Email format, password strength
|
||||
6. **Rate Limit Auth Endpoints**: Prevent brute force attacks
|
||||
7. **Implement CSRF Protection**: For session-based auth
|
||||
8. **Rotate Secrets Regularly**: JWT secrets, session secrets
|
||||
9. **Log Security Events**: Login attempts, failed auth
|
||||
10. **Use MFA When Possible**: Extra security layer
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Weak Passwords**: Enforce strong password policies
|
||||
- **JWT in localStorage**: Vulnerable to XSS, use httpOnly cookies
|
||||
- **No Token Expiration**: Tokens should expire
|
||||
- **Client-Side Auth Checks Only**: Always validate server-side
|
||||
- **Insecure Password Reset**: Use secure tokens with expiration
|
||||
- **No Rate Limiting**: Vulnerable to brute force
|
||||
- **Trusting Client Data**: Always validate on server
|
||||
6
.claude/skills/broken-authentication/.openskills.json
Normal file
6
.claude/skills/broken-authentication/.openskills.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/broken-authentication",
|
||||
"installedAt": "2026-04-07T00:45:24.780Z"
|
||||
}
|
||||
480
.claude/skills/broken-authentication/SKILL.md
Normal file
480
.claude/skills/broken-authentication/SKILL.md
Normal file
@@ -0,0 +1,480 @@
|
||||
---
|
||||
name: broken-authentication
|
||||
description: "Identify and exploit authentication and session management vulnerabilities in web applications. Broken authentication consistently ranks in the OWASP Top 10 and can lead to account takeover, identity theft, and unauthorized access to sensitive systems."
|
||||
risk: unknown
|
||||
source: community
|
||||
author: zebbern
|
||||
date_added: "2026-02-27"
|
||||
---
|
||||
|
||||
# Broken Authentication Testing
|
||||
|
||||
## Purpose
|
||||
|
||||
Identify and exploit authentication and session management vulnerabilities in web applications. Broken authentication consistently ranks in the OWASP Top 10 and can lead to account takeover, identity theft, and unauthorized access to sensitive systems. This skill covers testing methodologies for password policies, session handling, multi-factor authentication, and credential management.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required Knowledge
|
||||
- HTTP protocol and session mechanisms
|
||||
- Authentication types (SFA, 2FA, MFA)
|
||||
- Cookie and token handling
|
||||
- Common authentication frameworks
|
||||
|
||||
### Required Tools
|
||||
- Burp Suite Professional or Community
|
||||
- Hydra or similar brute-force tools
|
||||
- Custom wordlists for credential testing
|
||||
- Browser developer tools
|
||||
|
||||
### Required Access
|
||||
- Target application URL
|
||||
- Test account credentials
|
||||
- Written authorization for testing
|
||||
|
||||
## Outputs and Deliverables
|
||||
|
||||
1. **Authentication Assessment Report** - Document all identified vulnerabilities
|
||||
2. **Credential Testing Results** - Brute-force and dictionary attack outcomes
|
||||
3. **Session Security Analysis** - Token randomness and timeout evaluation
|
||||
4. **Remediation Recommendations** - Security hardening guidance
|
||||
|
||||
## Core Workflow
|
||||
|
||||
### Phase 1: Authentication Mechanism Analysis
|
||||
|
||||
Understand the application's authentication architecture:
|
||||
|
||||
```
|
||||
# Identify authentication type
|
||||
- Password-based (forms, basic auth, digest)
|
||||
- Token-based (JWT, OAuth, API keys)
|
||||
- Certificate-based (mutual TLS)
|
||||
- Multi-factor (SMS, TOTP, hardware tokens)
|
||||
|
||||
# Map authentication endpoints
|
||||
/login, /signin, /authenticate
|
||||
/register, /signup
|
||||
/forgot-password, /reset-password
|
||||
/logout, /signout
|
||||
/api/auth/*, /oauth/*
|
||||
```
|
||||
|
||||
Capture and analyze authentication requests:
|
||||
|
||||
```http
|
||||
POST /login HTTP/1.1
|
||||
Host: target.com
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
username=test&password=test123
|
||||
```
|
||||
|
||||
### Phase 2: Password Policy Testing
|
||||
|
||||
Evaluate password requirements and enforcement:
|
||||
|
||||
```bash
|
||||
# Test minimum length (a, ab, abcdefgh)
|
||||
# Test complexity (password, password1, Password1!)
|
||||
# Test common weak passwords (123456, password, qwerty, admin)
|
||||
# Test username as password (admin/admin, test/test)
|
||||
```
|
||||
|
||||
Document policy gaps: Minimum length <8, no complexity, common passwords allowed, username as password.
|
||||
|
||||
### Phase 3: Credential Enumeration
|
||||
|
||||
Test for username enumeration vulnerabilities:
|
||||
|
||||
```bash
|
||||
# Compare responses for valid vs invalid usernames
|
||||
# Invalid: "Invalid username" vs Valid: "Invalid password"
|
||||
# Check timing differences, response codes, registration messages
|
||||
```
|
||||
|
||||
# Password reset
|
||||
"Email sent if account exists" (secure)
|
||||
"No account with that email" (leaks info)
|
||||
|
||||
# API responses
|
||||
{"error": "user_not_found"}
|
||||
{"error": "invalid_password"}
|
||||
```
|
||||
|
||||
### Phase 4: Brute Force Testing
|
||||
|
||||
Test account lockout and rate limiting:
|
||||
|
||||
```bash
|
||||
# Using Hydra for form-based auth
|
||||
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
|
||||
target.com http-post-form \
|
||||
"/login:username=^USER^&password=^PASS^:Invalid credentials"
|
||||
|
||||
# Using Burp Intruder
|
||||
1. Capture login request
|
||||
2. Send to Intruder
|
||||
3. Set payload positions on password field
|
||||
4. Load wordlist
|
||||
5. Start attack
|
||||
6. Analyze response lengths/codes
|
||||
```
|
||||
|
||||
Check for protections:
|
||||
|
||||
```bash
|
||||
# Account lockout
|
||||
- After how many attempts?
|
||||
- Duration of lockout?
|
||||
- Lockout notification?
|
||||
|
||||
# Rate limiting
|
||||
- Requests per minute limit?
|
||||
- IP-based or account-based?
|
||||
- Bypass via headers (X-Forwarded-For)?
|
||||
|
||||
# CAPTCHA
|
||||
- After failed attempts?
|
||||
- Easily bypassable?
|
||||
```
|
||||
|
||||
### Phase 5: Credential Stuffing
|
||||
|
||||
Test with known breached credentials:
|
||||
|
||||
```bash
|
||||
# Credential stuffing differs from brute force
|
||||
# Uses known email:password pairs from breaches
|
||||
|
||||
# Using Burp Intruder with Pitchfork attack
|
||||
1. Set username and password as positions
|
||||
2. Load email list as payload 1
|
||||
3. Load password list as payload 2 (matched pairs)
|
||||
4. Analyze for successful logins
|
||||
|
||||
# Detection evasion
|
||||
- Slow request rate
|
||||
- Rotate source IPs
|
||||
- Randomize user agents
|
||||
- Add delays between attempts
|
||||
```
|
||||
|
||||
### Phase 6: Session Management Testing
|
||||
|
||||
Analyze session token security:
|
||||
|
||||
```bash
|
||||
# Capture session cookie
|
||||
Cookie: SESSIONID=abc123def456
|
||||
|
||||
# Test token characteristics
|
||||
1. Entropy - Is it random enough?
|
||||
2. Length - Sufficient length (128+ bits)?
|
||||
3. Predictability - Sequential patterns?
|
||||
4. Secure flags - HttpOnly, Secure, SameSite?
|
||||
```
|
||||
|
||||
Session token analysis:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import requests
|
||||
import hashlib
|
||||
|
||||
# Collect multiple session tokens
|
||||
tokens = []
|
||||
for i in range(100):
|
||||
response = requests.get("https://target.com/login")
|
||||
token = response.cookies.get("SESSIONID")
|
||||
tokens.append(token)
|
||||
|
||||
# Analyze for patterns
|
||||
# Check for sequential increments
|
||||
# Calculate entropy
|
||||
# Look for timestamp components
|
||||
```
|
||||
|
||||
### Phase 7: Session Fixation Testing
|
||||
|
||||
Test if session is regenerated after authentication:
|
||||
|
||||
```bash
|
||||
# Step 1: Get session before login
|
||||
GET /login HTTP/1.1
|
||||
Response: Set-Cookie: SESSIONID=abc123
|
||||
|
||||
# Step 2: Login with same session
|
||||
POST /login HTTP/1.1
|
||||
Cookie: SESSIONID=abc123
|
||||
username=valid&password=valid
|
||||
|
||||
# Step 3: Check if session changed
|
||||
# VULNERABLE if SESSIONID remains abc123
|
||||
# SECURE if new session assigned after login
|
||||
```
|
||||
|
||||
Attack scenario:
|
||||
|
||||
```bash
|
||||
# Attacker workflow:
|
||||
1. Attacker visits site, gets session: SESSIONID=attacker_session
|
||||
2. Attacker sends link to victim with fixed session:
|
||||
https://target.com/login?SESSIONID=attacker_session
|
||||
3. Victim logs in with attacker's session
|
||||
4. Attacker now has authenticated session
|
||||
```
|
||||
|
||||
### Phase 8: Session Timeout Testing
|
||||
|
||||
Verify session expiration policies:
|
||||
|
||||
```bash
|
||||
# Test idle timeout
|
||||
1. Login and note session cookie
|
||||
2. Wait without activity (15, 30, 60 minutes)
|
||||
3. Attempt to use session
|
||||
4. Check if session is still valid
|
||||
|
||||
# Test absolute timeout
|
||||
1. Login and continuously use session
|
||||
2. Check if forced logout after set period (8 hours, 24 hours)
|
||||
|
||||
# Test logout functionality
|
||||
1. Login and note session
|
||||
2. Click logout
|
||||
3. Attempt to reuse old session cookie
|
||||
4. Session should be invalidated server-side
|
||||
```
|
||||
|
||||
### Phase 9: Multi-Factor Authentication Testing
|
||||
|
||||
Assess MFA implementation security:
|
||||
|
||||
```bash
|
||||
# OTP brute force
|
||||
- 4-digit OTP = 10,000 combinations
|
||||
- 6-digit OTP = 1,000,000 combinations
|
||||
- Test rate limiting on OTP endpoint
|
||||
|
||||
# OTP bypass techniques
|
||||
- Skip MFA step by direct URL access
|
||||
- Modify response to indicate MFA passed
|
||||
- Null/empty OTP submission
|
||||
- Previous valid OTP reuse
|
||||
|
||||
# API Version Downgrade Attack (crAPI example)
|
||||
# If /api/v3/check-otp has rate limiting, try older versions:
|
||||
POST /api/v2/check-otp
|
||||
{"otp": "1234"}
|
||||
# Older API versions may lack security controls
|
||||
|
||||
# Using Burp for OTP testing
|
||||
1. Capture OTP verification request
|
||||
2. Send to Intruder
|
||||
3. Set OTP field as payload position
|
||||
4. Use numbers payload (0000-9999)
|
||||
5. Check for successful bypass
|
||||
```
|
||||
|
||||
Test MFA enrollment:
|
||||
|
||||
```bash
|
||||
# Forced enrollment
|
||||
- Can MFA be skipped during setup?
|
||||
- Can backup codes be accessed without verification?
|
||||
|
||||
# Recovery process
|
||||
- Can MFA be disabled via email alone?
|
||||
- Social engineering potential?
|
||||
```
|
||||
|
||||
### Phase 10: Password Reset Testing
|
||||
|
||||
Analyze password reset security:
|
||||
|
||||
```bash
|
||||
# Token security
|
||||
1. Request password reset
|
||||
2. Capture reset link
|
||||
3. Analyze token:
|
||||
- Length and randomness
|
||||
- Expiration time
|
||||
- Single-use enforcement
|
||||
- Account binding
|
||||
|
||||
# Token manipulation
|
||||
https://target.com/reset?token=abc123&user=victim
|
||||
# Try changing user parameter while using valid token
|
||||
|
||||
# Host header injection
|
||||
POST /forgot-password HTTP/1.1
|
||||
Host: attacker.com
|
||||
email=victim@email.com
|
||||
# Reset email may contain attacker's domain
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Common Vulnerability Types
|
||||
|
||||
| Vulnerability | Risk | Test Method |
|
||||
|--------------|------|-------------|
|
||||
| Weak passwords | High | Policy testing, dictionary attack |
|
||||
| No lockout | High | Brute force testing |
|
||||
| Username enumeration | Medium | Differential response analysis |
|
||||
| Session fixation | High | Pre/post-login session comparison |
|
||||
| Weak session tokens | High | Entropy analysis |
|
||||
| No session timeout | Medium | Long-duration session testing |
|
||||
| Insecure password reset | High | Token analysis, workflow bypass |
|
||||
| MFA bypass | Critical | Direct access, response manipulation |
|
||||
|
||||
### Credential Testing Payloads
|
||||
|
||||
```bash
|
||||
# Default credentials
|
||||
admin:admin
|
||||
admin:password
|
||||
admin:123456
|
||||
root:root
|
||||
test:test
|
||||
user:user
|
||||
|
||||
# Common passwords
|
||||
123456
|
||||
password
|
||||
12345678
|
||||
qwerty
|
||||
abc123
|
||||
password1
|
||||
admin123
|
||||
|
||||
# Breached credential databases
|
||||
- Have I Been Pwned dataset
|
||||
- SecLists passwords
|
||||
- Custom targeted lists
|
||||
```
|
||||
|
||||
### Session Cookie Flags
|
||||
|
||||
| Flag | Purpose | Vulnerability if Missing |
|
||||
|------|---------|------------------------|
|
||||
| HttpOnly | Prevent JS access | XSS can steal session |
|
||||
| Secure | HTTPS only | Sent over HTTP |
|
||||
| SameSite | CSRF protection | Cross-site requests allowed |
|
||||
| Path | URL scope | Broader exposure |
|
||||
| Domain | Domain scope | Subdomain access |
|
||||
| Expires | Lifetime | Persistent sessions |
|
||||
|
||||
### Rate Limiting Bypass Headers
|
||||
|
||||
```http
|
||||
X-Forwarded-For: 127.0.0.1
|
||||
X-Real-IP: 127.0.0.1
|
||||
X-Originating-IP: 127.0.0.1
|
||||
X-Client-IP: 127.0.0.1
|
||||
X-Remote-IP: 127.0.0.1
|
||||
True-Client-IP: 127.0.0.1
|
||||
```
|
||||
|
||||
## Constraints and Limitations
|
||||
|
||||
### Legal Requirements
|
||||
- Only test with explicit written authorization
|
||||
- Avoid testing with real breached credentials
|
||||
- Do not access actual user accounts
|
||||
- Document all testing activities
|
||||
|
||||
### Technical Limitations
|
||||
- CAPTCHA may prevent automated testing
|
||||
- Rate limiting affects brute force timing
|
||||
- MFA significantly increases attack difficulty
|
||||
- Some vulnerabilities require victim interaction
|
||||
|
||||
### Scope Considerations
|
||||
- Test accounts may behave differently than production
|
||||
- Some features may be disabled in test environments
|
||||
- Third-party authentication may be out of scope
|
||||
- Production testing requires extra caution
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Account Lockout Bypass
|
||||
|
||||
**Scenario:** Test if account lockout can be bypassed
|
||||
|
||||
```bash
|
||||
# Step 1: Identify lockout threshold
|
||||
# Try 5 wrong passwords for admin account
|
||||
# Result: "Account locked for 30 minutes"
|
||||
|
||||
# Step 2: Test bypass via IP rotation
|
||||
# Use X-Forwarded-For header
|
||||
POST /login HTTP/1.1
|
||||
X-Forwarded-For: 192.168.1.1
|
||||
username=admin&password=attempt1
|
||||
|
||||
# Increment IP for each attempt
|
||||
X-Forwarded-For: 192.168.1.2
|
||||
# Continue until successful or confirmed blocked
|
||||
|
||||
# Step 3: Test bypass via case manipulation
|
||||
username=Admin (vs admin)
|
||||
username=ADMIN
|
||||
# Some systems treat these as different accounts
|
||||
```
|
||||
|
||||
### Example 2: JWT Token Attack
|
||||
|
||||
**Scenario:** Exploit weak JWT implementation
|
||||
|
||||
```bash
|
||||
# Step 1: Capture JWT token
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdCJ9.signature
|
||||
|
||||
# Step 2: Decode and analyze
|
||||
# Header: {"alg":"HS256","typ":"JWT"}
|
||||
# Payload: {"user":"test","role":"user"}
|
||||
|
||||
# Step 3: Try "none" algorithm attack
|
||||
# Change header to: {"alg":"none","typ":"JWT"}
|
||||
# Remove signature
|
||||
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4ifQ.
|
||||
|
||||
# Step 4: Submit modified token
|
||||
Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYWRtaW4ifQ.
|
||||
```
|
||||
|
||||
### Example 3: Password Reset Token Exploitation
|
||||
|
||||
**Scenario:** Test password reset functionality
|
||||
|
||||
```bash
|
||||
# Step 1: Request reset for test account
|
||||
POST /forgot-password
|
||||
email=test@example.com
|
||||
|
||||
# Step 2: Capture reset link
|
||||
https://target.com/reset?token=a1b2c3d4e5f6
|
||||
|
||||
# Step 3: Test token properties
|
||||
# Reuse: Try using same token twice
|
||||
# Expiration: Wait 24+ hours and retry
|
||||
# Modification: Change characters in token
|
||||
|
||||
# Step 4: Test for user parameter manipulation
|
||||
https://target.com/reset?token=a1b2c3d4e5f6&email=admin@example.com
|
||||
# Check if admin's password can be reset with test user's token
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solutions |
|
||||
|-------|-----------|
|
||||
| Brute force too slow | Identify rate limit scope; IP rotation; add delays; use targeted wordlists |
|
||||
| Session analysis inconclusive | Collect 1000+ tokens; use statistical tools; check for timestamps; compare accounts |
|
||||
| MFA cannot be bypassed | Document as secure; test backup/recovery mechanisms; check MFA fatigue; verify enrollment |
|
||||
| Account lockout prevents testing | Request multiple test accounts; test threshold first; use slower timing |
|
||||
|
||||
## When to Use
|
||||
This skill is applicable to execute the workflow or actions described in the overview.
|
||||
6
.claude/skills/bun-development/.openskills.json
Normal file
6
.claude/skills/bun-development/.openskills.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/bun-development",
|
||||
"installedAt": "2026-04-07T00:45:24.781Z"
|
||||
}
|
||||
696
.claude/skills/bun-development/SKILL.md
Normal file
696
.claude/skills/bun-development/SKILL.md
Normal file
@@ -0,0 +1,696 @@
|
||||
---
|
||||
name: bun-development
|
||||
description: "Fast, modern JavaScript/TypeScript development with the Bun runtime, inspired by [oven-sh/bun](https://github.com/oven-sh/bun)."
|
||||
risk: critical
|
||||
source: community
|
||||
date_added: "2026-02-27"
|
||||
---
|
||||
|
||||
<!-- security-allowlist: curl-pipe-bash, irm-pipe-iex -->
|
||||
|
||||
# ⚡ Bun Development
|
||||
|
||||
> Fast, modern JavaScript/TypeScript development with the Bun runtime, inspired by [oven-sh/bun](https://github.com/oven-sh/bun).
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Starting new JS/TS projects with Bun
|
||||
- Migrating from Node.js to Bun
|
||||
- Optimizing development speed
|
||||
- Using Bun's built-in tools (bundler, test runner)
|
||||
- Troubleshooting Bun-specific issues
|
||||
|
||||
---
|
||||
|
||||
## 1. Getting Started
|
||||
|
||||
### 1.1 Installation
|
||||
|
||||
```bash
|
||||
# macOS / Linux
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
|
||||
# Windows
|
||||
powershell -c "irm bun.sh/install.ps1 | iex"
|
||||
|
||||
# Homebrew
|
||||
brew tap oven-sh/bun
|
||||
brew install bun
|
||||
|
||||
# npm (if needed)
|
||||
npm install -g bun
|
||||
|
||||
# Upgrade
|
||||
bun upgrade
|
||||
```
|
||||
|
||||
### 1.2 Why Bun?
|
||||
|
||||
| Feature | Bun | Node.js |
|
||||
| :-------------- | :------------- | :-------------------------- |
|
||||
| Startup time | ~25ms | ~100ms+ |
|
||||
| Package install | 10-100x faster | Baseline |
|
||||
| TypeScript | Native | Requires transpiler |
|
||||
| JSX | Native | Requires transpiler |
|
||||
| Test runner | Built-in | External (Jest, Vitest) |
|
||||
| Bundler | Built-in | External (Webpack, esbuild) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Project Setup
|
||||
|
||||
### 2.1 Create New Project
|
||||
|
||||
```bash
|
||||
# Initialize project
|
||||
bun init
|
||||
|
||||
# Creates:
|
||||
# ├── package.json
|
||||
# ├── tsconfig.json
|
||||
# ├── index.ts
|
||||
# └── README.md
|
||||
|
||||
# With specific template
|
||||
bun create <template> <project-name>
|
||||
|
||||
# Examples
|
||||
bun create react my-app # React app
|
||||
bun create next my-app # Next.js app
|
||||
bun create vite my-app # Vite app
|
||||
bun create elysia my-api # Elysia API
|
||||
```
|
||||
|
||||
### 2.2 package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-bun-project",
|
||||
"version": "1.0.0",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch index.ts",
|
||||
"start": "bun run index.ts",
|
||||
"test": "bun test",
|
||||
"build": "bun build ./index.ts --outdir ./dist",
|
||||
"lint": "bunx eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 tsconfig.json (Bun-optimized)
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react-jsx",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
"types": ["bun-types"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Package Management
|
||||
|
||||
### 3.1 Installing Packages
|
||||
|
||||
```bash
|
||||
# Install from package.json
|
||||
bun install # or 'bun i'
|
||||
|
||||
# Add dependencies
|
||||
bun add express # Regular dependency
|
||||
bun add -d typescript # Dev dependency
|
||||
bun add -D @types/node # Dev dependency (alias)
|
||||
bun add --optional pkg # Optional dependency
|
||||
|
||||
# From specific registry
|
||||
bun add lodash --registry https://registry.npmmirror.com
|
||||
|
||||
# Install specific version
|
||||
bun add react@18.2.0
|
||||
bun add react@latest
|
||||
bun add react@next
|
||||
|
||||
# From git
|
||||
bun add github:user/repo
|
||||
bun add git+https://github.com/user/repo.git
|
||||
```
|
||||
|
||||
### 3.2 Removing & Updating
|
||||
|
||||
```bash
|
||||
# Remove package
|
||||
bun remove lodash
|
||||
|
||||
# Update packages
|
||||
bun update # Update all
|
||||
bun update lodash # Update specific
|
||||
bun update --latest # Update to latest (ignore ranges)
|
||||
|
||||
# Check outdated
|
||||
bun outdated
|
||||
```
|
||||
|
||||
### 3.3 bunx (npx equivalent)
|
||||
|
||||
```bash
|
||||
# Execute package binaries
|
||||
bunx prettier --write .
|
||||
bunx tsc --init
|
||||
bunx create-react-app my-app
|
||||
|
||||
# With specific version
|
||||
bunx -p typescript@4.9 tsc --version
|
||||
|
||||
# Run without installing
|
||||
bunx cowsay "Hello from Bun!"
|
||||
```
|
||||
|
||||
### 3.4 Lockfile
|
||||
|
||||
```bash
|
||||
# bun.lockb is a binary lockfile (faster parsing)
|
||||
# To generate text lockfile for debugging:
|
||||
bun install --yarn # Creates yarn.lock
|
||||
|
||||
# Trust existing lockfile
|
||||
bun install --frozen-lockfile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Running Code
|
||||
|
||||
### 4.1 Basic Execution
|
||||
|
||||
```bash
|
||||
# Run TypeScript directly (no build step!)
|
||||
bun run index.ts
|
||||
|
||||
# Run JavaScript
|
||||
bun run index.js
|
||||
|
||||
# Run with arguments
|
||||
bun run server.ts --port 3000
|
||||
|
||||
# Run package.json script
|
||||
bun run dev
|
||||
bun run build
|
||||
|
||||
# Short form (for scripts)
|
||||
bun dev
|
||||
bun build
|
||||
```
|
||||
|
||||
### 4.2 Watch Mode
|
||||
|
||||
```bash
|
||||
# Auto-restart on file changes
|
||||
bun --watch run index.ts
|
||||
|
||||
# With hot reloading
|
||||
bun --hot run server.ts
|
||||
```
|
||||
|
||||
### 4.3 Environment Variables
|
||||
|
||||
```typescript
|
||||
// .env file is loaded automatically!
|
||||
|
||||
// Access environment variables
|
||||
const apiKey = Bun.env.API_KEY;
|
||||
const port = Bun.env.PORT ?? "3000";
|
||||
|
||||
// Or use process.env (Node.js compatible)
|
||||
const dbUrl = process.env.DATABASE_URL;
|
||||
```
|
||||
|
||||
```bash
|
||||
# Run with specific env file
|
||||
bun --env-file=.env.production run index.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Built-in APIs
|
||||
|
||||
### 5.1 File System (Bun.file)
|
||||
|
||||
```typescript
|
||||
// Read file
|
||||
const file = Bun.file("./data.json");
|
||||
const text = await file.text();
|
||||
const json = await file.json();
|
||||
const buffer = await file.arrayBuffer();
|
||||
|
||||
// File info
|
||||
console.log(file.size); // bytes
|
||||
console.log(file.type); // MIME type
|
||||
|
||||
// Write file
|
||||
await Bun.write("./output.txt", "Hello, Bun!");
|
||||
await Bun.write("./data.json", JSON.stringify({ foo: "bar" }));
|
||||
|
||||
// Stream large files
|
||||
const reader = file.stream();
|
||||
for await (const chunk of reader) {
|
||||
console.log(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 HTTP Server (Bun.serve)
|
||||
|
||||
```typescript
|
||||
const server = Bun.serve({
|
||||
port: 3000,
|
||||
|
||||
fetch(request) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === "/") {
|
||||
return new Response("Hello World!");
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/users") {
|
||||
return Response.json([
|
||||
{ id: 1, name: "Alice" },
|
||||
{ id: 2, name: "Bob" },
|
||||
]);
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 });
|
||||
},
|
||||
|
||||
error(error) {
|
||||
return new Response(`Error: ${error.message}`, { status: 500 });
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Server running at http://localhost:${server.port}`);
|
||||
```
|
||||
|
||||
### 5.3 WebSocket Server
|
||||
|
||||
```typescript
|
||||
const server = Bun.serve({
|
||||
port: 3000,
|
||||
|
||||
fetch(req, server) {
|
||||
// Upgrade to WebSocket
|
||||
if (server.upgrade(req)) {
|
||||
return; // Upgraded
|
||||
}
|
||||
return new Response("Upgrade failed", { status: 500 });
|
||||
},
|
||||
|
||||
websocket: {
|
||||
open(ws) {
|
||||
console.log("Client connected");
|
||||
ws.send("Welcome!");
|
||||
},
|
||||
|
||||
message(ws, message) {
|
||||
console.log(`Received: ${message}`);
|
||||
ws.send(`Echo: ${message}`);
|
||||
},
|
||||
|
||||
close(ws) {
|
||||
console.log("Client disconnected");
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 5.4 SQLite (Bun.sql)
|
||||
|
||||
```typescript
|
||||
import { Database } from "bun:sqlite";
|
||||
|
||||
const db = new Database("mydb.sqlite");
|
||||
|
||||
// Create table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE
|
||||
)
|
||||
`);
|
||||
|
||||
// Insert
|
||||
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
|
||||
insert.run("Alice", "alice@example.com");
|
||||
|
||||
// Query
|
||||
const query = db.prepare("SELECT * FROM users WHERE name = ?");
|
||||
const user = query.get("Alice");
|
||||
console.log(user); // { id: 1, name: "Alice", email: "alice@example.com" }
|
||||
|
||||
// Query all
|
||||
const allUsers = db.query("SELECT * FROM users").all();
|
||||
```
|
||||
|
||||
### 5.5 Password Hashing
|
||||
|
||||
```typescript
|
||||
// Hash password
|
||||
const password = "super-secret";
|
||||
const hash = await Bun.password.hash(password);
|
||||
|
||||
// Verify password
|
||||
const isValid = await Bun.password.verify(password, hash);
|
||||
console.log(isValid); // true
|
||||
|
||||
// With algorithm options
|
||||
const bcryptHash = await Bun.password.hash(password, {
|
||||
algorithm: "bcrypt",
|
||||
cost: 12,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Testing
|
||||
|
||||
### 6.1 Basic Tests
|
||||
|
||||
```typescript
|
||||
// math.test.ts
|
||||
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
||||
|
||||
describe("Math operations", () => {
|
||||
it("adds two numbers", () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
||||
it("subtracts two numbers", () => {
|
||||
expect(5 - 3).toBe(2);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 6.2 Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
bun test
|
||||
|
||||
# Run specific file
|
||||
bun test math.test.ts
|
||||
|
||||
# Run matching pattern
|
||||
bun test --grep "adds"
|
||||
|
||||
# Watch mode
|
||||
bun test --watch
|
||||
|
||||
# With coverage
|
||||
bun test --coverage
|
||||
|
||||
# Timeout
|
||||
bun test --timeout 5000
|
||||
```
|
||||
|
||||
### 6.3 Matchers
|
||||
|
||||
```typescript
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
test("matchers", () => {
|
||||
// Equality
|
||||
expect(1).toBe(1);
|
||||
expect({ a: 1 }).toEqual({ a: 1 });
|
||||
expect([1, 2]).toContain(1);
|
||||
|
||||
// Comparisons
|
||||
expect(10).toBeGreaterThan(5);
|
||||
expect(5).toBeLessThanOrEqual(5);
|
||||
|
||||
// Truthiness
|
||||
expect(true).toBeTruthy();
|
||||
expect(null).toBeNull();
|
||||
expect(undefined).toBeUndefined();
|
||||
|
||||
// Strings
|
||||
expect("hello").toMatch(/ell/);
|
||||
expect("hello").toContain("ell");
|
||||
|
||||
// Arrays
|
||||
expect([1, 2, 3]).toHaveLength(3);
|
||||
|
||||
// Exceptions
|
||||
expect(() => {
|
||||
throw new Error("fail");
|
||||
}).toThrow("fail");
|
||||
|
||||
// Async
|
||||
await expect(Promise.resolve(1)).resolves.toBe(1);
|
||||
await expect(Promise.reject("err")).rejects.toBe("err");
|
||||
});
|
||||
```
|
||||
|
||||
### 6.4 Mocking
|
||||
|
||||
```typescript
|
||||
import { mock, spyOn } from "bun:test";
|
||||
|
||||
// Mock function
|
||||
const mockFn = mock((x: number) => x * 2);
|
||||
mockFn(5);
|
||||
expect(mockFn).toHaveBeenCalled();
|
||||
expect(mockFn).toHaveBeenCalledWith(5);
|
||||
expect(mockFn.mock.results[0].value).toBe(10);
|
||||
|
||||
// Spy on method
|
||||
const obj = {
|
||||
method: () => "original",
|
||||
};
|
||||
const spy = spyOn(obj, "method").mockReturnValue("mocked");
|
||||
expect(obj.method()).toBe("mocked");
|
||||
expect(spy).toHaveBeenCalled();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Bundling
|
||||
|
||||
### 7.1 Basic Build
|
||||
|
||||
```bash
|
||||
# Bundle for production
|
||||
bun build ./src/index.ts --outdir ./dist
|
||||
|
||||
# With options
|
||||
bun build ./src/index.ts \
|
||||
--outdir ./dist \
|
||||
--target browser \
|
||||
--minify \
|
||||
--sourcemap
|
||||
```
|
||||
|
||||
### 7.2 Build API
|
||||
|
||||
```typescript
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/index.ts"],
|
||||
outdir: "./dist",
|
||||
target: "browser", // or "bun", "node"
|
||||
minify: true,
|
||||
sourcemap: "external",
|
||||
splitting: true,
|
||||
format: "esm",
|
||||
|
||||
// External packages (not bundled)
|
||||
external: ["react", "react-dom"],
|
||||
|
||||
// Define globals
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify("production"),
|
||||
},
|
||||
|
||||
// Naming
|
||||
naming: {
|
||||
entry: "[name].[hash].js",
|
||||
chunk: "chunks/[name].[hash].js",
|
||||
asset: "assets/[name].[hash][ext]",
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error(result.logs);
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 Compile to Executable
|
||||
|
||||
```bash
|
||||
# Create standalone executable
|
||||
bun build ./src/cli.ts --compile --outfile myapp
|
||||
|
||||
# Cross-compile
|
||||
bun build ./src/cli.ts --compile --target=bun-linux-x64 --outfile myapp-linux
|
||||
bun build ./src/cli.ts --compile --target=bun-darwin-arm64 --outfile myapp-mac
|
||||
|
||||
# With embedded assets
|
||||
bun build ./src/cli.ts --compile --outfile myapp --embed ./assets
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Migration from Node.js
|
||||
|
||||
### 8.1 Compatibility
|
||||
|
||||
```typescript
|
||||
// Most Node.js APIs work out of the box
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import crypto from "crypto";
|
||||
|
||||
// process is global
|
||||
console.log(process.cwd());
|
||||
console.log(process.env.HOME);
|
||||
|
||||
// Buffer is global
|
||||
const buf = Buffer.from("hello");
|
||||
|
||||
// __dirname and __filename work
|
||||
console.log(__dirname);
|
||||
console.log(__filename);
|
||||
```
|
||||
|
||||
### 8.2 Common Migration Steps
|
||||
|
||||
```bash
|
||||
# 1. Install Bun
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
|
||||
# 2. Replace package manager
|
||||
rm -rf node_modules package-lock.json
|
||||
bun install
|
||||
|
||||
# 3. Update scripts in package.json
|
||||
# "start": "node index.js" → "start": "bun run index.ts"
|
||||
# "test": "jest" → "test": "bun test"
|
||||
|
||||
# 4. Add Bun types
|
||||
bun add -d @types/bun
|
||||
```
|
||||
|
||||
### 8.3 Differences from Node.js
|
||||
|
||||
```typescript
|
||||
// ❌ Node.js specific (may not work)
|
||||
require("module") // Use import instead
|
||||
require.resolve("pkg") // Use import.meta.resolve
|
||||
__non_webpack_require__ // Not supported
|
||||
|
||||
// ✅ Bun equivalents
|
||||
import pkg from "pkg";
|
||||
const resolved = import.meta.resolve("pkg");
|
||||
Bun.resolveSync("pkg", process.cwd());
|
||||
|
||||
// ❌ These globals differ
|
||||
process.hrtime() // Use Bun.nanoseconds()
|
||||
setImmediate() // Use queueMicrotask()
|
||||
|
||||
// ✅ Bun-specific features
|
||||
const file = Bun.file("./data.txt"); // Fast file API
|
||||
Bun.serve({ port: 3000, fetch: ... }); // Fast HTTP server
|
||||
Bun.password.hash(password); // Built-in hashing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Performance Tips
|
||||
|
||||
### 9.1 Use Bun-native APIs
|
||||
|
||||
```typescript
|
||||
// Slow (Node.js compat)
|
||||
import fs from "fs/promises";
|
||||
const content = await fs.readFile("./data.txt", "utf-8");
|
||||
|
||||
// Fast (Bun-native)
|
||||
const file = Bun.file("./data.txt");
|
||||
const content = await file.text();
|
||||
```
|
||||
|
||||
### 9.2 Use Bun.serve for HTTP
|
||||
|
||||
```typescript
|
||||
// Don't: Express/Fastify (overhead)
|
||||
import express from "express";
|
||||
const app = express();
|
||||
|
||||
// Do: Bun.serve (native, 4-10x faster)
|
||||
Bun.serve({
|
||||
fetch(req) {
|
||||
return new Response("Hello!");
|
||||
},
|
||||
});
|
||||
|
||||
// Or use Elysia (Bun-optimized framework)
|
||||
import { Elysia } from "elysia";
|
||||
new Elysia().get("/", () => "Hello!").listen(3000);
|
||||
```
|
||||
|
||||
### 9.3 Bundle for Production
|
||||
|
||||
```bash
|
||||
# Always bundle and minify for production
|
||||
bun build ./src/index.ts --outdir ./dist --minify --target node
|
||||
|
||||
# Then run the bundle
|
||||
bun run ./dist/index.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Command |
|
||||
| :----------- | :----------------------------------------- |
|
||||
| Init project | `bun init` |
|
||||
| Install deps | `bun install` |
|
||||
| Add package | `bun add <pkg>` |
|
||||
| Run script | `bun run <script>` |
|
||||
| Run file | `bun run file.ts` |
|
||||
| Watch mode | `bun --watch run file.ts` |
|
||||
| Run tests | `bun test` |
|
||||
| Build | `bun build ./src/index.ts --outdir ./dist` |
|
||||
| Execute pkg | `bunx <pkg>` |
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Bun Documentation](https://bun.sh/docs)
|
||||
- [Bun GitHub](https://github.com/oven-sh/bun)
|
||||
- [Elysia Framework](https://elysiajs.com/)
|
||||
- [Bun Discord](https://bun.sh/discord)
|
||||
6
.claude/skills/drizzle-orm-expert/.openskills.json
Normal file
6
.claude/skills/drizzle-orm-expert/.openskills.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/drizzle-orm-expert",
|
||||
"installedAt": "2026-04-07T00:45:24.781Z"
|
||||
}
|
||||
363
.claude/skills/drizzle-orm-expert/SKILL.md
Normal file
363
.claude/skills/drizzle-orm-expert/SKILL.md
Normal file
@@ -0,0 +1,363 @@
|
||||
---
|
||||
name: drizzle-orm-expert
|
||||
description: "Expert in Drizzle ORM for TypeScript — schema design, relational queries, migrations, and serverless database integration. Use when building type-safe database layers with Drizzle."
|
||||
risk: safe
|
||||
source: community
|
||||
date_added: "2026-03-04"
|
||||
---
|
||||
|
||||
# Drizzle ORM Expert
|
||||
|
||||
You are a production-grade Drizzle ORM expert. You help developers build type-safe, performant database layers using Drizzle ORM with TypeScript. You know schema design, the relational query API, Drizzle Kit migrations, and integrations with Next.js, tRPC, and serverless databases (Neon, PlanetScale, Turso, Supabase).
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Use when the user asks to set up Drizzle ORM in a new or existing project
|
||||
- Use when designing database schemas with Drizzle's TypeScript-first approach
|
||||
- Use when writing complex relational queries (joins, subqueries, aggregations)
|
||||
- Use when setting up or troubleshooting Drizzle Kit migrations
|
||||
- Use when integrating Drizzle with Next.js App Router, tRPC, or Hono
|
||||
- Use when optimizing database performance (prepared statements, batching, connection pooling)
|
||||
- Use when migrating from Prisma, TypeORM, or Knex to Drizzle
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Why Drizzle
|
||||
|
||||
Drizzle ORM is a TypeScript-first ORM that generates zero runtime overhead. Unlike Prisma (which uses a query engine binary), Drizzle compiles to raw SQL — making it ideal for edge runtimes and serverless. Key advantages:
|
||||
|
||||
- **SQL-like API**: If you know SQL, you know Drizzle
|
||||
- **Zero dependencies**: Tiny bundle, works in Cloudflare Workers, Vercel Edge, Deno
|
||||
- **Full type inference**: Schema → types → queries are all connected at compile time
|
||||
- **Relational Query API**: Prisma-like nested includes without N+1 problems
|
||||
|
||||
## Schema Design Patterns
|
||||
|
||||
### Table Definitions
|
||||
|
||||
```typescript
|
||||
// db/schema.ts
|
||||
import { pgTable, text, integer, timestamp, boolean, uuid, pgEnum } from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
|
||||
// Enums
|
||||
export const roleEnum = pgEnum("role", ["admin", "user", "moderator"]);
|
||||
|
||||
// Users table
|
||||
export const users = pgTable("users", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
email: text("email").notNull().unique(),
|
||||
name: text("name").notNull(),
|
||||
role: roleEnum("role").default("user").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Posts table with foreign key
|
||||
export const posts = pgTable("posts", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
title: text("title").notNull(),
|
||||
content: text("content"),
|
||||
published: boolean("published").default(false).notNull(),
|
||||
authorId: uuid("author_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
```
|
||||
|
||||
### Relations
|
||||
|
||||
```typescript
|
||||
// db/relations.ts
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
posts: many(posts),
|
||||
}));
|
||||
|
||||
export const postsRelations = relations(posts, ({ one }) => ({
|
||||
author: one(users, {
|
||||
fields: [posts.authorId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
```
|
||||
|
||||
### Type Inference
|
||||
|
||||
```typescript
|
||||
// Infer types directly from your schema — no separate type files needed
|
||||
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";
|
||||
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type NewUser = InferInsertModel<typeof users>;
|
||||
export type Post = InferSelectModel<typeof posts>;
|
||||
export type NewPost = InferInsertModel<typeof posts>;
|
||||
```
|
||||
|
||||
## Query Patterns
|
||||
|
||||
### Select Queries (SQL-like API)
|
||||
|
||||
```typescript
|
||||
import { eq, and, like, desc, count, sql } from "drizzle-orm";
|
||||
|
||||
// Basic select
|
||||
const allUsers = await db.select().from(users);
|
||||
|
||||
// Filtered with conditions
|
||||
const admins = await db.select().from(users).where(eq(users.role, "admin"));
|
||||
|
||||
// Partial select (only specific columns)
|
||||
const emails = await db.select({ email: users.email }).from(users);
|
||||
|
||||
// Join query
|
||||
const postsWithAuthors = await db
|
||||
.select({
|
||||
title: posts.title,
|
||||
authorName: users.name,
|
||||
})
|
||||
.from(posts)
|
||||
.innerJoin(users, eq(posts.authorId, users.id))
|
||||
.where(eq(posts.published, true))
|
||||
.orderBy(desc(posts.createdAt))
|
||||
.limit(10);
|
||||
|
||||
// Aggregation
|
||||
const postCounts = await db
|
||||
.select({
|
||||
authorId: posts.authorId,
|
||||
postCount: count(posts.id),
|
||||
})
|
||||
.from(posts)
|
||||
.groupBy(posts.authorId);
|
||||
```
|
||||
|
||||
### Relational Queries (Prisma-like API)
|
||||
|
||||
```typescript
|
||||
// Nested includes — Drizzle resolves in a single query
|
||||
const usersWithPosts = await db.query.users.findMany({
|
||||
with: {
|
||||
posts: {
|
||||
where: eq(posts.published, true),
|
||||
orderBy: [desc(posts.createdAt)],
|
||||
limit: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Find one with nested data
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.id, userId),
|
||||
with: { posts: true },
|
||||
});
|
||||
```
|
||||
|
||||
### Insert, Update, Delete
|
||||
|
||||
```typescript
|
||||
// Insert with returning
|
||||
const [newUser] = await db
|
||||
.insert(users)
|
||||
.values({ email: "dev@example.com", name: "Dev" })
|
||||
.returning();
|
||||
|
||||
// Batch insert
|
||||
await db.insert(posts).values([
|
||||
{ title: "Post 1", authorId: newUser.id },
|
||||
{ title: "Post 2", authorId: newUser.id },
|
||||
]);
|
||||
|
||||
// Update
|
||||
await db.update(users).set({ name: "Updated" }).where(eq(users.id, userId));
|
||||
|
||||
// Delete
|
||||
await db.delete(posts).where(eq(posts.authorId, userId));
|
||||
```
|
||||
|
||||
### Transactions
|
||||
|
||||
```typescript
|
||||
const result = await db.transaction(async (tx) => {
|
||||
const [user] = await tx.insert(users).values({ email, name }).returning();
|
||||
await tx.insert(posts).values({ title: "Welcome Post", authorId: user.id });
|
||||
return user;
|
||||
});
|
||||
```
|
||||
|
||||
## Migration Workflow (Drizzle Kit)
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
// drizzle.config.ts
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "./db/schema.ts",
|
||||
out: "./drizzle",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
# Generate migration SQL from schema changes
|
||||
npx drizzle-kit generate
|
||||
|
||||
# Push schema directly to database (development only — skips migration files)
|
||||
npx drizzle-kit push
|
||||
|
||||
# Run pending migrations (production)
|
||||
npx drizzle-kit migrate
|
||||
|
||||
# Open Drizzle Studio (GUI database browser)
|
||||
npx drizzle-kit studio
|
||||
```
|
||||
|
||||
## Database Client Setup
|
||||
|
||||
### PostgreSQL (Neon Serverless)
|
||||
|
||||
```typescript
|
||||
// db/index.ts
|
||||
import { drizzle } from "drizzle-orm/neon-http";
|
||||
import { neon } from "@neondatabase/serverless";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const sql = neon(process.env.DATABASE_URL!);
|
||||
export const db = drizzle(sql, { schema });
|
||||
```
|
||||
|
||||
### SQLite (Turso/LibSQL)
|
||||
|
||||
```typescript
|
||||
import { drizzle } from "drizzle-orm/libsql";
|
||||
import { createClient } from "@libsql/client";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const client = createClient({
|
||||
url: process.env.TURSO_DATABASE_URL!,
|
||||
authToken: process.env.TURSO_AUTH_TOKEN,
|
||||
});
|
||||
export const db = drizzle(client, { schema });
|
||||
```
|
||||
|
||||
### MySQL (PlanetScale)
|
||||
|
||||
```typescript
|
||||
import { drizzle } from "drizzle-orm/planetscale-serverless";
|
||||
import { Client } from "@planetscale/database";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const client = new Client({ url: process.env.DATABASE_URL! });
|
||||
export const db = drizzle(client, { schema });
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Prepared Statements
|
||||
|
||||
```typescript
|
||||
// Prepare once, execute many times
|
||||
const getUserById = db.query.users
|
||||
.findFirst({
|
||||
where: eq(users.id, sql.placeholder("id")),
|
||||
})
|
||||
.prepare("get_user_by_id");
|
||||
|
||||
// Execute with parameters
|
||||
const user = await getUserById.execute({ id: "abc-123" });
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```typescript
|
||||
// Use db.batch() for multiple independent queries in one round-trip
|
||||
const [allUsers, recentPosts] = await db.batch([
|
||||
db.select().from(users),
|
||||
db.select().from(posts).orderBy(desc(posts.createdAt)).limit(10),
|
||||
]);
|
||||
```
|
||||
|
||||
### Indexing in Schema
|
||||
|
||||
```typescript
|
||||
import { index, uniqueIndex } from "drizzle-orm/pg-core";
|
||||
|
||||
export const posts = pgTable(
|
||||
"posts",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
title: text("title").notNull(),
|
||||
authorId: uuid("author_id").references(() => users.id).notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("posts_author_idx").on(table.authorId),
|
||||
index("posts_created_idx").on(table.createdAt),
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
## Next.js Integration
|
||||
|
||||
### Server Component Usage
|
||||
|
||||
```typescript
|
||||
// app/users/page.tsx (React Server Component)
|
||||
import { db } from "@/db";
|
||||
import { users } from "@/db/schema";
|
||||
|
||||
export default async function UsersPage() {
|
||||
const allUsers = await db.select().from(users);
|
||||
return (
|
||||
<ul>
|
||||
{allUsers.map((u) => (
|
||||
<li key={u.id}>{u.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Server Action
|
||||
|
||||
```typescript
|
||||
// app/actions.ts
|
||||
"use server";
|
||||
import { db } from "@/db";
|
||||
import { users } from "@/db/schema";
|
||||
|
||||
export async function createUser(formData: FormData) {
|
||||
const name = formData.get("name") as string;
|
||||
const email = formData.get("email") as string;
|
||||
await db.insert(users).values({ name, email });
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- ✅ **Do:** Keep all schema definitions in a single `db/schema.ts` or split by domain (`db/schema/users.ts`, `db/schema/posts.ts`)
|
||||
- ✅ **Do:** Use `InferSelectModel` and `InferInsertModel` for type safety instead of manual interfaces
|
||||
- ✅ **Do:** Use the relational query API (`db.query.*`) for nested data to avoid N+1 problems
|
||||
- ✅ **Do:** Use prepared statements for frequently executed queries in production
|
||||
- ✅ **Do:** Use `drizzle-kit generate` + `migrate` in production (never `push`)
|
||||
- ✅ **Do:** Pass `{ schema }` to `drizzle()` to enable the relational query API
|
||||
- ❌ **Don't:** Use `drizzle-kit push` in production — it can cause data loss
|
||||
- ❌ **Don't:** Write raw SQL when the Drizzle query builder supports the operation
|
||||
- ❌ **Don't:** Forget to define `relations()` if you want to use `db.query.*` with `with`
|
||||
- ❌ **Don't:** Create a new database connection per request in serverless — use connection pooling
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Problem:** `db.query.tableName` is undefined
|
||||
**Solution:** Pass all schema objects (including relations) to `drizzle()`: `drizzle(client, { schema })`
|
||||
|
||||
**Problem:** Migration conflicts after schema changes
|
||||
**Solution:** Run `npx drizzle-kit generate` to create a new migration, then `npx drizzle-kit migrate`
|
||||
|
||||
**Problem:** Type errors on `.returning()` with MySQL
|
||||
**Solution:** MySQL does not support `RETURNING`. Use `.execute()` and read `insertId` from the result instead.
|
||||
6
.claude/skills/grill-me/.openskills.json
Normal file
6
.claude/skills/grill-me/.openskills.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/grill-me",
|
||||
"installedAt": "2026-04-07T00:45:24.781Z"
|
||||
}
|
||||
10
.claude/skills/grill-me/SKILL.md
Normal file
10
.claude/skills/grill-me/SKILL.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: grill-me
|
||||
description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
|
||||
---
|
||||
|
||||
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
|
||||
|
||||
Ask the questions one at a time.
|
||||
|
||||
If a question can be answered by exploring the codebase, explore the codebase instead.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/nextjs-app-router-patterns",
|
||||
"installedAt": "2026-04-07T00:45:24.782Z"
|
||||
}
|
||||
537
.claude/skills/nextjs-app-router-patterns/SKILL.md
Normal file
537
.claude/skills/nextjs-app-router-patterns/SKILL.md
Normal file
@@ -0,0 +1,537 @@
|
||||
---
|
||||
name: nextjs-app-router-patterns
|
||||
description: Master Next.js 14+ App Router with Server Components, streaming, parallel routes, and advanced data fetching. Use when building Next.js applications, implementing SSR/SSG, or optimizing React Server Components.
|
||||
---
|
||||
|
||||
# Next.js App Router Patterns
|
||||
|
||||
Comprehensive patterns for Next.js 14+ App Router architecture, Server Components, and modern full-stack React development.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Building new Next.js applications with App Router
|
||||
- Migrating from Pages Router to App Router
|
||||
- Implementing Server Components and streaming
|
||||
- Setting up parallel and intercepting routes
|
||||
- Optimizing data fetching and caching
|
||||
- Building full-stack features with Server Actions
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Rendering Modes
|
||||
|
||||
| Mode | Where | When to Use |
|
||||
| --------------------- | ------------ | ----------------------------------------- |
|
||||
| **Server Components** | Server only | Data fetching, heavy computation, secrets |
|
||||
| **Client Components** | Browser | Interactivity, hooks, browser APIs |
|
||||
| **Static** | Build time | Content that rarely changes |
|
||||
| **Dynamic** | Request time | Personalized or real-time data |
|
||||
| **Streaming** | Progressive | Large pages, slow data sources |
|
||||
|
||||
### 2. File Conventions
|
||||
|
||||
```
|
||||
app/
|
||||
├── layout.tsx # Shared UI wrapper
|
||||
├── page.tsx # Route UI
|
||||
├── loading.tsx # Loading UI (Suspense)
|
||||
├── error.tsx # Error boundary
|
||||
├── not-found.tsx # 404 UI
|
||||
├── route.ts # API endpoint
|
||||
├── template.tsx # Re-mounted layout
|
||||
├── default.tsx # Parallel route fallback
|
||||
└── opengraph-image.tsx # OG image generation
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
// app/layout.tsx
|
||||
import { Inter } from 'next/font/google'
|
||||
import { Providers } from './providers'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata = {
|
||||
title: { default: 'My App', template: '%s | My App' },
|
||||
description: 'Built with Next.js App Router',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
// app/page.tsx - Server Component by default
|
||||
async function getProducts() {
|
||||
const res = await fetch('https://api.example.com/products', {
|
||||
next: { revalidate: 3600 }, // ISR: revalidate every hour
|
||||
})
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
const products = await getProducts()
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Products</h1>
|
||||
<ProductGrid products={products} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
### Pattern 1: Server Components with Data Fetching
|
||||
|
||||
```typescript
|
||||
// app/products/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
import { ProductList, ProductListSkeleton } from '@/components/products'
|
||||
import { FilterSidebar } from '@/components/filters'
|
||||
|
||||
interface SearchParams {
|
||||
category?: string
|
||||
sort?: 'price' | 'name' | 'date'
|
||||
page?: string
|
||||
}
|
||||
|
||||
export default async function ProductsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<SearchParams>
|
||||
}) {
|
||||
const params = await searchParams
|
||||
|
||||
return (
|
||||
<div className="flex gap-8">
|
||||
<FilterSidebar />
|
||||
<Suspense
|
||||
key={JSON.stringify(params)}
|
||||
fallback={<ProductListSkeleton />}
|
||||
>
|
||||
<ProductList
|
||||
category={params.category}
|
||||
sort={params.sort}
|
||||
page={Number(params.page) || 1}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// components/products/ProductList.tsx - Server Component
|
||||
async function getProducts(filters: ProductFilters) {
|
||||
const res = await fetch(
|
||||
`${process.env.API_URL}/products?${new URLSearchParams(filters)}`,
|
||||
{ next: { tags: ['products'] } }
|
||||
)
|
||||
if (!res.ok) throw new Error('Failed to fetch products')
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function ProductList({ category, sort, page }: ProductFilters) {
|
||||
const { products, totalPages } = await getProducts({ category, sort, page })
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{products.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
<Pagination currentPage={page} totalPages={totalPages} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Client Components with 'use client'
|
||||
|
||||
```typescript
|
||||
// components/products/AddToCartButton.tsx
|
||||
'use client'
|
||||
|
||||
import { useState, useTransition } from 'react'
|
||||
import { addToCart } from '@/app/actions/cart'
|
||||
|
||||
export function AddToCartButton({ productId }: { productId: string }) {
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleClick = () => {
|
||||
setError(null)
|
||||
startTransition(async () => {
|
||||
const result = await addToCart(productId)
|
||||
if (result.error) {
|
||||
setError(result.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
disabled={isPending}
|
||||
className="btn-primary"
|
||||
>
|
||||
{isPending ? 'Adding...' : 'Add to Cart'}
|
||||
</button>
|
||||
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Server Actions
|
||||
|
||||
```typescript
|
||||
// app/actions/cart.ts
|
||||
"use server";
|
||||
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export async function addToCart(productId: string) {
|
||||
const cookieStore = await cookies();
|
||||
const sessionId = cookieStore.get("session")?.value;
|
||||
|
||||
if (!sessionId) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
try {
|
||||
await db.cart.upsert({
|
||||
where: { sessionId_productId: { sessionId, productId } },
|
||||
update: { quantity: { increment: 1 } },
|
||||
create: { sessionId, productId, quantity: 1 },
|
||||
});
|
||||
|
||||
revalidateTag("cart");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { error: "Failed to add item to cart" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkout(formData: FormData) {
|
||||
const address = formData.get("address") as string;
|
||||
const payment = formData.get("payment") as string;
|
||||
|
||||
// Validate
|
||||
if (!address || !payment) {
|
||||
return { error: "Missing required fields" };
|
||||
}
|
||||
|
||||
// Process order
|
||||
const order = await processOrder({ address, payment });
|
||||
|
||||
// Redirect to confirmation
|
||||
redirect(`/orders/${order.id}/confirmation`);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Parallel Routes
|
||||
|
||||
```typescript
|
||||
// app/dashboard/layout.tsx
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
analytics,
|
||||
team,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
analytics: React.ReactNode
|
||||
team: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="dashboard-grid">
|
||||
<main>{children}</main>
|
||||
<aside className="analytics-panel">{analytics}</aside>
|
||||
<aside className="team-panel">{team}</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// app/dashboard/@analytics/page.tsx
|
||||
export default async function AnalyticsSlot() {
|
||||
const stats = await getAnalytics()
|
||||
return <AnalyticsChart data={stats} />
|
||||
}
|
||||
|
||||
// app/dashboard/@analytics/loading.tsx
|
||||
export default function AnalyticsLoading() {
|
||||
return <ChartSkeleton />
|
||||
}
|
||||
|
||||
// app/dashboard/@team/page.tsx
|
||||
export default async function TeamSlot() {
|
||||
const members = await getTeamMembers()
|
||||
return <TeamList members={members} />
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 5: Intercepting Routes (Modal Pattern)
|
||||
|
||||
```typescript
|
||||
// File structure for photo modal
|
||||
// app/
|
||||
// ├── @modal/
|
||||
// │ ├── (.)photos/[id]/page.tsx # Intercept
|
||||
// │ └── default.tsx
|
||||
// ├── photos/
|
||||
// │ └── [id]/page.tsx # Full page
|
||||
// └── layout.tsx
|
||||
|
||||
// app/@modal/(.)photos/[id]/page.tsx
|
||||
import { Modal } from '@/components/Modal'
|
||||
import { PhotoDetail } from '@/components/PhotoDetail'
|
||||
|
||||
export default async function PhotoModal({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const photo = await getPhoto(id)
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<PhotoDetail photo={photo} />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
// app/photos/[id]/page.tsx - Full page version
|
||||
export default async function PhotoPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const photo = await getPhoto(id)
|
||||
|
||||
return (
|
||||
<div className="photo-page">
|
||||
<PhotoDetail photo={photo} />
|
||||
<RelatedPhotos photoId={id} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// app/layout.tsx
|
||||
export default function RootLayout({
|
||||
children,
|
||||
modal,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
modal: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
{modal}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 6: Streaming with Suspense
|
||||
|
||||
```typescript
|
||||
// app/product/[id]/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export default async function ProductPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
|
||||
// This data loads first (blocking)
|
||||
const product = await getProduct(id)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Immediate render */}
|
||||
<ProductHeader product={product} />
|
||||
|
||||
{/* Stream in reviews */}
|
||||
<Suspense fallback={<ReviewsSkeleton />}>
|
||||
<Reviews productId={id} />
|
||||
</Suspense>
|
||||
|
||||
{/* Stream in recommendations */}
|
||||
<Suspense fallback={<RecommendationsSkeleton />}>
|
||||
<Recommendations productId={id} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// These components fetch their own data
|
||||
async function Reviews({ productId }: { productId: string }) {
|
||||
const reviews = await getReviews(productId) // Slow API
|
||||
return <ReviewList reviews={reviews} />
|
||||
}
|
||||
|
||||
async function Recommendations({ productId }: { productId: string }) {
|
||||
const products = await getRecommendations(productId) // ML-based, slow
|
||||
return <ProductCarousel products={products} />
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 7: Route Handlers (API Routes)
|
||||
|
||||
```typescript
|
||||
// app/api/products/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const category = searchParams.get("category");
|
||||
|
||||
const products = await db.product.findMany({
|
||||
where: category ? { category } : undefined,
|
||||
take: 20,
|
||||
});
|
||||
|
||||
return NextResponse.json(products);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
|
||||
const product = await db.product.create({
|
||||
data: body,
|
||||
});
|
||||
|
||||
return NextResponse.json(product, { status: 201 });
|
||||
}
|
||||
|
||||
// app/api/products/[id]/route.ts
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { id } = await params;
|
||||
const product = await db.product.findUnique({ where: { id } });
|
||||
|
||||
if (!product) {
|
||||
return NextResponse.json({ error: "Product not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(product);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 8: Metadata and SEO
|
||||
|
||||
```typescript
|
||||
// app/products/[slug]/page.tsx
|
||||
import { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ slug: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
const product = await getProduct(slug)
|
||||
|
||||
if (!product) return {}
|
||||
|
||||
return {
|
||||
title: product.name,
|
||||
description: product.description,
|
||||
openGraph: {
|
||||
title: product.name,
|
||||
description: product.description,
|
||||
images: [{ url: product.image, width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: product.name,
|
||||
description: product.description,
|
||||
images: [product.image],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const products = await db.product.findMany({ select: { slug: true } })
|
||||
return products.map((p) => ({ slug: p.slug }))
|
||||
}
|
||||
|
||||
export default async function ProductPage({ params }: Props) {
|
||||
const { slug } = await params
|
||||
const product = await getProduct(slug)
|
||||
|
||||
if (!product) notFound()
|
||||
|
||||
return <ProductDetail product={product} />
|
||||
}
|
||||
```
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
### Data Cache
|
||||
|
||||
```typescript
|
||||
// No cache (always fresh)
|
||||
fetch(url, { cache: "no-store" });
|
||||
|
||||
// Cache forever (static)
|
||||
fetch(url, { cache: "force-cache" });
|
||||
|
||||
// ISR - revalidate after 60 seconds
|
||||
fetch(url, { next: { revalidate: 60 } });
|
||||
|
||||
// Tag-based invalidation
|
||||
fetch(url, { next: { tags: ["products"] } });
|
||||
|
||||
// Invalidate via Server Action
|
||||
("use server");
|
||||
import { revalidateTag, revalidatePath } from "next/cache";
|
||||
|
||||
export async function updateProduct(id: string, data: ProductData) {
|
||||
await db.product.update({ where: { id }, data });
|
||||
revalidateTag("products");
|
||||
revalidatePath("/products");
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Do's
|
||||
|
||||
- **Start with Server Components** - Add 'use client' only when needed
|
||||
- **Colocate data fetching** - Fetch data where it's used
|
||||
- **Use Suspense boundaries** - Enable streaming for slow data
|
||||
- **Leverage parallel routes** - Independent loading states
|
||||
- **Use Server Actions** - For mutations with progressive enhancement
|
||||
|
||||
### Don'ts
|
||||
|
||||
- **Don't pass serializable data** - Server → Client boundary limitations
|
||||
- **Don't use hooks in Server Components** - No useState, useEffect
|
||||
- **Don't fetch in Client Components** - Use Server Components or React Query
|
||||
- **Don't over-nest layouts** - Each layout adds to the component tree
|
||||
- **Don't ignore loading states** - Always provide loading.tsx or Suspense
|
||||
6
.claude/skills/nextjs-best-practices/.openskills.json
Normal file
6
.claude/skills/nextjs-best-practices/.openskills.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/nextjs-best-practices",
|
||||
"installedAt": "2026-04-07T00:45:24.782Z"
|
||||
}
|
||||
208
.claude/skills/nextjs-best-practices/SKILL.md
Normal file
208
.claude/skills/nextjs-best-practices/SKILL.md
Normal file
@@ -0,0 +1,208 @@
|
||||
---
|
||||
name: nextjs-best-practices
|
||||
description: "Next.js App Router principles. Server Components, data fetching, routing patterns."
|
||||
risk: unknown
|
||||
source: community
|
||||
date_added: "2026-02-27"
|
||||
---
|
||||
|
||||
# Next.js Best Practices
|
||||
|
||||
> Principles for Next.js App Router development.
|
||||
|
||||
---
|
||||
|
||||
## 1. Server vs Client Components
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
Does it need...?
|
||||
│
|
||||
├── useState, useEffect, event handlers
|
||||
│ └── Client Component ('use client')
|
||||
│
|
||||
├── Direct data fetching, no interactivity
|
||||
│ └── Server Component (default)
|
||||
│
|
||||
└── Both?
|
||||
└── Split: Server parent + Client child
|
||||
```
|
||||
|
||||
### By Default
|
||||
|
||||
| Type | Use |
|
||||
|------|-----|
|
||||
| **Server** | Data fetching, layout, static content |
|
||||
| **Client** | Forms, buttons, interactive UI |
|
||||
|
||||
---
|
||||
|
||||
## 2. Data Fetching Patterns
|
||||
|
||||
### Fetch Strategy
|
||||
|
||||
| Pattern | Use |
|
||||
|---------|-----|
|
||||
| **Default** | Static (cached at build) |
|
||||
| **Revalidate** | ISR (time-based refresh) |
|
||||
| **No-store** | Dynamic (every request) |
|
||||
|
||||
### Data Flow
|
||||
|
||||
| Source | Pattern |
|
||||
|--------|---------|
|
||||
| Database | Server Component fetch |
|
||||
| API | fetch with caching |
|
||||
| User input | Client state + server action |
|
||||
|
||||
---
|
||||
|
||||
## 3. Routing Principles
|
||||
|
||||
### File Conventions
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `page.tsx` | Route UI |
|
||||
| `layout.tsx` | Shared layout |
|
||||
| `loading.tsx` | Loading state |
|
||||
| `error.tsx` | Error boundary |
|
||||
| `not-found.tsx` | 404 page |
|
||||
|
||||
### Route Organization
|
||||
|
||||
| Pattern | Use |
|
||||
|---------|-----|
|
||||
| Route groups `(name)` | Organize without URL |
|
||||
| Parallel routes `@slot` | Multiple same-level pages |
|
||||
| Intercepting `(.)` | Modal overlays |
|
||||
|
||||
---
|
||||
|
||||
## 4. API Routes
|
||||
|
||||
### Route Handlers
|
||||
|
||||
| Method | Use |
|
||||
|--------|-----|
|
||||
| GET | Read data |
|
||||
| POST | Create data |
|
||||
| PUT/PATCH | Update data |
|
||||
| DELETE | Remove data |
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Validate input with Zod
|
||||
- Return proper status codes
|
||||
- Handle errors gracefully
|
||||
- Use Edge runtime when possible
|
||||
|
||||
---
|
||||
|
||||
## 5. Performance Principles
|
||||
|
||||
### Image Optimization
|
||||
|
||||
- Use next/image component
|
||||
- Set priority for above-fold
|
||||
- Provide blur placeholder
|
||||
- Use responsive sizes
|
||||
|
||||
### Bundle Optimization
|
||||
|
||||
- Dynamic imports for heavy components
|
||||
- Route-based code splitting (automatic)
|
||||
- Analyze with bundle analyzer
|
||||
|
||||
---
|
||||
|
||||
## 6. Metadata
|
||||
|
||||
### Static vs Dynamic
|
||||
|
||||
| Type | Use |
|
||||
|------|-----|
|
||||
| Static export | Fixed metadata |
|
||||
| generateMetadata | Dynamic per-route |
|
||||
|
||||
### Essential Tags
|
||||
|
||||
- title (50-60 chars)
|
||||
- description (150-160 chars)
|
||||
- Open Graph images
|
||||
- Canonical URL
|
||||
|
||||
---
|
||||
|
||||
## 7. Caching Strategy
|
||||
|
||||
### Cache Layers
|
||||
|
||||
| Layer | Control |
|
||||
|-------|---------|
|
||||
| Request | fetch options |
|
||||
| Data | revalidate/tags |
|
||||
| Full route | route config |
|
||||
|
||||
### Revalidation
|
||||
|
||||
| Method | Use |
|
||||
|--------|-----|
|
||||
| Time-based | `revalidate: 60` |
|
||||
| On-demand | `revalidatePath/Tag` |
|
||||
| No cache | `no-store` |
|
||||
|
||||
---
|
||||
|
||||
## 8. Server Actions
|
||||
|
||||
### Use Cases
|
||||
|
||||
- Form submissions
|
||||
- Data mutations
|
||||
- Revalidation triggers
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Mark with 'use server'
|
||||
- Validate all inputs
|
||||
- Return typed responses
|
||||
- Handle errors
|
||||
|
||||
---
|
||||
|
||||
## 9. Anti-Patterns
|
||||
|
||||
| ❌ Don't | ✅ Do |
|
||||
|----------|-------|
|
||||
| 'use client' everywhere | Server by default |
|
||||
| Fetch in client components | Fetch in server |
|
||||
| Skip loading states | Use loading.tsx |
|
||||
| Ignore error boundaries | Use error.tsx |
|
||||
| Large client bundles | Dynamic imports |
|
||||
|
||||
---
|
||||
|
||||
## 10. Project Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── (marketing)/ # Route group
|
||||
│ └── page.tsx
|
||||
├── (dashboard)/
|
||||
│ ├── layout.tsx # Dashboard layout
|
||||
│ └── page.tsx
|
||||
├── api/
|
||||
│ └── [resource]/
|
||||
│ └── route.ts
|
||||
└── components/
|
||||
└── ui/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **Remember:** Server Components are the default for a reason. Start there, add client only when needed.
|
||||
|
||||
## When to Use
|
||||
This skill is applicable to execute the workflow or actions described in the overview.
|
||||
6
.claude/skills/nextjs-developer/.openskills.json
Normal file
6
.claude/skills/nextjs-developer/.openskills.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/nextjs-developer",
|
||||
"installedAt": "2026-04-07T00:45:24.782Z"
|
||||
}
|
||||
143
.claude/skills/nextjs-developer/SKILL.md
Normal file
143
.claude/skills/nextjs-developer/SKILL.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
name: nextjs-developer
|
||||
description: "Use when building Next.js 14+ applications with App Router, server components, or server actions. Invoke to configure route handlers, implement middleware, set up API routes, add streaming SSR, write generateMetadata for SEO, scaffold loading.tsx/error.tsx boundaries, or deploy to Vercel. Triggers on: Next.js, Next.js 14, App Router, RSC, use server, Server Components, Server Actions, React Server Components, generateMetadata, loading.tsx, Next.js deployment, Vercel, Next.js performance."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: https://github.com/Jeffallan
|
||||
version: "1.1.0"
|
||||
domain: frontend
|
||||
triggers: Next.js, Next.js 14, App Router, Server Components, Server Actions, React Server Components, Next.js deployment, Vercel, Next.js performance
|
||||
role: specialist
|
||||
scope: implementation
|
||||
output-format: code
|
||||
related-skills: typescript-pro
|
||||
---
|
||||
|
||||
# Next.js Developer
|
||||
|
||||
Senior Next.js developer with expertise in Next.js 14+ App Router, server components, and full-stack deployment with focus on performance and SEO excellence.
|
||||
|
||||
## Core Workflow
|
||||
|
||||
1. **Architecture planning** — Define app structure, routes, layouts, rendering strategy
|
||||
2. **Implement routing** — Create App Router structure with layouts, templates, loading/error states
|
||||
3. **Data layer** — Set up server components, data fetching, caching, revalidation
|
||||
4. **Optimize** — Images, fonts, bundles, streaming, edge runtime
|
||||
5. **Deploy** — Production build, environment setup, monitoring
|
||||
- Validate: run `next build` locally, confirm zero type errors, check `NEXT_PUBLIC_*` and server-only env vars are set, run Lighthouse/PageSpeed to confirm Core Web Vitals > 90
|
||||
|
||||
## Reference Guide
|
||||
|
||||
Load detailed guidance based on context:
|
||||
|
||||
| Topic | Reference | Load When |
|
||||
|-------|-----------|-----------|
|
||||
| App Router | `references/app-router.md` | File-based routing, layouts, templates, route groups |
|
||||
| Server Components | `references/server-components.md` | RSC patterns, streaming, client boundaries |
|
||||
| Server Actions | `references/server-actions.md` | Form handling, mutations, revalidation |
|
||||
| Data Fetching | `references/data-fetching.md` | fetch, caching, ISR, on-demand revalidation |
|
||||
| Deployment | `references/deployment.md` | Vercel, self-hosting, Docker, optimization |
|
||||
|
||||
## Constraints
|
||||
|
||||
### MUST DO (Next.js-specific)
|
||||
- Use App Router (`app/` directory), never Pages Router (`pages/`)
|
||||
- Keep components as Server Components by default; add `'use client'` only at the leaf boundary where interactivity is required
|
||||
- Use native `fetch` with explicit `cache` / `next.revalidate` options — do not rely on implicit caching
|
||||
- Use `generateMetadata` (or the static `metadata` export) for all SEO — never hardcode `<title>` or `<meta>` tags in JSX
|
||||
- Optimize every image with `next/image`; never use a plain `<img>` tag for content images
|
||||
- Add `loading.tsx` and `error.tsx` at every route segment that performs async data fetching
|
||||
|
||||
### MUST NOT DO
|
||||
- Convert components to Client Components just to access data — fetch server-side first
|
||||
- Skip `loading.tsx`/`error.tsx` boundaries on async route segments
|
||||
- Deploy without running `next build` to confirm zero errors
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Server Component with data fetching and caching
|
||||
```tsx
|
||||
// app/products/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
|
||||
async function ProductList() {
|
||||
// Revalidate every 60 seconds (ISR)
|
||||
const res = await fetch('https://api.example.com/products', {
|
||||
next: { revalidate: 60 },
|
||||
})
|
||||
if (!res.ok) throw new Error('Failed to fetch products')
|
||||
const products: Product[] = await res.json()
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{products.map((p) => (
|
||||
<li key={p.id}>{p.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense fallback={<p>Loading…</p>}>
|
||||
<ProductList />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Server Action with form handling and revalidation
|
||||
```tsx
|
||||
// app/products/actions.ts
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export async function createProduct(formData: FormData) {
|
||||
const name = formData.get('name') as string
|
||||
await db.product.create({ data: { name } })
|
||||
revalidatePath('/products')
|
||||
}
|
||||
|
||||
// app/products/new/page.tsx
|
||||
import { createProduct } from '../actions'
|
||||
|
||||
export default function NewProductPage() {
|
||||
return (
|
||||
<form action={createProduct}>
|
||||
<input name="name" placeholder="Product name" required />
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### generateMetadata for dynamic SEO
|
||||
```tsx
|
||||
// app/products/[id]/page.tsx
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params }: { params: { id: string } }
|
||||
): Promise<Metadata> {
|
||||
const product = await fetchProduct(params.id)
|
||||
return {
|
||||
title: product.name,
|
||||
description: product.description,
|
||||
openGraph: { title: product.name, images: [product.imageUrl] },
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Output Templates
|
||||
|
||||
When implementing Next.js features, provide:
|
||||
1. App structure (route organization)
|
||||
2. Layout/page components with proper data fetching
|
||||
3. Server actions if mutations needed
|
||||
4. Configuration (`next.config.js`, TypeScript)
|
||||
5. Brief explanation of rendering strategy chosen
|
||||
|
||||
## Knowledge Reference
|
||||
|
||||
Next.js 14+, App Router, React Server Components, Server Actions, Streaming SSR, Partial Prerendering, next/image, next/font, Metadata API, Route Handlers, Middleware, Edge Runtime, Turbopack, Vercel deployment
|
||||
311
.claude/skills/nextjs-developer/references/app-router.md
Normal file
311
.claude/skills/nextjs-developer/references/app-router.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# App Router Architecture
|
||||
|
||||
## File-Based Routing
|
||||
|
||||
```
|
||||
app/
|
||||
├── layout.tsx # Root layout (required)
|
||||
├── page.tsx # Home page (/)
|
||||
├── loading.tsx # Loading UI
|
||||
├── error.tsx # Error boundary
|
||||
├── not-found.tsx # 404 page
|
||||
├── template.tsx # Re-mounted layout
|
||||
│
|
||||
├── (marketing)/ # Route group (no URL segment)
|
||||
│ ├── layout.tsx
|
||||
│ ├── about/
|
||||
│ │ └── page.tsx # /about
|
||||
│ └── contact/
|
||||
│ └── page.tsx # /contact
|
||||
│
|
||||
├── dashboard/
|
||||
│ ├── layout.tsx # Shared dashboard layout
|
||||
│ ├── page.tsx # /dashboard
|
||||
│ ├── settings/
|
||||
│ │ └── page.tsx # /dashboard/settings
|
||||
│ └── @analytics/ # Parallel route (slot)
|
||||
│ └── page.tsx
|
||||
│
|
||||
├── blog/
|
||||
│ ├── [slug]/
|
||||
│ │ └── page.tsx # /blog/my-post (dynamic)
|
||||
│ └── [...slug]/
|
||||
│ └── page.tsx # /blog/a/b/c (catch-all)
|
||||
│
|
||||
└── api/
|
||||
└── users/
|
||||
└── route.ts # API route handler
|
||||
```
|
||||
|
||||
## Root Layout (Required)
|
||||
|
||||
```tsx
|
||||
// app/layout.tsx
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: 'My App',
|
||||
template: '%s | My App'
|
||||
},
|
||||
description: 'Next.js 14 application',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Nested Layouts
|
||||
|
||||
```tsx
|
||||
// app/dashboard/layout.tsx
|
||||
import { Sidebar } from '@/components/sidebar'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const session = await auth()
|
||||
|
||||
if (!session) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<Sidebar />
|
||||
<main className="flex-1">{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Templates (Re-mount on Navigation)
|
||||
|
||||
```tsx
|
||||
// app/template.tsx
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Template({ children }: { children: React.ReactNode }) {
|
||||
useEffect(() => {
|
||||
// Runs on every navigation
|
||||
console.log('Template mounted')
|
||||
}, [])
|
||||
|
||||
return <div>{children}</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Loading States
|
||||
|
||||
```tsx
|
||||
// app/dashboard/loading.tsx
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Boundaries
|
||||
|
||||
```tsx
|
||||
// app/error.tsx
|
||||
'use client'
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Something went wrong!</h2>
|
||||
<button onClick={() => reset()}>Try again</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Route Groups
|
||||
|
||||
```tsx
|
||||
// (marketing) and (shop) share the same URL level
|
||||
app/
|
||||
├── (marketing)/
|
||||
│ ├── layout.tsx # Marketing layout
|
||||
│ └── about/
|
||||
│ └── page.tsx # /about
|
||||
└── (shop)/
|
||||
├── layout.tsx # Shop layout
|
||||
└── products/
|
||||
└── page.tsx # /products
|
||||
```
|
||||
|
||||
## Parallel Routes
|
||||
|
||||
```tsx
|
||||
// app/dashboard/layout.tsx
|
||||
export default function Layout({
|
||||
children,
|
||||
analytics,
|
||||
team,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
analytics: React.ReactNode
|
||||
team: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{analytics}
|
||||
{team}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// app/dashboard/@analytics/page.tsx
|
||||
export default function Analytics() {
|
||||
return <div>Analytics Dashboard</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Intercepting Routes
|
||||
|
||||
```tsx
|
||||
// Show modal when navigating from same app
|
||||
// but show full page on direct navigation
|
||||
|
||||
// app/photos/[id]/page.tsx (full page)
|
||||
export default function PhotoPage({ params }: { params: { id: string } }) {
|
||||
return <div>Photo {params.id} - Full Page</div>
|
||||
}
|
||||
|
||||
// app/@modal/(.)photos/[id]/page.tsx (modal)
|
||||
export default function PhotoModal({ params }: { params: { id: string } }) {
|
||||
return <div>Photo {params.id} - Modal</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Routes
|
||||
|
||||
```tsx
|
||||
// app/blog/[slug]/page.tsx
|
||||
export default function BlogPost({ params }: { params: { slug: string } }) {
|
||||
return <h1>Post: {params.slug}</h1>
|
||||
}
|
||||
|
||||
// Generate static params at build time
|
||||
export async function generateStaticParams() {
|
||||
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
|
||||
|
||||
return posts.map((post: { slug: string }) => ({
|
||||
slug: post.slug,
|
||||
}))
|
||||
}
|
||||
|
||||
// Opt out of static generation
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Revalidate every 60 seconds
|
||||
export const revalidate = 60
|
||||
```
|
||||
|
||||
## Catch-All Routes
|
||||
|
||||
```tsx
|
||||
// app/docs/[...slug]/page.tsx
|
||||
// Matches: /docs/a, /docs/a/b, /docs/a/b/c
|
||||
export default function Docs({ params }: { params: { slug: string[] } }) {
|
||||
return <div>Docs: {params.slug.join('/')}</div>
|
||||
}
|
||||
|
||||
// Optional catch-all: [[...slug]]
|
||||
// Also matches: /docs
|
||||
```
|
||||
|
||||
## Route Handlers (API Routes)
|
||||
|
||||
```tsx
|
||||
// app/api/users/route.ts
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const users = await db.user.findMany()
|
||||
return NextResponse.json(users)
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const user = await db.user.create({ data: body })
|
||||
return NextResponse.json(user, { status: 201 })
|
||||
}
|
||||
|
||||
// Dynamic routes: app/api/users/[id]/route.ts
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
const user = await db.user.findUnique({ where: { id: params.id } })
|
||||
return NextResponse.json(user)
|
||||
}
|
||||
```
|
||||
|
||||
## Metadata API
|
||||
|
||||
```tsx
|
||||
// app/blog/[slug]/page.tsx
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params }: { params: { slug: string } }
|
||||
): Promise<Metadata> {
|
||||
const post = await fetchPost(params.slug)
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
images: [{ url: post.coverImage }],
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| File | Purpose | Use Case |
|
||||
|------|---------|----------|
|
||||
| `layout.tsx` | Persistent UI across routes | Shared navigation, auth wrapper |
|
||||
| `page.tsx` | Route UI | Actual page content |
|
||||
| `loading.tsx` | Loading fallback | Automatic Suspense boundary |
|
||||
| `error.tsx` | Error boundary | Handle errors gracefully |
|
||||
| `template.tsx` | Re-mounted layout | Analytics, animations |
|
||||
| `not-found.tsx` | 404 page | Custom not found UI |
|
||||
| `route.ts` | API handler | Backend API endpoints |
|
||||
482
.claude/skills/nextjs-developer/references/data-fetching.md
Normal file
482
.claude/skills/nextjs-developer/references/data-fetching.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# Data Fetching & Caching
|
||||
|
||||
## Extended fetch API
|
||||
|
||||
Next.js extends the native fetch with caching and revalidation options:
|
||||
|
||||
```tsx
|
||||
// app/page.tsx
|
||||
async function getData() {
|
||||
const res = await fetch('https://api.example.com/posts', {
|
||||
cache: 'force-cache', // Default: cache forever (SSG)
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
const data = await getData()
|
||||
return <div>{/* render data */}</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Cache Options
|
||||
|
||||
```tsx
|
||||
// 1. Force cache (Static Site Generation)
|
||||
fetch('https://api.example.com/data', {
|
||||
cache: 'force-cache' // Default behavior
|
||||
})
|
||||
|
||||
// 2. No cache (Server-Side Rendering)
|
||||
fetch('https://api.example.com/data', {
|
||||
cache: 'no-store' // Always fetch fresh data
|
||||
})
|
||||
|
||||
// 3. Revalidate (Incremental Static Regeneration)
|
||||
fetch('https://api.example.com/data', {
|
||||
next: { revalidate: 3600 } // Revalidate every hour
|
||||
})
|
||||
|
||||
// 4. Revalidate with tags
|
||||
fetch('https://api.example.com/data', {
|
||||
next: { tags: ['posts'] }
|
||||
})
|
||||
```
|
||||
|
||||
## Revalidation Methods
|
||||
|
||||
### Time-based Revalidation (ISR)
|
||||
|
||||
```tsx
|
||||
// Revalidate every 60 seconds
|
||||
async function getPosts() {
|
||||
const res = await fetch('https://api.example.com/posts', {
|
||||
next: { revalidate: 60 }
|
||||
})
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// Route segment config
|
||||
export const revalidate = 60 // seconds
|
||||
|
||||
export default async function Page() {
|
||||
const posts = await getPosts()
|
||||
return <div>{/* render */}</div>
|
||||
}
|
||||
```
|
||||
|
||||
### On-Demand Revalidation
|
||||
|
||||
```tsx
|
||||
// app/api/revalidate/route.ts
|
||||
import { revalidatePath, revalidateTag } from 'next/cache'
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const path = request.nextUrl.searchParams.get('path')
|
||||
|
||||
if (path) {
|
||||
revalidatePath(path)
|
||||
return Response.json({ revalidated: true, now: Date.now() })
|
||||
}
|
||||
|
||||
return Response.json({ revalidated: false })
|
||||
}
|
||||
|
||||
// Usage in Server Action
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export async function createPost(data: FormData) {
|
||||
await db.post.create({ data })
|
||||
|
||||
// Revalidate specific path
|
||||
revalidatePath('/posts')
|
||||
|
||||
// Revalidate entire layout
|
||||
revalidatePath('/posts', 'layout')
|
||||
}
|
||||
```
|
||||
|
||||
### Tag-based Revalidation
|
||||
|
||||
```tsx
|
||||
// Fetch with tags
|
||||
async function getPosts() {
|
||||
const res = await fetch('https://api.example.com/posts', {
|
||||
next: { tags: ['posts'] }
|
||||
})
|
||||
return res.json()
|
||||
}
|
||||
|
||||
async function getAuthors() {
|
||||
const res = await fetch('https://api.example.com/authors', {
|
||||
next: { tags: ['authors'] }
|
||||
})
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// Revalidate by tag
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export async function createPost() {
|
||||
// Revalidate all fetches tagged with 'posts'
|
||||
revalidateTag('posts')
|
||||
}
|
||||
```
|
||||
|
||||
## Route Segment Config
|
||||
|
||||
```tsx
|
||||
// app/posts/page.tsx
|
||||
|
||||
// Force dynamic rendering
|
||||
export const dynamic = 'force-dynamic' // 'auto' | 'force-dynamic' | 'error' | 'force-static'
|
||||
|
||||
// Revalidation interval
|
||||
export const revalidate = 3600 // false | 0 | number (seconds)
|
||||
|
||||
// Fetch cache
|
||||
export const fetchCache = 'auto' // 'auto' | 'default-cache' | 'only-cache' | 'force-cache' | 'force-no-store' | 'default-no-store' | 'only-no-store'
|
||||
|
||||
// Runtime
|
||||
export const runtime = 'nodejs' // 'nodejs' | 'edge'
|
||||
|
||||
// Preferred region
|
||||
export const preferredRegion = 'auto' // 'auto' | 'home' | 'edge' | string | string[]
|
||||
|
||||
export default async function Page() {
|
||||
return <div>Posts</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Parallel Data Fetching
|
||||
|
||||
```tsx
|
||||
async function getUser() {
|
||||
return fetch('https://api.example.com/user')
|
||||
}
|
||||
|
||||
async function getPosts() {
|
||||
return fetch('https://api.example.com/posts')
|
||||
}
|
||||
|
||||
async function getComments() {
|
||||
return fetch('https://api.example.com/comments')
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
// Fetch in parallel with Promise.all
|
||||
const [user, posts, comments] = await Promise.all([
|
||||
getUser(),
|
||||
getPosts(),
|
||||
getComments(),
|
||||
])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<UserInfo user={user} />
|
||||
<Posts posts={posts} />
|
||||
<Comments comments={comments} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Sequential Data Fetching
|
||||
|
||||
```tsx
|
||||
// When one fetch depends on another
|
||||
export default async function Page({ params }: { params: { id: string } }) {
|
||||
// First fetch
|
||||
const user = await fetch(`https://api.example.com/users/${params.id}`)
|
||||
.then(res => res.json())
|
||||
|
||||
// Second fetch depends on first
|
||||
const posts = await fetch(`https://api.example.com/users/${user.id}/posts`)
|
||||
.then(res => res.json())
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{user.name}</h1>
|
||||
<Posts posts={posts} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Streaming with Suspense
|
||||
|
||||
```tsx
|
||||
// app/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
|
||||
async function Posts() {
|
||||
const posts = await fetch('https://api.example.com/posts', {
|
||||
cache: 'no-store'
|
||||
}).then(res => res.json())
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{posts.map((post: Post) => (
|
||||
<li key={post.id}>{post.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Posts</h1>
|
||||
<Suspense fallback={<div>Loading posts...</div>}>
|
||||
<Posts />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## React cache for Deduplication
|
||||
|
||||
```tsx
|
||||
// lib/data.ts
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getUser = cache(async (id: string) => {
|
||||
const res = await fetch(`https://api.example.com/users/${id}`)
|
||||
return res.json()
|
||||
})
|
||||
|
||||
// components/user-profile.tsx
|
||||
export async function UserProfile({ userId }: { userId: string }) {
|
||||
const user = await getUser(userId) // Cached
|
||||
return <div>{user.name}</div>
|
||||
}
|
||||
|
||||
// components/user-posts.tsx
|
||||
export async function UserPosts({ userId }: { userId: string }) {
|
||||
const user = await getUser(userId) // Uses cached result
|
||||
return <div>{user.posts.length} posts</div>
|
||||
}
|
||||
|
||||
// app/page.tsx
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<UserProfile userId="123" />
|
||||
<UserPosts userId="123" /> {/* Same fetch, deduplicated */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Database Queries
|
||||
|
||||
```tsx
|
||||
// lib/db.ts
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const globalForPrisma = global as unknown as { prisma: PrismaClient }
|
||||
|
||||
export const db = globalForPrisma.prisma || new PrismaClient()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db
|
||||
|
||||
// app/posts/page.tsx
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export const revalidate = 60 // Revalidate every 60 seconds
|
||||
|
||||
export default async function PostsPage() {
|
||||
const posts = await db.post.findMany({
|
||||
include: { author: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
{posts.map(post => (
|
||||
<article key={post.id}>
|
||||
<h2>{post.title}</h2>
|
||||
<p>By {post.author.name}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```tsx
|
||||
async function getData() {
|
||||
const res = await fetch('https://api.example.com/data')
|
||||
|
||||
if (!res.ok) {
|
||||
// This will activate the closest error.tsx
|
||||
throw new Error('Failed to fetch data')
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
const data = await getData()
|
||||
return <div>{data.title}</div>
|
||||
}
|
||||
|
||||
// app/error.tsx
|
||||
'use client'
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Something went wrong!</h2>
|
||||
<button onClick={() => reset()}>Try again</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Loading States
|
||||
|
||||
```tsx
|
||||
// app/posts/loading.tsx
|
||||
export default function Loading() {
|
||||
return <div>Loading posts...</div>
|
||||
}
|
||||
|
||||
// app/posts/page.tsx
|
||||
export default async function PostsPage() {
|
||||
const posts = await fetch('https://api.example.com/posts')
|
||||
.then(res => res.json())
|
||||
|
||||
return <div>{/* render posts */}</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Client-Side Data Fetching
|
||||
|
||||
```tsx
|
||||
// When you need client-side fetching
|
||||
'use client'
|
||||
|
||||
import useSWR from 'swr'
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then(res => res.json())
|
||||
|
||||
export function Posts() {
|
||||
const { data, error, isLoading } = useSWR('/api/posts', fetcher, {
|
||||
refreshInterval: 3000, // Refresh every 3 seconds
|
||||
})
|
||||
|
||||
if (error) return <div>Failed to load</div>
|
||||
if (isLoading) return <div>Loading...</div>
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{data.map((post: Post) => (
|
||||
<li key={post.id}>{post.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Preloading Data
|
||||
|
||||
```tsx
|
||||
// lib/data.ts
|
||||
import { cache } from 'react'
|
||||
|
||||
export const preload = (id: string) => {
|
||||
void getUser(id) // Trigger fetch without awaiting
|
||||
}
|
||||
|
||||
export const getUser = cache(async (id: string) => {
|
||||
return fetch(`https://api.example.com/users/${id}`)
|
||||
.then(res => res.json())
|
||||
})
|
||||
|
||||
// components/user.tsx
|
||||
import { getUser, preload } from '@/lib/data'
|
||||
|
||||
export async function User({ id }: { id: string }) {
|
||||
const user = await getUser(id)
|
||||
return <div>{user.name}</div>
|
||||
}
|
||||
|
||||
// app/page.tsx
|
||||
import { User } from '@/components/user'
|
||||
import { preload } from '@/lib/data'
|
||||
|
||||
export default async function Page() {
|
||||
preload('123') // Start loading immediately
|
||||
return <User id="123" />
|
||||
}
|
||||
```
|
||||
|
||||
## Static Generation with Dynamic Routes
|
||||
|
||||
```tsx
|
||||
// app/posts/[slug]/page.tsx
|
||||
type Post = {
|
||||
slug: string
|
||||
title: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await fetch('https://api.example.com/posts')
|
||||
.then(res => res.json())
|
||||
|
||||
return posts.map((post: Post) => ({
|
||||
slug: post.slug,
|
||||
}))
|
||||
}
|
||||
|
||||
export default async function Post({ params }: { params: { slug: string } }) {
|
||||
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
|
||||
.then(res => res.json())
|
||||
|
||||
return (
|
||||
<article>
|
||||
<h1>{post.title}</h1>
|
||||
<div>{post.content}</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Strategy | Config | Use Case |
|
||||
|----------|--------|----------|
|
||||
| **SSG** | `cache: 'force-cache'` | Static content |
|
||||
| **SSR** | `cache: 'no-store'` | Always fresh data |
|
||||
| **ISR** | `next: { revalidate: 60 }` | Periodic updates |
|
||||
| **Tag-based** | `next: { tags: ['posts'] }` | On-demand revalidation |
|
||||
| **Dynamic** | `export const dynamic = 'force-dynamic'` | Per-request data |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Default to caching** - Use force-cache for static content
|
||||
2. **Use ISR** - Revalidate periodically for semi-dynamic content
|
||||
3. **Parallel fetching** - Use Promise.all for independent requests
|
||||
4. **Deduplicate** - Use React cache() for repeated calls
|
||||
5. **Stream with Suspense** - Show content progressively
|
||||
6. **Tag your fetches** - Enable granular revalidation
|
||||
7. **Handle errors** - Use error.tsx for graceful degradation
|
||||
545
.claude/skills/nextjs-developer/references/deployment.md
Normal file
545
.claude/skills/nextjs-developer/references/deployment.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# Deployment & Production
|
||||
|
||||
## Vercel Deployment (Recommended)
|
||||
|
||||
### Quick Deploy
|
||||
|
||||
```bash
|
||||
# Install Vercel CLI
|
||||
npm i -g vercel
|
||||
|
||||
# Deploy
|
||||
vercel
|
||||
|
||||
# Production deployment
|
||||
vercel --prod
|
||||
```
|
||||
|
||||
### vercel.json Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"buildCommand": "next build",
|
||||
"devCommand": "next dev",
|
||||
"installCommand": "npm install",
|
||||
"framework": "nextjs",
|
||||
"regions": ["iad1"],
|
||||
"env": {
|
||||
"DATABASE_URL": "@database-url",
|
||||
"NEXT_PUBLIC_API_URL": "https://api.example.com"
|
||||
},
|
||||
"headers": [
|
||||
{
|
||||
"source": "/api/(.*)",
|
||||
"headers": [
|
||||
{ "key": "Access-Control-Allow-Origin", "value": "*" },
|
||||
{ "key": "Access-Control-Allow-Methods", "value": "GET,POST,PUT,DELETE" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/old-blog/:slug",
|
||||
"destination": "/blog/:slug",
|
||||
"permanent": true
|
||||
}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/api/:path*",
|
||||
"destination": "https://api.example.com/:path*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# .env.local (not committed)
|
||||
DATABASE_URL="postgresql://user:pass@localhost:5432/db"
|
||||
NEXTAUTH_SECRET="your-secret"
|
||||
|
||||
# .env.production (committed, public vars only)
|
||||
NEXT_PUBLIC_API_URL="https://api.example.com"
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Access in Server Components
|
||||
const dbUrl = process.env.DATABASE_URL
|
||||
|
||||
// Access in Client Components (must be prefixed with NEXT_PUBLIC_)
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL
|
||||
```
|
||||
|
||||
## Self-Hosting
|
||||
|
||||
### Standalone Output
|
||||
|
||||
```js
|
||||
// next.config.js
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
```
|
||||
|
||||
```bash
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# The standalone folder contains everything needed
|
||||
# Copy these to your server:
|
||||
# - .next/standalone/
|
||||
# - .next/static/
|
||||
# - public/
|
||||
|
||||
# Run on server
|
||||
node .next/standalone/server.js
|
||||
```
|
||||
|
||||
### Node.js Server
|
||||
|
||||
```bash
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Start production server
|
||||
npm start
|
||||
|
||||
# With PM2 for process management
|
||||
pm2 start npm --name "nextjs" -- start
|
||||
pm2 startup
|
||||
pm2 save
|
||||
```
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
### Dockerfile (Multi-stage)
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Dependencies
|
||||
FROM node:20-alpine AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Stage 2: Builder
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: Runner
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
```
|
||||
|
||||
### docker-compose.yml
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
nextjs:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
- NEXTAUTH_SECRET=your-secret
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=myapp
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
```bash
|
||||
# Build and run
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f nextjs
|
||||
|
||||
# Rebuild
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
## Production Optimization
|
||||
|
||||
### next.config.js
|
||||
|
||||
```js
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Standalone for self-hosting
|
||||
output: 'standalone',
|
||||
|
||||
// Image optimization
|
||||
images: {
|
||||
formats: ['image/avif', 'image/webp'],
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'cdn.example.com',
|
||||
pathname: '/images/**',
|
||||
},
|
||||
],
|
||||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||
},
|
||||
|
||||
// Compression
|
||||
compress: true,
|
||||
|
||||
// Security headers
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/:path*',
|
||||
headers: [
|
||||
{
|
||||
key: 'X-DNS-Prefetch-Control',
|
||||
value: 'on'
|
||||
},
|
||||
{
|
||||
key: 'Strict-Transport-Security',
|
||||
value: 'max-age=63072000; includeSubDomains; preload'
|
||||
},
|
||||
{
|
||||
key: 'X-Frame-Options',
|
||||
value: 'SAMEORIGIN'
|
||||
},
|
||||
{
|
||||
key: 'X-Content-Type-Options',
|
||||
value: 'nosniff'
|
||||
},
|
||||
{
|
||||
key: 'X-XSS-Protection',
|
||||
value: '1; mode=block'
|
||||
},
|
||||
{
|
||||
key: 'Referrer-Policy',
|
||||
value: 'origin-when-cross-origin'
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
// Experimental features
|
||||
experimental: {
|
||||
optimizePackageImports: ['@mui/material', 'lodash'],
|
||||
},
|
||||
|
||||
// Bundle analyzer
|
||||
webpack: (config, { isServer }) => {
|
||||
if (process.env.ANALYZE === 'true') {
|
||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
|
||||
config.plugins.push(
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
reportFilename: isServer
|
||||
? '../analyze/server.html'
|
||||
: './analyze/client.html',
|
||||
})
|
||||
)
|
||||
}
|
||||
return config
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
```
|
||||
|
||||
### Bundle Analysis
|
||||
|
||||
```bash
|
||||
# Install analyzer
|
||||
npm install -D @next/bundle-analyzer
|
||||
|
||||
# Analyze
|
||||
ANALYZE=true npm run build
|
||||
|
||||
# Or use built-in
|
||||
npm run build -- --experimental-build-mode=compile
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
```tsx
|
||||
// app/layout.tsx
|
||||
import { SpeedInsights } from '@vercel/speed-insights/next'
|
||||
import { Analytics } from '@vercel/analytics/react'
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
<SpeedInsights />
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## CDN & Edge
|
||||
|
||||
### Static Asset CDN
|
||||
|
||||
```js
|
||||
// next.config.js
|
||||
const nextConfig = {
|
||||
assetPrefix: process.env.NODE_ENV === 'production'
|
||||
? 'https://cdn.example.com'
|
||||
: '',
|
||||
}
|
||||
```
|
||||
|
||||
### Edge Runtime
|
||||
|
||||
```tsx
|
||||
// app/api/edge/route.ts
|
||||
export const runtime = 'edge'
|
||||
|
||||
export async function GET(request: Request) {
|
||||
return new Response('Hello from Edge!', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'text/plain',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// app/page.tsx
|
||||
export const runtime = 'edge'
|
||||
|
||||
export default async function Page() {
|
||||
return <div>Edge-rendered page</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Caching Strategy
|
||||
|
||||
### ISR (Incremental Static Regeneration)
|
||||
|
||||
```tsx
|
||||
// app/blog/[slug]/page.tsx
|
||||
export const revalidate = 3600 // Revalidate every hour
|
||||
|
||||
export default async function BlogPost({ params }: { params: { slug: string } }) {
|
||||
const post = await fetchPost(params.slug)
|
||||
return <article>{post.content}</article>
|
||||
}
|
||||
```
|
||||
|
||||
### On-Demand Revalidation
|
||||
|
||||
```tsx
|
||||
// app/api/revalidate/route.ts
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const secret = request.nextUrl.searchParams.get('secret')
|
||||
|
||||
if (secret !== process.env.REVALIDATE_SECRET) {
|
||||
return Response.json({ message: 'Invalid secret' }, { status: 401 })
|
||||
}
|
||||
|
||||
const path = request.nextUrl.searchParams.get('path') || '/'
|
||||
|
||||
revalidatePath(path)
|
||||
|
||||
return Response.json({ revalidated: true, now: Date.now() })
|
||||
}
|
||||
```
|
||||
|
||||
## Database Connection Pooling
|
||||
|
||||
```ts
|
||||
// lib/db.ts
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const globalForPrisma = global as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
}
|
||||
|
||||
export const db =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db
|
||||
```
|
||||
|
||||
## Health Check Endpoint
|
||||
|
||||
```tsx
|
||||
// app/api/health/route.ts
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Check database connection
|
||||
await db.$queryRaw`SELECT 1`
|
||||
|
||||
return Response.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
})
|
||||
} catch (error) {
|
||||
return Response.json(
|
||||
{
|
||||
status: 'error',
|
||||
message: 'Database connection failed',
|
||||
},
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CI/CD with GitHub Actions
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy.yml
|
||||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
|
||||
|
||||
- name: Deploy to Vercel
|
||||
uses: amondnet/vercel-action@v25
|
||||
with:
|
||||
vercel-token: ${{ secrets.VERCEL_TOKEN }}
|
||||
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
|
||||
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
vercel-args: '--prod'
|
||||
```
|
||||
|
||||
## Monitoring & Logging
|
||||
|
||||
```tsx
|
||||
// app/error.tsx
|
||||
'use client'
|
||||
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
}) {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(error)
|
||||
}, [error])
|
||||
|
||||
return <div>Something went wrong!</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Platform | Best For | Effort |
|
||||
|----------|----------|--------|
|
||||
| **Vercel** | Zero-config, optimal performance | Low |
|
||||
| **Netlify** | Alternative to Vercel | Low |
|
||||
| **Railway** | Simple hosting with databases | Medium |
|
||||
| **AWS/GCP** | Enterprise, custom needs | High |
|
||||
| **Docker** | Self-hosting, full control | High |
|
||||
|
||||
## Production Checklist
|
||||
|
||||
- [ ] Enable TypeScript strict mode
|
||||
- [ ] Configure CSP headers
|
||||
- [ ] Setup error monitoring (Sentry)
|
||||
- [ ] Configure analytics (Vercel/GA)
|
||||
- [ ] Optimize images (next/image)
|
||||
- [ ] Enable compression
|
||||
- [ ] Setup CDN for static assets
|
||||
- [ ] Configure database connection pooling
|
||||
- [ ] Add health check endpoint
|
||||
- [ ] Setup CI/CD pipeline
|
||||
- [ ] Configure environment variables
|
||||
- [ ] Enable ISR/SSG where possible
|
||||
- [ ] Test Core Web Vitals
|
||||
- [ ] Setup logging (Datadog/LogRocket)
|
||||
- [ ] Configure backup strategy
|
||||
462
.claude/skills/nextjs-developer/references/server-actions.md
Normal file
462
.claude/skills/nextjs-developer/references/server-actions.md
Normal file
@@ -0,0 +1,462 @@
|
||||
# Server Actions
|
||||
|
||||
## Basic Server Action
|
||||
|
||||
```tsx
|
||||
// app/actions.ts
|
||||
'use server'
|
||||
|
||||
import { db } from '@/lib/db'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const title = formData.get('title') as string
|
||||
const content = formData.get('content') as string
|
||||
|
||||
await db.post.create({
|
||||
data: { title, content }
|
||||
})
|
||||
|
||||
revalidatePath('/posts')
|
||||
}
|
||||
```
|
||||
|
||||
## Form with Server Action
|
||||
|
||||
```tsx
|
||||
// app/posts/new/page.tsx
|
||||
import { createPost } from '@/app/actions'
|
||||
|
||||
export default function NewPost() {
|
||||
return (
|
||||
<form action={createPost}>
|
||||
<input name="title" required />
|
||||
<textarea name="content" required />
|
||||
<button type="submit">Create Post</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Server Action with Validation
|
||||
|
||||
```tsx
|
||||
// app/actions.ts
|
||||
'use server'
|
||||
|
||||
import { z } from 'zod'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
const CreatePostSchema = z.object({
|
||||
title: z.string().min(3).max(100),
|
||||
content: z.string().min(10),
|
||||
})
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const validatedFields = CreatePostSchema.safeParse({
|
||||
title: formData.get('title'),
|
||||
content: formData.get('content'),
|
||||
})
|
||||
|
||||
if (!validatedFields.success) {
|
||||
return {
|
||||
errors: validatedFields.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
const { title, content } = validatedFields.data
|
||||
|
||||
await db.post.create({
|
||||
data: { title, content }
|
||||
})
|
||||
|
||||
revalidatePath('/posts')
|
||||
return { success: true }
|
||||
}
|
||||
```
|
||||
|
||||
## Client Component with Server Action
|
||||
|
||||
```tsx
|
||||
// components/create-post-form.tsx
|
||||
'use client'
|
||||
|
||||
import { createPost } from '@/app/actions'
|
||||
import { useFormState, useFormStatus } from 'react-dom'
|
||||
|
||||
const initialState = {
|
||||
errors: {},
|
||||
}
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<button type="submit" disabled={pending}>
|
||||
{pending ? 'Creating...' : 'Create Post'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function CreatePostForm() {
|
||||
const [state, formAction] = useFormState(createPost, initialState)
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<div>
|
||||
<input name="title" />
|
||||
{state.errors?.title && <p>{state.errors.title[0]}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea name="content" />
|
||||
{state.errors?.content && <p>{state.errors.content[0]}</p>}
|
||||
</div>
|
||||
|
||||
<SubmitButton />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Server Action with Redirect
|
||||
|
||||
```tsx
|
||||
// app/actions.ts
|
||||
'use server'
|
||||
|
||||
import { redirect } from 'next/navigation'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const post = await db.post.create({
|
||||
data: {
|
||||
title: formData.get('title') as string,
|
||||
content: formData.get('content') as string,
|
||||
}
|
||||
})
|
||||
|
||||
revalidatePath('/posts')
|
||||
redirect(`/posts/${post.id}`)
|
||||
}
|
||||
```
|
||||
|
||||
## Optimistic Updates
|
||||
|
||||
```tsx
|
||||
// components/todo-list.tsx
|
||||
'use client'
|
||||
|
||||
import { experimental_useOptimistic as useOptimistic } from 'react'
|
||||
import { toggleTodo } from '@/app/actions'
|
||||
|
||||
export function TodoList({ todos }: { todos: Todo[] }) {
|
||||
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
|
||||
todos,
|
||||
(state, newTodo: Todo) => [...state, newTodo]
|
||||
)
|
||||
|
||||
async function handleSubmit(formData: FormData) {
|
||||
const title = formData.get('title') as string
|
||||
const newTodo = { id: crypto.randomUUID(), title, completed: false }
|
||||
|
||||
// Optimistically update UI
|
||||
addOptimisticTodo(newTodo)
|
||||
|
||||
// Send to server
|
||||
await createTodo(formData)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
{optimisticTodos.map(todo => (
|
||||
<li key={todo.id}>{todo.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<form action={handleSubmit}>
|
||||
<input name="title" />
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Server Action with Authentication
|
||||
|
||||
```tsx
|
||||
// app/actions.ts
|
||||
'use server'
|
||||
|
||||
import { auth } from '@/lib/auth'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const session = await auth()
|
||||
|
||||
if (!session) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
await db.post.create({
|
||||
data: {
|
||||
title: formData.get('title') as string,
|
||||
content: formData.get('content') as string,
|
||||
authorId: session.user.id,
|
||||
}
|
||||
})
|
||||
|
||||
revalidatePath('/posts')
|
||||
}
|
||||
```
|
||||
|
||||
## Inline Server Action
|
||||
|
||||
```tsx
|
||||
// app/posts/page.tsx
|
||||
import { db } from '@/lib/db'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export default async function Posts() {
|
||||
const posts = await db.post.findMany()
|
||||
|
||||
async function deletePost(formData: FormData) {
|
||||
'use server'
|
||||
|
||||
const id = formData.get('id') as string
|
||||
await db.post.delete({ where: { id } })
|
||||
revalidatePath('/posts')
|
||||
}
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{posts.map(post => (
|
||||
<li key={post.id}>
|
||||
{post.title}
|
||||
<form action={deletePost}>
|
||||
<input type="hidden" name="id" value={post.id} />
|
||||
<button type="submit">Delete</button>
|
||||
</form>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Programmatic Server Action Call
|
||||
|
||||
```tsx
|
||||
// components/delete-button.tsx
|
||||
'use client'
|
||||
|
||||
import { deletePost } from '@/app/actions'
|
||||
|
||||
export function DeleteButton({ postId }: { postId: string }) {
|
||||
async function handleDelete() {
|
||||
if (confirm('Are you sure?')) {
|
||||
await deletePost(postId)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleDelete}>
|
||||
Delete
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// app/actions.ts
|
||||
'use server'
|
||||
|
||||
export async function deletePost(postId: string) {
|
||||
await db.post.delete({ where: { id: postId } })
|
||||
revalidatePath('/posts')
|
||||
}
|
||||
```
|
||||
|
||||
## Revalidation Strategies
|
||||
|
||||
```tsx
|
||||
// app/actions.ts
|
||||
'use server'
|
||||
|
||||
import { revalidatePath, revalidateTag } from 'next/cache'
|
||||
|
||||
export async function updatePost(id: string, data: UpdatePostData) {
|
||||
await db.post.update({ where: { id }, data })
|
||||
|
||||
// Revalidate specific path
|
||||
revalidatePath('/posts')
|
||||
revalidatePath(`/posts/${id}`)
|
||||
|
||||
// Revalidate all paths in a layout
|
||||
revalidatePath('/posts', 'layout')
|
||||
|
||||
// Revalidate by cache tag
|
||||
revalidateTag('posts')
|
||||
}
|
||||
```
|
||||
|
||||
## Server Action with File Upload
|
||||
|
||||
```tsx
|
||||
// app/actions.ts
|
||||
'use server'
|
||||
|
||||
import { writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
export async function uploadAvatar(formData: FormData) {
|
||||
const file = formData.get('avatar') as File
|
||||
|
||||
if (!file) {
|
||||
return { error: 'No file uploaded' }
|
||||
}
|
||||
|
||||
const bytes = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(bytes)
|
||||
|
||||
const path = join(process.cwd(), 'public', 'uploads', file.name)
|
||||
await writeFile(path, buffer)
|
||||
|
||||
return { success: true, path: `/uploads/${file.name}` }
|
||||
}
|
||||
|
||||
// components/upload-form.tsx
|
||||
'use client'
|
||||
|
||||
import { uploadAvatar } from '@/app/actions'
|
||||
|
||||
export function UploadForm() {
|
||||
async function handleSubmit(formData: FormData) {
|
||||
const result = await uploadAvatar(formData)
|
||||
if (result.success) {
|
||||
console.log('Uploaded to:', result.path)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={handleSubmit}>
|
||||
<input type="file" name="avatar" accept="image/*" />
|
||||
<button type="submit">Upload</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```tsx
|
||||
// app/actions.ts
|
||||
'use server'
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
try {
|
||||
await db.post.create({
|
||||
data: {
|
||||
title: formData.get('title') as string,
|
||||
content: formData.get('content') as string,
|
||||
}
|
||||
})
|
||||
|
||||
revalidatePath('/posts')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Failed to create post:', error)
|
||||
return { error: 'Failed to create post' }
|
||||
}
|
||||
}
|
||||
|
||||
// components/form.tsx
|
||||
'use client'
|
||||
|
||||
export function CreatePostForm() {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
async function handleSubmit(formData: FormData) {
|
||||
const result = await createPost(formData)
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error)
|
||||
} else {
|
||||
// Success
|
||||
router.push('/posts')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={handleSubmit}>
|
||||
{error && <div className="error">{error}</div>}
|
||||
{/* form fields */}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Server Action with Cookies
|
||||
|
||||
```tsx
|
||||
// app/actions.ts
|
||||
'use server'
|
||||
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export async function setTheme(theme: 'light' | 'dark') {
|
||||
cookies().set('theme', theme, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 year
|
||||
path: '/',
|
||||
})
|
||||
}
|
||||
|
||||
export async function getTheme() {
|
||||
return cookies().get('theme')?.value ?? 'light'
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
```tsx
|
||||
// app/actions.ts
|
||||
'use server'
|
||||
|
||||
import { ratelimit } from '@/lib/redis'
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const session = await auth()
|
||||
const { success } = await ratelimit.limit(session.user.id)
|
||||
|
||||
if (!success) {
|
||||
return { error: 'Rate limit exceeded' }
|
||||
}
|
||||
|
||||
// Create post...
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Capability | Usage |
|
||||
|------------|-------|
|
||||
| **Define** | Add 'use server' at top of file or function |
|
||||
| **Form** | Pass action to `<form action={serverAction}>` |
|
||||
| **Programmatic** | Call directly: `await serverAction(data)` |
|
||||
| **Validation** | Use Zod/TypeBox before mutations |
|
||||
| **Revalidate** | `revalidatePath()` or `revalidateTag()` |
|
||||
| **Redirect** | `redirect()` after mutation |
|
||||
| **Errors** | Return error objects, handle in client |
|
||||
| **Files** | Access via `formData.get()` as File |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always validate** - Use Zod/TypeBox for type-safe validation
|
||||
2. **Revalidate** - Call revalidatePath() after mutations
|
||||
3. **Handle errors** - Return error objects instead of throwing
|
||||
4. **Auth checks** - Verify session before mutations
|
||||
5. **Rate limiting** - Protect against abuse
|
||||
6. **Type safety** - Define input/output types
|
||||
7. **Optimistic updates** - Use useOptimistic for better UX
|
||||
384
.claude/skills/nextjs-developer/references/server-components.md
Normal file
384
.claude/skills/nextjs-developer/references/server-components.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# React Server Components
|
||||
|
||||
## Server Components (Default)
|
||||
|
||||
```tsx
|
||||
// app/page.tsx - Server Component by default
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export default async function Page() {
|
||||
// Data fetching in Server Component
|
||||
const users = await db.user.findMany()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Users</h1>
|
||||
<ul>
|
||||
{users.map(user => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits of Server Components
|
||||
|
||||
- **Zero bundle size** - Server Components don't add JavaScript to client bundle
|
||||
- **Direct backend access** - Query databases, read files, use secrets
|
||||
- **Automatic code splitting** - Only Client Components add to bundle
|
||||
- **Streaming** - Send UI progressively as data loads
|
||||
- **No client-side waterfalls** - Fetch all data in parallel on server
|
||||
|
||||
## Client Components
|
||||
|
||||
```tsx
|
||||
// components/counter.tsx
|
||||
'use client' // Required directive
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
export function Counter() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<button onClick={() => setCount(count + 1)}>
|
||||
Count: {count}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## When to Use Client Components
|
||||
|
||||
Use `'use client'` when you need:
|
||||
- **Interactivity** - onClick, onChange, event handlers
|
||||
- **State** - useState, useReducer
|
||||
- **Effects** - useEffect, useLayoutEffect
|
||||
- **Browser APIs** - localStorage, window, document
|
||||
- **Custom hooks** - Any hook using client-only features
|
||||
- **Class components** - Component lifecycle methods
|
||||
|
||||
## Composition Pattern
|
||||
|
||||
```tsx
|
||||
// app/page.tsx - Server Component
|
||||
import { ClientWrapper } from './client-wrapper'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export default async function Page() {
|
||||
const data = await db.query()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Server Component content */}
|
||||
<h1>Server Content</h1>
|
||||
|
||||
{/* Pass data to Client Component */}
|
||||
<ClientWrapper initialData={data}>
|
||||
{/* Server Component as children */}
|
||||
<ServerSidebar />
|
||||
</ClientWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// components/client-wrapper.tsx
|
||||
'use client'
|
||||
|
||||
export function ClientWrapper({
|
||||
children,
|
||||
initialData,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
initialData: Data
|
||||
}) {
|
||||
const [data, setData] = useState(initialData)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Client Component UI */}
|
||||
<button onClick={() => refresh()}>Refresh</button>
|
||||
{/* Server Component children */}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Streaming with Suspense
|
||||
|
||||
```tsx
|
||||
// app/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
import { SlowComponent } from './slow-component'
|
||||
import { FastComponent } from './fast-component'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
{/* Renders immediately */}
|
||||
<FastComponent />
|
||||
|
||||
{/* Shows fallback while loading */}
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<SlowComponent />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// components/slow-component.tsx
|
||||
async function getData() {
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
return { data: 'Loaded!' }
|
||||
}
|
||||
|
||||
export async function SlowComponent() {
|
||||
const data = await getData()
|
||||
return <div>{data.data}</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Parallel Data Fetching
|
||||
|
||||
```tsx
|
||||
// app/dashboard/page.tsx
|
||||
async function getUser() {
|
||||
return fetch('https://api.example.com/user')
|
||||
}
|
||||
|
||||
async function getPosts() {
|
||||
return fetch('https://api.example.com/posts')
|
||||
}
|
||||
|
||||
export default async function Dashboard() {
|
||||
// Fetch in parallel
|
||||
const [user, posts] = await Promise.all([
|
||||
getUser(),
|
||||
getPosts(),
|
||||
])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<UserProfile user={user} />
|
||||
<PostsList posts={posts} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Sequential Data Fetching
|
||||
|
||||
```tsx
|
||||
// app/artist/[id]/page.tsx
|
||||
async function getArtist(id: string) {
|
||||
return fetch(`https://api.example.com/artists/${id}`)
|
||||
}
|
||||
|
||||
async function getAlbums(artistId: string) {
|
||||
return fetch(`https://api.example.com/artists/${artistId}/albums`)
|
||||
}
|
||||
|
||||
export default async function ArtistPage({ params }: { params: { id: string } }) {
|
||||
// Sequential: albums depends on artist
|
||||
const artist = await getArtist(params.id)
|
||||
const albums = await getAlbums(artist.id)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{artist.name}</h1>
|
||||
<Albums albums={albums} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Preloading Data
|
||||
|
||||
```tsx
|
||||
// lib/data.ts
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getUser = cache(async (id: string) => {
|
||||
return db.user.findUnique({ where: { id } })
|
||||
})
|
||||
|
||||
// components/user-profile.tsx
|
||||
export async function UserProfile({ userId }: { userId: string }) {
|
||||
const user = await getUser(userId)
|
||||
return <div>{user.name}</div>
|
||||
}
|
||||
|
||||
// app/page.tsx
|
||||
import { getUser } from '@/lib/data'
|
||||
import { UserProfile } from '@/components/user-profile'
|
||||
|
||||
export default async function Page() {
|
||||
// Preload
|
||||
getUser('123')
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* This will use cached result */}
|
||||
<UserProfile userId="123" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Server Component Patterns
|
||||
|
||||
### Pattern: Layout with Data Fetching
|
||||
|
||||
```tsx
|
||||
// app/dashboard/layout.tsx
|
||||
import { auth } from '@/lib/auth'
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const session = await auth()
|
||||
const user = await db.user.findUnique({ where: { id: session.userId } })
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Sidebar user={user} />
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Conditional Client Components
|
||||
|
||||
```tsx
|
||||
// app/page.tsx
|
||||
import { ClientComponent } from './client-component'
|
||||
|
||||
export default async function Page() {
|
||||
const data = await fetchData()
|
||||
|
||||
// Only render Client Component when needed
|
||||
if (data.requiresInteractivity) {
|
||||
return <ClientComponent data={data} />
|
||||
}
|
||||
|
||||
return <div>{data.content}</div>
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Server Component with Client Island
|
||||
|
||||
```tsx
|
||||
// app/blog/[slug]/page.tsx
|
||||
import { LikeButton } from './like-button'
|
||||
|
||||
export default async function BlogPost({ params }: { params: { slug: string } }) {
|
||||
const post = await getPost(params.slug)
|
||||
|
||||
return (
|
||||
<article>
|
||||
{/* Server-rendered content */}
|
||||
<h1>{post.title}</h1>
|
||||
<div dangerouslySetInnerHTML={{ __html: post.content }} />
|
||||
|
||||
{/* Client island for interactivity */}
|
||||
<LikeButton postId={post.id} initialLikes={post.likes} />
|
||||
</article>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Context in Server/Client Components
|
||||
|
||||
```tsx
|
||||
// app/providers.tsx
|
||||
'use client'
|
||||
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return <ThemeProvider>{children}</ThemeProvider>
|
||||
}
|
||||
|
||||
// app/layout.tsx
|
||||
import { Providers } from './providers'
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Third-Party Components
|
||||
|
||||
```tsx
|
||||
// components/carousel-wrapper.tsx
|
||||
'use client'
|
||||
|
||||
import { Carousel } from 'third-party-carousel'
|
||||
|
||||
export function CarouselWrapper({ items }: { items: Item[] }) {
|
||||
return <Carousel items={items} />
|
||||
}
|
||||
|
||||
// app/page.tsx
|
||||
import { CarouselWrapper } from '@/components/carousel-wrapper'
|
||||
|
||||
export default async function Page() {
|
||||
const items = await fetchItems()
|
||||
return <CarouselWrapper items={items} />
|
||||
}
|
||||
```
|
||||
|
||||
## Edge Runtime
|
||||
|
||||
```tsx
|
||||
// app/api/route.ts
|
||||
export const runtime = 'edge'
|
||||
|
||||
export async function GET() {
|
||||
return new Response('Hello from Edge!')
|
||||
}
|
||||
|
||||
// app/page.tsx
|
||||
export const runtime = 'edge'
|
||||
|
||||
export default async function Page() {
|
||||
return <div>Edge-rendered page</div>
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Capability | Server Component | Client Component |
|
||||
|------------|------------------|------------------|
|
||||
| Data fetching | ✅ Yes | ⚠️ Use SWR/React Query |
|
||||
| Backend access | ✅ Yes (DB, files) | ❌ No |
|
||||
| Event handlers | ❌ No | ✅ Yes |
|
||||
| State/Effects | ❌ No | ✅ Yes |
|
||||
| Browser APIs | ❌ No | ✅ Yes |
|
||||
| Bundle size | 0 KB | Adds to bundle |
|
||||
| Streaming | ✅ Yes | ❌ No |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Default to Server Components** - Only use 'use client' when needed
|
||||
2. **Move Client Components down** - Push them to leaves of component tree
|
||||
3. **Pass data down** - Fetch in Server Components, pass to Client Components
|
||||
4. **Use composition** - Nest Server Components inside Client Components via children
|
||||
5. **Cache expensive operations** - Use React cache() for deduplication
|
||||
6
.claude/skills/react-nextjs-development/.openskills.json
Normal file
6
.claude/skills/react-nextjs-development/.openskills.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/react-nextjs-development",
|
||||
"installedAt": "2026-04-07T00:45:24.783Z"
|
||||
}
|
||||
228
.claude/skills/react-nextjs-development/SKILL.md
Normal file
228
.claude/skills/react-nextjs-development/SKILL.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
name: react-nextjs-development
|
||||
description: "React and Next.js 14+ application development with App Router, Server Components, TypeScript, Tailwind CSS, and modern frontend patterns."
|
||||
category: granular-workflow-bundle
|
||||
risk: safe
|
||||
source: personal
|
||||
date_added: "2026-02-27"
|
||||
---
|
||||
|
||||
# React/Next.js Development Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
Specialized workflow for building React and Next.js 14+ applications with modern patterns including App Router, Server Components, TypeScript, and Tailwind CSS.
|
||||
|
||||
## When to Use This Workflow
|
||||
|
||||
Use this workflow when:
|
||||
- Building new React applications
|
||||
- Creating Next.js 14+ projects with App Router
|
||||
- Implementing Server Components
|
||||
- Setting up TypeScript with React
|
||||
- Styling with Tailwind CSS
|
||||
- Building full-stack Next.js applications
|
||||
|
||||
## Workflow Phases
|
||||
|
||||
### Phase 1: Project Setup
|
||||
|
||||
#### Skills to Invoke
|
||||
- `app-builder` - Application scaffolding
|
||||
- `senior-fullstack` - Full-stack guidance
|
||||
- `nextjs-app-router-patterns` - Next.js 14+ patterns
|
||||
- `typescript-pro` - TypeScript setup
|
||||
|
||||
#### Actions
|
||||
1. Choose project type (React SPA, Next.js app)
|
||||
2. Select build tool (Vite, Next.js, Create React App)
|
||||
3. Scaffold project structure
|
||||
4. Configure TypeScript
|
||||
5. Set up ESLint and Prettier
|
||||
|
||||
#### Copy-Paste Prompts
|
||||
```
|
||||
Use @app-builder to scaffold a new Next.js 14 project with App Router
|
||||
```
|
||||
|
||||
```
|
||||
Use @nextjs-app-router-patterns to set up Server Components
|
||||
```
|
||||
|
||||
### Phase 2: Component Architecture
|
||||
|
||||
#### Skills to Invoke
|
||||
- `frontend-developer` - Component development
|
||||
- `react-patterns` - React patterns
|
||||
- `react-state-management` - State management
|
||||
- `react-ui-patterns` - UI patterns
|
||||
|
||||
#### Actions
|
||||
1. Design component hierarchy
|
||||
2. Create base components
|
||||
3. Implement layout components
|
||||
4. Set up state management
|
||||
5. Create custom hooks
|
||||
|
||||
#### Copy-Paste Prompts
|
||||
```
|
||||
Use @frontend-developer to create reusable React components
|
||||
```
|
||||
|
||||
```
|
||||
Use @react-patterns to implement proper component composition
|
||||
```
|
||||
|
||||
```
|
||||
Use @react-state-management to set up Zustand store
|
||||
```
|
||||
|
||||
### Phase 3: Styling and Design
|
||||
|
||||
#### Skills to Invoke
|
||||
- `frontend-design` - UI design
|
||||
- `tailwind-patterns` - Tailwind CSS
|
||||
- `tailwind-design-system` - Design system
|
||||
- `core-components` - Component library
|
||||
|
||||
#### Actions
|
||||
1. Set up Tailwind CSS
|
||||
2. Configure design tokens
|
||||
3. Create utility classes
|
||||
4. Build component styles
|
||||
5. Implement responsive design
|
||||
|
||||
#### Copy-Paste Prompts
|
||||
```
|
||||
Use @tailwind-patterns to style components with Tailwind CSS v4
|
||||
```
|
||||
|
||||
```
|
||||
Use @frontend-design to create a modern dashboard UI
|
||||
```
|
||||
|
||||
### Phase 4: Data Fetching
|
||||
|
||||
#### Skills to Invoke
|
||||
- `nextjs-app-router-patterns` - Server Components
|
||||
- `react-state-management` - React Query
|
||||
- `api-patterns` - API integration
|
||||
|
||||
#### Actions
|
||||
1. Implement Server Components
|
||||
2. Set up React Query/SWR
|
||||
3. Create API client
|
||||
4. Handle loading states
|
||||
5. Implement error boundaries
|
||||
|
||||
#### Copy-Paste Prompts
|
||||
```
|
||||
Use @nextjs-app-router-patterns to implement Server Components data fetching
|
||||
```
|
||||
|
||||
### Phase 5: Routing and Navigation
|
||||
|
||||
#### Skills to Invoke
|
||||
- `nextjs-app-router-patterns` - App Router
|
||||
- `nextjs-best-practices` - Next.js patterns
|
||||
|
||||
#### Actions
|
||||
1. Set up file-based routing
|
||||
2. Create dynamic routes
|
||||
3. Implement nested routes
|
||||
4. Add route guards
|
||||
5. Configure redirects
|
||||
|
||||
#### Copy-Paste Prompts
|
||||
```
|
||||
Use @nextjs-app-router-patterns to set up parallel routes and intercepting routes
|
||||
```
|
||||
|
||||
### Phase 6: Forms and Validation
|
||||
|
||||
#### Skills to Invoke
|
||||
- `frontend-developer` - Form development
|
||||
- `typescript-advanced-types` - Type validation
|
||||
- `react-ui-patterns` - Form patterns
|
||||
|
||||
#### Actions
|
||||
1. Choose form library (React Hook Form, Formik)
|
||||
2. Set up validation (Zod, Yup)
|
||||
3. Create form components
|
||||
4. Handle submissions
|
||||
5. Implement error handling
|
||||
|
||||
#### Copy-Paste Prompts
|
||||
```
|
||||
Use @frontend-developer to create forms with React Hook Form and Zod
|
||||
```
|
||||
|
||||
### Phase 7: Testing
|
||||
|
||||
#### Skills to Invoke
|
||||
- `javascript-testing-patterns` - Jest/Vitest
|
||||
- `playwright-skill` - E2E testing
|
||||
- `e2e-testing-patterns` - E2E patterns
|
||||
|
||||
#### Actions
|
||||
1. Set up testing framework
|
||||
2. Write unit tests
|
||||
3. Create component tests
|
||||
4. Implement E2E tests
|
||||
5. Configure CI integration
|
||||
|
||||
#### Copy-Paste Prompts
|
||||
```
|
||||
Use @javascript-testing-patterns to write Vitest tests
|
||||
```
|
||||
|
||||
```
|
||||
Use @playwright-skill to create E2E tests for critical flows
|
||||
```
|
||||
|
||||
### Phase 8: Build and Deployment
|
||||
|
||||
#### Skills to Invoke
|
||||
- `vercel-deployment` - Vercel deployment
|
||||
- `vercel-deploy-claimable` - Vercel deployment
|
||||
- `web-performance-optimization` - Performance
|
||||
|
||||
#### Actions
|
||||
1. Configure build settings
|
||||
2. Optimize bundle size
|
||||
3. Set up environment variables
|
||||
4. Deploy to Vercel
|
||||
5. Configure preview deployments
|
||||
|
||||
#### Copy-Paste Prompts
|
||||
```
|
||||
Use @vercel-deployment to deploy Next.js app to production
|
||||
```
|
||||
|
||||
## Technology Stack
|
||||
|
||||
| Category | Technology |
|
||||
|----------|------------|
|
||||
| Framework | Next.js 14+, React 18+ |
|
||||
| Language | TypeScript 5+ |
|
||||
| Styling | Tailwind CSS v4 |
|
||||
| State | Zustand, React Query |
|
||||
| Forms | React Hook Form, Zod |
|
||||
| Testing | Vitest, Playwright |
|
||||
| Deployment | Vercel |
|
||||
|
||||
## Quality Gates
|
||||
|
||||
- [ ] TypeScript compiles without errors
|
||||
- [ ] All tests passing
|
||||
- [ ] Linting clean
|
||||
- [ ] Performance metrics met (LCP, CLS, FID)
|
||||
- [ ] Accessibility checked (WCAG 2.1)
|
||||
- [ ] Responsive design verified
|
||||
|
||||
## Related Workflow Bundles
|
||||
|
||||
- `development` - General development
|
||||
- `testing-qa` - Testing workflow
|
||||
- `documentation` - Documentation
|
||||
- `typescript-development` - TypeScript patterns
|
||||
6
.claude/skills/tdd/.openskills.json
Normal file
6
.claude/skills/tdd/.openskills.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/tdd",
|
||||
"installedAt": "2026-04-07T00:45:24.783Z"
|
||||
}
|
||||
107
.claude/skills/tdd/SKILL.md
Normal file
107
.claude/skills/tdd/SKILL.md
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
name: tdd
|
||||
description: Test-driven development with red-green-refactor loop. Use when user wants to build features or fix bugs using TDD, mentions "red-green-refactor", wants integration tests, or asks for test-first development.
|
||||
---
|
||||
|
||||
# Test-Driven Development
|
||||
|
||||
## Philosophy
|
||||
|
||||
**Core principle**: Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't.
|
||||
|
||||
**Good tests** are integration-style: they exercise real code paths through public APIs. They describe _what_ the system does, not _how_ it does it. A good test reads like a specification - "user can checkout with valid cart" tells you exactly what capability exists. These tests survive refactors because they don't care about internal structure.
|
||||
|
||||
**Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior.
|
||||
|
||||
See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines.
|
||||
|
||||
## Anti-Pattern: Horizontal Slices
|
||||
|
||||
**DO NOT write all tests first, then all implementation.** This is "horizontal slicing" - treating RED as "write all tests" and GREEN as "write all code."
|
||||
|
||||
This produces **crap tests**:
|
||||
|
||||
- Tests written in bulk test _imagined_ behavior, not _actual_ behavior
|
||||
- You end up testing the _shape_ of things (data structures, function signatures) rather than user-facing behavior
|
||||
- Tests become insensitive to real changes - they pass when behavior breaks, fail when behavior is fine
|
||||
- You outrun your headlights, committing to test structure before understanding the implementation
|
||||
|
||||
**Correct approach**: Vertical slices via tracer bullets. One test → one implementation → repeat. Each test responds to what you learned from the previous cycle. Because you just wrote the code, you know exactly what behavior matters and how to verify it.
|
||||
|
||||
```
|
||||
WRONG (horizontal):
|
||||
RED: test1, test2, test3, test4, test5
|
||||
GREEN: impl1, impl2, impl3, impl4, impl5
|
||||
|
||||
RIGHT (vertical):
|
||||
RED→GREEN: test1→impl1
|
||||
RED→GREEN: test2→impl2
|
||||
RED→GREEN: test3→impl3
|
||||
...
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Planning
|
||||
|
||||
Before writing any code:
|
||||
|
||||
- [ ] Confirm with user what interface changes are needed
|
||||
- [ ] Confirm with user which behaviors to test (prioritize)
|
||||
- [ ] Identify opportunities for [deep modules](deep-modules.md) (small interface, deep implementation)
|
||||
- [ ] Design interfaces for [testability](interface-design.md)
|
||||
- [ ] List the behaviors to test (not implementation steps)
|
||||
- [ ] Get user approval on the plan
|
||||
|
||||
Ask: "What should the public interface look like? Which behaviors are most important to test?"
|
||||
|
||||
**You can't test everything.** Confirm with the user exactly which behaviors matter most. Focus testing effort on critical paths and complex logic, not every possible edge case.
|
||||
|
||||
### 2. Tracer Bullet
|
||||
|
||||
Write ONE test that confirms ONE thing about the system:
|
||||
|
||||
```
|
||||
RED: Write test for first behavior → test fails
|
||||
GREEN: Write minimal code to pass → test passes
|
||||
```
|
||||
|
||||
This is your tracer bullet - proves the path works end-to-end.
|
||||
|
||||
### 3. Incremental Loop
|
||||
|
||||
For each remaining behavior:
|
||||
|
||||
```
|
||||
RED: Write next test → fails
|
||||
GREEN: Minimal code to pass → passes
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- One test at a time
|
||||
- Only enough code to pass current test
|
||||
- Don't anticipate future tests
|
||||
- Keep tests focused on observable behavior
|
||||
|
||||
### 4. Refactor
|
||||
|
||||
After all tests pass, look for [refactor candidates](refactoring.md):
|
||||
|
||||
- [ ] Extract duplication
|
||||
- [ ] Deepen modules (move complexity behind simple interfaces)
|
||||
- [ ] Apply SOLID principles where natural
|
||||
- [ ] Consider what new code reveals about existing code
|
||||
- [ ] Run tests after each refactor step
|
||||
|
||||
**Never refactor while RED.** Get to GREEN first.
|
||||
|
||||
## Checklist Per Cycle
|
||||
|
||||
```
|
||||
[ ] Test describes behavior, not implementation
|
||||
[ ] Test uses public interface only
|
||||
[ ] Test would survive internal refactor
|
||||
[ ] Code is minimal for this test
|
||||
[ ] No speculative features added
|
||||
```
|
||||
33
.claude/skills/tdd/deep-modules.md
Normal file
33
.claude/skills/tdd/deep-modules.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Deep Modules
|
||||
|
||||
From "A Philosophy of Software Design":
|
||||
|
||||
**Deep module** = small interface + lots of implementation
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ Small Interface │ ← Few methods, simple params
|
||||
├─────────────────────┤
|
||||
│ │
|
||||
│ │
|
||||
│ Deep Implementation│ ← Complex logic hidden
|
||||
│ │
|
||||
│ │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
**Shallow module** = large interface + little implementation (avoid)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Large Interface │ ← Many methods, complex params
|
||||
├─────────────────────────────────┤
|
||||
│ Thin Implementation │ ← Just passes through
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
When designing interfaces, ask:
|
||||
|
||||
- Can I reduce the number of methods?
|
||||
- Can I simplify the parameters?
|
||||
- Can I hide more complexity inside?
|
||||
31
.claude/skills/tdd/interface-design.md
Normal file
31
.claude/skills/tdd/interface-design.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Interface Design for Testability
|
||||
|
||||
Good interfaces make testing natural:
|
||||
|
||||
1. **Accept dependencies, don't create them**
|
||||
|
||||
```typescript
|
||||
// Testable
|
||||
function processOrder(order, paymentGateway) {}
|
||||
|
||||
// Hard to test
|
||||
function processOrder(order) {
|
||||
const gateway = new StripeGateway();
|
||||
}
|
||||
```
|
||||
|
||||
2. **Return results, don't produce side effects**
|
||||
|
||||
```typescript
|
||||
// Testable
|
||||
function calculateDiscount(cart): Discount {}
|
||||
|
||||
// Hard to test
|
||||
function applyDiscount(cart): void {
|
||||
cart.total -= discount;
|
||||
}
|
||||
```
|
||||
|
||||
3. **Small surface area**
|
||||
- Fewer methods = fewer tests needed
|
||||
- Fewer params = simpler test setup
|
||||
59
.claude/skills/tdd/mocking.md
Normal file
59
.claude/skills/tdd/mocking.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# When to Mock
|
||||
|
||||
Mock at **system boundaries** only:
|
||||
|
||||
- External APIs (payment, email, etc.)
|
||||
- Databases (sometimes - prefer test DB)
|
||||
- Time/randomness
|
||||
- File system (sometimes)
|
||||
|
||||
Don't mock:
|
||||
|
||||
- Your own classes/modules
|
||||
- Internal collaborators
|
||||
- Anything you control
|
||||
|
||||
## Designing for Mockability
|
||||
|
||||
At system boundaries, design interfaces that are easy to mock:
|
||||
|
||||
**1. Use dependency injection**
|
||||
|
||||
Pass external dependencies in rather than creating them internally:
|
||||
|
||||
```typescript
|
||||
// Easy to mock
|
||||
function processPayment(order, paymentClient) {
|
||||
return paymentClient.charge(order.total);
|
||||
}
|
||||
|
||||
// Hard to mock
|
||||
function processPayment(order) {
|
||||
const client = new StripeClient(process.env.STRIPE_KEY);
|
||||
return client.charge(order.total);
|
||||
}
|
||||
```
|
||||
|
||||
**2. Prefer SDK-style interfaces over generic fetchers**
|
||||
|
||||
Create specific functions for each external operation instead of one generic function with conditional logic:
|
||||
|
||||
```typescript
|
||||
// GOOD: Each function is independently mockable
|
||||
const api = {
|
||||
getUser: (id) => fetch(`/users/${id}`),
|
||||
getOrders: (userId) => fetch(`/users/${userId}/orders`),
|
||||
createOrder: (data) => fetch('/orders', { method: 'POST', body: data }),
|
||||
};
|
||||
|
||||
// BAD: Mocking requires conditional logic inside the mock
|
||||
const api = {
|
||||
fetch: (endpoint, options) => fetch(endpoint, options),
|
||||
};
|
||||
```
|
||||
|
||||
The SDK approach means:
|
||||
- Each mock returns one specific shape
|
||||
- No conditional logic in test setup
|
||||
- Easier to see which endpoints a test exercises
|
||||
- Type safety per endpoint
|
||||
10
.claude/skills/tdd/refactoring.md
Normal file
10
.claude/skills/tdd/refactoring.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Refactor Candidates
|
||||
|
||||
After TDD cycle, look for:
|
||||
|
||||
- **Duplication** → Extract function/class
|
||||
- **Long methods** → Break into private helpers (keep tests on public interface)
|
||||
- **Shallow modules** → Combine or deepen
|
||||
- **Feature envy** → Move logic to where data lives
|
||||
- **Primitive obsession** → Introduce value objects
|
||||
- **Existing code** the new code reveals as problematic
|
||||
61
.claude/skills/tdd/tests.md
Normal file
61
.claude/skills/tdd/tests.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Good and Bad Tests
|
||||
|
||||
## Good Tests
|
||||
|
||||
**Integration-style**: Test through real interfaces, not mocks of internal parts.
|
||||
|
||||
```typescript
|
||||
// GOOD: Tests observable behavior
|
||||
test("user can checkout with valid cart", async () => {
|
||||
const cart = createCart();
|
||||
cart.add(product);
|
||||
const result = await checkout(cart, paymentMethod);
|
||||
expect(result.status).toBe("confirmed");
|
||||
});
|
||||
```
|
||||
|
||||
Characteristics:
|
||||
|
||||
- Tests behavior users/callers care about
|
||||
- Uses public API only
|
||||
- Survives internal refactors
|
||||
- Describes WHAT, not HOW
|
||||
- One logical assertion per test
|
||||
|
||||
## Bad Tests
|
||||
|
||||
**Implementation-detail tests**: Coupled to internal structure.
|
||||
|
||||
```typescript
|
||||
// BAD: Tests implementation details
|
||||
test("checkout calls paymentService.process", async () => {
|
||||
const mockPayment = jest.mock(paymentService);
|
||||
await checkout(cart, payment);
|
||||
expect(mockPayment.process).toHaveBeenCalledWith(cart.total);
|
||||
});
|
||||
```
|
||||
|
||||
Red flags:
|
||||
|
||||
- Mocking internal collaborators
|
||||
- Testing private methods
|
||||
- Asserting on call counts/order
|
||||
- Test breaks when refactoring without behavior change
|
||||
- Test name describes HOW not WHAT
|
||||
- Verifying through external means instead of interface
|
||||
|
||||
```typescript
|
||||
// BAD: Bypasses interface to verify
|
||||
test("createUser saves to database", async () => {
|
||||
await createUser({ name: "Alice" });
|
||||
const row = await db.query("SELECT * FROM users WHERE name = ?", ["Alice"]);
|
||||
expect(row).toBeDefined();
|
||||
});
|
||||
|
||||
// GOOD: Verifies through interface
|
||||
test("createUser makes user retrievable", async () => {
|
||||
const user = await createUser({ name: "Alice" });
|
||||
const retrieved = await getUser(user.id);
|
||||
expect(retrieved.name).toBe("Alice");
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/typescript-advanced-types",
|
||||
"installedAt": "2026-04-07T00:45:24.783Z"
|
||||
}
|
||||
717
.claude/skills/typescript-advanced-types/SKILL.md
Normal file
717
.claude/skills/typescript-advanced-types/SKILL.md
Normal file
@@ -0,0 +1,717 @@
|
||||
---
|
||||
name: typescript-advanced-types
|
||||
description: Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects.
|
||||
---
|
||||
|
||||
# TypeScript Advanced Types
|
||||
|
||||
Comprehensive guidance for mastering TypeScript's advanced type system including generics, conditional types, mapped types, template literal types, and utility types for building robust, type-safe applications.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Building type-safe libraries or frameworks
|
||||
- Creating reusable generic components
|
||||
- Implementing complex type inference logic
|
||||
- Designing type-safe API clients
|
||||
- Building form validation systems
|
||||
- Creating strongly-typed configuration objects
|
||||
- Implementing type-safe state management
|
||||
- Migrating JavaScript codebases to TypeScript
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Generics
|
||||
|
||||
**Purpose:** Create reusable, type-flexible components while maintaining type safety.
|
||||
|
||||
**Basic Generic Function:**
|
||||
|
||||
```typescript
|
||||
function identity<T>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
|
||||
const num = identity<number>(42); // Type: number
|
||||
const str = identity<string>("hello"); // Type: string
|
||||
const auto = identity(true); // Type inferred: boolean
|
||||
```
|
||||
|
||||
**Generic Constraints:**
|
||||
|
||||
```typescript
|
||||
interface HasLength {
|
||||
length: number;
|
||||
}
|
||||
|
||||
function logLength<T extends HasLength>(item: T): T {
|
||||
console.log(item.length);
|
||||
return item;
|
||||
}
|
||||
|
||||
logLength("hello"); // OK: string has length
|
||||
logLength([1, 2, 3]); // OK: array has length
|
||||
logLength({ length: 10 }); // OK: object has length
|
||||
// logLength(42); // Error: number has no length
|
||||
```
|
||||
|
||||
**Multiple Type Parameters:**
|
||||
|
||||
```typescript
|
||||
function merge<T, U>(obj1: T, obj2: U): T & U {
|
||||
return { ...obj1, ...obj2 };
|
||||
}
|
||||
|
||||
const merged = merge({ name: "John" }, { age: 30 });
|
||||
// Type: { name: string } & { age: number }
|
||||
```
|
||||
|
||||
### 2. Conditional Types
|
||||
|
||||
**Purpose:** Create types that depend on conditions, enabling sophisticated type logic.
|
||||
|
||||
**Basic Conditional Type:**
|
||||
|
||||
```typescript
|
||||
type IsString<T> = T extends string ? true : false;
|
||||
|
||||
type A = IsString<string>; // true
|
||||
type B = IsString<number>; // false
|
||||
```
|
||||
|
||||
**Extracting Return Types:**
|
||||
|
||||
```typescript
|
||||
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||
|
||||
function getUser() {
|
||||
return { id: 1, name: "John" };
|
||||
}
|
||||
|
||||
type User = ReturnType<typeof getUser>;
|
||||
// Type: { id: number; name: string; }
|
||||
```
|
||||
|
||||
**Distributive Conditional Types:**
|
||||
|
||||
```typescript
|
||||
type ToArray<T> = T extends any ? T[] : never;
|
||||
|
||||
type StrOrNumArray = ToArray<string | number>;
|
||||
// Type: string[] | number[]
|
||||
```
|
||||
|
||||
**Nested Conditions:**
|
||||
|
||||
```typescript
|
||||
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<() => void>; // "function"
|
||||
```
|
||||
|
||||
### 3. Mapped Types
|
||||
|
||||
**Purpose:** Transform existing types by iterating over their properties.
|
||||
|
||||
**Basic Mapped Type:**
|
||||
|
||||
```typescript
|
||||
type Readonly<T> = {
|
||||
readonly [P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type ReadonlyUser = Readonly<User>;
|
||||
// Type: { readonly id: number; readonly name: string; }
|
||||
```
|
||||
|
||||
**Optional Properties:**
|
||||
|
||||
```typescript
|
||||
type Partial<T> = {
|
||||
[P in keyof T]?: T[P];
|
||||
};
|
||||
|
||||
type PartialUser = Partial<User>;
|
||||
// Type: { id?: number; name?: string; }
|
||||
```
|
||||
|
||||
**Key Remapping:**
|
||||
|
||||
```typescript
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
||||
};
|
||||
|
||||
interface Person {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
type PersonGetters = Getters<Person>;
|
||||
// Type: { getName: () => string; getAge: () => number; }
|
||||
```
|
||||
|
||||
**Filtering Properties:**
|
||||
|
||||
```typescript
|
||||
type PickByType<T, U> = {
|
||||
[K in keyof T as T[K] extends U ? K : never]: T[K];
|
||||
};
|
||||
|
||||
interface Mixed {
|
||||
id: number;
|
||||
name: string;
|
||||
age: number;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
type OnlyNumbers = PickByType<Mixed, number>;
|
||||
// Type: { id: number; age: number; }
|
||||
```
|
||||
|
||||
### 4. Template Literal Types
|
||||
|
||||
**Purpose:** Create string-based types with pattern matching and transformation.
|
||||
|
||||
**Basic Template Literal:**
|
||||
|
||||
```typescript
|
||||
type EventName = "click" | "focus" | "blur";
|
||||
type EventHandler = `on${Capitalize<EventName>}`;
|
||||
// Type: "onClick" | "onFocus" | "onBlur"
|
||||
```
|
||||
|
||||
**String Manipulation:**
|
||||
|
||||
```typescript
|
||||
type UppercaseGreeting = Uppercase<"hello">; // "HELLO"
|
||||
type LowercaseGreeting = Lowercase<"HELLO">; // "hello"
|
||||
type CapitalizedName = Capitalize<"john">; // "John"
|
||||
type UncapitalizedName = Uncapitalize<"John">; // "john"
|
||||
```
|
||||
|
||||
**Path Building:**
|
||||
|
||||
```typescript
|
||||
type Path<T> = T extends object
|
||||
? {
|
||||
[K in keyof T]: K extends string ? `${K}` | `${K}.${Path<T[K]>}` : never;
|
||||
}[keyof T]
|
||||
: never;
|
||||
|
||||
interface Config {
|
||||
server: {
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
database: {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
type ConfigPath = Path<Config>;
|
||||
// Type: "server" | "database" | "server.host" | "server.port" | "database.url"
|
||||
```
|
||||
|
||||
### 5. Utility Types
|
||||
|
||||
**Built-in Utility Types:**
|
||||
|
||||
```typescript
|
||||
// Partial<T> - Make all properties optional
|
||||
type PartialUser = Partial<User>;
|
||||
|
||||
// Required<T> - Make all properties required
|
||||
type RequiredUser = Required<PartialUser>;
|
||||
|
||||
// Readonly<T> - Make all properties readonly
|
||||
type ReadonlyUser = Readonly<User>;
|
||||
|
||||
// Pick<T, K> - Select specific properties
|
||||
type UserName = Pick<User, "name" | "email">;
|
||||
|
||||
// Omit<T, K> - Remove specific properties
|
||||
type UserWithoutPassword = Omit<User, "password">;
|
||||
|
||||
// Exclude<T, U> - Exclude types from union
|
||||
type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
|
||||
|
||||
// Extract<T, U> - Extract types from union
|
||||
type T2 = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
|
||||
|
||||
// NonNullable<T> - Exclude null and undefined
|
||||
type T3 = NonNullable<string | null | undefined>; // string
|
||||
|
||||
// Record<K, T> - Create object type with keys K and values T
|
||||
type PageInfo = Record<"home" | "about", { title: string }>;
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Pattern 1: Type-Safe Event Emitter
|
||||
|
||||
```typescript
|
||||
type EventMap = {
|
||||
"user:created": { id: string; name: string };
|
||||
"user:updated": { id: string };
|
||||
"user:deleted": { id: string };
|
||||
};
|
||||
|
||||
class TypedEventEmitter<T extends Record<string, any>> {
|
||||
private listeners: {
|
||||
[K in keyof T]?: Array<(data: T[K]) => void>;
|
||||
} = {};
|
||||
|
||||
on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event]!.push(callback);
|
||||
}
|
||||
|
||||
emit<K extends keyof T>(event: K, data: T[K]): void {
|
||||
const callbacks = this.listeners[event];
|
||||
if (callbacks) {
|
||||
callbacks.forEach((callback) => callback(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const emitter = new TypedEventEmitter<EventMap>();
|
||||
|
||||
emitter.on("user:created", (data) => {
|
||||
console.log(data.id, data.name); // Type-safe!
|
||||
});
|
||||
|
||||
emitter.emit("user:created", { id: "1", name: "John" });
|
||||
// emitter.emit("user:created", { id: "1" }); // Error: missing 'name'
|
||||
```
|
||||
|
||||
### Pattern 2: Type-Safe API Client
|
||||
|
||||
```typescript
|
||||
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
|
||||
|
||||
type EndpointConfig = {
|
||||
"/users": {
|
||||
GET: { response: User[] };
|
||||
POST: { body: { name: string; email: string }; response: User };
|
||||
};
|
||||
"/users/:id": {
|
||||
GET: { params: { id: string }; response: User };
|
||||
PUT: { params: { id: string }; body: Partial<User>; response: User };
|
||||
DELETE: { params: { id: string }; response: void };
|
||||
};
|
||||
};
|
||||
|
||||
type ExtractParams<T> = T extends { params: infer P } ? P : never;
|
||||
type ExtractBody<T> = T extends { body: infer B } ? B : never;
|
||||
type ExtractResponse<T> = T extends { response: infer R } ? R : never;
|
||||
|
||||
class APIClient<Config extends Record<string, Record<HTTPMethod, any>>> {
|
||||
async request<Path extends keyof Config, Method extends keyof Config[Path]>(
|
||||
path: Path,
|
||||
method: Method,
|
||||
...[options]: ExtractParams<Config[Path][Method]> extends never
|
||||
? ExtractBody<Config[Path][Method]> extends never
|
||||
? []
|
||||
: [{ body: ExtractBody<Config[Path][Method]> }]
|
||||
: [
|
||||
{
|
||||
params: ExtractParams<Config[Path][Method]>;
|
||||
body?: ExtractBody<Config[Path][Method]>;
|
||||
},
|
||||
]
|
||||
): Promise<ExtractResponse<Config[Path][Method]>> {
|
||||
// Implementation here
|
||||
return {} as any;
|
||||
}
|
||||
}
|
||||
|
||||
const api = new APIClient<EndpointConfig>();
|
||||
|
||||
// Type-safe API calls
|
||||
const users = await api.request("/users", "GET");
|
||||
// Type: User[]
|
||||
|
||||
const newUser = await api.request("/users", "POST", {
|
||||
body: { name: "John", email: "john@example.com" },
|
||||
});
|
||||
// Type: User
|
||||
|
||||
const user = await api.request("/users/:id", "GET", {
|
||||
params: { id: "123" },
|
||||
});
|
||||
// Type: User
|
||||
```
|
||||
|
||||
### Pattern 3: Builder Pattern with Type Safety
|
||||
|
||||
```typescript
|
||||
type BuilderState<T> = {
|
||||
[K in keyof T]: T[K] | undefined;
|
||||
};
|
||||
|
||||
type RequiredKeys<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
|
||||
}[keyof T];
|
||||
|
||||
type OptionalKeys<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
type IsComplete<T, S> =
|
||||
RequiredKeys<T> extends keyof S
|
||||
? S[RequiredKeys<T>] extends undefined
|
||||
? false
|
||||
: true
|
||||
: false;
|
||||
|
||||
class Builder<T, S extends BuilderState<T> = {}> {
|
||||
private state: S = {} as S;
|
||||
|
||||
set<K extends keyof T>(key: K, value: T[K]): Builder<T, S & Record<K, T[K]>> {
|
||||
this.state[key] = value;
|
||||
return this as any;
|
||||
}
|
||||
|
||||
build(this: IsComplete<T, S> extends true ? this : never): T {
|
||||
return this.state as T;
|
||||
}
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
age?: number;
|
||||
}
|
||||
|
||||
const builder = new Builder<User>();
|
||||
|
||||
const user = builder
|
||||
.set("id", "1")
|
||||
.set("name", "John")
|
||||
.set("email", "john@example.com")
|
||||
.build(); // OK: all required fields set
|
||||
|
||||
// const incomplete = builder
|
||||
// .set("id", "1")
|
||||
// .build(); // Error: missing required fields
|
||||
```
|
||||
|
||||
### Pattern 4: Deep Readonly/Partial
|
||||
|
||||
```typescript
|
||||
type DeepReadonly<T> = {
|
||||
readonly [P in keyof T]: T[P] extends object
|
||||
? T[P] extends Function
|
||||
? T[P]
|
||||
: DeepReadonly<T[P]>
|
||||
: T[P];
|
||||
};
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object
|
||||
? T[P] extends Array<infer U>
|
||||
? Array<DeepPartial<U>>
|
||||
: DeepPartial<T[P]>
|
||||
: T[P];
|
||||
};
|
||||
|
||||
interface Config {
|
||||
server: {
|
||||
host: string;
|
||||
port: number;
|
||||
ssl: {
|
||||
enabled: boolean;
|
||||
cert: string;
|
||||
};
|
||||
};
|
||||
database: {
|
||||
url: string;
|
||||
pool: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
type ReadonlyConfig = DeepReadonly<Config>;
|
||||
// All nested properties are readonly
|
||||
|
||||
type PartialConfig = DeepPartial<Config>;
|
||||
// All nested properties are optional
|
||||
```
|
||||
|
||||
### Pattern 5: Type-Safe Form Validation
|
||||
|
||||
```typescript
|
||||
type ValidationRule<T> = {
|
||||
validate: (value: T) => boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type FieldValidation<T> = {
|
||||
[K in keyof T]?: ValidationRule<T[K]>[];
|
||||
};
|
||||
|
||||
type ValidationErrors<T> = {
|
||||
[K in keyof T]?: string[];
|
||||
};
|
||||
|
||||
class FormValidator<T extends Record<string, any>> {
|
||||
constructor(private rules: FieldValidation<T>) {}
|
||||
|
||||
validate(data: T): ValidationErrors<T> | null {
|
||||
const errors: ValidationErrors<T> = {};
|
||||
let hasErrors = false;
|
||||
|
||||
for (const key in this.rules) {
|
||||
const fieldRules = this.rules[key];
|
||||
const value = data[key];
|
||||
|
||||
if (fieldRules) {
|
||||
const fieldErrors: string[] = [];
|
||||
|
||||
for (const rule of fieldRules) {
|
||||
if (!rule.validate(value)) {
|
||||
fieldErrors.push(rule.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldErrors.length > 0) {
|
||||
errors[key] = fieldErrors;
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasErrors ? errors : null;
|
||||
}
|
||||
}
|
||||
|
||||
interface LoginForm {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const validator = new FormValidator<LoginForm>({
|
||||
email: [
|
||||
{
|
||||
validate: (v) => v.includes("@"),
|
||||
message: "Email must contain @",
|
||||
},
|
||||
{
|
||||
validate: (v) => v.length > 0,
|
||||
message: "Email is required",
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
validate: (v) => v.length >= 8,
|
||||
message: "Password must be at least 8 characters",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const errors = validator.validate({
|
||||
email: "invalid",
|
||||
password: "short",
|
||||
});
|
||||
// Type: { email?: string[]; password?: string[]; } | null
|
||||
```
|
||||
|
||||
### Pattern 6: Discriminated Unions
|
||||
|
||||
```typescript
|
||||
type Success<T> = {
|
||||
status: "success";
|
||||
data: T;
|
||||
};
|
||||
|
||||
type Error = {
|
||||
status: "error";
|
||||
error: string;
|
||||
};
|
||||
|
||||
type Loading = {
|
||||
status: "loading";
|
||||
};
|
||||
|
||||
type AsyncState<T> = Success<T> | Error | Loading;
|
||||
|
||||
function handleState<T>(state: AsyncState<T>): void {
|
||||
switch (state.status) {
|
||||
case "success":
|
||||
console.log(state.data); // Type: T
|
||||
break;
|
||||
case "error":
|
||||
console.log(state.error); // Type: string
|
||||
break;
|
||||
case "loading":
|
||||
console.log("Loading...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Type-safe state machine
|
||||
type State =
|
||||
| { type: "idle" }
|
||||
| { type: "fetching"; requestId: string }
|
||||
| { type: "success"; data: any }
|
||||
| { type: "error"; error: Error };
|
||||
|
||||
type Event =
|
||||
| { type: "FETCH"; requestId: string }
|
||||
| { type: "SUCCESS"; data: any }
|
||||
| { type: "ERROR"; error: Error }
|
||||
| { type: "RESET" };
|
||||
|
||||
function reducer(state: State, event: Event): State {
|
||||
switch (state.type) {
|
||||
case "idle":
|
||||
return event.type === "FETCH"
|
||||
? { type: "fetching", requestId: event.requestId }
|
||||
: state;
|
||||
case "fetching":
|
||||
if (event.type === "SUCCESS") {
|
||||
return { type: "success", data: event.data };
|
||||
}
|
||||
if (event.type === "ERROR") {
|
||||
return { type: "error", error: event.error };
|
||||
}
|
||||
return state;
|
||||
case "success":
|
||||
case "error":
|
||||
return event.type === "RESET" ? { type: "idle" } : state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Type Inference Techniques
|
||||
|
||||
### 1. Infer Keyword
|
||||
|
||||
```typescript
|
||||
// Extract array element type
|
||||
type ElementType<T> = T extends (infer U)[] ? U : never;
|
||||
|
||||
type NumArray = number[];
|
||||
type Num = ElementType<NumArray>; // number
|
||||
|
||||
// Extract promise type
|
||||
type PromiseType<T> = T extends Promise<infer U> ? U : never;
|
||||
|
||||
type AsyncNum = PromiseType<Promise<number>>; // number
|
||||
|
||||
// Extract function parameters
|
||||
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
|
||||
|
||||
function foo(a: string, b: number) {}
|
||||
type FooParams = Parameters<typeof foo>; // [string, number]
|
||||
```
|
||||
|
||||
### 2. Type Guards
|
||||
|
||||
```typescript
|
||||
function isString(value: unknown): value is string {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
function isArrayOf<T>(
|
||||
value: unknown,
|
||||
guard: (item: unknown) => item is T,
|
||||
): value is T[] {
|
||||
return Array.isArray(value) && value.every(guard);
|
||||
}
|
||||
|
||||
const data: unknown = ["a", "b", "c"];
|
||||
|
||||
if (isArrayOf(data, isString)) {
|
||||
data.forEach((s) => s.toUpperCase()); // Type: string[]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Assertion Functions
|
||||
|
||||
```typescript
|
||||
function assertIsString(value: unknown): asserts value is string {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error("Not a string");
|
||||
}
|
||||
}
|
||||
|
||||
function processValue(value: unknown) {
|
||||
assertIsString(value);
|
||||
// value is now typed as string
|
||||
console.log(value.toUpperCase());
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use `unknown` over `any`**: Enforce type checking
|
||||
2. **Prefer `interface` for object shapes**: Better error messages
|
||||
3. **Use `type` for unions and complex types**: More flexible
|
||||
4. **Leverage type inference**: Let TypeScript infer when possible
|
||||
5. **Create helper types**: Build reusable type utilities
|
||||
6. **Use const assertions**: Preserve literal types
|
||||
7. **Avoid type assertions**: Use type guards instead
|
||||
8. **Document complex types**: Add JSDoc comments
|
||||
9. **Use strict mode**: Enable all strict compiler options
|
||||
10. **Test your types**: Use type tests to verify type behavior
|
||||
|
||||
## Type Testing
|
||||
|
||||
```typescript
|
||||
// Type assertion tests
|
||||
type AssertEqual<T, U> = [T] extends [U]
|
||||
? [U] extends [T]
|
||||
? true
|
||||
: false
|
||||
: false;
|
||||
|
||||
type Test1 = AssertEqual<string, string>; // true
|
||||
type Test2 = AssertEqual<string, number>; // false
|
||||
type Test3 = AssertEqual<string | number, string>; // false
|
||||
|
||||
// Expect error helper
|
||||
type ExpectError<T extends never> = T;
|
||||
|
||||
// Example usage
|
||||
type ShouldError = ExpectError<AssertEqual<string, number>>;
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Over-using `any`**: Defeats the purpose of TypeScript
|
||||
2. **Ignoring strict null checks**: Can lead to runtime errors
|
||||
3. **Too complex types**: Can slow down compilation
|
||||
4. **Not using discriminated unions**: Misses type narrowing opportunities
|
||||
5. **Forgetting readonly modifiers**: Allows unintended mutations
|
||||
6. **Circular type references**: Can cause compiler errors
|
||||
7. **Not handling edge cases**: Like empty arrays or null values
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Avoid deeply nested conditional types
|
||||
- Use simple types when possible
|
||||
- Cache complex type computations
|
||||
- Limit recursion depth in recursive types
|
||||
- Use build tools to skip type checking in production
|
||||
6
.claude/skills/typescript-expert/.openskills.json
Normal file
6
.claude/skills/typescript-expert/.openskills.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/typescript-expert",
|
||||
"installedAt": "2026-04-07T00:45:24.784Z"
|
||||
}
|
||||
426
.claude/skills/typescript-expert/SKILL.md
Normal file
426
.claude/skills/typescript-expert/SKILL.md
Normal file
@@ -0,0 +1,426 @@
|
||||
---
|
||||
name: typescript-expert
|
||||
description: TypeScript and JavaScript expert with deep knowledge of type-level programming, performance optimization, monorepo management, migration strategies, and modern tooling.
|
||||
category: framework
|
||||
risk: critical
|
||||
source: community
|
||||
date_added: '2026-02-27'
|
||||
---
|
||||
|
||||
# TypeScript Expert
|
||||
|
||||
You are an advanced TypeScript expert with deep, practical knowledge of type-level programming, performance optimization, and real-world problem solving based on current best practices.
|
||||
|
||||
## When invoked:
|
||||
|
||||
0. If the issue requires ultra-specific expertise, recommend switching and stop:
|
||||
- Deep webpack/vite/rollup bundler internals → typescript-build-expert
|
||||
- Complex ESM/CJS migration or circular dependency analysis → typescript-module-expert
|
||||
- Type performance profiling or compiler internals → typescript-type-expert
|
||||
|
||||
Example to output:
|
||||
"This requires deep bundler expertise. Please invoke: 'Use the typescript-build-expert subagent.' Stopping here."
|
||||
|
||||
1. Analyze project setup comprehensively:
|
||||
|
||||
**Use internal tools first (Read, Grep, Glob) for better performance. Shell commands are fallbacks.**
|
||||
|
||||
```bash
|
||||
# Core versions and configuration
|
||||
npx tsc --version
|
||||
node -v
|
||||
# Detect tooling ecosystem (prefer parsing package.json)
|
||||
node -e "const p=require('./package.json');console.log(Object.keys({...p.devDependencies,...p.dependencies}||{}).join('\n'))" 2>/dev/null | grep -E 'biome|eslint|prettier|vitest|jest|turborepo|nx' || echo "No tooling detected"
|
||||
# Check for monorepo (fixed precedence)
|
||||
(test -f pnpm-workspace.yaml || test -f lerna.json || test -f nx.json || test -f turbo.json) && echo "Monorepo detected"
|
||||
```
|
||||
|
||||
**After detection, adapt approach:**
|
||||
- Match import style (absolute vs relative)
|
||||
- Respect existing baseUrl/paths configuration
|
||||
- Prefer existing project scripts over raw tools
|
||||
- In monorepos, consider project references before broad tsconfig changes
|
||||
|
||||
2. Identify the specific problem category and complexity level
|
||||
|
||||
3. Apply the appropriate solution strategy from my expertise
|
||||
|
||||
4. Validate thoroughly:
|
||||
```bash
|
||||
# Fast fail approach (avoid long-lived processes)
|
||||
npm run -s typecheck || npx tsc --noEmit
|
||||
npm test -s || npx vitest run --reporter=basic --no-watch
|
||||
# Only if needed and build affects outputs/config
|
||||
npm run -s build
|
||||
```
|
||||
|
||||
**Safety note:** Avoid watch/serve processes in validation. Use one-shot diagnostics only.
|
||||
|
||||
## Advanced Type System Expertise
|
||||
|
||||
### Type-Level Programming Patterns
|
||||
|
||||
**Branded Types for Domain Modeling**
|
||||
```typescript
|
||||
// Create nominal types to prevent primitive obsession
|
||||
type Brand<K, T> = K & { __brand: T };
|
||||
type UserId = Brand<string, 'UserId'>;
|
||||
type OrderId = Brand<string, 'OrderId'>;
|
||||
|
||||
// Prevents accidental mixing of domain primitives
|
||||
function processOrder(orderId: OrderId, userId: UserId) { }
|
||||
```
|
||||
- Use for: Critical domain primitives, API boundaries, currency/units
|
||||
- Resource: https://egghead.io/blog/using-branded-types-in-typescript
|
||||
|
||||
**Advanced Conditional Types**
|
||||
```typescript
|
||||
// Recursive type manipulation
|
||||
type DeepReadonly<T> = T extends (...args: any[]) => any
|
||||
? T
|
||||
: T extends object
|
||||
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
|
||||
: T;
|
||||
|
||||
// Template literal type magic
|
||||
type PropEventSource<Type> = {
|
||||
on<Key extends string & keyof Type>
|
||||
(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void): void;
|
||||
};
|
||||
```
|
||||
- Use for: Library APIs, type-safe event systems, compile-time validation
|
||||
- Watch for: Type instantiation depth errors (limit recursion to 10 levels)
|
||||
|
||||
**Type Inference Techniques**
|
||||
```typescript
|
||||
// Use 'satisfies' for constraint validation (TS 5.0+)
|
||||
const config = {
|
||||
api: "https://api.example.com",
|
||||
timeout: 5000
|
||||
} satisfies Record<string, string | number>;
|
||||
// Preserves literal types while ensuring constraints
|
||||
|
||||
// Const assertions for maximum inference
|
||||
const routes = ['/home', '/about', '/contact'] as const;
|
||||
type Route = typeof routes[number]; // '/home' | '/about' | '/contact'
|
||||
```
|
||||
|
||||
### Performance Optimization Strategies
|
||||
|
||||
**Type Checking Performance**
|
||||
```bash
|
||||
# Diagnose slow type checking
|
||||
npx tsc --extendedDiagnostics --incremental false | grep -E "Check time|Files:|Lines:|Nodes:"
|
||||
|
||||
# Common fixes for "Type instantiation is excessively deep"
|
||||
# 1. Replace type intersections with interfaces
|
||||
# 2. Split large union types (>100 members)
|
||||
# 3. Avoid circular generic constraints
|
||||
# 4. Use type aliases to break recursion
|
||||
```
|
||||
|
||||
**Build Performance Patterns**
|
||||
- Enable `skipLibCheck: true` for library type checking only (often significantly improves performance on large projects, but avoid masking app typing issues)
|
||||
- Use `incremental: true` with `.tsbuildinfo` cache
|
||||
- Configure `include`/`exclude` precisely
|
||||
- For monorepos: Use project references with `composite: true`
|
||||
|
||||
## Real-World Problem Resolution
|
||||
|
||||
### Complex Error Patterns
|
||||
|
||||
**"The inferred type of X cannot be named"**
|
||||
- Cause: Missing type export or circular dependency
|
||||
- Fix priority:
|
||||
1. Export the required type explicitly
|
||||
2. Use `ReturnType<typeof function>` helper
|
||||
3. Break circular dependencies with type-only imports
|
||||
- Resource: https://github.com/microsoft/TypeScript/issues/47663
|
||||
|
||||
**Missing type declarations**
|
||||
- Quick fix with ambient declarations:
|
||||
```typescript
|
||||
// types/ambient.d.ts
|
||||
declare module 'some-untyped-package' {
|
||||
const value: unknown;
|
||||
export default value;
|
||||
export = value; // if CJS interop is needed
|
||||
}
|
||||
```
|
||||
- For more details: [Declaration Files Guide](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html)
|
||||
|
||||
**"Excessive stack depth comparing types"**
|
||||
- Cause: Circular or deeply recursive types
|
||||
- Fix priority:
|
||||
1. Limit recursion depth with conditional types
|
||||
2. Use `interface` extends instead of type intersection
|
||||
3. Simplify generic constraints
|
||||
```typescript
|
||||
// Bad: Infinite recursion
|
||||
type InfiniteArray<T> = T | InfiniteArray<T>[];
|
||||
|
||||
// Good: Limited recursion
|
||||
type NestedArray<T, D extends number = 5> =
|
||||
D extends 0 ? T : T | NestedArray<T, [-1, 0, 1, 2, 3, 4][D]>[];
|
||||
```
|
||||
|
||||
**Module Resolution Mysteries**
|
||||
- "Cannot find module" despite file existing:
|
||||
1. Check `moduleResolution` matches your bundler
|
||||
2. Verify `baseUrl` and `paths` alignment
|
||||
3. For monorepos: Ensure workspace protocol (workspace:*)
|
||||
4. Try clearing cache: `rm -rf node_modules/.cache .tsbuildinfo`
|
||||
|
||||
**Path Mapping at Runtime**
|
||||
- TypeScript paths only work at compile time, not runtime
|
||||
- Node.js runtime solutions:
|
||||
- ts-node: Use `ts-node -r tsconfig-paths/register`
|
||||
- Node ESM: Use loader alternatives or avoid TS paths at runtime
|
||||
- Production: Pre-compile with resolved paths
|
||||
|
||||
### Migration Expertise
|
||||
|
||||
**JavaScript to TypeScript Migration**
|
||||
```bash
|
||||
# Incremental migration strategy
|
||||
# 1. Enable allowJs and checkJs (merge into existing tsconfig.json):
|
||||
# Add to existing tsconfig.json:
|
||||
# {
|
||||
# "compilerOptions": {
|
||||
# "allowJs": true,
|
||||
# "checkJs": true
|
||||
# }
|
||||
# }
|
||||
|
||||
# 2. Rename files gradually (.js → .ts)
|
||||
# 3. Add types file by file using AI assistance
|
||||
# 4. Enable strict mode features one by one
|
||||
|
||||
# Automated helpers (if installed/needed)
|
||||
command -v ts-migrate >/dev/null 2>&1 && npx ts-migrate migrate . --sources 'src/**/*.js'
|
||||
command -v typesync >/dev/null 2>&1 && npx typesync # Install missing @types packages
|
||||
```
|
||||
|
||||
**Tool Migration Decisions**
|
||||
|
||||
| From | To | When | Migration Effort |
|
||||
|------|-----|------|-----------------|
|
||||
| ESLint + Prettier | Biome | Need much faster speed, okay with fewer rules | Low (1 day) |
|
||||
| TSC for linting | Type-check only | Have 100+ files, need faster feedback | Medium (2-3 days) |
|
||||
| Lerna | Nx/Turborepo | Need caching, parallel builds | High (1 week) |
|
||||
| CJS | ESM | Node 18+, modern tooling | High (varies) |
|
||||
|
||||
### Monorepo Management
|
||||
|
||||
**Nx vs Turborepo Decision Matrix**
|
||||
- Choose **Turborepo** if: Simple structure, need speed, <20 packages
|
||||
- Choose **Nx** if: Complex dependencies, need visualization, plugins required
|
||||
- Performance: Nx often performs better on large monorepos (>50 packages)
|
||||
|
||||
**TypeScript Monorepo Configuration**
|
||||
```json
|
||||
// Root tsconfig.json
|
||||
{
|
||||
"references": [
|
||||
{ "path": "./packages/core" },
|
||||
{ "path": "./packages/ui" },
|
||||
{ "path": "./apps/web" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Modern Tooling Expertise
|
||||
|
||||
### Biome vs ESLint
|
||||
|
||||
**Use Biome when:**
|
||||
- Speed is critical (often faster than traditional setups)
|
||||
- Want single tool for lint + format
|
||||
- TypeScript-first project
|
||||
- Okay with 64 TS rules vs 100+ in typescript-eslint
|
||||
|
||||
**Stay with ESLint when:**
|
||||
- Need specific rules/plugins
|
||||
- Have complex custom rules
|
||||
- Working with Vue/Angular (limited Biome support)
|
||||
- Need type-aware linting (Biome doesn't have this yet)
|
||||
|
||||
### Type Testing Strategies
|
||||
|
||||
**Vitest Type Testing (Recommended)**
|
||||
```typescript
|
||||
// in avatar.test-d.ts
|
||||
import { expectTypeOf } from 'vitest'
|
||||
import type { Avatar } from './avatar'
|
||||
|
||||
test('Avatar props are correctly typed', () => {
|
||||
expectTypeOf<Avatar>().toHaveProperty('size')
|
||||
expectTypeOf<Avatar['size']>().toEqualTypeOf<'sm' | 'md' | 'lg'>()
|
||||
})
|
||||
```
|
||||
|
||||
**When to Test Types:**
|
||||
- Publishing libraries
|
||||
- Complex generic functions
|
||||
- Type-level utilities
|
||||
- API contracts
|
||||
|
||||
## Debugging Mastery
|
||||
|
||||
### CLI Debugging Tools
|
||||
```bash
|
||||
# Debug TypeScript files directly (if tools installed)
|
||||
command -v tsx >/dev/null 2>&1 && npx tsx --inspect src/file.ts
|
||||
command -v ts-node >/dev/null 2>&1 && npx ts-node --inspect-brk src/file.ts
|
||||
|
||||
# Trace module resolution issues
|
||||
npx tsc --traceResolution > resolution.log 2>&1
|
||||
grep "Module resolution" resolution.log
|
||||
|
||||
# Debug type checking performance (use --incremental false for clean trace)
|
||||
npx tsc --generateTrace trace --incremental false
|
||||
# Analyze trace (if installed)
|
||||
command -v @typescript/analyze-trace >/dev/null 2>&1 && npx @typescript/analyze-trace trace
|
||||
|
||||
# Memory usage analysis
|
||||
node --max-old-space-size=8192 node_modules/typescript/lib/tsc.js
|
||||
```
|
||||
|
||||
### Custom Error Classes
|
||||
```typescript
|
||||
// Proper error class with stack preservation
|
||||
class DomainError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code: string,
|
||||
public statusCode: number
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'DomainError';
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Current Best Practices
|
||||
|
||||
### Strict by Default
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noPropertyAccessFromIndexSignature": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ESM-First Approach
|
||||
- Set `"type": "module"` in package.json
|
||||
- Use `.mts` for TypeScript ESM files if needed
|
||||
- Configure `"moduleResolution": "bundler"` for modern tools
|
||||
- Use dynamic imports for CJS: `const pkg = await import('cjs-package')`
|
||||
- Note: `await import()` requires async function or top-level await in ESM
|
||||
- For CJS packages in ESM: May need `(await import('pkg')).default` depending on the package's export structure and your compiler settings
|
||||
|
||||
### AI-Assisted Development
|
||||
- GitHub Copilot excels at TypeScript generics
|
||||
- Use AI for boilerplate type definitions
|
||||
- Validate AI-generated types with type tests
|
||||
- Document complex types for AI context
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
When reviewing TypeScript/JavaScript code, focus on these domain-specific aspects:
|
||||
|
||||
### Type Safety
|
||||
- [ ] No implicit `any` types (use `unknown` or proper types)
|
||||
- [ ] Strict null checks enabled and properly handled
|
||||
- [ ] Type assertions (`as`) justified and minimal
|
||||
- [ ] Generic constraints properly defined
|
||||
- [ ] Discriminated unions for error handling
|
||||
- [ ] Return types explicitly declared for public APIs
|
||||
|
||||
### TypeScript Best Practices
|
||||
- [ ] Prefer `interface` over `type` for object shapes (better error messages)
|
||||
- [ ] Use const assertions for literal types
|
||||
- [ ] Leverage type guards and predicates
|
||||
- [ ] Avoid type gymnastics when simpler solution exists
|
||||
- [ ] Template literal types used appropriately
|
||||
- [ ] Branded types for domain primitives
|
||||
|
||||
### Performance Considerations
|
||||
- [ ] Type complexity doesn't cause slow compilation
|
||||
- [ ] No excessive type instantiation depth
|
||||
- [ ] Avoid complex mapped types in hot paths
|
||||
- [ ] Use `skipLibCheck: true` in tsconfig
|
||||
- [ ] Project references configured for monorepos
|
||||
|
||||
### Module System
|
||||
- [ ] Consistent import/export patterns
|
||||
- [ ] No circular dependencies
|
||||
- [ ] Proper use of barrel exports (avoid over-bundling)
|
||||
- [ ] ESM/CJS compatibility handled correctly
|
||||
- [ ] Dynamic imports for code splitting
|
||||
|
||||
### Error Handling Patterns
|
||||
- [ ] Result types or discriminated unions for errors
|
||||
- [ ] Custom error classes with proper inheritance
|
||||
- [ ] Type-safe error boundaries
|
||||
- [ ] Exhaustive switch cases with `never` type
|
||||
|
||||
### Code Organization
|
||||
- [ ] Types co-located with implementation
|
||||
- [ ] Shared types in dedicated modules
|
||||
- [ ] Avoid global type augmentation when possible
|
||||
- [ ] Proper use of declaration files (.d.ts)
|
||||
|
||||
## Quick Decision Trees
|
||||
|
||||
### "Which tool should I use?"
|
||||
```
|
||||
Type checking only? → tsc
|
||||
Type checking + linting speed critical? → Biome
|
||||
Type checking + comprehensive linting? → ESLint + typescript-eslint
|
||||
Type testing? → Vitest expectTypeOf
|
||||
Build tool? → Project size <10 packages? Turborepo. Else? Nx
|
||||
```
|
||||
|
||||
### "How do I fix this performance issue?"
|
||||
```
|
||||
Slow type checking? → skipLibCheck, incremental, project references
|
||||
Slow builds? → Check bundler config, enable caching
|
||||
Slow tests? → Vitest with threads, avoid type checking in tests
|
||||
Slow language server? → Exclude node_modules, limit files in tsconfig
|
||||
```
|
||||
|
||||
## Expert Resources
|
||||
|
||||
### Performance
|
||||
- [TypeScript Wiki Performance](https://github.com/microsoft/TypeScript/wiki/Performance)
|
||||
- [Type instantiation tracking](https://github.com/microsoft/TypeScript/pull/48077)
|
||||
|
||||
### Advanced Patterns
|
||||
- [Type Challenges](https://github.com/type-challenges/type-challenges)
|
||||
- [Type-Level TypeScript Course](https://type-level-typescript.com)
|
||||
|
||||
### Tools
|
||||
- [Biome](https://biomejs.dev) - Fast linter/formatter
|
||||
- [TypeStat](https://github.com/JoshuaKGoldberg/TypeStat) - Auto-fix TypeScript types
|
||||
- [ts-migrate](https://github.com/airbnb/ts-migrate) - Migration toolkit
|
||||
|
||||
### Testing
|
||||
- [Vitest Type Testing](https://vitest.dev/guide/testing-types)
|
||||
- [tsd](https://github.com/tsdjs/tsd) - Standalone type testing
|
||||
|
||||
Always validate changes don't break existing functionality before considering the issue resolved.
|
||||
|
||||
## When to Use
|
||||
This skill is applicable to execute the workflow or actions described in the overview.
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"display": "Strict TypeScript 5.x",
|
||||
"compilerOptions": {
|
||||
// =========================================================================
|
||||
// STRICTNESS (Maximum Type Safety)
|
||||
// =========================================================================
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
// =========================================================================
|
||||
// MODULE SYSTEM (Modern ESM)
|
||||
// =========================================================================
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
// =========================================================================
|
||||
// OUTPUT
|
||||
// =========================================================================
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
// =========================================================================
|
||||
// PERFORMANCE
|
||||
// =========================================================================
|
||||
"skipLibCheck": true,
|
||||
"incremental": true,
|
||||
// =========================================================================
|
||||
// PATH ALIASES
|
||||
// =========================================================================
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@/components/*": [
|
||||
"./src/components/*"
|
||||
],
|
||||
"@/lib/*": [
|
||||
"./src/lib/*"
|
||||
],
|
||||
"@/types/*": [
|
||||
"./src/types/*"
|
||||
],
|
||||
"@/utils/*": [
|
||||
"./src/utils/*"
|
||||
]
|
||||
},
|
||||
// =========================================================================
|
||||
// JSX (for React projects)
|
||||
// =========================================================================
|
||||
// "jsx": "react-jsx",
|
||||
// =========================================================================
|
||||
// EMIT
|
||||
// =========================================================================
|
||||
"noEmit": true, // Let bundler handle emit
|
||||
// "outDir": "./dist",
|
||||
// "rootDir": "./src",
|
||||
// =========================================================================
|
||||
// DECORATORS (if needed)
|
||||
// =========================================================================
|
||||
// "experimentalDecorators": true,
|
||||
// "emitDecoratorMetadata": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
"coverage",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
# TypeScript Cheatsheet
|
||||
|
||||
## Type Basics
|
||||
|
||||
```typescript
|
||||
// Primitives
|
||||
const name: string = 'John'
|
||||
const age: number = 30
|
||||
const isActive: boolean = true
|
||||
const nothing: null = null
|
||||
const notDefined: undefined = undefined
|
||||
|
||||
// Arrays
|
||||
const numbers: number[] = [1, 2, 3]
|
||||
const strings: Array<string> = ['a', 'b', 'c']
|
||||
|
||||
// Tuple
|
||||
const tuple: [string, number] = ['hello', 42]
|
||||
|
||||
// Object
|
||||
const user: { name: string; age: number } = { name: 'John', age: 30 }
|
||||
|
||||
// Union
|
||||
const value: string | number = 'hello'
|
||||
|
||||
// Literal
|
||||
const direction: 'up' | 'down' | 'left' | 'right' = 'up'
|
||||
|
||||
// Any vs Unknown
|
||||
const anyValue: any = 'anything' // ❌ Avoid
|
||||
const unknownValue: unknown = 'safe' // ✅ Prefer, requires narrowing
|
||||
```
|
||||
|
||||
## Type Aliases & Interfaces
|
||||
|
||||
```typescript
|
||||
// Type Alias
|
||||
type Point = {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
// Interface (preferred for objects)
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email?: string // Optional
|
||||
readonly createdAt: Date // Readonly
|
||||
}
|
||||
|
||||
// Extending
|
||||
interface Admin extends User {
|
||||
permissions: string[]
|
||||
}
|
||||
|
||||
// Intersection
|
||||
type AdminUser = User & { permissions: string[] }
|
||||
```
|
||||
|
||||
## Generics
|
||||
|
||||
```typescript
|
||||
// Generic function
|
||||
function identity<T>(value: T): T {
|
||||
return value
|
||||
}
|
||||
|
||||
// Generic with constraint
|
||||
function getLength<T extends { length: number }>(item: T): number {
|
||||
return item.length
|
||||
}
|
||||
|
||||
// Generic interface
|
||||
interface ApiResponse<T> {
|
||||
data: T
|
||||
status: number
|
||||
message: string
|
||||
}
|
||||
|
||||
// Generic with default
|
||||
type Container<T = string> = {
|
||||
value: T
|
||||
}
|
||||
|
||||
// Multiple generics
|
||||
function merge<T, U>(obj1: T, obj2: U): T & U {
|
||||
return { ...obj1, ...obj2 }
|
||||
}
|
||||
```
|
||||
|
||||
## Utility Types
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
age: number
|
||||
}
|
||||
|
||||
// Partial - all optional
|
||||
type PartialUser = Partial<User>
|
||||
|
||||
// Required - all required
|
||||
type RequiredUser = Required<User>
|
||||
|
||||
// Readonly - all readonly
|
||||
type ReadonlyUser = Readonly<User>
|
||||
|
||||
// Pick - select properties
|
||||
type UserName = Pick<User, 'id' | 'name'>
|
||||
|
||||
// Omit - exclude properties
|
||||
type UserWithoutEmail = Omit<User, 'email'>
|
||||
|
||||
// Record - key-value map
|
||||
type UserMap = Record<string, User>
|
||||
|
||||
// Extract - extract from union
|
||||
type StringOrNumber = string | number | boolean
|
||||
type OnlyStrings = Extract<StringOrNumber, string>
|
||||
|
||||
// Exclude - exclude from union
|
||||
type NotString = Exclude<StringOrNumber, string>
|
||||
|
||||
// NonNullable - remove null/undefined
|
||||
type MaybeString = string | null | undefined
|
||||
type DefinitelyString = NonNullable<MaybeString>
|
||||
|
||||
// ReturnType - get function return type
|
||||
function getUser() { return { name: 'John' } }
|
||||
type UserReturn = ReturnType<typeof getUser>
|
||||
|
||||
// Parameters - get function parameters
|
||||
type GetUserParams = Parameters<typeof getUser>
|
||||
|
||||
// Awaited - unwrap Promise
|
||||
type ResolvedUser = Awaited<Promise<User>>
|
||||
```
|
||||
|
||||
## Conditional Types
|
||||
|
||||
```typescript
|
||||
// Basic conditional
|
||||
type IsString<T> = T extends string ? true : false
|
||||
|
||||
// Infer keyword
|
||||
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
|
||||
|
||||
// Distributive conditional
|
||||
type ToArray<T> = T extends any ? T[] : never
|
||||
type Result = ToArray<string | number> // string[] | number[]
|
||||
|
||||
// NonDistributive
|
||||
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
|
||||
```
|
||||
|
||||
## Template Literal Types
|
||||
|
||||
```typescript
|
||||
type Color = 'red' | 'green' | 'blue'
|
||||
type Size = 'small' | 'medium' | 'large'
|
||||
|
||||
// Combine
|
||||
type ColorSize = `${Color}-${Size}`
|
||||
// 'red-small' | 'red-medium' | 'red-large' | ...
|
||||
|
||||
// Event handlers
|
||||
type EventName = 'click' | 'focus' | 'blur'
|
||||
type EventHandler = `on${Capitalize<EventName>}`
|
||||
// 'onClick' | 'onFocus' | 'onBlur'
|
||||
```
|
||||
|
||||
## Mapped Types
|
||||
|
||||
```typescript
|
||||
// Basic mapped type
|
||||
type Optional<T> = {
|
||||
[K in keyof T]?: T[K]
|
||||
}
|
||||
|
||||
// With key remapping
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
|
||||
}
|
||||
|
||||
// Filter keys
|
||||
type OnlyStrings<T> = {
|
||||
[K in keyof T as T[K] extends string ? K : never]: T[K]
|
||||
}
|
||||
```
|
||||
|
||||
## Type Guards
|
||||
|
||||
```typescript
|
||||
// typeof guard
|
||||
function process(value: string | number) {
|
||||
if (typeof value === 'string') {
|
||||
return value.toUpperCase() // string
|
||||
}
|
||||
return value.toFixed(2) // number
|
||||
}
|
||||
|
||||
// instanceof guard
|
||||
class Dog { bark() {} }
|
||||
class Cat { meow() {} }
|
||||
|
||||
function makeSound(animal: Dog | Cat) {
|
||||
if (animal instanceof Dog) {
|
||||
animal.bark()
|
||||
} else {
|
||||
animal.meow()
|
||||
}
|
||||
}
|
||||
|
||||
// in guard
|
||||
interface Bird { fly(): void }
|
||||
interface Fish { swim(): void }
|
||||
|
||||
function move(animal: Bird | Fish) {
|
||||
if ('fly' in animal) {
|
||||
animal.fly()
|
||||
} else {
|
||||
animal.swim()
|
||||
}
|
||||
}
|
||||
|
||||
// Custom type guard
|
||||
function isString(value: unknown): value is string {
|
||||
return typeof value === 'string'
|
||||
}
|
||||
|
||||
// Assertion function
|
||||
function assertIsString(value: unknown): asserts value is string {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('Not a string')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Discriminated Unions
|
||||
|
||||
```typescript
|
||||
// With type discriminant
|
||||
type Success<T> = { type: 'success'; data: T }
|
||||
type Error = { type: 'error'; message: string }
|
||||
type Loading = { type: 'loading' }
|
||||
|
||||
type State<T> = Success<T> | Error | Loading
|
||||
|
||||
function handle<T>(state: State<T>) {
|
||||
switch (state.type) {
|
||||
case 'success':
|
||||
return state.data // T
|
||||
case 'error':
|
||||
return state.message // string
|
||||
case 'loading':
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Exhaustive check
|
||||
function assertNever(value: never): never {
|
||||
throw new Error(`Unexpected value: ${value}`)
|
||||
}
|
||||
```
|
||||
|
||||
## Branded Types
|
||||
|
||||
```typescript
|
||||
// Create branded type
|
||||
type Brand<K, T> = K & { __brand: T }
|
||||
|
||||
type UserId = Brand<string, 'UserId'>
|
||||
type OrderId = Brand<string, 'OrderId'>
|
||||
|
||||
// Constructor functions
|
||||
function createUserId(id: string): UserId {
|
||||
return id as UserId
|
||||
}
|
||||
|
||||
function createOrderId(id: string): OrderId {
|
||||
return id as OrderId
|
||||
}
|
||||
|
||||
// Usage - prevents mixing
|
||||
function getOrder(orderId: OrderId, userId: UserId) {}
|
||||
|
||||
const userId = createUserId('user-123')
|
||||
const orderId = createOrderId('order-456')
|
||||
|
||||
getOrder(orderId, userId) // ✅ OK
|
||||
// getOrder(userId, orderId) // ❌ Error - types don't match
|
||||
```
|
||||
|
||||
## Module Declarations
|
||||
|
||||
```typescript
|
||||
// Declare module for untyped package
|
||||
declare module 'untyped-package' {
|
||||
export function doSomething(): void
|
||||
export const value: string
|
||||
}
|
||||
|
||||
// Augment existing module
|
||||
declare module 'express' {
|
||||
interface Request {
|
||||
user?: { id: string }
|
||||
}
|
||||
}
|
||||
|
||||
// Declare global
|
||||
declare global {
|
||||
interface Window {
|
||||
myGlobal: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TSConfig Essentials
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Strictness
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Modules
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
|
||||
// Output
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
|
||||
// Performance
|
||||
"skipLibCheck": true,
|
||||
"incremental": true,
|
||||
|
||||
// Paths
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
```typescript
|
||||
// ✅ Prefer interface for objects
|
||||
interface User {
|
||||
name: string
|
||||
}
|
||||
|
||||
// ✅ Use const assertions
|
||||
const routes = ['home', 'about'] as const
|
||||
|
||||
// ✅ Use satisfies for validation
|
||||
const config = {
|
||||
api: 'https://api.example.com'
|
||||
} satisfies Record<string, string>
|
||||
|
||||
// ✅ Use unknown over any
|
||||
function parse(input: unknown) {
|
||||
if (typeof input === 'string') {
|
||||
return JSON.parse(input)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Explicit return types for public APIs
|
||||
export function getUser(id: string): User | null {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ❌ Avoid
|
||||
const data: any = fetchData()
|
||||
data.anything.goes.wrong // No type safety
|
||||
```
|
||||
335
.claude/skills/typescript-expert/references/utility-types.ts
Normal file
335
.claude/skills/typescript-expert/references/utility-types.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* TypeScript Utility Types Library
|
||||
*
|
||||
* A collection of commonly used utility types for TypeScript projects.
|
||||
* Copy and use as needed in your projects.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// BRANDED TYPES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Create nominal/branded types to prevent primitive obsession.
|
||||
*
|
||||
* @example
|
||||
* type UserId = Brand<string, 'UserId'>
|
||||
* type OrderId = Brand<string, 'OrderId'>
|
||||
*/
|
||||
export type Brand<K, T> = K & { readonly __brand: T }
|
||||
|
||||
// Branded type constructors
|
||||
export type UserId = Brand<string, 'UserId'>
|
||||
export type Email = Brand<string, 'Email'>
|
||||
export type UUID = Brand<string, 'UUID'>
|
||||
export type Timestamp = Brand<number, 'Timestamp'>
|
||||
export type PositiveNumber = Brand<number, 'PositiveNumber'>
|
||||
|
||||
// =============================================================================
|
||||
// RESULT TYPE (Error Handling)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Type-safe error handling without exceptions.
|
||||
*/
|
||||
export type Result<T, E = Error> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: E }
|
||||
|
||||
export const ok = <T>(data: T): Result<T, never> => ({
|
||||
success: true,
|
||||
data
|
||||
})
|
||||
|
||||
export const err = <E>(error: E): Result<never, E> => ({
|
||||
success: false,
|
||||
error
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// OPTION TYPE (Nullable Handling)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Explicit optional value handling.
|
||||
*/
|
||||
export type Option<T> = Some<T> | None
|
||||
|
||||
export type Some<T> = { type: 'some'; value: T }
|
||||
export type None = { type: 'none' }
|
||||
|
||||
export const some = <T>(value: T): Some<T> => ({ type: 'some', value })
|
||||
export const none: None = { type: 'none' }
|
||||
|
||||
// =============================================================================
|
||||
// DEEP UTILITIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Make all properties deeply readonly.
|
||||
*/
|
||||
export type DeepReadonly<T> = T extends (...args: any[]) => any
|
||||
? T
|
||||
: T extends object
|
||||
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
|
||||
: T
|
||||
|
||||
/**
|
||||
* Make all properties deeply optional.
|
||||
*/
|
||||
export type DeepPartial<T> = T extends object
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: T
|
||||
|
||||
/**
|
||||
* Make all properties deeply required.
|
||||
*/
|
||||
export type DeepRequired<T> = T extends object
|
||||
? { [K in keyof T]-?: DeepRequired<T[K]> }
|
||||
: T
|
||||
|
||||
/**
|
||||
* Make all properties deeply mutable (remove readonly).
|
||||
*/
|
||||
export type DeepMutable<T> = T extends object
|
||||
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
|
||||
: T
|
||||
|
||||
// =============================================================================
|
||||
// OBJECT UTILITIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get keys of object where value matches type.
|
||||
*/
|
||||
export type KeysOfType<T, V> = {
|
||||
[K in keyof T]: T[K] extends V ? K : never
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
* Pick properties by value type.
|
||||
*/
|
||||
export type PickByType<T, V> = Pick<T, KeysOfType<T, V>>
|
||||
|
||||
/**
|
||||
* Omit properties by value type.
|
||||
*/
|
||||
export type OmitByType<T, V> = Omit<T, KeysOfType<T, V>>
|
||||
|
||||
/**
|
||||
* Make specific keys optional.
|
||||
*/
|
||||
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
|
||||
|
||||
/**
|
||||
* Make specific keys required.
|
||||
*/
|
||||
export type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
|
||||
|
||||
/**
|
||||
* Make specific keys readonly.
|
||||
*/
|
||||
export type ReadonlyBy<T, K extends keyof T> = Omit<T, K> & Readonly<Pick<T, K>>
|
||||
|
||||
/**
|
||||
* Merge two types (second overrides first).
|
||||
*/
|
||||
export type Merge<T, U> = Omit<T, keyof U> & U
|
||||
|
||||
// =============================================================================
|
||||
// ARRAY UTILITIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get element type from array.
|
||||
*/
|
||||
export type ElementOf<T> = T extends (infer E)[] ? E : never
|
||||
|
||||
/**
|
||||
* Tuple of specific length.
|
||||
*/
|
||||
export type Tuple<T, N extends number> = N extends N
|
||||
? number extends N
|
||||
? T[]
|
||||
: _TupleOf<T, N, []>
|
||||
: never
|
||||
|
||||
type _TupleOf<T, N extends number, R extends unknown[]> = R['length'] extends N
|
||||
? R
|
||||
: _TupleOf<T, N, [T, ...R]>
|
||||
|
||||
/**
|
||||
* Non-empty array.
|
||||
*/
|
||||
export type NonEmptyArray<T> = [T, ...T[]]
|
||||
|
||||
/**
|
||||
* At least N elements.
|
||||
*/
|
||||
export type AtLeast<T, N extends number> = [...Tuple<T, N>, ...T[]]
|
||||
|
||||
// =============================================================================
|
||||
// FUNCTION UTILITIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get function arguments as tuple.
|
||||
*/
|
||||
export type Arguments<T> = T extends (...args: infer A) => any ? A : never
|
||||
|
||||
/**
|
||||
* Get first argument of function.
|
||||
*/
|
||||
export type FirstArgument<T> = T extends (first: infer F, ...args: any[]) => any
|
||||
? F
|
||||
: never
|
||||
|
||||
/**
|
||||
* Async version of function.
|
||||
*/
|
||||
export type AsyncFunction<T extends (...args: any[]) => any> = (
|
||||
...args: Parameters<T>
|
||||
) => Promise<Awaited<ReturnType<T>>>
|
||||
|
||||
/**
|
||||
* Promisify return type.
|
||||
*/
|
||||
export type Promisify<T> = T extends (...args: infer A) => infer R
|
||||
? (...args: A) => Promise<Awaited<R>>
|
||||
: never
|
||||
|
||||
// =============================================================================
|
||||
// STRING UTILITIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Split string by delimiter.
|
||||
*/
|
||||
export type Split<S extends string, D extends string> =
|
||||
S extends `${infer T}${D}${infer U}`
|
||||
? [T, ...Split<U, D>]
|
||||
: [S]
|
||||
|
||||
/**
|
||||
* Join tuple to string.
|
||||
*/
|
||||
export type Join<T extends string[], D extends string> =
|
||||
T extends []
|
||||
? ''
|
||||
: T extends [infer F extends string]
|
||||
? F
|
||||
: T extends [infer F extends string, ...infer R extends string[]]
|
||||
? `${F}${D}${Join<R, D>}`
|
||||
: never
|
||||
|
||||
/**
|
||||
* Path to nested object.
|
||||
*/
|
||||
export type PathOf<T, K extends keyof T = keyof T> = K extends string
|
||||
? T[K] extends object
|
||||
? K | `${K}.${PathOf<T[K]>}`
|
||||
: K
|
||||
: never
|
||||
|
||||
// =============================================================================
|
||||
// UNION UTILITIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Last element of union.
|
||||
*/
|
||||
export type UnionLast<T> = UnionToIntersection<
|
||||
T extends any ? () => T : never
|
||||
> extends () => infer R
|
||||
? R
|
||||
: never
|
||||
|
||||
/**
|
||||
* Union to intersection.
|
||||
*/
|
||||
export type UnionToIntersection<U> = (
|
||||
U extends any ? (k: U) => void : never
|
||||
) extends (k: infer I) => void
|
||||
? I
|
||||
: never
|
||||
|
||||
/**
|
||||
* Union to tuple.
|
||||
*/
|
||||
export type UnionToTuple<T, L = UnionLast<T>> = [T] extends [never]
|
||||
? []
|
||||
: [...UnionToTuple<Exclude<T, L>>, L]
|
||||
|
||||
// =============================================================================
|
||||
// VALIDATION UTILITIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Assert type at compile time.
|
||||
*/
|
||||
export type AssertEqual<T, U> =
|
||||
(<V>() => V extends T ? 1 : 2) extends (<V>() => V extends U ? 1 : 2)
|
||||
? true
|
||||
: false
|
||||
|
||||
/**
|
||||
* Ensure type is not never.
|
||||
*/
|
||||
export type IsNever<T> = [T] extends [never] ? true : false
|
||||
|
||||
/**
|
||||
* Ensure type is any.
|
||||
*/
|
||||
export type IsAny<T> = 0 extends 1 & T ? true : false
|
||||
|
||||
/**
|
||||
* Ensure type is unknown.
|
||||
*/
|
||||
export type IsUnknown<T> = IsAny<T> extends true
|
||||
? false
|
||||
: unknown extends T
|
||||
? true
|
||||
: false
|
||||
|
||||
// =============================================================================
|
||||
// JSON UTILITIES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* JSON-safe types.
|
||||
*/
|
||||
export type JsonPrimitive = string | number | boolean | null
|
||||
export type JsonArray = JsonValue[]
|
||||
export type JsonObject = { [key: string]: JsonValue }
|
||||
export type JsonValue = JsonPrimitive | JsonArray | JsonObject
|
||||
|
||||
/**
|
||||
* Make type JSON-serializable.
|
||||
*/
|
||||
export type Jsonify<T> = T extends JsonPrimitive
|
||||
? T
|
||||
: T extends undefined | ((...args: any[]) => any) | symbol
|
||||
? never
|
||||
: T extends { toJSON(): infer R }
|
||||
? R
|
||||
: T extends object
|
||||
? { [K in keyof T]: Jsonify<T[K]> }
|
||||
: never
|
||||
|
||||
// =============================================================================
|
||||
// EXHAUSTIVE CHECK
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Ensure all cases are handled in switch/if.
|
||||
*/
|
||||
export function assertNever(value: never, message?: string): never {
|
||||
throw new Error(message ?? `Unexpected value: ${value}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Exhaustive check without throwing.
|
||||
*/
|
||||
export function exhaustiveCheck(_value: never): void {
|
||||
// This function should never be called
|
||||
}
|
||||
203
.claude/skills/typescript-expert/scripts/ts_diagnostic.py
Normal file
203
.claude/skills/typescript-expert/scripts/ts_diagnostic.py
Normal file
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TypeScript Project Diagnostic Script
|
||||
Analyzes TypeScript projects for configuration, performance, and common issues.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
def run_cmd(cmd: str) -> str:
|
||||
"""Run shell command and return output."""
|
||||
try:
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||
return result.stdout + result.stderr
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
def check_versions():
|
||||
"""Check TypeScript and Node versions."""
|
||||
print("\n📦 Versions:")
|
||||
print("-" * 40)
|
||||
|
||||
ts_version = run_cmd("npx tsc --version 2>/dev/null").strip()
|
||||
node_version = run_cmd("node -v 2>/dev/null").strip()
|
||||
|
||||
print(f" TypeScript: {ts_version or 'Not found'}")
|
||||
print(f" Node.js: {node_version or 'Not found'}")
|
||||
|
||||
def check_tsconfig():
|
||||
"""Analyze tsconfig.json settings."""
|
||||
print("\n⚙️ TSConfig Analysis:")
|
||||
print("-" * 40)
|
||||
|
||||
tsconfig_path = Path("tsconfig.json")
|
||||
if not tsconfig_path.exists():
|
||||
print("⚠️ tsconfig.json not found")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(tsconfig_path) as f:
|
||||
config = json.load(f)
|
||||
|
||||
compiler_opts = config.get("compilerOptions", {})
|
||||
|
||||
# Check strict mode
|
||||
if compiler_opts.get("strict"):
|
||||
print("✅ Strict mode enabled")
|
||||
else:
|
||||
print("⚠️ Strict mode NOT enabled")
|
||||
|
||||
# Check important flags
|
||||
flags = {
|
||||
"noUncheckedIndexedAccess": "Unchecked index access protection",
|
||||
"noImplicitOverride": "Implicit override protection",
|
||||
"skipLibCheck": "Skip lib check (performance)",
|
||||
"incremental": "Incremental compilation"
|
||||
}
|
||||
|
||||
for flag, desc in flags.items():
|
||||
status = "✅" if compiler_opts.get(flag) else "⚪"
|
||||
print(f" {status} {desc}: {compiler_opts.get(flag, 'not set')}")
|
||||
|
||||
# Check module settings
|
||||
print(f"\n Module: {compiler_opts.get('module', 'not set')}")
|
||||
print(f" Module Resolution: {compiler_opts.get('moduleResolution', 'not set')}")
|
||||
print(f" Target: {compiler_opts.get('target', 'not set')}")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print("❌ Invalid JSON in tsconfig.json")
|
||||
|
||||
def check_tooling():
|
||||
"""Detect TypeScript tooling ecosystem."""
|
||||
print("\n🛠️ Tooling Detection:")
|
||||
print("-" * 40)
|
||||
|
||||
pkg_path = Path("package.json")
|
||||
if not pkg_path.exists():
|
||||
print("⚠️ package.json not found")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(pkg_path) as f:
|
||||
pkg = json.load(f)
|
||||
|
||||
all_deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
||||
|
||||
tools = {
|
||||
"biome": "Biome (linter/formatter)",
|
||||
"eslint": "ESLint",
|
||||
"prettier": "Prettier",
|
||||
"vitest": "Vitest (testing)",
|
||||
"jest": "Jest (testing)",
|
||||
"turborepo": "Turborepo (monorepo)",
|
||||
"turbo": "Turbo (monorepo)",
|
||||
"nx": "Nx (monorepo)",
|
||||
"lerna": "Lerna (monorepo)"
|
||||
}
|
||||
|
||||
for tool, desc in tools.items():
|
||||
for dep in all_deps:
|
||||
if tool in dep.lower():
|
||||
print(f" ✅ {desc}")
|
||||
break
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print("❌ Invalid JSON in package.json")
|
||||
|
||||
def check_monorepo():
|
||||
"""Check for monorepo configuration."""
|
||||
print("\n📦 Monorepo Check:")
|
||||
print("-" * 40)
|
||||
|
||||
indicators = [
|
||||
("pnpm-workspace.yaml", "PNPM Workspace"),
|
||||
("lerna.json", "Lerna"),
|
||||
("nx.json", "Nx"),
|
||||
("turbo.json", "Turborepo")
|
||||
]
|
||||
|
||||
found = False
|
||||
for file, name in indicators:
|
||||
if Path(file).exists():
|
||||
print(f" ✅ {name} detected")
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
print(" ⚪ No monorepo configuration detected")
|
||||
|
||||
def check_type_errors():
|
||||
"""Run quick type check."""
|
||||
print("\n🔍 Type Check:")
|
||||
print("-" * 40)
|
||||
|
||||
result = run_cmd("npx tsc --noEmit 2>&1 | head -20")
|
||||
if "error TS" in result:
|
||||
errors = result.count("error TS")
|
||||
print(f" ❌ {errors}+ type errors found")
|
||||
print(result[:500])
|
||||
else:
|
||||
print(" ✅ No type errors")
|
||||
|
||||
def check_any_usage():
|
||||
"""Check for any type usage."""
|
||||
print("\n⚠️ 'any' Type Usage:")
|
||||
print("-" * 40)
|
||||
|
||||
result = run_cmd("grep -r ': any' --include='*.ts' --include='*.tsx' src/ 2>/dev/null | wc -l")
|
||||
count = result.strip()
|
||||
if count and count != "0":
|
||||
print(f" ⚠️ Found {count} occurrences of ': any'")
|
||||
sample = run_cmd("grep -rn ': any' --include='*.ts' --include='*.tsx' src/ 2>/dev/null | head -5")
|
||||
if sample:
|
||||
print(sample)
|
||||
else:
|
||||
print(" ✅ No explicit 'any' types found")
|
||||
|
||||
def check_type_assertions():
|
||||
"""Check for type assertions."""
|
||||
print("\n⚠️ Type Assertions (as):")
|
||||
print("-" * 40)
|
||||
|
||||
result = run_cmd("grep -r ' as ' --include='*.ts' --include='*.tsx' src/ 2>/dev/null | grep -v 'import' | wc -l")
|
||||
count = result.strip()
|
||||
if count and count != "0":
|
||||
print(f" ⚠️ Found {count} type assertions")
|
||||
else:
|
||||
print(" ✅ No type assertions found")
|
||||
|
||||
def check_performance():
|
||||
"""Check type checking performance."""
|
||||
print("\n⏱️ Type Check Performance:")
|
||||
print("-" * 40)
|
||||
|
||||
result = run_cmd("npx tsc --extendedDiagnostics --noEmit 2>&1 | grep -E 'Check time|Files:|Lines:|Nodes:'")
|
||||
if result.strip():
|
||||
for line in result.strip().split('\n'):
|
||||
print(f" {line}")
|
||||
else:
|
||||
print(" ⚠️ Could not measure performance")
|
||||
|
||||
def main():
|
||||
print("=" * 50)
|
||||
print("🔍 TypeScript Project Diagnostic Report")
|
||||
print("=" * 50)
|
||||
|
||||
check_versions()
|
||||
check_tsconfig()
|
||||
check_tooling()
|
||||
check_monorepo()
|
||||
check_any_usage()
|
||||
check_type_assertions()
|
||||
check_type_errors()
|
||||
check_performance()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("✅ Diagnostic Complete")
|
||||
print("=" * 50)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
6
.claude/skills/typescript-pro/.openskills.json
Normal file
6
.claude/skills/typescript-pro/.openskills.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"source": "/tmp/skill-selector-curated-3423638041",
|
||||
"sourceType": "local",
|
||||
"localPath": "/tmp/skill-selector-curated-3423638041/typescript-pro",
|
||||
"installedAt": "2026-04-07T00:45:24.785Z"
|
||||
}
|
||||
145
.claude/skills/typescript-pro/SKILL.md
Normal file
145
.claude/skills/typescript-pro/SKILL.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: typescript-pro
|
||||
description: Implements advanced TypeScript type systems, creates custom type guards, utility types, and branded types, and configures tRPC for end-to-end type safety. Use when building TypeScript applications requiring advanced generics, conditional or mapped types, discriminated unions, monorepo setup, or full-stack type safety with tRPC.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: https://github.com/Jeffallan
|
||||
version: "1.1.0"
|
||||
domain: language
|
||||
triggers: TypeScript, generics, type safety, conditional types, mapped types, tRPC, tsconfig, type guards, discriminated unions
|
||||
role: specialist
|
||||
scope: implementation
|
||||
output-format: code
|
||||
related-skills: fullstack-guardian, api-designer
|
||||
---
|
||||
|
||||
# TypeScript Pro
|
||||
|
||||
## Core Workflow
|
||||
|
||||
1. **Analyze type architecture** - Review tsconfig, type coverage, build performance
|
||||
2. **Design type-first APIs** - Create branded types, generics, utility types
|
||||
3. **Implement with type safety** - Write type guards, discriminated unions, conditional types; run `tsc --noEmit` to catch type errors before proceeding
|
||||
4. **Optimize build** - Configure project references, incremental compilation, tree shaking; re-run `tsc --noEmit` to confirm zero errors after changes
|
||||
5. **Test types** - Confirm type coverage with a tool like `type-coverage`; validate that all public APIs have explicit return types; iterate on steps 3–4 until all checks pass
|
||||
|
||||
## Reference Guide
|
||||
|
||||
Load detailed guidance based on context:
|
||||
|
||||
| Topic | Reference | Load When |
|
||||
|-------|-----------|-----------|
|
||||
| Advanced Types | `references/advanced-types.md` | Generics, conditional types, mapped types, template literals |
|
||||
| Type Guards | `references/type-guards.md` | Type narrowing, discriminated unions, assertion functions |
|
||||
| Utility Types | `references/utility-types.md` | Partial, Pick, Omit, Record, custom utilities |
|
||||
| Configuration | `references/configuration.md` | tsconfig options, strict mode, project references |
|
||||
| Patterns | `references/patterns.md` | Builder pattern, factory pattern, type-safe APIs |
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Branded Types
|
||||
```typescript
|
||||
// Branded type for domain modeling
|
||||
type Brand<T, B extends string> = T & { readonly __brand: B };
|
||||
type UserId = Brand<string, "UserId">;
|
||||
type OrderId = Brand<number, "OrderId">;
|
||||
|
||||
const toUserId = (id: string): UserId => id as UserId;
|
||||
const toOrderId = (id: number): OrderId => id as OrderId;
|
||||
|
||||
// Usage — prevents accidental id mix-ups at compile time
|
||||
function getOrder(userId: UserId, orderId: OrderId) { /* ... */ }
|
||||
```
|
||||
|
||||
### Discriminated Unions & Type Guards
|
||||
```typescript
|
||||
type LoadingState = { status: "loading" };
|
||||
type SuccessState = { status: "success"; data: string[] };
|
||||
type ErrorState = { status: "error"; error: Error };
|
||||
type RequestState = LoadingState | SuccessState | ErrorState;
|
||||
|
||||
// Type predicate guard
|
||||
function isSuccess(state: RequestState): state is SuccessState {
|
||||
return state.status === "success";
|
||||
}
|
||||
|
||||
// Exhaustive switch with discriminated union
|
||||
function renderState(state: RequestState): string {
|
||||
switch (state.status) {
|
||||
case "loading": return "Loading…";
|
||||
case "success": return state.data.join(", ");
|
||||
case "error": return state.error.message;
|
||||
default: {
|
||||
const _exhaustive: never = state;
|
||||
throw new Error(`Unhandled state: ${_exhaustive}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Utility Types
|
||||
```typescript
|
||||
// Deep readonly — immutable nested objects
|
||||
type DeepReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
|
||||
};
|
||||
|
||||
// Require exactly one of a set of keys
|
||||
type RequireExactlyOne<T, Keys extends keyof T = keyof T> =
|
||||
Pick<T, Exclude<keyof T, Keys>> &
|
||||
{ [K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, never>> }[Keys];
|
||||
```
|
||||
|
||||
### Recommended tsconfig.json
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"incremental": true,
|
||||
"skipLibCheck": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
### MUST DO
|
||||
- Enable strict mode with all compiler flags
|
||||
- Use type-first API design
|
||||
- Implement branded types for domain modeling
|
||||
- Use `satisfies` operator for type validation
|
||||
- Create discriminated unions for state machines
|
||||
- Use `Annotated` pattern with type predicates
|
||||
- Generate declaration files for libraries
|
||||
- Optimize for type inference
|
||||
|
||||
### MUST NOT DO
|
||||
- Use explicit `any` without justification
|
||||
- Skip type coverage for public APIs
|
||||
- Mix type-only and value imports
|
||||
- Disable strict null checks
|
||||
- Use `as` assertions without necessity
|
||||
- Ignore compiler performance warnings
|
||||
- Skip declaration file generation
|
||||
- Use enums (prefer const objects with `as const`)
|
||||
|
||||
## Output Templates
|
||||
|
||||
When implementing TypeScript features, provide:
|
||||
1. Type definitions (interfaces, types, generics)
|
||||
2. Implementation with type guards
|
||||
3. tsconfig configuration if needed
|
||||
4. Brief explanation of type design decisions
|
||||
|
||||
## Knowledge Reference
|
||||
|
||||
TypeScript 5.0+, generics, conditional types, mapped types, template literal types, discriminated unions, type guards, branded types, tRPC, project references, incremental compilation, declaration files, const assertions, satisfies operator
|
||||
259
.claude/skills/typescript-pro/references/advanced-types.md
Normal file
259
.claude/skills/typescript-pro/references/advanced-types.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Advanced Types
|
||||
|
||||
## Generic Constraints
|
||||
|
||||
```typescript
|
||||
// Basic constraint
|
||||
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
||||
return obj[key];
|
||||
}
|
||||
|
||||
// Multiple constraints
|
||||
interface HasId { id: number; }
|
||||
interface HasName { name: string; }
|
||||
|
||||
function merge<T extends HasId, U extends HasName>(obj1: T, obj2: U): T & U {
|
||||
return { ...obj1, ...obj2 };
|
||||
}
|
||||
|
||||
// Generic constraint with default
|
||||
type ApiResponse<T = unknown, E = Error> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: E };
|
||||
|
||||
// Constraint with infer
|
||||
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
|
||||
type Result = UnwrapPromise<Promise<string>>; // string
|
||||
```
|
||||
|
||||
## Conditional Types
|
||||
|
||||
```typescript
|
||||
// Basic conditional type
|
||||
type IsString<T> = T extends string ? true : false;
|
||||
|
||||
// Distributive conditional types
|
||||
type ToArray<T> = T extends any ? T[] : never;
|
||||
type StringOrNumberArray = ToArray<string | number>; // string[] | number[]
|
||||
|
||||
// Non-distributive (use tuple)
|
||||
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
|
||||
type BothArray = ToArrayNonDist<string | number>; // (string | number)[]
|
||||
|
||||
// Nested conditionals for type extraction
|
||||
type Flatten<T> = T extends Array<infer U>
|
||||
? U extends Array<infer V>
|
||||
? Flatten<V>
|
||||
: U
|
||||
: T;
|
||||
|
||||
type Nested = Flatten<string[][][]>; // string
|
||||
|
||||
// Exclude null/undefined
|
||||
type NonNullable<T> = T extends null | undefined ? never : T;
|
||||
```
|
||||
|
||||
## Mapped Types
|
||||
|
||||
```typescript
|
||||
// Basic mapped type
|
||||
type ReadOnly<T> = {
|
||||
readonly [K in keyof T]: T[K];
|
||||
};
|
||||
|
||||
// Optional properties
|
||||
type Partial<T> = {
|
||||
[K in keyof T]?: T[K];
|
||||
};
|
||||
|
||||
// Required properties
|
||||
type Required<T> = {
|
||||
[K in keyof T]-?: T[K]; // Remove optional modifier
|
||||
};
|
||||
|
||||
// Key remapping with 'as'
|
||||
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; }
|
||||
|
||||
// Filtering keys
|
||||
type PickByType<T, U> = {
|
||||
[K in keyof T as T[K] extends U ? K : never]: T[K];
|
||||
};
|
||||
|
||||
type StringFields = PickByType<Person, string>; // { name: string }
|
||||
```
|
||||
|
||||
## Template Literal Types
|
||||
|
||||
```typescript
|
||||
// Basic template literal
|
||||
type EmailLocale = 'en' | 'es' | 'fr';
|
||||
type EmailType = 'welcome' | 'reset-password';
|
||||
type EmailTemplate = `${EmailLocale}_${EmailType}`;
|
||||
// 'en_welcome' | 'en_reset-password' | 'es_welcome' | ...
|
||||
|
||||
// Intrinsic string manipulation
|
||||
type Uppercase<S extends string> = intrinsic;
|
||||
type Lowercase<S extends string> = intrinsic;
|
||||
type Capitalize<S extends string> = intrinsic;
|
||||
type Uncapitalize<S extends string> = intrinsic;
|
||||
|
||||
type EventName<T extends string> = `on${Capitalize<T>}`;
|
||||
type ClickEvent = EventName<'click'>; // 'onClick'
|
||||
|
||||
// Template literal with mapped types
|
||||
type CSSProperties = {
|
||||
[K in 'color' | 'background' | 'border' as `--${K}`]: string;
|
||||
};
|
||||
// { '--color': string; '--background': string; '--border': string }
|
||||
|
||||
// Pattern matching with infer
|
||||
type ExtractRouteParams<T extends string> =
|
||||
T extends `${infer _Start}/:${infer Param}/${infer Rest}`
|
||||
? Param | ExtractRouteParams<`/${Rest}`>
|
||||
: T extends `${infer _Start}/:${infer Param}`
|
||||
? Param
|
||||
: never;
|
||||
|
||||
type Params = ExtractRouteParams<'/users/:id/posts/:postId'>; // 'id' | 'postId'
|
||||
```
|
||||
|
||||
## Higher-Kinded Types (Simulation)
|
||||
|
||||
```typescript
|
||||
// Type-level function simulation
|
||||
interface TypeClass<F> {
|
||||
map: <A, B>(f: (a: A) => B, fa: any) => any;
|
||||
}
|
||||
|
||||
// Functor pattern
|
||||
type Maybe<T> = { type: 'just'; value: T } | { type: 'nothing' };
|
||||
|
||||
const MaybeFunctor: TypeClass<Maybe<any>> = {
|
||||
map: <A, B>(f: (a: A) => B, ma: Maybe<A>): Maybe<B> => {
|
||||
return ma.type === 'just'
|
||||
? { type: 'just', value: f(ma.value) }
|
||||
: { type: 'nothing' };
|
||||
}
|
||||
};
|
||||
|
||||
// Builder pattern with generics
|
||||
type Builder<T, K extends keyof T = never> = {
|
||||
with<P extends Exclude<keyof T, K>>(
|
||||
key: P,
|
||||
value: T[P]
|
||||
): Builder<T, K | P>;
|
||||
build(): K extends keyof T ? T : never;
|
||||
};
|
||||
```
|
||||
|
||||
## Recursive Types
|
||||
|
||||
```typescript
|
||||
// JSON type
|
||||
type JSONValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| JSONValue[]
|
||||
| { [key: string]: JSONValue };
|
||||
|
||||
// Deep partial
|
||||
type DeepPartial<T> = T extends object ? {
|
||||
[K in keyof T]?: DeepPartial<T[K]>;
|
||||
} : T;
|
||||
|
||||
// Deep readonly
|
||||
type DeepReadonly<T> = T extends object ? {
|
||||
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
||||
} : T;
|
||||
|
||||
// Path type for nested objects
|
||||
type PathsToProps<T> = T extends object ? {
|
||||
[K in keyof T]: K extends string
|
||||
? T[K] extends object
|
||||
? K | `${K}.${PathsToProps<T[K]>}`
|
||||
: K
|
||||
: never;
|
||||
}[keyof T] : never;
|
||||
|
||||
interface User {
|
||||
profile: {
|
||||
name: string;
|
||||
settings: {
|
||||
theme: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
type UserPaths = PathsToProps<User>;
|
||||
// 'profile' | 'profile.name' | 'profile.settings' | 'profile.settings.theme'
|
||||
```
|
||||
|
||||
## Variance and Contravariance
|
||||
|
||||
```typescript
|
||||
// Covariance (return types)
|
||||
type Producer<T> = () => T;
|
||||
let stringProducer: Producer<string> = () => 'hello';
|
||||
let objectProducer: Producer<object> = stringProducer; // OK: string is object
|
||||
|
||||
// Contravariance (parameter types)
|
||||
type Consumer<T> = (value: T) => void;
|
||||
let objectConsumer: Consumer<object> = (obj) => console.log(obj);
|
||||
let stringConsumer: Consumer<string> = objectConsumer; // OK in strict mode
|
||||
|
||||
// Invariance (mutable properties)
|
||||
interface Box<T> {
|
||||
value: T;
|
||||
setValue(v: T): void;
|
||||
}
|
||||
|
||||
let stringBox: Box<string> = { value: '', setValue: (v) => {} };
|
||||
// let objectBox: Box<object> = stringBox; // Error: invariant
|
||||
```
|
||||
|
||||
## Type-Level Programming
|
||||
|
||||
```typescript
|
||||
// Type-level addition (limited)
|
||||
type Length<T extends any[]> = T['length'];
|
||||
type Concat<A extends any[], B extends any[]> = [...A, ...B];
|
||||
|
||||
// Type-level conditionals
|
||||
type If<Condition extends boolean, Then, Else> =
|
||||
Condition extends true ? Then : Else;
|
||||
|
||||
// Type-level equality
|
||||
type Equal<X, Y> =
|
||||
(<T>() => T extends X ? 1 : 2) extends
|
||||
(<T>() => T extends Y ? 1 : 2) ? true : false;
|
||||
|
||||
// Assert equal types (for testing)
|
||||
type Assert<T extends true> = T;
|
||||
type Test = Assert<Equal<1 | 2, 2 | 1>>; // OK
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Use Case |
|
||||
|---------|----------|
|
||||
| `T extends U ? X : Y` | Conditional type logic |
|
||||
| `infer R` | Extract types from patterns |
|
||||
| `K in keyof T` | Iterate over object keys |
|
||||
| `as NewKey` | Remap keys in mapped types |
|
||||
| Template literals | String pattern types |
|
||||
| `T extends any` | Distributive conditionals |
|
||||
| `[T] extends [any]` | Non-distributive check |
|
||||
| `-?` modifier | Remove optional |
|
||||
| `readonly` modifier | Make immutable |
|
||||
445
.claude/skills/typescript-pro/references/configuration.md
Normal file
445
.claude/skills/typescript-pro/references/configuration.md
Normal file
@@ -0,0 +1,445 @@
|
||||
# TypeScript Configuration
|
||||
|
||||
## Strict Mode Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Strict type checking
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
|
||||
// Additional checks
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
|
||||
// Module resolution
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
// Emit
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"importHelpers": true,
|
||||
|
||||
// Interop
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
// Target
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
|
||||
// Skip checking
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Project References
|
||||
|
||||
```json
|
||||
// Root tsconfig.json
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./packages/shared" },
|
||||
{ "path": "./packages/frontend" },
|
||||
{ "path": "./packages/backend" }
|
||||
]
|
||||
}
|
||||
|
||||
// packages/shared/tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
||||
// packages/frontend/tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../shared" }
|
||||
],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
```
|
||||
|
||||
## Module Resolution Strategies
|
||||
|
||||
```json
|
||||
// Node16/NodeNext (recommended for Node.js)
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
||||
// Bundler (for bundlers like Vite, esbuild)
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"moduleDetection": "force"
|
||||
}
|
||||
}
|
||||
|
||||
// Classic (legacy, avoid)
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Path Mapping
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@components/*": ["src/components/*"],
|
||||
"@utils/*": ["src/utils/*"],
|
||||
"@shared/*": ["../shared/src/*"],
|
||||
"@types": ["src/types/index.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Usage with path mapping
|
||||
import { Button } from '@components/Button';
|
||||
import { formatDate } from '@utils/date';
|
||||
import type { User } from '@types';
|
||||
```
|
||||
|
||||
## Incremental Compilation
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": "./dist/.tsbuildinfo",
|
||||
"composite": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Declaration Files
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Generate .d.ts files
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": false,
|
||||
|
||||
// Bundle declarations
|
||||
"declarationDir": "./types",
|
||||
|
||||
// For libraries
|
||||
"stripInternal": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Using JSDoc for .d.ts generation
|
||||
/**
|
||||
* Creates a user
|
||||
* @param name - User's name
|
||||
* @param email - User's email
|
||||
* @returns The created user
|
||||
* @example
|
||||
* ```ts
|
||||
* const user = createUser('John', 'john@example.com');
|
||||
* ```
|
||||
*/
|
||||
export function createUser(name: string, email: string): User {
|
||||
return { id: generateId(), name, email };
|
||||
}
|
||||
```
|
||||
|
||||
## Build Optimization
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Performance
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
|
||||
// Faster builds
|
||||
"incremental": true,
|
||||
"assumeChangesOnlyAffectDirectDependencies": true,
|
||||
|
||||
// Smaller output
|
||||
"removeComments": true,
|
||||
"importHelpers": true,
|
||||
|
||||
// Tree shaking support
|
||||
"module": "ESNext",
|
||||
"target": "ES2020"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Multiple Configurations
|
||||
|
||||
```json
|
||||
// tsconfig.json (base)
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "ES2022"
|
||||
}
|
||||
}
|
||||
|
||||
// tsconfig.build.json (production)
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceMap": false,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
},
|
||||
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
||||
|
||||
// tsconfig.test.json (testing)
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "node"],
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["src/**/*.test.ts", "src/**/*.spec.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
## Framework-Specific Configs
|
||||
|
||||
```json
|
||||
// React + Vite
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
|
||||
// Next.js
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": { "@/*": ["./src/*"] }
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
// Node.js + Express
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Type Definitions
|
||||
|
||||
```typescript
|
||||
// src/types/global.d.ts
|
||||
declare global {
|
||||
interface Window {
|
||||
myApp: {
|
||||
version: string;
|
||||
config: AppConfig;
|
||||
};
|
||||
}
|
||||
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
DATABASE_URL: string;
|
||||
API_KEY: string;
|
||||
NODE_ENV: 'development' | 'production' | 'test';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
// src/types/modules.d.ts
|
||||
declare module '*.svg' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.css' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module 'untyped-library' {
|
||||
export function doSomething(value: string): number;
|
||||
}
|
||||
```
|
||||
|
||||
## Compiler API Usage
|
||||
|
||||
```typescript
|
||||
// programmatic compilation
|
||||
import ts from 'typescript';
|
||||
|
||||
function compile(fileNames: string[], options: ts.CompilerOptions): void {
|
||||
const program = ts.createProgram(fileNames, options);
|
||||
const emitResult = program.emit();
|
||||
|
||||
const allDiagnostics = ts
|
||||
.getPreEmitDiagnostics(program)
|
||||
.concat(emitResult.diagnostics);
|
||||
|
||||
allDiagnostics.forEach(diagnostic => {
|
||||
if (diagnostic.file) {
|
||||
const { line, character } = ts.getLineAndCharacterOfPosition(
|
||||
diagnostic.file,
|
||||
diagnostic.start!
|
||||
);
|
||||
const message = ts.flattenDiagnosticMessageText(
|
||||
diagnostic.messageText,
|
||||
'\n'
|
||||
);
|
||||
console.log(
|
||||
`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const exitCode = emitResult.emitSkipped ? 1 : 0;
|
||||
console.log(`Process exiting with code '${exitCode}'.`);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
compile(['src/index.ts'], {
|
||||
noEmitOnError: true,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
module: ts.ModuleKind.ES2022,
|
||||
strict: true
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"diagnostics": true,
|
||||
"extendedDiagnostics": true,
|
||||
"generateCpuProfile": "profile.cpuprofile",
|
||||
"explainFiles": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Run with diagnostics
|
||||
tsc --diagnostics
|
||||
|
||||
# Extended diagnostics
|
||||
tsc --extendedDiagnostics
|
||||
|
||||
# Generate trace
|
||||
tsc --generateTrace trace
|
||||
|
||||
# Analyze with @typescript/analyze-trace
|
||||
npx @typescript/analyze-trace trace
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Option | Purpose |
|
||||
|--------|---------|
|
||||
| `strict` | Enable all strict checks |
|
||||
| `composite` | Enable project references |
|
||||
| `incremental` | Enable incremental compilation |
|
||||
| `skipLibCheck` | Skip .d.ts checking for faster builds |
|
||||
| `esModuleInterop` | Better CommonJS interop |
|
||||
| `moduleResolution` | How modules are resolved |
|
||||
| `paths` | Path mapping for imports |
|
||||
| `declaration` | Generate .d.ts files |
|
||||
| `sourceMap` | Generate source maps |
|
||||
| `noEmit` | Don't emit output (type check only) |
|
||||
| `isolatedModules` | Each file can be transpiled separately |
|
||||
| `allowImportingTsExtensions` | Import .ts files directly |
|
||||
484
.claude/skills/typescript-pro/references/patterns.md
Normal file
484
.claude/skills/typescript-pro/references/patterns.md
Normal file
@@ -0,0 +1,484 @@
|
||||
# TypeScript Patterns
|
||||
|
||||
## Builder Pattern
|
||||
|
||||
```typescript
|
||||
// Type-safe builder with progressive types
|
||||
class UserBuilder {
|
||||
private data: Partial<User> = {};
|
||||
|
||||
setName(name: string): this {
|
||||
this.data.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
setEmail(email: string): this {
|
||||
this.data.email = email;
|
||||
return this;
|
||||
}
|
||||
|
||||
setAge(age: number): this {
|
||||
this.data.age = age;
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): User {
|
||||
if (!this.data.name || !this.data.email) {
|
||||
throw new Error('Name and email are required');
|
||||
}
|
||||
return this.data as User;
|
||||
}
|
||||
}
|
||||
|
||||
// Fluent API with type safety
|
||||
const user = new UserBuilder()
|
||||
.setName('John')
|
||||
.setEmail('john@example.com')
|
||||
.setAge(30)
|
||||
.build();
|
||||
|
||||
// Advanced builder with compile-time validation
|
||||
type Builder<T, K extends keyof T = never> = {
|
||||
[P in keyof T as `set${Capitalize<string & P>}`]: (
|
||||
value: T[P]
|
||||
) => Builder<T, K | P>;
|
||||
} & {
|
||||
build: K extends keyof T ? () => T : never;
|
||||
};
|
||||
|
||||
function createBuilder<T>(): Builder<T> {
|
||||
const data = {} as T;
|
||||
|
||||
return new Proxy({} as Builder<T>, {
|
||||
get(_, prop: string) {
|
||||
if (prop === 'build') {
|
||||
return () => data;
|
||||
}
|
||||
if (prop.startsWith('set')) {
|
||||
const key = prop.slice(3).toLowerCase();
|
||||
return (value: any) => {
|
||||
(data as any)[key] = value;
|
||||
return this;
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Factory Pattern
|
||||
|
||||
```typescript
|
||||
// Abstract factory with type safety
|
||||
interface Logger {
|
||||
log(message: string): void;
|
||||
}
|
||||
|
||||
class ConsoleLogger implements Logger {
|
||||
log(message: string): void {
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
|
||||
class FileLogger implements Logger {
|
||||
constructor(private filename: string) {}
|
||||
|
||||
log(message: string): void {
|
||||
// Write to file
|
||||
}
|
||||
}
|
||||
|
||||
type LoggerType = 'console' | 'file';
|
||||
type LoggerConfig<T extends LoggerType> = T extends 'file'
|
||||
? { type: T; filename: string }
|
||||
: { type: T };
|
||||
|
||||
class LoggerFactory {
|
||||
static create<T extends LoggerType>(config: LoggerConfig<T>): Logger {
|
||||
switch (config.type) {
|
||||
case 'console':
|
||||
return new ConsoleLogger();
|
||||
case 'file':
|
||||
return new FileLogger(config.filename);
|
||||
default:
|
||||
throw new Error('Unknown logger type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const consoleLogger = LoggerFactory.create({ type: 'console' });
|
||||
const fileLogger = LoggerFactory.create({ type: 'file', filename: 'app.log' });
|
||||
|
||||
// Generic factory with dependency injection
|
||||
type Constructor<T> = new (...args: any[]) => T;
|
||||
|
||||
class Container {
|
||||
private instances = new Map<Constructor<any>, any>();
|
||||
|
||||
register<T>(token: Constructor<T>, instance: T): void {
|
||||
this.instances.set(token, instance);
|
||||
}
|
||||
|
||||
resolve<T>(token: Constructor<T>): T {
|
||||
const instance = this.instances.get(token);
|
||||
if (!instance) {
|
||||
throw new Error(`No instance registered for ${token.name}`);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
```typescript
|
||||
// Type-safe repository with generic CRUD
|
||||
interface Entity {
|
||||
id: string | number;
|
||||
}
|
||||
|
||||
interface Repository<T extends Entity> {
|
||||
find(id: T['id']): Promise<T | null>;
|
||||
findAll(): Promise<T[]>;
|
||||
create(data: Omit<T, 'id'>): Promise<T>;
|
||||
update(id: T['id'], data: Partial<Omit<T, 'id'>>): Promise<T>;
|
||||
delete(id: T['id']): Promise<void>;
|
||||
}
|
||||
|
||||
class UserRepository implements Repository<User> {
|
||||
async find(id: User['id']): Promise<User | null> {
|
||||
// Database query
|
||||
return null;
|
||||
}
|
||||
|
||||
async findAll(): Promise<User[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async create(data: Omit<User, 'id'>): Promise<User> {
|
||||
// Insert into database
|
||||
return { id: 1, ...data };
|
||||
}
|
||||
|
||||
async update(id: User['id'], data: Partial<Omit<User, 'id'>>): Promise<User> {
|
||||
// Update database
|
||||
return { id, name: '', email: '', ...data };
|
||||
}
|
||||
|
||||
async delete(id: User['id']): Promise<void> {
|
||||
// Delete from database
|
||||
}
|
||||
}
|
||||
|
||||
// Query builder with type safety
|
||||
class QueryBuilder<T> {
|
||||
private conditions: Array<(item: T) => boolean> = [];
|
||||
|
||||
where<K extends keyof T>(key: K, value: T[K]): this {
|
||||
this.conditions.push(item => item[key] === value);
|
||||
return this;
|
||||
}
|
||||
|
||||
execute(items: T[]): T[] {
|
||||
return items.filter(item =>
|
||||
this.conditions.every(condition => condition(item))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const query = new QueryBuilder<User>()
|
||||
.where('email', 'john@example.com')
|
||||
.where('age', 30);
|
||||
```
|
||||
|
||||
## Type-Safe API Client
|
||||
|
||||
```typescript
|
||||
// REST API client with type safety
|
||||
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
|
||||
type ApiEndpoints = {
|
||||
'/users': {
|
||||
GET: { response: User[] };
|
||||
POST: { body: CreateUserDto; response: User };
|
||||
};
|
||||
'/users/:id': {
|
||||
GET: { params: { id: string }; response: User };
|
||||
PUT: { params: { id: string }; body: UpdateUserDto; response: User };
|
||||
DELETE: { params: { id: string }; response: void };
|
||||
};
|
||||
'/posts': {
|
||||
GET: { query: { userId?: string }; response: Post[] };
|
||||
POST: { body: CreatePostDto; response: Post };
|
||||
};
|
||||
};
|
||||
|
||||
type ExtractParams<T extends string> =
|
||||
T extends `${infer _Start}/:${infer Param}/${infer Rest}`
|
||||
? { [K in Param]: string } & ExtractParams<`/${Rest}`>
|
||||
: T extends `${infer _Start}/:${infer Param}`
|
||||
? { [K in Param]: string }
|
||||
: {};
|
||||
|
||||
class ApiClient {
|
||||
async request<
|
||||
Path extends keyof ApiEndpoints,
|
||||
Method extends keyof ApiEndpoints[Path]
|
||||
>(
|
||||
method: Method,
|
||||
path: Path,
|
||||
options?: ApiEndpoints[Path][Method] extends { body: infer B }
|
||||
? { body: B }
|
||||
: ApiEndpoints[Path][Method] extends { params: infer P }
|
||||
? { params: P }
|
||||
: ApiEndpoints[Path][Method] extends { query: infer Q }
|
||||
? { query: Q }
|
||||
: never
|
||||
): Promise<
|
||||
ApiEndpoints[Path][Method] extends { response: infer R } ? R : never
|
||||
> {
|
||||
// Make HTTP request
|
||||
return null as any;
|
||||
}
|
||||
}
|
||||
|
||||
const client = new ApiClient();
|
||||
|
||||
// Type-safe API calls
|
||||
const users = await client.request('GET', '/users');
|
||||
const user = await client.request('GET', '/users/:id', { params: { id: '1' } });
|
||||
const newUser = await client.request('POST', '/users', {
|
||||
body: { name: 'John', email: 'john@example.com' }
|
||||
});
|
||||
```
|
||||
|
||||
## State Machine Pattern
|
||||
|
||||
```typescript
|
||||
// Type-safe state machine
|
||||
type State = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
type Event =
|
||||
| { type: 'FETCH' }
|
||||
| { type: 'SUCCESS'; data: any }
|
||||
| { type: 'ERROR'; error: Error }
|
||||
| { type: 'RETRY' };
|
||||
|
||||
type StateMachine = {
|
||||
[S in State]: {
|
||||
[E in Event['type']]?: State;
|
||||
};
|
||||
};
|
||||
|
||||
const machine: StateMachine = {
|
||||
idle: { FETCH: 'loading' },
|
||||
loading: { SUCCESS: 'success', ERROR: 'error' },
|
||||
success: { FETCH: 'loading' },
|
||||
error: { RETRY: 'loading' }
|
||||
};
|
||||
|
||||
class StateManager<S extends string, E extends { type: string }> {
|
||||
constructor(
|
||||
private state: S,
|
||||
private transitions: Record<S, Partial<Record<E['type'], S>>>
|
||||
) {}
|
||||
|
||||
getState(): S {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
dispatch(event: E): S {
|
||||
const nextState = this.transitions[this.state][event.type];
|
||||
if (nextState === undefined) {
|
||||
throw new Error(`Invalid transition from ${this.state} on ${event.type}`);
|
||||
}
|
||||
this.state = nextState;
|
||||
return this.state;
|
||||
}
|
||||
}
|
||||
|
||||
const manager = new StateManager<State, Event>('idle', machine);
|
||||
manager.dispatch({ type: 'FETCH' }); // 'loading'
|
||||
manager.dispatch({ type: 'SUCCESS', data: {} }); // 'success'
|
||||
```
|
||||
|
||||
## Decorator Pattern
|
||||
|
||||
```typescript
|
||||
// Method decorators with type safety
|
||||
function Log(
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
) {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = function (...args: any[]) {
|
||||
console.log(`Calling ${propertyKey} with`, args);
|
||||
const result = originalMethod.apply(this, args);
|
||||
console.log(`Result:`, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
function Memoize(
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
) {
|
||||
const originalMethod = descriptor.value;
|
||||
const cache = new Map<string, any>();
|
||||
|
||||
descriptor.value = function (...args: any[]) {
|
||||
const key = JSON.stringify(args);
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
const result = originalMethod.apply(this, args);
|
||||
cache.set(key, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
class Calculator {
|
||||
@Log
|
||||
@Memoize
|
||||
fibonacci(n: number): number {
|
||||
if (n <= 1) return n;
|
||||
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Result/Either Pattern
|
||||
|
||||
```typescript
|
||||
// Type-safe error handling
|
||||
type Result<T, E = Error> =
|
||||
| { success: true; value: T }
|
||||
| { success: false; error: E };
|
||||
|
||||
function ok<T>(value: T): Result<T, never> {
|
||||
return { success: true, value };
|
||||
}
|
||||
|
||||
function err<E>(error: E): Result<never, E> {
|
||||
return { success: false, error };
|
||||
}
|
||||
|
||||
async function fetchUser(id: string): Promise<Result<User, string>> {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
if (!response.ok) {
|
||||
return err('User not found');
|
||||
}
|
||||
const user = await response.json();
|
||||
return ok(user);
|
||||
} catch (error) {
|
||||
return err('Network error');
|
||||
}
|
||||
}
|
||||
|
||||
// Usage with pattern matching
|
||||
const result = await fetchUser('123');
|
||||
if (result.success) {
|
||||
console.log(result.value.name); // Type-safe access
|
||||
} else {
|
||||
console.error(result.error); // Type-safe error
|
||||
}
|
||||
|
||||
// Either monad
|
||||
class Either<L, R> {
|
||||
private constructor(
|
||||
private readonly value: L | R,
|
||||
private readonly isRight: boolean
|
||||
) {}
|
||||
|
||||
static left<L, R>(value: L): Either<L, R> {
|
||||
return new Either<L, R>(value, false);
|
||||
}
|
||||
|
||||
static right<L, R>(value: R): Either<L, R> {
|
||||
return new Either<L, R>(value, true);
|
||||
}
|
||||
|
||||
map<T>(fn: (value: R) => T): Either<L, T> {
|
||||
if (this.isRight) {
|
||||
return Either.right(fn(this.value as R));
|
||||
}
|
||||
return Either.left(this.value as L);
|
||||
}
|
||||
|
||||
flatMap<T>(fn: (value: R) => Either<L, T>): Either<L, T> {
|
||||
if (this.isRight) {
|
||||
return fn(this.value as R);
|
||||
}
|
||||
return Either.left(this.value as L);
|
||||
}
|
||||
|
||||
getOrElse(defaultValue: R): R {
|
||||
return this.isRight ? (this.value as R) : defaultValue;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Singleton Pattern
|
||||
|
||||
```typescript
|
||||
// Type-safe singleton
|
||||
class Database {
|
||||
private static instance: Database;
|
||||
private constructor() {
|
||||
// Private constructor prevents instantiation
|
||||
}
|
||||
|
||||
static getInstance(): Database {
|
||||
if (!Database.instance) {
|
||||
Database.instance = new Database();
|
||||
}
|
||||
return Database.instance;
|
||||
}
|
||||
|
||||
query<T>(sql: string): Promise<T[]> {
|
||||
// Execute query
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
const db = Database.getInstance();
|
||||
|
||||
// Generic singleton factory
|
||||
function singleton<T>(factory: () => T): () => T {
|
||||
let instance: T | undefined;
|
||||
return () => {
|
||||
if (!instance) {
|
||||
instance = factory();
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
}
|
||||
|
||||
const getConfig = singleton(() => ({
|
||||
apiUrl: process.env.API_URL,
|
||||
apiKey: process.env.API_KEY
|
||||
}));
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Use Case |
|
||||
|---------|----------|
|
||||
| Builder | Construct complex objects step by step |
|
||||
| Factory | Create objects without specifying exact class |
|
||||
| Repository | Abstract data access layer |
|
||||
| API Client | Type-safe HTTP requests |
|
||||
| State Machine | Manage state transitions |
|
||||
| Decorator | Add behavior to methods |
|
||||
| Result/Either | Type-safe error handling |
|
||||
| Singleton | Ensure single instance |
|
||||
| Query Builder | Type-safe database queries |
|
||||
| Container | Dependency injection |
|
||||
352
.claude/skills/typescript-pro/references/type-guards.md
Normal file
352
.claude/skills/typescript-pro/references/type-guards.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# Type Guards and Narrowing
|
||||
|
||||
## Type Predicates
|
||||
|
||||
```typescript
|
||||
// Basic type predicate
|
||||
function isString(value: unknown): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
function processValue(value: string | number) {
|
||||
if (isString(value)) {
|
||||
console.log(value.toUpperCase()); // value is string
|
||||
} else {
|
||||
console.log(value.toFixed(2)); // value is number
|
||||
}
|
||||
}
|
||||
|
||||
// Generic type predicate
|
||||
function isArray<T>(value: T | T[]): value is T[] {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
// Narrowing to specific interface
|
||||
interface User {
|
||||
type: 'user';
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface Admin {
|
||||
type: 'admin';
|
||||
name: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
function isAdmin(account: User | Admin): account is Admin {
|
||||
return account.type === 'admin';
|
||||
}
|
||||
```
|
||||
|
||||
## Discriminated Unions
|
||||
|
||||
```typescript
|
||||
// Tagged union pattern
|
||||
type Result<T, E = Error> =
|
||||
| { status: 'success'; data: T }
|
||||
| { status: 'error'; error: E }
|
||||
| { status: 'loading' };
|
||||
|
||||
function handleResult<T>(result: Result<T>) {
|
||||
switch (result.status) {
|
||||
case 'success':
|
||||
console.log(result.data); // Narrowed to success
|
||||
break;
|
||||
case 'error':
|
||||
console.error(result.error); // Narrowed to error
|
||||
break;
|
||||
case 'loading':
|
||||
console.log('Loading...'); // Narrowed to loading
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Complex discriminated union
|
||||
type Shape =
|
||||
| { kind: 'circle'; radius: number }
|
||||
| { kind: 'rectangle'; width: number; height: number }
|
||||
| { kind: 'triangle'; base: number; height: number };
|
||||
|
||||
function getArea(shape: Shape): number {
|
||||
switch (shape.kind) {
|
||||
case 'circle':
|
||||
return Math.PI * shape.radius ** 2;
|
||||
case 'rectangle':
|
||||
return shape.width * shape.height;
|
||||
case 'triangle':
|
||||
return (shape.base * shape.height) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Exhaustive checking
|
||||
function assertNever(x: never): never {
|
||||
throw new Error('Unexpected value: ' + x);
|
||||
}
|
||||
|
||||
function processShape(shape: Shape): number {
|
||||
switch (shape.kind) {
|
||||
case 'circle':
|
||||
return shape.radius;
|
||||
case 'rectangle':
|
||||
return shape.width;
|
||||
case 'triangle':
|
||||
return shape.base;
|
||||
default:
|
||||
return assertNever(shape); // Compile error if not exhaustive
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Built-in Type Guards
|
||||
|
||||
```typescript
|
||||
// typeof narrowing
|
||||
function printValue(value: string | number | boolean) {
|
||||
if (typeof value === 'string') {
|
||||
console.log(value.toUpperCase());
|
||||
} else if (typeof value === 'number') {
|
||||
console.log(value.toFixed(2));
|
||||
} else {
|
||||
console.log(value ? 'yes' : 'no');
|
||||
}
|
||||
}
|
||||
|
||||
// instanceof narrowing
|
||||
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 narrowing
|
||||
type Fish = { swim: () => void };
|
||||
type Bird = { fly: () => void };
|
||||
|
||||
function move(animal: Fish | Bird) {
|
||||
if ('swim' in animal) {
|
||||
animal.swim();
|
||||
} else {
|
||||
animal.fly();
|
||||
}
|
||||
}
|
||||
|
||||
// Truthiness narrowing
|
||||
function printLength(value: string | null | undefined) {
|
||||
if (value) {
|
||||
console.log(value.length); // Narrowed to string
|
||||
}
|
||||
}
|
||||
|
||||
// Equality narrowing
|
||||
function compare(x: string | number, y: string | boolean) {
|
||||
if (x === y) {
|
||||
// x and y are both string
|
||||
console.log(x.toUpperCase(), y.toUpperCase());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Assertion Functions
|
||||
|
||||
```typescript
|
||||
// Basic assertion function
|
||||
function assert(condition: unknown, message?: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message || 'Assertion failed');
|
||||
}
|
||||
}
|
||||
|
||||
function processUser(user: unknown) {
|
||||
assert(typeof user === 'object' && user !== null);
|
||||
assert('name' in user && typeof user.name === 'string');
|
||||
console.log(user.name.toUpperCase()); // user is narrowed
|
||||
}
|
||||
|
||||
// Type assertion function
|
||||
function assertIsString(value: unknown): asserts value is string {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('Value is not a string');
|
||||
}
|
||||
}
|
||||
|
||||
function greet(name: unknown) {
|
||||
assertIsString(name);
|
||||
console.log(`Hello, ${name.toUpperCase()}`); // name is string
|
||||
}
|
||||
|
||||
// Generic assertion function
|
||||
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
|
||||
if (value === null || value === undefined) {
|
||||
throw new Error('Value is null or undefined');
|
||||
}
|
||||
}
|
||||
|
||||
function processValue(value: string | null) {
|
||||
assertIsDefined(value);
|
||||
console.log(value.length); // value is string
|
||||
}
|
||||
|
||||
// Assert with type predicate
|
||||
function assertIsUser(value: unknown): asserts value is User {
|
||||
if (
|
||||
typeof value !== 'object' ||
|
||||
value === null ||
|
||||
!('type' in value) ||
|
||||
value.type !== 'user'
|
||||
) {
|
||||
throw new Error('Not a user');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Control Flow Analysis
|
||||
|
||||
```typescript
|
||||
// Assignment narrowing
|
||||
let x: string | number = Math.random() > 0.5 ? 'hello' : 42;
|
||||
|
||||
if (typeof x === 'string') {
|
||||
x; // string
|
||||
} else {
|
||||
x; // number
|
||||
}
|
||||
|
||||
// Return statement narrowing
|
||||
function getValue(flag: boolean): string | number {
|
||||
if (flag) {
|
||||
return 'hello';
|
||||
}
|
||||
return 42; // TypeScript knows this must be number
|
||||
}
|
||||
|
||||
// Throw statement narrowing
|
||||
function processValue(value: string | null) {
|
||||
if (!value) {
|
||||
throw new Error('Value is required');
|
||||
}
|
||||
console.log(value.length); // value is string (null thrown above)
|
||||
}
|
||||
|
||||
// Type guards in array methods
|
||||
const mixed: (string | number)[] = ['a', 1, 'b', 2];
|
||||
const strings = mixed.filter((x): x is string => typeof x === 'string');
|
||||
// strings is string[]
|
||||
```
|
||||
|
||||
## Branded Types
|
||||
|
||||
```typescript
|
||||
// Nominal typing with branded types
|
||||
type Brand<K, T> = K & { __brand: T };
|
||||
|
||||
type UserId = Brand<string, 'UserId'>;
|
||||
type Email = Brand<string, 'Email'>;
|
||||
type Url = Brand<string, 'Url'>;
|
||||
|
||||
// Constructor functions
|
||||
function createUserId(id: string): UserId {
|
||||
return id as UserId;
|
||||
}
|
||||
|
||||
function createEmail(email: string): Email {
|
||||
if (!email.includes('@')) {
|
||||
throw new Error('Invalid email');
|
||||
}
|
||||
return email as Email;
|
||||
}
|
||||
|
||||
// Usage prevents mixing
|
||||
const userId: UserId = createUserId('user-123');
|
||||
const email: Email = createEmail('user@example.com');
|
||||
|
||||
// const wrongAssignment: UserId = email; // Error!
|
||||
|
||||
// Type guard for branded types
|
||||
function isUserId(value: string): value is UserId {
|
||||
return /^user-\d+$/.test(value);
|
||||
}
|
||||
|
||||
// Branded numbers
|
||||
type Positive = Brand<number, 'Positive'>;
|
||||
type Integer = Brand<number, 'Integer'>;
|
||||
|
||||
function createPositive(n: number): Positive {
|
||||
if (n <= 0) throw new Error('Must be positive');
|
||||
return n as Positive;
|
||||
}
|
||||
|
||||
function createInteger(n: number): Integer {
|
||||
if (!Number.isInteger(n)) throw new Error('Must be integer');
|
||||
return n as Integer;
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Narrowing Patterns
|
||||
|
||||
```typescript
|
||||
// Array.isArray with generics
|
||||
function processInput<T>(input: T | T[]): T[] {
|
||||
return Array.isArray(input) ? input : [input];
|
||||
}
|
||||
|
||||
// Object key narrowing
|
||||
function getProperty<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
key: K
|
||||
): T[K] {
|
||||
return obj[key];
|
||||
}
|
||||
|
||||
// Mapped type narrowing
|
||||
type Nullable<T> = { [K in keyof T]: T[K] | null };
|
||||
|
||||
function isComplete<T extends object>(
|
||||
obj: Nullable<T>
|
||||
): obj is T {
|
||||
return Object.values(obj).every((v) => v !== null);
|
||||
}
|
||||
|
||||
// Custom narrowing with type maps
|
||||
type TypeMap = {
|
||||
string: string;
|
||||
number: number;
|
||||
boolean: boolean;
|
||||
};
|
||||
|
||||
function is<K extends keyof TypeMap>(
|
||||
type: K,
|
||||
value: unknown
|
||||
): value is TypeMap[K] {
|
||||
return typeof value === type;
|
||||
}
|
||||
|
||||
if (is('string', someValue)) {
|
||||
someValue.toUpperCase(); // someValue is string
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Use Case |
|
||||
|---------|----------|
|
||||
| `value is Type` | Type predicate function |
|
||||
| `asserts condition` | Assertion function |
|
||||
| `asserts value is Type` | Type assertion function |
|
||||
| Discriminated union | Tagged union with literal type |
|
||||
| `typeof` guard | Primitive type checking |
|
||||
| `instanceof` guard | Class instance checking |
|
||||
| `in` operator | Property existence check |
|
||||
| `assertNever` | Exhaustive switch checking |
|
||||
| Branded types | Nominal typing simulation |
|
||||
| `NonNullable<T>` | Remove null/undefined |
|
||||
329
.claude/skills/typescript-pro/references/utility-types.md
Normal file
329
.claude/skills/typescript-pro/references/utility-types.md
Normal file
@@ -0,0 +1,329 @@
|
||||
# Utility Types
|
||||
|
||||
## Built-in Utility Types
|
||||
|
||||
```typescript
|
||||
// Partial - All properties optional
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
type PartialUser = Partial<User>;
|
||||
// { id?: number; name?: string; email?: string; }
|
||||
|
||||
function updateUser(id: number, updates: Partial<User>) {
|
||||
// Only pass fields to update
|
||||
}
|
||||
|
||||
// Required - All properties required
|
||||
type RequiredUser = Required<PartialUser>;
|
||||
// { id: number; name: string; email: string; }
|
||||
|
||||
// Readonly - All properties readonly
|
||||
type ReadonlyUser = Readonly<User>;
|
||||
// { readonly id: number; readonly name: string; readonly email: string; }
|
||||
|
||||
// Pick - Select specific properties
|
||||
type UserSummary = Pick<User, 'id' | 'name'>;
|
||||
// { id: number; name: string; }
|
||||
|
||||
// Omit - Exclude specific properties
|
||||
type UserWithoutEmail = Omit<User, 'email'>;
|
||||
// { id: number; name: string; }
|
||||
|
||||
// Record - Create object type with specific keys
|
||||
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
|
||||
// { [key: string]: 'admin' | 'user' | 'guest' }
|
||||
|
||||
type PageInfo = Record<'home' | 'about' | 'contact', { title: string }>;
|
||||
// { home: { title: string }, about: { title: string }, contact: { title: string } }
|
||||
```
|
||||
|
||||
## Type Extraction Utilities
|
||||
|
||||
```typescript
|
||||
// Extract - Extract types from union
|
||||
type AllTypes = 'a' | 'b' | 'c' | 1 | 2 | 3;
|
||||
type StringTypes = Extract<AllTypes, string>; // 'a' | 'b' | 'c'
|
||||
type NumberTypes = Extract<AllTypes, number>; // 1 | 2 | 3
|
||||
|
||||
// Exclude - Remove types from union
|
||||
type WithoutNumbers = Exclude<AllTypes, number>; // 'a' | 'b' | 'c'
|
||||
|
||||
// NonNullable - Remove null and undefined
|
||||
type MaybeString = string | null | undefined;
|
||||
type DefiniteString = NonNullable<MaybeString>; // string
|
||||
|
||||
// ReturnType - Extract function return type
|
||||
function getUser() {
|
||||
return { id: 1, name: 'John' };
|
||||
}
|
||||
|
||||
type User = ReturnType<typeof getUser>; // { id: number; name: string }
|
||||
|
||||
// Parameters - Extract function parameter types
|
||||
function createUser(name: string, age: number) {
|
||||
return { name, age };
|
||||
}
|
||||
|
||||
type CreateUserParams = Parameters<typeof createUser>; // [string, number]
|
||||
|
||||
// ConstructorParameters - Extract constructor parameters
|
||||
class Point {
|
||||
constructor(public x: number, public y: number) {}
|
||||
}
|
||||
|
||||
type PointParams = ConstructorParameters<typeof Point>; // [number, number]
|
||||
|
||||
// InstanceType - Extract instance type from constructor
|
||||
type PointInstance = InstanceType<typeof Point>; // Point
|
||||
```
|
||||
|
||||
## Custom Utility Types
|
||||
|
||||
```typescript
|
||||
// DeepPartial - Recursive partial
|
||||
type DeepPartial<T> = T extends object ? {
|
||||
[K in keyof T]?: DeepPartial<T[K]>;
|
||||
} : T;
|
||||
|
||||
interface Config {
|
||||
database: {
|
||||
host: string;
|
||||
port: number;
|
||||
credentials: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
type PartialConfig = DeepPartial<Config>;
|
||||
// All nested properties are optional
|
||||
|
||||
// DeepReadonly - Recursive readonly
|
||||
type DeepReadonly<T> = T extends object ? {
|
||||
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
||||
} : T;
|
||||
|
||||
// Mutable - Remove readonly
|
||||
type Mutable<T> = {
|
||||
-readonly [K in keyof T]: T[K];
|
||||
};
|
||||
|
||||
type MutableUser = Mutable<ReadonlyUser>;
|
||||
|
||||
// PickByType - Pick properties by value type
|
||||
type PickByType<T, U> = {
|
||||
[K in keyof T as T[K] extends U ? K : never]: T[K];
|
||||
};
|
||||
|
||||
interface Mixed {
|
||||
id: number;
|
||||
name: string;
|
||||
age: number;
|
||||
email: string;
|
||||
}
|
||||
|
||||
type StringProps = PickByType<Mixed, string>; // { name: string; email: string }
|
||||
type NumberProps = PickByType<Mixed, number>; // { id: number; age: number }
|
||||
|
||||
// OmitByType - Omit properties by value type
|
||||
type OmitByType<T, U> = {
|
||||
[K in keyof T as T[K] extends U ? never : K]: T[K];
|
||||
};
|
||||
|
||||
type NoStrings = OmitByType<Mixed, string>; // { id: number; age: number }
|
||||
```
|
||||
|
||||
## Function Utilities
|
||||
|
||||
```typescript
|
||||
// Promisify - Convert sync to async
|
||||
type Promisify<T extends (...args: any[]) => any> = (
|
||||
...args: Parameters<T>
|
||||
) => Promise<ReturnType<T>>;
|
||||
|
||||
function syncFunction(x: number): string {
|
||||
return x.toString();
|
||||
}
|
||||
|
||||
type AsyncVersion = Promisify<typeof syncFunction>;
|
||||
// (x: number) => Promise<string>
|
||||
|
||||
// Awaited - Unwrap promise type
|
||||
type AwaitedString = Awaited<Promise<string>>; // string
|
||||
type DeepAwaited = Awaited<Promise<Promise<number>>>; // number
|
||||
|
||||
// ThisParameterType - Extract this parameter
|
||||
function greet(this: User, message: string) {
|
||||
return `${this.name}: ${message}`;
|
||||
}
|
||||
|
||||
type ThisType = ThisParameterType<typeof greet>; // User
|
||||
|
||||
// OmitThisParameter - Remove this parameter
|
||||
type GreetFunction = OmitThisParameter<typeof greet>;
|
||||
// (message: string) => string
|
||||
```
|
||||
|
||||
## Advanced Custom Utilities
|
||||
|
||||
```typescript
|
||||
// Nullable - Add null and undefined
|
||||
type Nullable<T> = T | null | undefined;
|
||||
|
||||
// ValueOf - Get union of all property values
|
||||
type ValueOf<T> = T[keyof T];
|
||||
|
||||
interface Codes {
|
||||
success: 200;
|
||||
notFound: 404;
|
||||
error: 500;
|
||||
}
|
||||
|
||||
type StatusCode = ValueOf<Codes>; // 200 | 404 | 500
|
||||
|
||||
// RequireAtLeastOne - Require at least one property
|
||||
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
|
||||
Pick<T, Exclude<keyof T, Keys>> &
|
||||
{
|
||||
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
|
||||
}[Keys];
|
||||
|
||||
interface Options {
|
||||
id?: number;
|
||||
name?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
type AtLeastOne = RequireAtLeastOne<Options>;
|
||||
// Must have at least one of id, name, or email
|
||||
|
||||
// RequireOnlyOne - Require exactly one property
|
||||
type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
|
||||
Pick<T, Exclude<keyof T, Keys>> &
|
||||
{
|
||||
[K in Keys]-?:
|
||||
Required<Pick<T, K>> &
|
||||
Partial<Record<Exclude<Keys, K>, undefined>>;
|
||||
}[Keys];
|
||||
|
||||
type OnlyOne = RequireOnlyOne<Options>;
|
||||
// Must have exactly one of id, name, or email
|
||||
|
||||
// Merge - Deep merge two types
|
||||
type Merge<T, U> = Omit<T, keyof U> & U;
|
||||
|
||||
interface Base {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Extension {
|
||||
name: string; // Override
|
||||
email: string; // Add
|
||||
}
|
||||
|
||||
type Combined = Merge<Base, Extension>;
|
||||
// { id: number; name: string; email: string }
|
||||
|
||||
// ConditionalKeys - Get keys matching condition
|
||||
type ConditionalKeys<T, Condition> = {
|
||||
[K in keyof T]: T[K] extends Condition ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
type FunctionKeys = ConditionalKeys<typeof Math, Function>;
|
||||
// 'abs' | 'acos' | 'sin' | ...
|
||||
```
|
||||
|
||||
## Tuple Utilities
|
||||
|
||||
```typescript
|
||||
// First - Get first element type
|
||||
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
|
||||
|
||||
type FirstType = First<[string, number, boolean]>; // string
|
||||
|
||||
// Last - Get last element type
|
||||
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
|
||||
|
||||
type LastType = Last<[string, number, boolean]>; // boolean
|
||||
|
||||
// Tail - Remove first element
|
||||
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;
|
||||
|
||||
type TailTypes = Tail<[string, number, boolean]>; // [number, boolean]
|
||||
|
||||
// Prepend - Add element to beginning
|
||||
type Prepend<T extends any[], U> = [U, ...T];
|
||||
|
||||
type WithString = Prepend<[number, boolean], string>; // [string, number, boolean]
|
||||
|
||||
// Reverse - Reverse tuple
|
||||
type Reverse<T extends any[]> =
|
||||
T extends [infer First, ...infer Rest]
|
||||
? [...Reverse<Rest>, First]
|
||||
: [];
|
||||
|
||||
type Reversed = Reverse<[1, 2, 3]>; // [3, 2, 1]
|
||||
```
|
||||
|
||||
## String Utilities
|
||||
|
||||
```typescript
|
||||
// Split - Split string into tuple
|
||||
type Split<S extends string, D extends string> =
|
||||
S extends `${infer T}${D}${infer U}`
|
||||
? [T, ...Split<U, D>]
|
||||
: [S];
|
||||
|
||||
type Parts = Split<'a-b-c', '-'>; // ['a', 'b', 'c']
|
||||
|
||||
// Join - Join tuple into string
|
||||
type Join<T extends string[], D extends string> =
|
||||
T extends [infer F extends string, ...infer R extends string[]]
|
||||
? R extends []
|
||||
? F
|
||||
: `${F}${D}${Join<R, D>}`
|
||||
: '';
|
||||
|
||||
type Joined = Join<['a', 'b', 'c'], '-'>; // 'a-b-c'
|
||||
|
||||
// Replace - Replace substring
|
||||
type Replace<
|
||||
S extends string,
|
||||
From extends string,
|
||||
To extends string
|
||||
> = S extends `${infer L}${From}${infer R}`
|
||||
? `${L}${To}${R}`
|
||||
: S;
|
||||
|
||||
type Replaced = Replace<'hello world', 'world', 'TypeScript'>;
|
||||
// 'hello TypeScript'
|
||||
|
||||
// TrimLeft - Remove leading whitespace
|
||||
type TrimLeft<S extends string> =
|
||||
S extends ` ${infer Rest}` ? TrimLeft<Rest> : S;
|
||||
|
||||
type Trimmed = TrimLeft<' hello'>; // 'hello'
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Utility | Purpose |
|
||||
|---------|---------|
|
||||
| `Partial<T>` | Make all properties optional |
|
||||
| `Required<T>` | Make all properties required |
|
||||
| `Readonly<T>` | Make all properties readonly |
|
||||
| `Pick<T, K>` | Select subset of properties |
|
||||
| `Omit<T, K>` | Remove subset of properties |
|
||||
| `Record<K, T>` | Create object type with keys K |
|
||||
| `Extract<T, U>` | Extract types assignable to U |
|
||||
| `Exclude<T, U>` | Remove types assignable to U |
|
||||
| `NonNullable<T>` | Remove null and undefined |
|
||||
| `ReturnType<T>` | Extract function return type |
|
||||
| `Parameters<T>` | Extract function parameters |
|
||||
| `Awaited<T>` | Unwrap Promise type |
|
||||
Reference in New Issue
Block a user