mirror of
https://github.com/duthaho/claudekit.git
synced 2026-09-03 00:30:52 +03:00
feat: adding new skills, including testing patterns and methodologies, along with bundled resources for better usability.
This commit is contained in:
@@ -1,101 +1,724 @@
|
||||
---
|
||||
name: javascript
|
||||
description: >
|
||||
Trigger this skill whenever working with JavaScript files (.js, .mjs, .cjs), writing Node.js applications without TypeScript, or using ES6+ patterns like destructuring, async/await, optional chaining, and modules. Activate for browser scripting, vanilla JS projects, or when the user asks about JavaScript-specific idioms, ESLint configuration, or modern syntax. Also use when dealing with package.json scripts, CommonJS vs ESM, or JavaScript class patterns.
|
||||
---
|
||||
|
||||
# JavaScript
|
||||
|
||||
## Description
|
||||
|
||||
Modern JavaScript (ES6+) patterns and best practices for Node.js and browser environments.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Working with JavaScript files (.js, .mjs)
|
||||
- Browser scripting
|
||||
- Node.js applications without TypeScript
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- TypeScript projects -- use the `languages/typescript` skill instead, which covers typed JavaScript patterns
|
||||
- Python-only projects with no JavaScript components
|
||||
|
||||
---
|
||||
|
||||
## Core Patterns
|
||||
|
||||
### Modern Syntax
|
||||
### 1. Modern Syntax
|
||||
|
||||
#### Destructuring (Nested, Defaults, Rest)
|
||||
|
||||
```javascript
|
||||
// Destructuring
|
||||
const { name, email } = user;
|
||||
const [first, ...rest] = items;
|
||||
// Object destructuring with defaults and rename
|
||||
const { name, email, role = "user", address: { city } = {} } = user;
|
||||
|
||||
// Spread operator
|
||||
const merged = { ...defaults, ...options };
|
||||
const combined = [...array1, ...array2];
|
||||
// Nested destructuring
|
||||
const {
|
||||
data: {
|
||||
attributes: { title, body },
|
||||
},
|
||||
} = apiResponse;
|
||||
|
||||
// Template literals
|
||||
const message = `Hello, ${name}!`;
|
||||
// Array destructuring with rest
|
||||
const [first, second, ...remaining] = items;
|
||||
|
||||
// Optional chaining and nullish coalescing
|
||||
const city = user?.address?.city ?? 'Unknown';
|
||||
// Swap variables
|
||||
let a = 1, b = 2;
|
||||
[a, b] = [b, a];
|
||||
|
||||
// Function parameter destructuring
|
||||
function createUser({ name, email, role = "user" }) {
|
||||
return { name, email, role, createdAt: new Date() };
|
||||
}
|
||||
```
|
||||
|
||||
### Async Patterns
|
||||
#### Optional Chaining (?.)
|
||||
|
||||
```javascript
|
||||
// Async/await
|
||||
async function fetchData(url) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Fetch failed');
|
||||
return response.json();
|
||||
// Property access
|
||||
const city = user?.address?.city;
|
||||
|
||||
// Method call
|
||||
const uppercased = value?.toString?.();
|
||||
|
||||
// Array element
|
||||
const firstItem = data?.items?.[0];
|
||||
|
||||
// Combine with nullish coalescing for defaults
|
||||
const displayName = user?.profile?.displayName ?? user?.name ?? "Anonymous";
|
||||
```
|
||||
|
||||
#### Nullish Coalescing (??)
|
||||
|
||||
```javascript
|
||||
// Only falls through on null/undefined (not 0, "", false)
|
||||
const port = config.port ?? 3000;
|
||||
const name = input ?? "default";
|
||||
|
||||
// Contrast with || which falls through on all falsy values
|
||||
const count = data.count ?? 0; // preserves 0
|
||||
const count2 = data.count || 0; // replaces 0 with 0 (same here, but misleading)
|
||||
const label = data.label ?? ""; // preserves ""
|
||||
const label2 = data.label || "fallback"; // replaces "" with "fallback"
|
||||
```
|
||||
|
||||
#### Logical Assignment (&&=, ||=, ??=)
|
||||
|
||||
```javascript
|
||||
// ??= assigns only if null/undefined
|
||||
user.name ??= "Anonymous";
|
||||
|
||||
// ||= assigns if falsy
|
||||
config.retries ||= 3;
|
||||
|
||||
// &&= assigns only if truthy
|
||||
user.session &&= refreshSession(user.session);
|
||||
|
||||
// Practical: initialize nested objects
|
||||
const cache = {};
|
||||
(cache.users ??= []).push(newUser);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Async Patterns
|
||||
|
||||
#### Promises
|
||||
|
||||
```javascript
|
||||
function fetchJson(url) {
|
||||
return fetch(url)
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
|
||||
// Promise.all for parallel
|
||||
const results = await Promise.all([
|
||||
fetchData(url1),
|
||||
fetchData(url2),
|
||||
// Chaining
|
||||
fetchJson("/api/user")
|
||||
.then((user) => fetchJson(`/api/posts?userId=${user.id}`))
|
||||
.then((posts) => console.log(posts))
|
||||
.catch((error) => console.error("Failed:", error.message));
|
||||
```
|
||||
|
||||
#### async/await
|
||||
|
||||
```javascript
|
||||
async function loadUserDashboard(userId) {
|
||||
const user = await fetchJson(`/api/users/${userId}`);
|
||||
const posts = await fetchJson(`/api/users/${userId}/posts`);
|
||||
return { user, posts };
|
||||
}
|
||||
```
|
||||
|
||||
#### Promise.all / allSettled / race / any
|
||||
|
||||
```javascript
|
||||
// Promise.all -- fail fast on first rejection
|
||||
const [users, posts, comments] = await Promise.all([
|
||||
fetchJson("/api/users"),
|
||||
fetchJson("/api/posts"),
|
||||
fetchJson("/api/comments"),
|
||||
]);
|
||||
|
||||
// Error handling
|
||||
try {
|
||||
const data = await fetchData(url);
|
||||
} catch (error) {
|
||||
console.error('Failed:', error.message);
|
||||
// Promise.allSettled -- wait for all, get status of each
|
||||
const results = await Promise.allSettled([
|
||||
fetchJson("/api/fast"),
|
||||
fetchJson("/api/slow"),
|
||||
fetchJson("/api/flaky"),
|
||||
]);
|
||||
const successes = results
|
||||
.filter((r) => r.status === "fulfilled")
|
||||
.map((r) => r.value);
|
||||
const failures = results
|
||||
.filter((r) => r.status === "rejected")
|
||||
.map((r) => r.reason);
|
||||
|
||||
// Promise.race -- first to settle wins
|
||||
const result = await Promise.race([
|
||||
fetchJson("/api/primary"),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Timeout")), 5000)
|
||||
),
|
||||
]);
|
||||
|
||||
// Promise.any -- first to fulfill wins (ignores rejections)
|
||||
const fastest = await Promise.any([
|
||||
fetchJson("/api/mirror1"),
|
||||
fetchJson("/api/mirror2"),
|
||||
fetchJson("/api/mirror3"),
|
||||
]);
|
||||
```
|
||||
|
||||
#### AbortController
|
||||
|
||||
```javascript
|
||||
async function fetchWithTimeout(url, timeoutMs = 5000) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal });
|
||||
return await response.json();
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
// Cancellable request pattern
|
||||
function createRequest(url) {
|
||||
const controller = new AbortController();
|
||||
return {
|
||||
promise: fetch(url, { signal: controller.signal }),
|
||||
cancel: () => controller.abort(),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Array Methods
|
||||
#### Async Iterators (for await...of)
|
||||
|
||||
```javascript
|
||||
// Map, filter, reduce
|
||||
const names = users.map(u => u.name);
|
||||
const active = users.filter(u => u.active);
|
||||
const total = items.reduce((sum, i) => sum + i.price, 0);
|
||||
async function* paginateApi(baseUrl) {
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const data = await fetchJson(`${baseUrl}?page=${page}`);
|
||||
if (data.items.length === 0) break;
|
||||
yield* data.items;
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
// Find and includes
|
||||
const user = users.find(u => u.id === id);
|
||||
const hasAdmin = users.some(u => u.role === 'admin');
|
||||
// Consume the async iterator
|
||||
for await (const item of paginateApi("/api/records")) {
|
||||
processItem(item);
|
||||
}
|
||||
```
|
||||
|
||||
### Classes
|
||||
---
|
||||
|
||||
### 3. Closures & Scope
|
||||
|
||||
#### Closure Patterns
|
||||
|
||||
```javascript
|
||||
class UserService {
|
||||
#db; // Private field
|
||||
// Counter with private state
|
||||
function createCounter(initial = 0) {
|
||||
let count = initial;
|
||||
return {
|
||||
increment: () => ++count,
|
||||
decrement: () => --count,
|
||||
getCount: () => count,
|
||||
reset: () => { count = initial; },
|
||||
};
|
||||
}
|
||||
|
||||
constructor(database) {
|
||||
this.#db = database;
|
||||
const counter = createCounter(10);
|
||||
counter.increment(); // 11
|
||||
counter.getCount(); // 11
|
||||
```
|
||||
|
||||
#### Module Pattern (Private State via Closures)
|
||||
|
||||
```javascript
|
||||
const rateLimiter = (() => {
|
||||
const requests = new Map();
|
||||
|
||||
function isAllowed(clientId, maxPerMinute = 60) {
|
||||
const now = Date.now();
|
||||
const windowStart = now - 60_000;
|
||||
const clientRequests = (requests.get(clientId) ?? []).filter(
|
||||
(t) => t > windowStart
|
||||
);
|
||||
if (clientRequests.length >= maxPerMinute) return false;
|
||||
clientRequests.push(now);
|
||||
requests.set(clientId, clientRequests);
|
||||
return true;
|
||||
}
|
||||
|
||||
async findById(id) {
|
||||
return this.#db.users.find(u => u.id === id);
|
||||
function reset(clientId) {
|
||||
requests.delete(clientId);
|
||||
}
|
||||
|
||||
return { isAllowed, reset };
|
||||
})();
|
||||
```
|
||||
|
||||
#### WeakRef and FinalizationRegistry
|
||||
|
||||
```javascript
|
||||
// Cache that does not prevent garbage collection
|
||||
const cache = new Map();
|
||||
|
||||
function getCached(key, factory) {
|
||||
const ref = cache.get(key);
|
||||
const cached = ref?.deref();
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const value = factory();
|
||||
cache.set(key, new WeakRef(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
// Cleanup when objects are garbage collected
|
||||
const registry = new FinalizationRegistry((key) => {
|
||||
cache.delete(key);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Iteration Protocols
|
||||
|
||||
#### Custom Iterator
|
||||
|
||||
```javascript
|
||||
class Range {
|
||||
constructor(start, end, step = 1) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
let current = this.start;
|
||||
const { end, step } = this;
|
||||
return {
|
||||
next() {
|
||||
if (current < end) {
|
||||
const value = current;
|
||||
current += step;
|
||||
return { value, done: false };
|
||||
}
|
||||
return { done: true };
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (const n of new Range(0, 10, 2)) {
|
||||
console.log(n); // 0, 2, 4, 6, 8
|
||||
}
|
||||
```
|
||||
|
||||
#### Generators
|
||||
|
||||
```javascript
|
||||
function* fibonacci() {
|
||||
let a = 0, b = 1;
|
||||
while (true) {
|
||||
yield a;
|
||||
[a, b] = [b, a + b];
|
||||
}
|
||||
}
|
||||
|
||||
// Take first N values
|
||||
function take(iterable, count) {
|
||||
const result = [];
|
||||
for (const value of iterable) {
|
||||
result.push(value);
|
||||
if (result.length >= count) break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
take(fibonacci(), 8); // [0, 1, 1, 2, 3, 5, 8, 13]
|
||||
```
|
||||
|
||||
#### Lazy Evaluation with Generators
|
||||
|
||||
```javascript
|
||||
function* map(iterable, fn) {
|
||||
for (const item of iterable) {
|
||||
yield fn(item);
|
||||
}
|
||||
}
|
||||
|
||||
function* filter(iterable, predicate) {
|
||||
for (const item of iterable) {
|
||||
if (predicate(item)) yield item;
|
||||
}
|
||||
}
|
||||
|
||||
// Compose lazily -- no intermediate arrays
|
||||
const data = filter(
|
||||
map(readLargeFile(), (line) => line.trim()),
|
||||
(line) => line.length > 0
|
||||
);
|
||||
```
|
||||
|
||||
#### Async Generators
|
||||
|
||||
```javascript
|
||||
async function* readChunks(reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
|
||||
// Stream processing
|
||||
const response = await fetch("/api/large-data");
|
||||
for await (const chunk of readChunks(response.body.getReader())) {
|
||||
processChunk(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Proxy & Reflect
|
||||
|
||||
#### Validation Proxy
|
||||
|
||||
```javascript
|
||||
function createValidated(target, validators) {
|
||||
return new Proxy(target, {
|
||||
set(obj, prop, value) {
|
||||
const validate = validators[prop];
|
||||
if (validate && !validate(value)) {
|
||||
throw new TypeError(`Invalid value for ${String(prop)}: ${value}`);
|
||||
}
|
||||
return Reflect.set(obj, prop, value);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const user = createValidated(
|
||||
{ name: "", age: 0 },
|
||||
{
|
||||
name: (v) => typeof v === "string" && v.length > 0,
|
||||
age: (v) => typeof v === "number" && v >= 0 && v <= 150,
|
||||
}
|
||||
);
|
||||
|
||||
user.name = "Alice"; // works
|
||||
user.age = -1; // throws TypeError
|
||||
```
|
||||
|
||||
#### Observable Object
|
||||
|
||||
```javascript
|
||||
function createObservable(target, onChange) {
|
||||
return new Proxy(target, {
|
||||
set(obj, prop, value) {
|
||||
const oldValue = obj[prop];
|
||||
const result = Reflect.set(obj, prop, value);
|
||||
if (oldValue !== value) {
|
||||
onChange(prop, value, oldValue);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
deleteProperty(obj, prop) {
|
||||
const oldValue = obj[prop];
|
||||
const result = Reflect.deleteProperty(obj, prop);
|
||||
onChange(prop, undefined, oldValue);
|
||||
return result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const state = createObservable({}, (prop, newVal, oldVal) => {
|
||||
console.log(`${prop}: ${oldVal} -> ${newVal}`);
|
||||
});
|
||||
```
|
||||
|
||||
#### Property Access Logging
|
||||
|
||||
```javascript
|
||||
function withLogging(target, label = "access") {
|
||||
return new Proxy(target, {
|
||||
get(obj, prop) {
|
||||
console.log(`[${label}] get .${String(prop)}`);
|
||||
return Reflect.get(obj, prop);
|
||||
},
|
||||
has(obj, prop) {
|
||||
console.log(`[${label}] has .${String(prop)}`);
|
||||
return Reflect.has(obj, prop);
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Module System
|
||||
|
||||
#### ESM (import/export)
|
||||
|
||||
```javascript
|
||||
// Named exports
|
||||
export function formatDate(date) { ... }
|
||||
export const MAX_RETRIES = 3;
|
||||
|
||||
// Default export
|
||||
export default class ApiClient { ... }
|
||||
|
||||
// Re-exports
|
||||
export { formatDate } from "./utils.js";
|
||||
export { default as ApiClient } from "./api-client.js";
|
||||
```
|
||||
|
||||
#### Dynamic import()
|
||||
|
||||
```javascript
|
||||
// Lazy load modules
|
||||
async function loadChart(type) {
|
||||
const module = await import(`./charts/${type}.js`);
|
||||
return new module.default();
|
||||
}
|
||||
|
||||
// Conditional loading
|
||||
const { marked } = await import("marked");
|
||||
|
||||
// With error handling
|
||||
async function tryLoadPlugin(name) {
|
||||
try {
|
||||
return await import(`./plugins/${name}.js`);
|
||||
} catch {
|
||||
console.warn(`Plugin ${name} not available`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### import.meta
|
||||
|
||||
```javascript
|
||||
// Current module URL
|
||||
console.log(import.meta.url);
|
||||
|
||||
// Resolve relative paths (Node.js)
|
||||
const configPath = new URL("./config.json", import.meta.url);
|
||||
|
||||
// Check if file is the entry point (Node.js)
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
|
||||
// Vite environment variables
|
||||
const apiUrl = import.meta.env.VITE_API_URL;
|
||||
```
|
||||
|
||||
#### Top-level await
|
||||
|
||||
```javascript
|
||||
// config.js -- top-level await in ESM modules
|
||||
const response = await fetch("/api/config");
|
||||
export const config = await response.json();
|
||||
|
||||
// db.js
|
||||
import { createPool } from "./db-pool.js";
|
||||
export const pool = await createPool(process.env.DATABASE_URL);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Performance
|
||||
|
||||
#### structuredClone
|
||||
|
||||
```javascript
|
||||
// Deep clone without library (replaces JSON.parse(JSON.stringify(...)))
|
||||
const original = { nested: { array: [1, 2, 3], date: new Date() } };
|
||||
const clone = structuredClone(original);
|
||||
|
||||
// Handles Date, Map, Set, ArrayBuffer, RegExp (but not functions)
|
||||
clone.nested.array.push(4);
|
||||
console.log(original.nested.array.length); // still 3
|
||||
```
|
||||
|
||||
#### requestAnimationFrame
|
||||
|
||||
```javascript
|
||||
// Smooth animation loop
|
||||
function animate(timestamp) {
|
||||
updatePosition(timestamp);
|
||||
render();
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
// Throttle DOM updates to frame rate
|
||||
let rafId = null;
|
||||
function scheduleUpdate(data) {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
rafId = requestAnimationFrame(() => {
|
||||
applyDOMUpdate(data);
|
||||
rafId = null;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### requestIdleCallback
|
||||
|
||||
```javascript
|
||||
// Run low-priority work when the browser is idle
|
||||
function processQueue(queue) {
|
||||
requestIdleCallback((deadline) => {
|
||||
while (deadline.timeRemaining() > 0 && queue.length > 0) {
|
||||
const task = queue.shift();
|
||||
task();
|
||||
}
|
||||
if (queue.length > 0) {
|
||||
processQueue(queue); // schedule remaining
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Web Workers Basics
|
||||
|
||||
```javascript
|
||||
// main.js
|
||||
const worker = new Worker(new URL("./worker.js", import.meta.url), {
|
||||
type: "module",
|
||||
});
|
||||
|
||||
worker.postMessage({ data: largeDataSet, operation: "sort" });
|
||||
worker.onmessage = (event) => {
|
||||
const sorted = event.data;
|
||||
renderResults(sorted);
|
||||
};
|
||||
|
||||
// worker.js
|
||||
self.onmessage = (event) => {
|
||||
const { data, operation } = event.data;
|
||||
if (operation === "sort") {
|
||||
self.postMessage(data.sort((a, b) => a - b));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### performance.mark / measure
|
||||
|
||||
```javascript
|
||||
// Measure operation duration
|
||||
performance.mark("fetch-start");
|
||||
const data = await fetchJson("/api/data");
|
||||
performance.mark("fetch-end");
|
||||
|
||||
performance.measure("fetch-duration", "fetch-start", "fetch-end");
|
||||
const measurement = performance.getEntriesByName("fetch-duration")[0];
|
||||
console.log(`Fetch took ${measurement.duration.toFixed(2)}ms`);
|
||||
|
||||
// Clean up
|
||||
performance.clearMarks();
|
||||
performance.clearMeasures();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Use `const` by default, `let` when needed
|
||||
2. Avoid `var` - use block-scoped declarations
|
||||
3. Use arrow functions for callbacks
|
||||
4. Handle all promise rejections
|
||||
5. Use ESLint for consistent style
|
||||
1. **Use `const` by default, `let` only when reassignment is needed** -- never use `var`. Block scoping prevents entire categories of bugs from hoisting and accidental mutation.
|
||||
|
||||
2. **Handle all promise rejections** -- unhandled rejections crash Node.js processes. Always use try/catch with await, or attach `.catch()` to promise chains. Add a global handler as a safety net.
|
||||
```javascript
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
console.error("Unhandled rejection:", reason);
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
3. **Use arrow functions for callbacks, regular functions for methods** -- arrow functions capture `this` from the enclosing scope, which is correct for callbacks but breaks object methods that need their own `this`.
|
||||
|
||||
4. **Prefer `for...of` over `for...in` for iteration** -- `for...in` iterates over all enumerable properties including inherited ones. Use `for...of` for arrays and iterables, `Object.entries()` for objects.
|
||||
|
||||
5. **Use ESLint and Prettier** -- enforce consistent style automatically. Configure in the project root and run on pre-commit hooks.
|
||||
|
||||
6. **Avoid mutating function arguments** -- create new objects and arrays with spread syntax instead of modifying inputs in place. This prevents action-at-a-distance bugs.
|
||||
|
||||
7. **Use `structuredClone` for deep copies** -- replaces the `JSON.parse(JSON.stringify(x))` hack. Handles Dates, Maps, Sets, and circular references correctly.
|
||||
|
||||
8. **Use private class fields (`#field`)** -- the `#` prefix creates truly private fields that cannot be accessed outside the class, unlike the `_` convention which is only a hint.
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Implicit type coercion**: Use `===` instead of `==`
|
||||
- **Callback hell**: Use async/await
|
||||
- **Mutating objects**: Create new objects with spread
|
||||
- **Not handling errors**: Always catch promise rejections
|
||||
1. **Implicit type coercion** -- always use `===` and `!==`. The `==` operator performs type coercion with surprising rules (`"" == false`, `0 == null` is false but `0 == undefined` is also false, yet `null == undefined` is true).
|
||||
|
||||
2. **Forgetting `await`** -- a missing `await` silently returns a Promise object instead of the resolved value, causing hard-to-debug issues.
|
||||
```javascript
|
||||
// BAD -- data is a Promise, not the response
|
||||
const data = fetchJson("/api/data");
|
||||
|
||||
// GOOD
|
||||
const data = await fetchJson("/api/data");
|
||||
```
|
||||
|
||||
3. **`this` binding in callbacks** -- regular functions in callbacks lose their `this` context. Use arrow functions or `.bind()`.
|
||||
```javascript
|
||||
// BAD
|
||||
class Timer {
|
||||
start() { setTimeout(function() { this.tick(); }, 1000); }
|
||||
}
|
||||
|
||||
// GOOD
|
||||
class Timer {
|
||||
start() { setTimeout(() => this.tick(), 1000); }
|
||||
}
|
||||
```
|
||||
|
||||
4. **Mutating objects passed by reference** -- objects and arrays are passed by reference. Modifying a parameter modifies the original.
|
||||
```javascript
|
||||
// BAD
|
||||
function addDefaults(config) {
|
||||
config.retries = config.retries ?? 3; // mutates caller's object
|
||||
return config;
|
||||
}
|
||||
|
||||
// GOOD
|
||||
function addDefaults(config) {
|
||||
return { retries: 3, ...config };
|
||||
}
|
||||
```
|
||||
|
||||
5. **`for...in` on arrays** -- iterates over indices as strings and includes inherited properties. Use `for...of` or array methods.
|
||||
```javascript
|
||||
// BAD
|
||||
for (const i in [10, 20, 30]) {
|
||||
console.log(typeof i); // "string", not "number"
|
||||
}
|
||||
|
||||
// GOOD
|
||||
for (const value of [10, 20, 30]) {
|
||||
console.log(value); // 10, 20, 30
|
||||
}
|
||||
```
|
||||
|
||||
6. **Floating point arithmetic** -- `0.1 + 0.2 !== 0.3` in JavaScript. For financial calculations, work in integer cents or use a decimal library.
|
||||
```javascript
|
||||
// BAD
|
||||
const total = 0.1 + 0.2; // 0.30000000000000004
|
||||
|
||||
// GOOD
|
||||
const totalCents = 10 + 20; // 30
|
||||
const total = totalCents / 100; // 0.3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `languages/typescript` -- TypeScript for typed JavaScript development
|
||||
- `frameworks/react` -- React component patterns
|
||||
- `frameworks/nextjs` -- Next.js full-stack framework
|
||||
- `testing/vitest` -- JavaScript/TypeScript testing with Vitest
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
# Modern JavaScript Patterns Quick Reference
|
||||
|
||||
> ES2020+ patterns. All examples work in current Node.js (18+) and modern browsers.
|
||||
|
||||
## Destructuring Tricks
|
||||
|
||||
```javascript
|
||||
// Nested destructuring with rename and default
|
||||
const { data: { users: members = [] } = {} } = response;
|
||||
|
||||
// Array destructuring: skip elements
|
||||
const [first, , third] = [1, 2, 3];
|
||||
|
||||
// Swap variables
|
||||
[a, b] = [b, a];
|
||||
|
||||
// Rest in both arrays and objects
|
||||
const { id, ...rest } = user;
|
||||
const [head, ...tail] = items;
|
||||
|
||||
// Destructure function parameters
|
||||
function draw({ x = 0, y = 0, color = "black" } = {}) { /* ... */ }
|
||||
|
||||
// Dynamic property destructuring
|
||||
const key = "name";
|
||||
const { [key]: value } = { name: "Alice" }; // value = "Alice"
|
||||
|
||||
// Destructure from iterables
|
||||
const [a, b] = new Map([["a", 1], ["b", 2]]);
|
||||
```
|
||||
|
||||
## Optional Chaining (?.)
|
||||
|
||||
```javascript
|
||||
// Property access
|
||||
const city = user?.address?.city;
|
||||
|
||||
// Method call (only calls if method exists)
|
||||
const result = api?.getData?.();
|
||||
|
||||
// Bracket notation
|
||||
const val = obj?.["dynamic-key"];
|
||||
|
||||
// Array index
|
||||
const first = arr?.[0];
|
||||
|
||||
// Combine with nullish coalescing
|
||||
const name = user?.profile?.name ?? "Anonymous";
|
||||
|
||||
// Short-circuit: stops evaluating after first nullish
|
||||
const len = response?.data?.items?.length; // undefined if any is nullish
|
||||
```
|
||||
|
||||
## Nullish Coalescing (??) vs OR (||)
|
||||
|
||||
```javascript
|
||||
// ?? only triggers on null/undefined (NOT 0, "", false)
|
||||
0 ?? "fallback" // 0
|
||||
"" ?? "fallback" // ""
|
||||
null ?? "fallback" // "fallback"
|
||||
|
||||
// || triggers on any falsy value
|
||||
0 || "fallback" // "fallback"
|
||||
"" || "fallback" // "fallback"
|
||||
null || "fallback" // "fallback"
|
||||
|
||||
// Use ?? for values where 0/empty string are valid
|
||||
const port = config.port ?? 3000;
|
||||
const title = config.title ?? "Untitled";
|
||||
```
|
||||
|
||||
## Logical Assignment Operators
|
||||
|
||||
```javascript
|
||||
// ??= assigns only if current value is null/undefined
|
||||
user.name ??= "Anonymous";
|
||||
// Equivalent: user.name = user.name ?? "Anonymous"
|
||||
|
||||
// ||= assigns if current value is falsy
|
||||
opts.verbose ||= false;
|
||||
// Equivalent: opts.verbose = opts.verbose || false
|
||||
|
||||
// &&= assigns only if current value is truthy
|
||||
user.token &&= encrypt(user.token);
|
||||
// Equivalent: user.token = user.token && encrypt(user.token)
|
||||
```
|
||||
|
||||
## structuredClone (Deep Copy)
|
||||
|
||||
```javascript
|
||||
// Deep clone objects, arrays, Maps, Sets, Dates, RegExp, etc.
|
||||
const original = { date: new Date(), nested: { arr: [1, 2] } };
|
||||
const clone = structuredClone(original);
|
||||
clone.nested.arr.push(3); // original not affected
|
||||
|
||||
// Works with circular references
|
||||
const obj = { self: null };
|
||||
obj.self = obj;
|
||||
const copy = structuredClone(obj); // OK
|
||||
|
||||
// Does NOT clone: functions, DOM nodes, symbols, prototype chain
|
||||
// Throws on: functions, Error objects (in some engines)
|
||||
```
|
||||
|
||||
## Proxy
|
||||
|
||||
```javascript
|
||||
// Validation proxy
|
||||
const validated = new Proxy({}, {
|
||||
set(target, prop, value) {
|
||||
if (prop === "age" && (typeof value !== "number" || value < 0)) {
|
||||
throw new TypeError("Age must be a non-negative number");
|
||||
}
|
||||
target[prop] = value;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Read-only proxy
|
||||
function readonly(target) {
|
||||
return new Proxy(target, {
|
||||
set() { throw new Error("Read-only object"); },
|
||||
deleteProperty() { throw new Error("Read-only object"); }
|
||||
});
|
||||
}
|
||||
|
||||
// Default values proxy
|
||||
function withDefaults(target, defaults) {
|
||||
return new Proxy(target, {
|
||||
get(obj, prop) {
|
||||
return prop in obj ? obj[prop] : defaults[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
const config = withDefaults({}, { theme: "dark", lang: "en" });
|
||||
config.theme; // "dark"
|
||||
|
||||
// Logging / observation proxy
|
||||
function observable(target, onChange) {
|
||||
return new Proxy(target, {
|
||||
set(obj, prop, value) {
|
||||
const old = obj[prop];
|
||||
obj[prop] = value;
|
||||
onChange(prop, old, value);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Generators
|
||||
|
||||
```javascript
|
||||
// Basic generator
|
||||
function* range(start, end, step = 1) {
|
||||
for (let i = start; i < end; i += step) {
|
||||
yield i;
|
||||
}
|
||||
}
|
||||
for (const n of range(0, 10, 2)) { /* 0, 2, 4, 6, 8 */ }
|
||||
|
||||
// Infinite sequence
|
||||
function* ids() {
|
||||
let id = 0;
|
||||
while (true) yield id++;
|
||||
}
|
||||
const gen = ids();
|
||||
gen.next().value; // 0
|
||||
gen.next().value; // 1
|
||||
|
||||
// Delegate to another generator
|
||||
function* concat(...iterables) {
|
||||
for (const it of iterables) {
|
||||
yield* it;
|
||||
}
|
||||
}
|
||||
|
||||
// Two-way communication
|
||||
function* stateMachine() {
|
||||
let input;
|
||||
while (true) {
|
||||
input = yield `received: ${input}`;
|
||||
}
|
||||
}
|
||||
const sm = stateMachine();
|
||||
sm.next(); // { value: "received: undefined" }
|
||||
sm.next("hello"); // { value: "received: hello" }
|
||||
```
|
||||
|
||||
## Async Iterators
|
||||
|
||||
```javascript
|
||||
// for-await-of
|
||||
async function processStream(stream) {
|
||||
for await (const chunk of stream) {
|
||||
console.log(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
// Async generator
|
||||
async function* fetchPages(url) {
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const res = await fetch(`${url}?page=${page}`);
|
||||
const data = await res.json();
|
||||
if (data.items.length === 0) return;
|
||||
yield data.items;
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
for await (const items of fetchPages("/api/users")) {
|
||||
console.log(items);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Other Modern Patterns
|
||||
|
||||
```javascript
|
||||
// Object.groupBy (ES2024)
|
||||
const grouped = Object.groupBy(users, u => u.role);
|
||||
|
||||
// at() - negative indexing
|
||||
[1, 2, 3].at(-1); // 3
|
||||
|
||||
// Object.hasOwn (replaces hasOwnProperty)
|
||||
Object.hasOwn(obj, "key"); // true/false
|
||||
|
||||
// Error cause chaining
|
||||
throw new Error("DB failed", { cause: originalError });
|
||||
|
||||
// AbortSignal.timeout (built-in timeout)
|
||||
fetch(url, { signal: AbortSignal.timeout(5000) });
|
||||
|
||||
// using keyword (explicit resource management, ES2024+)
|
||||
{ using handle = openFile("data.txt"); } // auto-disposed at block exit
|
||||
```
|
||||
|
||||
## Promise Combinators
|
||||
|
||||
| Method | Settles when | Returns |
|
||||
|--------|-------------|---------|
|
||||
| `Promise.all(ps)` | All fulfill or one rejects | Array of values |
|
||||
| `Promise.allSettled(ps)` | All settle | Array of `{status, value/reason}` |
|
||||
| `Promise.race(ps)` | First settles | First value or rejection |
|
||||
| `Promise.any(ps)` | First fulfills | First value (AggregateError if all reject) |
|
||||
@@ -1,9 +1,11 @@
|
||||
---
|
||||
name: python
|
||||
description: >
|
||||
Trigger this skill whenever working with Python files (.py), writing Python scripts or applications, or using Python frameworks like Django, FastAPI, or Flask. Activate for any Python-specific patterns including type hints, async/await with asyncio, dataclasses, Pydantic models, context managers, virtual environments, or PEP 8 style questions. Also use when the user references Python package management, pip, or pyproject.toml.
|
||||
---
|
||||
|
||||
# Python
|
||||
|
||||
## Description
|
||||
|
||||
Python development expertise including type hints, async patterns, virtual environments, and Pythonic idioms.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Working with Python files (.py)
|
||||
@@ -11,59 +13,148 @@ Python development expertise including type hints, async patterns, virtual envir
|
||||
- Using Python frameworks (Django, FastAPI, Flask)
|
||||
- Data processing and automation
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- JavaScript or TypeScript-only projects with no Python components
|
||||
- Non-Python environments where another language skill is more appropriate
|
||||
|
||||
---
|
||||
|
||||
## Core Patterns
|
||||
|
||||
### Type Hints
|
||||
### 1. Type Hints
|
||||
|
||||
Use type hints on all public functions and module-level variables. Python 3.10+ syntax is preferred (use `X | Y` instead of `Union[X, Y]`).
|
||||
|
||||
#### Basic Types
|
||||
|
||||
```python
|
||||
from typing import Optional, List, Dict, Union
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
def process_items(
|
||||
items: List[str],
|
||||
callback: Callable[[str], None],
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
) -> List[str]:
|
||||
"""Process items with optional callback."""
|
||||
return [callback(item) for item in items]
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}"
|
||||
|
||||
def process(count: int, factor: float = 1.0) -> float:
|
||||
return count * factor
|
||||
|
||||
def is_valid(data: bytes | None) -> bool:
|
||||
return data is not None and len(data) > 0
|
||||
```
|
||||
|
||||
### Async/Await
|
||||
#### Optional and Union
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from typing import List
|
||||
# Python 3.10+ syntax (preferred)
|
||||
def find_user(user_id: int) -> User | None:
|
||||
...
|
||||
|
||||
async def fetch_data(url: str) -> dict:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
return await response.json()
|
||||
# Pre-3.10 fallback
|
||||
from typing import Optional, Union
|
||||
|
||||
async def fetch_all(urls: List[str]) -> List[dict]:
|
||||
return await asyncio.gather(*[fetch_data(url) for url in urls])
|
||||
def find_user(user_id: int) -> Optional[User]:
|
||||
...
|
||||
|
||||
def parse_input(value: Union[str, int]) -> str:
|
||||
return str(value)
|
||||
```
|
||||
|
||||
### Context Managers
|
||||
#### Generic Collections
|
||||
|
||||
```python
|
||||
from contextlib import contextmanager
|
||||
# Python 3.9+ built-in generics (preferred)
|
||||
def process_items(items: list[str]) -> dict[str, int]:
|
||||
return {item: len(item) for item in items}
|
||||
|
||||
@contextmanager
|
||||
def managed_resource():
|
||||
resource = acquire_resource()
|
||||
try:
|
||||
yield resource
|
||||
finally:
|
||||
release_resource(resource)
|
||||
def merge_configs(base: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
|
||||
return {**base, **overrides}
|
||||
|
||||
# Usage
|
||||
with managed_resource() as r:
|
||||
r.do_something()
|
||||
# Nested generics
|
||||
def group_by_key(pairs: list[tuple[str, int]]) -> dict[str, list[int]]:
|
||||
result: dict[str, list[int]] = {}
|
||||
for key, value in pairs:
|
||||
result.setdefault(key, []).append(value)
|
||||
return result
|
||||
```
|
||||
|
||||
### Dataclasses
|
||||
#### Protocol for Structural Subtyping
|
||||
|
||||
```python
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
@runtime_checkable
|
||||
class Renderable(Protocol):
|
||||
def render(self) -> str: ...
|
||||
|
||||
class HtmlWidget:
|
||||
def render(self) -> str:
|
||||
return "<div>widget</div>"
|
||||
|
||||
def display(item: Renderable) -> None:
|
||||
print(item.render())
|
||||
|
||||
# HtmlWidget satisfies Renderable without inheriting from it
|
||||
display(HtmlWidget()) # works
|
||||
```
|
||||
|
||||
#### TypeVar for Generic Functions
|
||||
|
||||
```python
|
||||
from typing import TypeVar, Sequence
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def first(items: Sequence[T]) -> T:
|
||||
return items[0]
|
||||
|
||||
# Bounded TypeVar
|
||||
Numeric = TypeVar("Numeric", int, float)
|
||||
|
||||
def clamp(value: Numeric, low: Numeric, high: Numeric) -> Numeric:
|
||||
return max(low, min(high, value))
|
||||
```
|
||||
|
||||
#### @overload for Multiple Signatures
|
||||
|
||||
```python
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def parse(raw: str) -> dict[str, Any]: ...
|
||||
@overload
|
||||
def parse(raw: bytes) -> dict[str, Any]: ...
|
||||
@overload
|
||||
def parse(raw: str, as_list: bool) -> list[Any]: ...
|
||||
|
||||
def parse(raw: str | bytes, as_list: bool = False) -> dict[str, Any] | list[Any]:
|
||||
data = raw if isinstance(raw, str) else raw.decode()
|
||||
parsed = json.loads(data)
|
||||
return list(parsed) if as_list else parsed
|
||||
```
|
||||
|
||||
#### TypeAlias and TypeGuard
|
||||
|
||||
```python
|
||||
from typing import TypeAlias, TypeGuard
|
||||
|
||||
# TypeAlias for complex types
|
||||
JsonValue: TypeAlias = str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
|
||||
Headers: TypeAlias = dict[str, str]
|
||||
|
||||
# TypeGuard for narrowing
|
||||
def is_string_list(val: list[Any]) -> TypeGuard[list[str]]:
|
||||
return all(isinstance(item, str) for item in val)
|
||||
|
||||
def process(items: list[Any]) -> None:
|
||||
if is_string_list(items):
|
||||
# items is now list[str] inside this branch
|
||||
print(", ".join(items))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Dataclasses & Pydantic
|
||||
|
||||
#### @dataclass with Options
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass, field
|
||||
@@ -75,36 +166,535 @@ class User:
|
||||
email: str
|
||||
name: str
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
|
||||
def __post_init__(self):
|
||||
self.email = self.email.lower()
|
||||
self.email = self.email.strip().lower()
|
||||
```
|
||||
|
||||
### Pydantic Models
|
||||
#### Frozen and Slots
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Coordinate:
|
||||
"""Immutable, memory-efficient value object."""
|
||||
x: float
|
||||
y: float
|
||||
|
||||
@property
|
||||
def magnitude(self) -> float:
|
||||
return (self.x ** 2 + self.y ** 2) ** 0.5
|
||||
```
|
||||
|
||||
#### Pydantic BaseModel
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, EmailStr, Field, field_validator, computed_field, model_validator
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
model_config = {"str_strip_whitespace": True, "frozen": False}
|
||||
|
||||
email: EmailStr
|
||||
name: str = Field(min_length=1, max_length=100)
|
||||
password: str = Field(min_length=8)
|
||||
age: int = Field(ge=0, le=150)
|
||||
|
||||
class Config:
|
||||
str_strip_whitespace = True
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def name_must_not_be_blank(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("Name must not be blank")
|
||||
return v.title()
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
return f"{self.name} <{self.email}>"
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_consistency(self) -> "UserCreate":
|
||||
if "admin" in self.name.lower() and self.age < 18:
|
||||
raise ValueError("Admins must be 18+")
|
||||
return self
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Async Patterns
|
||||
|
||||
#### Basic async/await
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
async def fetch_json(url: str) -> dict:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
response.raise_for_status()
|
||||
return await response.json()
|
||||
```
|
||||
|
||||
#### asyncio.gather for Parallel Work
|
||||
|
||||
```python
|
||||
async def fetch_all(urls: list[str]) -> list[dict]:
|
||||
return await asyncio.gather(*[fetch_json(url) for url in urls])
|
||||
```
|
||||
|
||||
#### asyncio.TaskGroup (Python 3.11+)
|
||||
|
||||
```python
|
||||
async def fetch_all_safe(urls: list[str]) -> list[dict]:
|
||||
results: list[dict] = []
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for url in urls:
|
||||
tg.create_task(fetch_and_append(url, results))
|
||||
return results
|
||||
|
||||
async def fetch_and_append(url: str, results: list[dict]) -> None:
|
||||
data = await fetch_json(url)
|
||||
results.append(data)
|
||||
```
|
||||
|
||||
#### Async Generators
|
||||
|
||||
```python
|
||||
async def paginate(url: str) -> AsyncIterator[dict]:
|
||||
page = 1
|
||||
while True:
|
||||
data = await fetch_json(f"{url}?page={page}")
|
||||
if not data["items"]:
|
||||
break
|
||||
for item in data["items"]:
|
||||
yield item
|
||||
page += 1
|
||||
|
||||
# Usage
|
||||
async for item in paginate("/api/users"):
|
||||
process(item)
|
||||
```
|
||||
|
||||
#### Async Context Managers
|
||||
|
||||
```python
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def db_transaction(pool):
|
||||
conn = await pool.acquire()
|
||||
tx = await conn.begin()
|
||||
try:
|
||||
yield conn
|
||||
await tx.commit()
|
||||
except Exception:
|
||||
await tx.rollback()
|
||||
raise
|
||||
finally:
|
||||
await pool.release(conn)
|
||||
```
|
||||
|
||||
#### Semaphores for Concurrency Limiting
|
||||
|
||||
```python
|
||||
async def fetch_with_limit(urls: list[str], max_concurrent: int = 10) -> list[dict]:
|
||||
semaphore = asyncio.Semaphore(max_concurrent)
|
||||
|
||||
async def limited_fetch(url: str) -> dict:
|
||||
async with semaphore:
|
||||
return await fetch_json(url)
|
||||
|
||||
return await asyncio.gather(*[limited_fetch(url) for url in urls])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Decorators
|
||||
|
||||
#### Function Decorator with functools.wraps
|
||||
|
||||
```python
|
||||
import functools
|
||||
import time
|
||||
|
||||
def timing(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
start = time.perf_counter()
|
||||
result = func(*args, **kwargs)
|
||||
elapsed = time.perf_counter() - start
|
||||
print(f"{func.__name__} took {elapsed:.4f}s")
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
@timing
|
||||
def slow_operation():
|
||||
time.sleep(1)
|
||||
```
|
||||
|
||||
#### Decorator with Arguments
|
||||
|
||||
```python
|
||||
def retry(max_attempts: int = 3, delay: float = 1.0):
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
last_error: Exception | None = None
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
if attempt < max_attempts - 1:
|
||||
await asyncio.sleep(delay * (2 ** attempt))
|
||||
raise last_error
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
@retry(max_attempts=5, delay=0.5)
|
||||
async def unreliable_call(url: str) -> dict:
|
||||
return await fetch_json(url)
|
||||
```
|
||||
|
||||
#### Class Decorator
|
||||
|
||||
```python
|
||||
def singleton(cls):
|
||||
instances: dict[type, Any] = {}
|
||||
|
||||
@functools.wraps(cls)
|
||||
def get_instance(*args, **kwargs):
|
||||
if cls not in instances:
|
||||
instances[cls] = cls(*args, **kwargs)
|
||||
return instances[cls]
|
||||
|
||||
return get_instance
|
||||
|
||||
@singleton
|
||||
class AppConfig:
|
||||
def __init__(self):
|
||||
self.settings = load_settings()
|
||||
```
|
||||
|
||||
#### Caching Decorator
|
||||
|
||||
```python
|
||||
from functools import lru_cache, cache
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def fibonacci(n: int) -> int:
|
||||
if n < 2:
|
||||
return n
|
||||
return fibonacci(n - 1) + fibonacci(n - 2)
|
||||
|
||||
# Python 3.9+ unbounded cache
|
||||
@cache
|
||||
def load_config(path: str) -> dict:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Context Managers
|
||||
|
||||
#### Basic @contextmanager
|
||||
|
||||
```python
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def managed_connection(dsn: str):
|
||||
conn = connect(dsn)
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
with managed_connection("postgres://...") as conn:
|
||||
conn.execute("SELECT 1")
|
||||
```
|
||||
|
||||
#### Temporary File Context Manager
|
||||
|
||||
```python
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
@contextmanager
|
||||
def temp_directory():
|
||||
dirpath = tempfile.mkdtemp()
|
||||
try:
|
||||
yield dirpath
|
||||
finally:
|
||||
shutil.rmtree(dirpath)
|
||||
|
||||
with temp_directory() as tmpdir:
|
||||
filepath = os.path.join(tmpdir, "data.json")
|
||||
write_json(filepath, data)
|
||||
```
|
||||
|
||||
#### Lock Context Manager
|
||||
|
||||
```python
|
||||
import threading
|
||||
|
||||
@contextmanager
|
||||
def timed_lock(lock: threading.Lock, timeout: float = 5.0):
|
||||
acquired = lock.acquire(timeout=timeout)
|
||||
if not acquired:
|
||||
raise TimeoutError("Could not acquire lock")
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
lock.release()
|
||||
```
|
||||
|
||||
#### Async Context Manager
|
||||
|
||||
```python
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def http_session():
|
||||
session = aiohttp.ClientSession()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Pattern Matching
|
||||
|
||||
#### Basic match/case
|
||||
|
||||
```python
|
||||
def handle_command(command: str) -> str:
|
||||
match command.split():
|
||||
case ["quit"]:
|
||||
return "Goodbye"
|
||||
case ["hello", name]:
|
||||
return f"Hello, {name}"
|
||||
case ["add", *items]:
|
||||
return f"Adding {len(items)} items"
|
||||
case _:
|
||||
return "Unknown command"
|
||||
```
|
||||
|
||||
#### Structural Patterns
|
||||
|
||||
```python
|
||||
def process_event(event: dict) -> None:
|
||||
match event:
|
||||
case {"type": "click", "x": int(x), "y": int(y)}:
|
||||
handle_click(x, y)
|
||||
case {"type": "keypress", "key": str(key)} if len(key) == 1:
|
||||
handle_keypress(key)
|
||||
case {"type": "resize", "width": w, "height": h}:
|
||||
handle_resize(w, h)
|
||||
```
|
||||
|
||||
#### Guard Clauses and OR Patterns
|
||||
|
||||
```python
|
||||
def classify_status(code: int) -> str:
|
||||
match code:
|
||||
case 200 | 201 | 204:
|
||||
return "success"
|
||||
case code if 300 <= code < 400:
|
||||
return "redirect"
|
||||
case 401 | 403:
|
||||
return "auth_error"
|
||||
case code if 400 <= code < 500:
|
||||
return "client_error"
|
||||
case code if 500 <= code < 600:
|
||||
return "server_error"
|
||||
case _:
|
||||
return "unknown"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Error Handling
|
||||
|
||||
#### Custom Exception Hierarchies
|
||||
|
||||
```python
|
||||
class AppError(Exception):
|
||||
"""Base exception for the application."""
|
||||
def __init__(self, message: str, code: str | None = None):
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
|
||||
class NotFoundError(AppError):
|
||||
"""Resource was not found."""
|
||||
def __init__(self, resource: str, resource_id: str):
|
||||
super().__init__(f"{resource} {resource_id} not found", code="NOT_FOUND")
|
||||
self.resource = resource
|
||||
self.resource_id = resource_id
|
||||
|
||||
class ValidationError(AppError):
|
||||
"""Input validation failed."""
|
||||
def __init__(self, errors: list[str]):
|
||||
super().__init__(f"Validation failed: {'; '.join(errors)}", code="VALIDATION")
|
||||
self.errors = errors
|
||||
```
|
||||
|
||||
#### ExceptionGroup (Python 3.11+)
|
||||
|
||||
```python
|
||||
async def process_batch(items: list[dict]) -> list[dict]:
|
||||
results = []
|
||||
errors = []
|
||||
for item in items:
|
||||
try:
|
||||
results.append(await process(item))
|
||||
except Exception as e:
|
||||
errors.append(e)
|
||||
if errors:
|
||||
raise ExceptionGroup("Batch processing errors", errors)
|
||||
return results
|
||||
|
||||
# Handling with except*
|
||||
try:
|
||||
await process_batch(items)
|
||||
except* ValueError as eg:
|
||||
print(f"Validation errors: {len(eg.exceptions)}")
|
||||
except* ConnectionError as eg:
|
||||
print(f"Connection errors: {len(eg.exceptions)}")
|
||||
```
|
||||
|
||||
#### Exception Chaining
|
||||
|
||||
```python
|
||||
def load_config(path: str) -> dict:
|
||||
try:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError as e:
|
||||
raise AppError(f"Config file missing: {path}") from e
|
||||
except json.JSONDecodeError as e:
|
||||
raise AppError(f"Invalid JSON in {path}") from e
|
||||
```
|
||||
|
||||
#### contextlib.suppress
|
||||
|
||||
```python
|
||||
from contextlib import suppress
|
||||
|
||||
# Instead of try/except/pass
|
||||
with suppress(FileNotFoundError):
|
||||
os.remove("temp_file.txt")
|
||||
|
||||
# Instead of:
|
||||
# try:
|
||||
# os.remove("temp_file.txt")
|
||||
# except FileNotFoundError:
|
||||
# pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Use type hints for all public functions
|
||||
2. Use dataclasses or Pydantic for data models
|
||||
3. Prefer context managers for resource management
|
||||
4. Use async for I/O-bound operations
|
||||
5. Follow PEP 8 style guidelines
|
||||
1. **Use type hints on all public functions** -- they serve as documentation, enable IDE autocompletion, and allow static analysis with mypy or pyright.
|
||||
|
||||
2. **Prefer dataclasses or Pydantic for structured data** -- avoid passing raw dicts around. Use `@dataclass` for internal data, Pydantic `BaseModel` for external boundaries (API input/output, config files).
|
||||
|
||||
3. **Use context managers for resource management** -- database connections, file handles, locks, and temporary resources should always be wrapped in `with` statements to guarantee cleanup.
|
||||
|
||||
4. **Prefer `asyncio.TaskGroup` over bare `gather`** -- TaskGroup (3.11+) provides proper error handling by cancelling sibling tasks when one fails, avoiding orphaned coroutines.
|
||||
|
||||
5. **Follow PEP 8 and use a formatter** -- use `ruff format` or `black` for consistent formatting, and `ruff check` for linting. Configure in `pyproject.toml`.
|
||||
|
||||
6. **Write small, composable functions** -- each function should do one thing. Prefer returning values over mutating state. Limit functions to ~20 lines when practical.
|
||||
|
||||
7. **Use `__all__` in public modules** -- explicitly declare the public API of a module to prevent accidental imports of internal helpers.
|
||||
|
||||
8. **Use `pathlib.Path` over `os.path`** -- pathlib provides a cleaner, object-oriented API for file system operations and works cross-platform.
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Mutable default arguments**: Use `None` and initialize in function
|
||||
- **Not closing resources**: Use `with` statements
|
||||
- **Blocking in async**: Use `asyncio.to_thread()` for CPU work
|
||||
- **Catching bare exceptions**: Be specific with exception types
|
||||
1. **Mutable default arguments** -- default values are shared across calls. Use `None` and initialize inside the function body.
|
||||
```python
|
||||
# BAD
|
||||
def add_item(item: str, items: list[str] = []) -> list[str]: ...
|
||||
|
||||
# GOOD
|
||||
def add_item(item: str, items: list[str] | None = None) -> list[str]:
|
||||
if items is None:
|
||||
items = []
|
||||
items.append(item)
|
||||
return items
|
||||
```
|
||||
|
||||
2. **Blocking calls inside async functions** -- calling `time.sleep()`, `requests.get()`, or CPU-heavy code in an async function blocks the entire event loop. Use `asyncio.to_thread()` or `asyncio.sleep()`.
|
||||
```python
|
||||
# BAD
|
||||
async def fetch():
|
||||
return requests.get(url) # blocks event loop
|
||||
|
||||
# GOOD
|
||||
async def fetch():
|
||||
return await asyncio.to_thread(requests.get, url)
|
||||
```
|
||||
|
||||
3. **Catching bare `Exception`** -- always be specific about which exceptions you catch. Bare `except:` or `except Exception:` hides bugs.
|
||||
```python
|
||||
# BAD
|
||||
try:
|
||||
result = compute()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# GOOD
|
||||
try:
|
||||
result = compute()
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning("Computation failed: %s", e)
|
||||
result = default_value
|
||||
```
|
||||
|
||||
4. **Using `is` for value comparison** -- `is` checks identity, not equality. Only use `is` for `None`, `True`, `False`, and sentinel objects.
|
||||
```python
|
||||
# BAD
|
||||
if x is 42: ...
|
||||
|
||||
# GOOD
|
||||
if x == 42: ...
|
||||
if x is None: ...
|
||||
```
|
||||
|
||||
5. **Forgetting to close resources** -- file handles, database connections, and HTTP sessions leak if not closed. Always use context managers.
|
||||
```python
|
||||
# BAD
|
||||
f = open("data.txt")
|
||||
data = f.read()
|
||||
|
||||
# GOOD
|
||||
with open("data.txt") as f:
|
||||
data = f.read()
|
||||
```
|
||||
|
||||
6. **Circular imports** -- restructure code to avoid circular dependencies. Move shared types into a separate module, use `TYPE_CHECKING` for type-only imports, or use lazy imports.
|
||||
```python
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from myapp.models import User # only imported during type checking
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `languages/typescript` -- TypeScript language patterns for polyglot projects
|
||||
- `frameworks/fastapi` -- FastAPI web framework built on Python
|
||||
- `frameworks/django` -- Django web framework for Python
|
||||
- `testing/pytest` -- Python testing with pytest
|
||||
- `patterns/error-handling` -- Python error handling and exception hierarchies
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
# Python Type Hints Quick Reference
|
||||
|
||||
> Python 3.10+ syntax preferred. For 3.9, use `from __future__ import annotations`.
|
||||
|
||||
## Basic Types
|
||||
|
||||
| Type | Example | Notes |
|
||||
|------|---------|-------|
|
||||
| `int` | `x: int = 1` | |
|
||||
| `float` | `x: float = 1.0` | |
|
||||
| `str` | `x: str = "hi"` | |
|
||||
| `bool` | `x: bool = True` | |
|
||||
| `bytes` | `x: bytes = b"hi"` | |
|
||||
| `None` | `x: None = None` | Use as return type for side-effect functions |
|
||||
| `object` | `x: object` | Accepts anything, but no attribute access |
|
||||
| `Any` | `x: Any` | Escapes type checking entirely |
|
||||
|
||||
## Collection Types (3.10+)
|
||||
|
||||
| Type | Example | Notes |
|
||||
|------|---------|-------|
|
||||
| `list[int]` | `x: list[int] = [1, 2]` | Mutable sequence |
|
||||
| `tuple[int, str]` | `x: tuple[int, str]` | Fixed length |
|
||||
| `tuple[int, ...]` | `x: tuple[int, ...]` | Variable length |
|
||||
| `dict[str, int]` | `x: dict[str, int]` | |
|
||||
| `set[str]` | `x: set[str]` | |
|
||||
| `frozenset[str]` | `x: frozenset[str]` | |
|
||||
|
||||
## Union and Optional
|
||||
|
||||
```python
|
||||
# 3.10+ syntax
|
||||
def f(x: int | str) -> None: ...
|
||||
def g(x: int | None = None) -> None: ...
|
||||
|
||||
# Pre-3.10
|
||||
from typing import Union, Optional
|
||||
def f(x: Union[int, str]) -> None: ...
|
||||
def g(x: Optional[int] = None) -> None: ...
|
||||
```
|
||||
|
||||
## TypeAlias
|
||||
|
||||
```python
|
||||
from typing import TypeAlias
|
||||
|
||||
# Explicit alias (3.10+)
|
||||
Vector: TypeAlias = list[float]
|
||||
|
||||
# 3.12+ syntax
|
||||
type Vector = list[float]
|
||||
type Tree[T] = T | list["Tree[T]"] # recursive
|
||||
```
|
||||
|
||||
## Generics with TypeVar
|
||||
|
||||
```python
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
K = TypeVar("K", bound=str) # upper bound
|
||||
N = TypeVar("N", int, float) # constrained
|
||||
|
||||
def first(items: list[T]) -> T:
|
||||
return items[0]
|
||||
|
||||
# 3.12+ syntax (no TypeVar needed)
|
||||
def first[T](items: list[T]) -> T:
|
||||
return items[0]
|
||||
```
|
||||
|
||||
## ParamSpec and Concatenate
|
||||
|
||||
```python
|
||||
from typing import ParamSpec, Concatenate, Callable
|
||||
|
||||
P = ParamSpec("P")
|
||||
T = TypeVar("T")
|
||||
|
||||
# Preserve function signatures through decorators
|
||||
def logged(fn: Callable[P, T]) -> Callable[P, T]:
|
||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||
print(f"Calling {fn.__name__}")
|
||||
return fn(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
# Add a parameter to a function signature
|
||||
def with_user(
|
||||
fn: Callable[Concatenate[User, P], T]
|
||||
) -> Callable[P, T]:
|
||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
||||
return fn(get_current_user(), *args, **kwargs)
|
||||
return wrapper
|
||||
```
|
||||
|
||||
## Protocol (Structural Typing)
|
||||
|
||||
```python
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
class Renderable(Protocol):
|
||||
def render(self) -> str: ...
|
||||
|
||||
class Widget(Protocol):
|
||||
name: str
|
||||
def resize(self, width: int, height: int) -> None: ...
|
||||
|
||||
# Any class with matching methods satisfies the protocol
|
||||
class Button:
|
||||
def render(self) -> str:
|
||||
return "<button/>"
|
||||
|
||||
def draw(item: Renderable) -> None: # Button works here
|
||||
print(item.render())
|
||||
|
||||
# Runtime checking
|
||||
@runtime_checkable
|
||||
class Sized(Protocol):
|
||||
def __len__(self) -> int: ...
|
||||
|
||||
assert isinstance([1, 2], Sized) # True at runtime
|
||||
```
|
||||
|
||||
## @overload
|
||||
|
||||
```python
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def parse(data: str) -> dict[str, Any]: ...
|
||||
@overload
|
||||
def parse(data: bytes) -> list[int]: ...
|
||||
@overload
|
||||
def parse(data: str, raw: Literal[True]) -> str: ...
|
||||
|
||||
def parse(data: str | bytes, raw: bool = False) -> dict | list | str:
|
||||
"""Implementation handles all overloads."""
|
||||
...
|
||||
```
|
||||
|
||||
## TypeGuard and TypeIs
|
||||
|
||||
```python
|
||||
from typing import TypeGuard, TypeIs # TypeIs: 3.13+
|
||||
|
||||
# TypeGuard: narrows type in True branch only
|
||||
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
|
||||
return all(isinstance(x, str) for x in val)
|
||||
|
||||
# TypeIs: narrows in both True and False branches
|
||||
def is_int(val: int | str) -> TypeIs[int]:
|
||||
return isinstance(val, int)
|
||||
|
||||
def f(val: int | str) -> None:
|
||||
if is_int(val):
|
||||
reveal_type(val) # int
|
||||
else:
|
||||
reveal_type(val) # str (only with TypeIs)
|
||||
```
|
||||
|
||||
## Literal and Final
|
||||
|
||||
```python
|
||||
from typing import Literal, Final
|
||||
|
||||
def set_mode(mode: Literal["read", "write", "append"]) -> None: ...
|
||||
|
||||
MAX_SIZE: Final = 100 # Cannot be reassigned
|
||||
PREFIX: Final[str] = "app_" # With explicit type
|
||||
```
|
||||
|
||||
## Callable
|
||||
|
||||
| Signature | Meaning |
|
||||
|-----------|---------|
|
||||
| `Callable[[int, str], bool]` | Function taking int and str, returning bool |
|
||||
| `Callable[..., bool]` | Any args, returning bool |
|
||||
| `Callable[P, T]` | Generic (use with ParamSpec) |
|
||||
|
||||
## TypedDict
|
||||
|
||||
```python
|
||||
from typing import TypedDict, NotRequired, Required
|
||||
|
||||
class Config(TypedDict):
|
||||
name: str
|
||||
debug: NotRequired[bool] # optional key
|
||||
|
||||
class PartialConfig(TypedDict, total=False):
|
||||
name: Required[str] # required even though total=False
|
||||
debug: bool # optional
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
| Pattern | Type Hint |
|
||||
|---------|-----------|
|
||||
| JSON value | `dict[str, Any]` or custom TypedDict |
|
||||
| Decorator preserving sig | `Callable[P, T] -> Callable[P, T]` |
|
||||
| Context manager | `AbstractContextManager[T]` |
|
||||
| Async context manager | `AbstractAsyncContextManager[T]` |
|
||||
| Generator | `Generator[YieldType, SendType, ReturnType]` |
|
||||
| Async generator | `AsyncGenerator[YieldType, SendType]` |
|
||||
| Class method returning self | `-> Self` (3.11+, `from typing import Self`) |
|
||||
| Numeric tower | `int | float` (avoid `numbers.Number`) |
|
||||
|
||||
## Type Narrowing Cheat Sheet
|
||||
|
||||
| Technique | Example |
|
||||
|-----------|---------|
|
||||
| `isinstance` | `if isinstance(x, str):` |
|
||||
| `is None` / `is not None` | `if x is not None:` |
|
||||
| `TypeGuard` / `TypeIs` | Custom narrowing functions |
|
||||
| `assert` | `assert isinstance(x, str)` |
|
||||
| `Literal` checks | `if x == "read":` |
|
||||
| `hasattr` | `if hasattr(x, "render"):` (limited) |
|
||||
@@ -1,9 +1,11 @@
|
||||
---
|
||||
name: typescript
|
||||
description: >
|
||||
Trigger this skill whenever working with TypeScript files (.ts, .tsx), configuring tsconfig.json, or using TypeScript-specific features like strict typing, generics, utility types, or type guards. Activate for any TypeScript project setup, type definition authoring, Zod schema validation, or discriminated union patterns. Also use when the user asks about avoiding `any`, enabling strict mode, or migrating JavaScript to TypeScript.
|
||||
---
|
||||
|
||||
# TypeScript
|
||||
|
||||
## Description
|
||||
|
||||
TypeScript development with strict typing, advanced type utilities, and modern patterns.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Working with TypeScript files (.ts, .tsx)
|
||||
@@ -11,50 +13,284 @@ TypeScript development with strict typing, advanced type utilities, and modern p
|
||||
- React/Next.js development
|
||||
- Node.js backend development
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- Pure Python projects with no TypeScript components
|
||||
- JavaScript projects that have no TypeScript setup and are not being migrated to TypeScript
|
||||
|
||||
---
|
||||
|
||||
## Core Patterns
|
||||
|
||||
### Type Definitions
|
||||
### 1. Advanced Types
|
||||
|
||||
#### Discriminated Unions
|
||||
|
||||
```typescript
|
||||
type Shape =
|
||||
| { kind: "circle"; radius: number }
|
||||
| { kind: "rectangle"; width: number; height: number }
|
||||
| { kind: "triangle"; base: number; height: number };
|
||||
|
||||
function area(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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Branded Types
|
||||
|
||||
```typescript
|
||||
type UserId = string & { readonly __brand: unique symbol };
|
||||
type OrderId = string & { readonly __brand: unique symbol };
|
||||
|
||||
function createUserId(id: string): UserId {
|
||||
if (!id.startsWith("usr_")) throw new Error("Invalid user ID");
|
||||
return id as UserId;
|
||||
}
|
||||
|
||||
function getUser(id: UserId): User { ... }
|
||||
|
||||
// Prevents mixing IDs:
|
||||
const userId = createUserId("usr_123");
|
||||
const orderId = "ord_456" as OrderId;
|
||||
// getUser(orderId); // compile error
|
||||
```
|
||||
|
||||
#### Template Literal Types
|
||||
|
||||
```typescript
|
||||
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
|
||||
type ApiRoute = `/api/${string}`;
|
||||
type EventName = `on${Capitalize<string>}`;
|
||||
|
||||
// Combine for precise route definitions
|
||||
type Endpoint = `${Uppercase<HttpMethod>} ${ApiRoute}`;
|
||||
|
||||
// Pattern matching on string types
|
||||
type ExtractParam<T extends string> =
|
||||
T extends `${string}:${infer Param}/${infer Rest}`
|
||||
? Param | ExtractParam<Rest>
|
||||
: T extends `${string}:${infer Param}`
|
||||
? Param
|
||||
: never;
|
||||
|
||||
type Params = ExtractParam<"/users/:userId/posts/:postId">;
|
||||
// Result: "userId" | "postId"
|
||||
```
|
||||
|
||||
#### Conditional Types with infer
|
||||
|
||||
```typescript
|
||||
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
|
||||
type UnwrapArray<T> = T extends (infer U)[] ? U : T;
|
||||
|
||||
// Deeply unwrap nested promises
|
||||
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
|
||||
|
||||
// Extract function return type conditionally
|
||||
type AsyncReturnType<T extends (...args: any[]) => any> =
|
||||
ReturnType<T> extends Promise<infer U> ? U : ReturnType<T>;
|
||||
```
|
||||
|
||||
#### Mapped Types
|
||||
|
||||
```typescript
|
||||
// Make all properties optional and nullable
|
||||
type Nullable<T> = { [K in keyof T]: T[K] | null };
|
||||
|
||||
// Create a readonly version with getters
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
||||
};
|
||||
|
||||
// Remove specific keys
|
||||
type RemoveKind<T> = {
|
||||
[K in keyof T as Exclude<K, "kind">]: T[K];
|
||||
};
|
||||
```
|
||||
|
||||
#### Recursive Types
|
||||
|
||||
```typescript
|
||||
type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| Json[]
|
||||
| { [key: string]: Json };
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
||||
};
|
||||
|
||||
type DeepReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Utility Types
|
||||
|
||||
```typescript
|
||||
// Interfaces for objects
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: "admin" | "user" | "guest";
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// Types for unions and utilities
|
||||
type Status = 'pending' | 'active' | 'inactive';
|
||||
type UserWithStatus = User & { status: Status };
|
||||
|
||||
// Generic types
|
||||
type ApiResponse<T> = {
|
||||
data: T;
|
||||
error?: string;
|
||||
status: number;
|
||||
};
|
||||
```
|
||||
|
||||
### Utility Types
|
||||
|
||||
```typescript
|
||||
// Partial - all properties optional
|
||||
// Partial -- all properties optional (useful for update payloads)
|
||||
type UserUpdate = Partial<User>;
|
||||
|
||||
// Pick - select properties
|
||||
type UserPreview = Pick<User, 'id' | 'name'>;
|
||||
// Required -- all properties required (undo optionality)
|
||||
type CompleteUser = Required<User>;
|
||||
|
||||
// Omit - exclude properties
|
||||
type UserWithoutId = Omit<User, 'id'>;
|
||||
// Pick -- select specific properties
|
||||
type UserPreview = Pick<User, "id" | "name">;
|
||||
|
||||
// Record - dictionary type
|
||||
type UserMap = Record<string, User>;
|
||||
// Omit -- exclude specific properties
|
||||
type UserCreate = Omit<User, "id" | "createdAt">;
|
||||
|
||||
// Record -- dictionary with typed keys and values
|
||||
type RolePermissions = Record<User["role"], string[]>;
|
||||
|
||||
// Exclude -- remove members from a union
|
||||
type NonGuestRole = Exclude<User["role"], "guest">;
|
||||
// Result: "admin" | "user"
|
||||
|
||||
// Extract -- keep only matching members from a union
|
||||
type PrivilegedRole = Extract<User["role"], "admin" | "moderator">;
|
||||
// Result: "admin"
|
||||
|
||||
// ReturnType -- extract return type of a function
|
||||
declare function getUser(): Promise<User>;
|
||||
type GetUserResult = ReturnType<typeof getUser>;
|
||||
// Result: Promise<User>
|
||||
|
||||
// Parameters -- extract parameter types as a tuple
|
||||
type GetUserParams = Parameters<typeof getUser>;
|
||||
|
||||
// Awaited -- unwrap Promise types
|
||||
type ResolvedUser = Awaited<ReturnType<typeof getUser>>;
|
||||
// Result: User
|
||||
|
||||
// NonNullable -- remove null and undefined
|
||||
type DefinitelyString = NonNullable<string | null | undefined>;
|
||||
// Result: string
|
||||
```
|
||||
|
||||
### Async Patterns
|
||||
---
|
||||
|
||||
### 3. Generics
|
||||
|
||||
#### Generic Functions
|
||||
|
||||
```typescript
|
||||
function first<T>(items: T[]): T | undefined {
|
||||
return items[0];
|
||||
}
|
||||
|
||||
function groupBy<T, K extends string | number>(
|
||||
items: T[],
|
||||
keyFn: (item: T) => K,
|
||||
): Record<K, T[]> {
|
||||
const result = {} as Record<K, T[]>;
|
||||
for (const item of items) {
|
||||
const key = keyFn(item);
|
||||
(result[key] ??= []).push(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
#### Generic Constraints with extends
|
||||
|
||||
```typescript
|
||||
interface HasId {
|
||||
id: string;
|
||||
}
|
||||
|
||||
function findById<T extends HasId>(items: T[], id: string): T | undefined {
|
||||
return items.find((item) => item.id === id);
|
||||
}
|
||||
|
||||
// Multiple constraints
|
||||
function merge<T extends object, U extends object>(a: T, b: U): T & U {
|
||||
return { ...a, ...b };
|
||||
}
|
||||
```
|
||||
|
||||
#### Generic Classes
|
||||
|
||||
```typescript
|
||||
class Repository<T extends HasId> {
|
||||
private items = new Map<string, T>();
|
||||
|
||||
save(item: T): void {
|
||||
this.items.set(item.id, item);
|
||||
}
|
||||
|
||||
findById(id: string): T | undefined {
|
||||
return this.items.get(id);
|
||||
}
|
||||
|
||||
findAll(): T[] {
|
||||
return [...this.items.values()];
|
||||
}
|
||||
}
|
||||
|
||||
const userRepo = new Repository<User>();
|
||||
```
|
||||
|
||||
#### Default Type Parameters
|
||||
|
||||
```typescript
|
||||
type ApiResponse<T, E = Error> = {
|
||||
data: T | null;
|
||||
error: E | null;
|
||||
status: number;
|
||||
};
|
||||
|
||||
// Uses default Error type
|
||||
const response: ApiResponse<User> = {
|
||||
data: user,
|
||||
error: null,
|
||||
status: 200,
|
||||
};
|
||||
|
||||
// Override with custom error
|
||||
const response2: ApiResponse<User, ValidationError> = { ... };
|
||||
```
|
||||
|
||||
#### const Type Parameters (TypeScript 5.0+)
|
||||
|
||||
```typescript
|
||||
function createRoute<const T extends readonly string[]>(
|
||||
methods: T,
|
||||
path: string,
|
||||
) {
|
||||
return { methods, path };
|
||||
}
|
||||
|
||||
// Infers literal tuple type ["GET", "POST"] instead of string[]
|
||||
const route = createRoute(["GET", "POST"], "/api/users");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Async Patterns
|
||||
|
||||
#### Promise Typing
|
||||
|
||||
```typescript
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
@@ -62,67 +298,396 @@ async function fetchUser(id: string): Promise<User> {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch user: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
return response.json() as Promise<User>;
|
||||
}
|
||||
```
|
||||
|
||||
// Error handling
|
||||
async function safeOperation<T>(
|
||||
operation: () => Promise<T>
|
||||
): Promise<[T, null] | [null, Error]> {
|
||||
#### Promise.all with Tuple Types
|
||||
|
||||
```typescript
|
||||
async function loadDashboard(userId: string) {
|
||||
const [user, posts, notifications] = await Promise.all([
|
||||
fetchUser(userId),
|
||||
fetchPosts(userId),
|
||||
fetchNotifications(userId),
|
||||
] as const);
|
||||
// user: User, posts: Post[], notifications: Notification[]
|
||||
return { user, posts, notifications };
|
||||
}
|
||||
```
|
||||
|
||||
#### Result Pattern for Error Handling
|
||||
|
||||
```typescript
|
||||
type Result<T, E = Error> =
|
||||
| { ok: true; value: T }
|
||||
| { ok: false; error: E };
|
||||
|
||||
async function safeAsync<T>(
|
||||
fn: () => Promise<T>,
|
||||
): Promise<Result<T>> {
|
||||
try {
|
||||
const result = await operation();
|
||||
return [result, null];
|
||||
return { ok: true, value: await fn() };
|
||||
} catch (error) {
|
||||
return [null, error as Error];
|
||||
return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
|
||||
}
|
||||
}
|
||||
|
||||
const result = await safeAsync(() => fetchUser("123"));
|
||||
if (result.ok) {
|
||||
console.log(result.value.name);
|
||||
} else {
|
||||
console.error(result.error.message);
|
||||
}
|
||||
```
|
||||
|
||||
### Class Patterns
|
||||
#### AbortController Patterns
|
||||
|
||||
```typescript
|
||||
class UserService {
|
||||
constructor(private readonly db: Database) {}
|
||||
async function fetchWithTimeout(
|
||||
url: string,
|
||||
timeoutMs: number = 5000,
|
||||
): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
async findById(id: string): Promise<User | null> {
|
||||
return this.db.users.findUnique({ where: { id } });
|
||||
try {
|
||||
return await fetch(url, { signal: controller.signal });
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
async create(data: UserCreate): Promise<User> {
|
||||
return this.db.users.create({ data });
|
||||
}
|
||||
// Cancellable operation
|
||||
function createCancellableRequest(url: string) {
|
||||
const controller = new AbortController();
|
||||
const promise = fetch(url, { signal: controller.signal });
|
||||
return {
|
||||
promise,
|
||||
cancel: () => controller.abort(),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Zod Validation
|
||||
---
|
||||
|
||||
### 5. Zod Integration
|
||||
|
||||
#### Schema Definition and Inference
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import { z } from "zod";
|
||||
|
||||
const UserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1).max(100),
|
||||
password: z.string().min(8),
|
||||
age: z.number().int().min(0).max(150),
|
||||
role: z.enum(["admin", "user", "guest"]),
|
||||
});
|
||||
|
||||
type UserInput = z.infer<typeof UserSchema>;
|
||||
type User = z.infer<typeof UserSchema>;
|
||||
```
|
||||
|
||||
function validateUser(data: unknown): UserInput {
|
||||
return UserSchema.parse(data);
|
||||
#### Refinements and Transforms
|
||||
|
||||
```typescript
|
||||
const PasswordSchema = z
|
||||
.string()
|
||||
.min(8)
|
||||
.refine((val) => /[A-Z]/.test(val), "Must contain uppercase")
|
||||
.refine((val) => /[0-9]/.test(val), "Must contain number");
|
||||
|
||||
const DateStringSchema = z
|
||||
.string()
|
||||
.transform((val) => new Date(val))
|
||||
.refine((date) => !isNaN(date.getTime()), "Invalid date");
|
||||
|
||||
const MoneySchema = z
|
||||
.string()
|
||||
.transform((val) => parseFloat(val.replace(/[$,]/g, "")))
|
||||
.pipe(z.number().positive());
|
||||
```
|
||||
|
||||
#### Discriminated Unions with Zod
|
||||
|
||||
```typescript
|
||||
const ShapeSchema = z.discriminatedUnion("kind", [
|
||||
z.object({ kind: z.literal("circle"), radius: z.number().positive() }),
|
||||
z.object({ kind: z.literal("rectangle"), width: z.number(), height: z.number() }),
|
||||
]);
|
||||
|
||||
type Shape = z.infer<typeof ShapeSchema>;
|
||||
|
||||
function validateShape(input: unknown): Shape {
|
||||
return ShapeSchema.parse(input);
|
||||
}
|
||||
```
|
||||
|
||||
#### Zod with API Validation
|
||||
|
||||
```typescript
|
||||
const QueryParamsSchema = z.object({
|
||||
page: z.coerce.number().int().min(1).default(1),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
search: z.string().optional(),
|
||||
sort: z.enum(["name", "date", "relevance"]).default("date"),
|
||||
});
|
||||
|
||||
type QueryParams = z.infer<typeof QueryParamsSchema>;
|
||||
|
||||
function parseQuery(raw: Record<string, string>): QueryParams {
|
||||
return QueryParamsSchema.parse(raw);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Module Patterns
|
||||
|
||||
#### Barrel Exports
|
||||
|
||||
```typescript
|
||||
// src/models/index.ts
|
||||
export { User, type UserCreate } from "./user.js";
|
||||
export { Post, type PostCreate } from "./post.js";
|
||||
export { Comment } from "./comment.js";
|
||||
```
|
||||
|
||||
#### Declaration Merging
|
||||
|
||||
```typescript
|
||||
// Extend an existing interface
|
||||
interface Window {
|
||||
analytics: AnalyticsClient;
|
||||
}
|
||||
|
||||
// Extend Express Request
|
||||
declare namespace Express {
|
||||
interface Request {
|
||||
user?: AuthenticatedUser;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Module Augmentation
|
||||
|
||||
```typescript
|
||||
// Augment a third-party module
|
||||
import "express";
|
||||
|
||||
declare module "express" {
|
||||
interface Request {
|
||||
requestId: string;
|
||||
startTime: number;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Ambient Declarations (.d.ts)
|
||||
|
||||
```typescript
|
||||
// global.d.ts
|
||||
declare global {
|
||||
interface ImportMeta {
|
||||
env: {
|
||||
VITE_API_URL: string;
|
||||
VITE_APP_TITLE: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Declare untyped modules
|
||||
declare module "legacy-lib" {
|
||||
export function doSomething(input: string): string;
|
||||
}
|
||||
|
||||
export {};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Type Guards
|
||||
|
||||
#### Built-in Narrowing
|
||||
|
||||
```typescript
|
||||
function process(value: string | number | null) {
|
||||
if (typeof value === "string") {
|
||||
// value: string
|
||||
return value.toUpperCase();
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
// value: number
|
||||
return value.toFixed(2);
|
||||
}
|
||||
// value: null
|
||||
return "N/A";
|
||||
}
|
||||
```
|
||||
|
||||
#### instanceof Guard
|
||||
|
||||
```typescript
|
||||
class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public statusCode: number,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(error: unknown): string {
|
||||
if (error instanceof ApiError) {
|
||||
return `API Error ${error.statusCode}: ${error.message}`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
```
|
||||
|
||||
#### in Operator Guard
|
||||
|
||||
```typescript
|
||||
interface Dog { bark(): void; breed: string; }
|
||||
interface Cat { meow(): void; color: string; }
|
||||
|
||||
function speak(animal: Dog | Cat): void {
|
||||
if ("bark" in animal) {
|
||||
animal.bark(); // animal: Dog
|
||||
} else {
|
||||
animal.meow(); // animal: Cat
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Custom Type Predicates (is)
|
||||
|
||||
```typescript
|
||||
function isUser(value: unknown): value is User {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"id" in value &&
|
||||
"email" in value &&
|
||||
typeof (value as User).id === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function processInput(data: unknown) {
|
||||
if (isUser(data)) {
|
||||
// data: User -- fully narrowed
|
||||
console.log(data.email);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Assertion Functions (asserts)
|
||||
|
||||
```typescript
|
||||
function assertDefined<T>(
|
||||
value: T | null | undefined,
|
||||
message?: string,
|
||||
): asserts value is T {
|
||||
if (value === null || value === undefined) {
|
||||
throw new Error(message ?? "Value is null or undefined");
|
||||
}
|
||||
}
|
||||
|
||||
function processUser(maybeUser: User | null): string {
|
||||
assertDefined(maybeUser, "User is required");
|
||||
// maybeUser: User after this point
|
||||
return maybeUser.name;
|
||||
}
|
||||
|
||||
function assertNever(value: never): never {
|
||||
throw new Error(`Unexpected value: ${value}`);
|
||||
}
|
||||
|
||||
// Exhaustiveness checking in switch
|
||||
function getLabel(role: "admin" | "user" | "guest"): string {
|
||||
switch (role) {
|
||||
case "admin": return "Administrator";
|
||||
case "user": return "Standard User";
|
||||
case "guest": return "Guest";
|
||||
default: return assertNever(role); // compile error if a case is missed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Enable strict mode in tsconfig.json
|
||||
2. Avoid `any` - use `unknown` and type guards
|
||||
3. Use interfaces for object shapes, types for unions
|
||||
4. Prefer `const` assertions for literal types
|
||||
5. Use discriminated unions for state
|
||||
1. **Enable strict mode in tsconfig.json** -- set `"strict": true` which enables `strictNullChecks`, `noImplicitAny`, `strictFunctionTypes`, and other safety checks.
|
||||
|
||||
2. **Never use `any` -- use `unknown` instead** -- when the type is truly unknown, use `unknown` and narrow with type guards. Reserve `any` only for exceptional migration scenarios, and flag it with `// eslint-disable-next-line`.
|
||||
|
||||
3. **Use interfaces for object shapes, types for unions** -- interfaces support declaration merging and produce clearer error messages. Types are better for unions, intersections, and mapped types.
|
||||
|
||||
4. **Prefer discriminated unions for state modeling** -- use a shared literal `kind` or `type` field to enable exhaustive switch statements and precise narrowing.
|
||||
|
||||
5. **Use `as const` for literal inference** -- `const assertions` preserve literal types in arrays and objects, avoiding unwanted widening to `string[]` or `number[]`.
|
||||
|
||||
6. **Validate external data at boundaries** -- use Zod or a similar runtime validator at API boundaries, config loading, and form inputs. Never trust `as` casts for unknown data.
|
||||
|
||||
7. **Prefer type predicates over type assertions** -- custom `is` guards are safer than `as` casts because they include a runtime check.
|
||||
|
||||
8. **Use `satisfies` for type checking without widening** -- the `satisfies` operator (TS 5.0+) validates that a value conforms to a type while preserving the narrower inferred type.
|
||||
```typescript
|
||||
const config = {
|
||||
apiUrl: "https://api.example.com",
|
||||
retries: 3,
|
||||
} satisfies Record<string, string | number>;
|
||||
// config.apiUrl is still string (not string | number)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Using `any`**: Defeats type safety
|
||||
- **Not handling null/undefined**: Use strict null checks
|
||||
- **Type assertions**: Prefer type guards
|
||||
- **Ignoring errors**: Handle all promise rejections
|
||||
1. **Overusing type assertions (`as`)** -- assertions bypass the type checker. Use type guards or schema validation instead.
|
||||
```typescript
|
||||
// BAD
|
||||
const user = data as User;
|
||||
|
||||
// GOOD
|
||||
if (isUser(data)) { ... }
|
||||
```
|
||||
|
||||
2. **Ignoring strict null checks** -- `undefined` and `null` cause runtime crashes when not handled. Always enable `strictNullChecks` and handle nullable values explicitly.
|
||||
|
||||
3. **Returning `any` from catch blocks** -- `catch (e)` gives `unknown` in strict mode. Always narrow before using the error.
|
||||
```typescript
|
||||
catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
```
|
||||
|
||||
4. **Mutation of readonly types at runtime** -- `Readonly<T>` and `readonly` only prevent mutation at compile time. The underlying object can still be mutated at runtime via `Object.assign` or casts.
|
||||
|
||||
5. **Forgetting `export {}` in ambient files** -- `.d.ts` files without any import/export are treated as global scripts rather than modules, which can cause unexpected declaration collisions.
|
||||
|
||||
6. **Using enums instead of const objects** -- TypeScript enums have quirks (reverse mappings, tree-shaking issues). Prefer `as const` objects or union types.
|
||||
```typescript
|
||||
// Prefer this:
|
||||
const Role = { Admin: "admin", User: "user" } as const;
|
||||
type Role = (typeof Role)[keyof typeof Role];
|
||||
|
||||
// Over this:
|
||||
enum Role { Admin = "admin", User = "user" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `languages/javascript` -- JavaScript patterns for JS interop and migration
|
||||
- `languages/python` -- Python language patterns for polyglot projects
|
||||
- `frameworks/react` -- React component patterns with TypeScript
|
||||
- `frameworks/nextjs` -- Next.js framework with TypeScript support
|
||||
- `testing/vitest` -- TypeScript testing with Vitest
|
||||
- `patterns/error-handling` -- TypeScript error handling patterns
|
||||
- `patterns/state-management` -- State management with TypeScript types
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
# TypeScript Advanced Types Quick Reference
|
||||
|
||||
## Discriminated Unions
|
||||
|
||||
```typescript
|
||||
// Tag each variant with a literal type field
|
||||
type Shape =
|
||||
| { kind: "circle"; radius: number }
|
||||
| { kind: "rect"; width: number; height: number }
|
||||
| { kind: "triangle"; base: number; height: number };
|
||||
|
||||
function area(s: Shape): number {
|
||||
switch (s.kind) {
|
||||
case "circle": return Math.PI * s.radius ** 2;
|
||||
case "rect": return s.width * s.height;
|
||||
case "triangle": return (s.base * s.height) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Exhaustiveness check helper
|
||||
function assertNever(x: never): never {
|
||||
throw new Error(`Unexpected value: ${x}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Branded Types
|
||||
|
||||
```typescript
|
||||
// Prevent mixing structurally identical types
|
||||
type Brand<T, B extends string> = T & { readonly __brand: B };
|
||||
|
||||
type UserId = Brand<string, "UserId">;
|
||||
type OrderId = Brand<string, "OrderId">;
|
||||
|
||||
function getUser(id: UserId) { /* ... */ }
|
||||
|
||||
const userId = "abc" as UserId;
|
||||
const orderId = "abc" as OrderId;
|
||||
|
||||
getUser(userId); // OK
|
||||
getUser(orderId); // Error: OrderId not assignable to UserId
|
||||
|
||||
// Validation-based branding
|
||||
type Email = Brand<string, "Email">;
|
||||
function parseEmail(input: string): Email {
|
||||
if (!input.includes("@")) throw new Error("Invalid email");
|
||||
return input as Email;
|
||||
}
|
||||
```
|
||||
|
||||
## Template Literal Types
|
||||
|
||||
```typescript
|
||||
// Build string types from unions
|
||||
type Method = "get" | "post" | "put" | "delete";
|
||||
type Route = "/users" | "/orders";
|
||||
type Endpoint = `${Uppercase<Method>} ${Route}`;
|
||||
// "GET /users" | "GET /orders" | "POST /users" | ...
|
||||
|
||||
// Event handler pattern
|
||||
type EventName = "click" | "focus" | "blur";
|
||||
type Handler = `on${Capitalize<EventName>}`;
|
||||
// "onClick" | "onFocus" | "onBlur"
|
||||
|
||||
// Extract parts from string types
|
||||
type ExtractParam<T extends string> =
|
||||
T extends `${string}:${infer Param}/${infer Rest}`
|
||||
? Param | ExtractParam<Rest>
|
||||
: T extends `${string}:${infer Param}`
|
||||
? Param
|
||||
: never;
|
||||
|
||||
type Params = ExtractParam<"/users/:id/posts/:postId">;
|
||||
// "id" | "postId"
|
||||
```
|
||||
|
||||
## Conditional Types
|
||||
|
||||
```typescript
|
||||
// Basic conditional
|
||||
type IsString<T> = T extends string ? true : false;
|
||||
|
||||
// Distributive over unions (when T is naked type parameter)
|
||||
type ToArray<T> = T extends unknown ? T[] : never;
|
||||
type Result = ToArray<string | number>; // string[] | number[]
|
||||
|
||||
// Prevent distribution with tuple wrapping
|
||||
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
|
||||
type Result2 = ToArrayNonDist<string | number>; // (string | number)[]
|
||||
|
||||
// infer keyword
|
||||
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||
type Unpacked<T> = T extends Promise<infer U> ? U :
|
||||
T extends Array<infer U> ? U : T;
|
||||
|
||||
// infer with constraints (TS 4.7+)
|
||||
type FirstString<T> =
|
||||
T extends [infer S extends string, ...unknown[]] ? S : never;
|
||||
```
|
||||
|
||||
## Mapped Types
|
||||
|
||||
```typescript
|
||||
// Transform all properties
|
||||
type Readonly<T> = { readonly [K in keyof T]: T[K] };
|
||||
type Optional<T> = { [K in keyof T]?: T[K] };
|
||||
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
|
||||
type Required<T> = { [K in keyof T]-?: T[K] };
|
||||
|
||||
// Map to new value types
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
||||
};
|
||||
|
||||
interface Person { name: string; age: number }
|
||||
type PersonGetters = Getters<Person>;
|
||||
// { getName: () => string; getAge: () => number }
|
||||
```
|
||||
|
||||
## Key Remapping (via `as`)
|
||||
|
||||
```typescript
|
||||
// Filter keys
|
||||
type OnlyStrings<T> = {
|
||||
[K in keyof T as T[K] extends string ? K : never]: T[K];
|
||||
};
|
||||
|
||||
// Rename keys
|
||||
type Prefixed<T, P extends string> = {
|
||||
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
|
||||
};
|
||||
|
||||
// Remove specific keys
|
||||
type RemoveKind<T> = {
|
||||
[K in keyof T as Exclude<K, "kind">]: T[K];
|
||||
};
|
||||
|
||||
// Build from union
|
||||
type EventMap<T extends string> = {
|
||||
[K in T as `on${Capitalize<K>}`]: (event: K) => void;
|
||||
};
|
||||
type Handlers = EventMap<"click" | "scroll">;
|
||||
// { onClick: (event: "click") => void; onScroll: (event: "scroll") => void }
|
||||
```
|
||||
|
||||
## Recursive Types
|
||||
|
||||
```typescript
|
||||
// JSON type
|
||||
type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| Json[]
|
||||
| { [key: string]: Json };
|
||||
|
||||
// Deep readonly
|
||||
type DeepReadonly<T> = T extends Function
|
||||
? T
|
||||
: T extends object
|
||||
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
|
||||
: T;
|
||||
|
||||
// Deep partial
|
||||
type DeepPartial<T> = T extends object
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: T;
|
||||
|
||||
// Flatten nested object paths
|
||||
type Paths<T, Prefix extends string = ""> = T extends object
|
||||
? {
|
||||
[K in keyof T & string]: T[K] extends object
|
||||
? Paths<T[K], `${Prefix}${K}.`>
|
||||
: `${Prefix}${K}`;
|
||||
}[keyof T & string]
|
||||
: never;
|
||||
|
||||
type P = Paths<{ a: { b: number; c: { d: string } } }>;
|
||||
// "a.b" | "a.c.d"
|
||||
```
|
||||
|
||||
## Utility Types Cheat Sheet
|
||||
|
||||
| Utility | Effect |
|
||||
|---------|--------|
|
||||
| `Partial<T>` | All properties optional |
|
||||
| `Required<T>` | All properties required |
|
||||
| `Readonly<T>` | All properties readonly |
|
||||
| `Record<K, V>` | Object with keys K and values V |
|
||||
| `Pick<T, K>` | Subset of properties |
|
||||
| `Omit<T, K>` | All except listed properties |
|
||||
| `Exclude<U, E>` | Remove members from union |
|
||||
| `Extract<U, E>` | Keep matching members from union |
|
||||
| `NonNullable<T>` | Remove null and undefined |
|
||||
| `ReturnType<F>` | Return type of function |
|
||||
| `Parameters<F>` | Tuple of parameter types |
|
||||
| `ConstructorParameters<C>` | Constructor parameter types |
|
||||
| `InstanceType<C>` | Instance type of constructor |
|
||||
| `Awaited<T>` | Unwrap Promise (deeply) |
|
||||
| `NoInfer<T>` | Prevent inference from this position (5.4+) |
|
||||
|
||||
## Satisfies Operator (4.9+)
|
||||
|
||||
```typescript
|
||||
// Validate type without widening
|
||||
const palette = {
|
||||
red: "#ff0000",
|
||||
green: [0, 255, 0],
|
||||
} satisfies Record<string, string | number[]>;
|
||||
|
||||
palette.red.toUpperCase(); // OK - knows it's string
|
||||
palette.green.map(x => x); // OK - knows it's number[]
|
||||
```
|
||||
|
||||
## const Type Parameters (5.0+)
|
||||
|
||||
```typescript
|
||||
// Infer narrow literal types from arguments
|
||||
function routes<const T extends readonly string[]>(paths: T): T {
|
||||
return paths;
|
||||
}
|
||||
|
||||
const r = routes(["/home", "/about"]);
|
||||
// Type: readonly ["/home", "/about"] (not string[])
|
||||
```
|
||||
|
||||
## Pattern: Type-Safe Event Emitter
|
||||
|
||||
```typescript
|
||||
type EventMap = {
|
||||
login: { userId: string };
|
||||
logout: undefined;
|
||||
error: { code: number; message: string };
|
||||
};
|
||||
|
||||
class Emitter<E extends Record<string, unknown>> {
|
||||
on<K extends keyof E>(
|
||||
event: K,
|
||||
handler: E[K] extends undefined
|
||||
? () => void
|
||||
: (payload: E[K]) => void
|
||||
): void { /* ... */ }
|
||||
|
||||
emit<K extends keyof E>(
|
||||
...args: E[K] extends undefined ? [K] : [K, E[K]]
|
||||
): void { /* ... */ }
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user