All skills
jwynia avatar

/typescript-best-practices

@99a8797
by J Wyniajwynia/agent-skills160 stars
20

Guide AI agents through TypeScript coding best practices including type safety, error handling, code organization, and architecture patterns. This skill should be used when generating TypeScript code, reviewing TypeScript files, creating new TypeScript modules, refactoring JavaScript to TypeScript, or when the user asks about TypeScript patterns, types, or coding standards. Keywords: typescript, types, coding standards, best practices, type safety, generics, architecture, refactoring.

Use this Skill: https://skilld.dev/gh/jwynia/agent-skills/typescript-best-practices

This session only. Nothing lands on disk.

referencespatternsasync-patterns.md

≈2.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Async Patterns

Best practices for async/await, Promise handling, cancellation, and concurrent operations in TypeScript.

Async/Await Basics

Proper Async Function Signatures

// Always specify return type for public APIs
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/users/${id}`);
  return response.json();
}

// Use Result types for error handling
async function fetchUserSafe(id: string): Promise<Result<User, FetchError>> {
  try {
    const response = await fetch(`/users/${id}`);
    if (!response.ok) {
      return err({ type: "http", status: response.status });
    }
    return ok(await response.json());
  } catch (error) {
    return err({ type: "network", message: String(error) });
  }
}

Avoid Unnecessary Async

// WRONG: Unnecessary async wrapper
async function getConfig(): Promise<Config> {
  return config; // No await needed
}

// CORRECT: Just return the promise or value
function getConfig(): Config {
  return config;
}

// WRONG: Wrapping a promise in async
async function fetchData(): Promise<Data> {
  return await fetch("/data").then(r => r.json());
}

// CORRECT: Return the promise directly
function fetchData(): Promise<Data> {
  return fetch("/data").then(r => r.json());
}

// CORRECT: Use async when you need multiple awaits
async function processData(): Promise<ProcessedData> {
  const raw = await fetch("/data");
  const json = await raw.json();
  return transform(json);
}

Sequential vs Parallel Execution

Sequential Execution

// Each request waits for the previous one
async function fetchSequential(ids: string[]): Promise<User[]> {
  const users: User[] = [];

  for (const id of ids) {
    const user = await fetchUser(id);
    users.push(user);
  }

  return users;
}

// Use when:
// - Order matters
// - Rate limiting required
// - Each request depends on previous result

Parallel Execution

// All requests run simultaneously
async function fetchParallel(ids: string[]): Promise<User[]> {
  const promises = ids.map(id => fetchUser(id));
  return Promise.all(promises);
}

// Use when:
// - Requests are independent
// - Faster total execution time needed
// - No rate limiting concerns

Controlled Concurrency

// Limit concurrent requests
async function fetchWithConcurrency<T>(
  items: string[],
  fetcher: (item: string) => Promise<T>,
  concurrency: number
): Promise<T[]> {
  const results: T[] = [];
  const executing: Promise<void>[] = [];

  for (const item of items) {
    const promise = fetcher(item).then(result => {
      results.push(result);
    });

    executing.push(promise);

    if (executing.length >= concurrency) {
      await Promise.race(executing);
      // Remove completed promises
      executing.splice(
        executing.findIndex(p => p === promise),
        1
      );
    }
  }

  await Promise.all(executing);
  return results;
}

// Usage: Max 3 concurrent requests
const users = await fetchWithConcurrency(ids, fetchUser, 3);

Promise Utilities

Promise.all with Error Handling

// Fail-fast: First error rejects all
async function fetchAll<T>(promises: Promise<T>[]): Promise<T[]> {
  return Promise.all(promises);
}

// Collect all results (success and failure)
async function fetchAllSettled<T>(
  promises: Promise<T>[]
): Promise<Array<{ status: "fulfilled"; value: T } | { status: "rejected"; reason: unknown }>> {
  return Promise.allSettled(promises);
}

// Separate successes and failures
async function partitionResults<T>(
  promises: Promise<T>[]
): Promise<{ successes: T[]; failures: unknown[] }> {
  const results = await Promise.allSettled(promises);

  const successes: T[] = [];
  const failures: unknown[] = [];

  for (const result of results) {
    if (result.status === "fulfilled") {
      successes.push(result.value);
    } else {
      failures.push(result.reason);
    }
  }

  return { successes, failures };
}

Promise.race Patterns

// Timeout wrapper
function withTimeout<T>(
  promise: Promise<T>,
  timeoutMs: number,
  message = "Operation timed out"
): Promise<T> {
  const timeout = new Promise<never>((_, reject) => {
    setTimeout(() => reject(new Error(message)), timeoutMs);
  });

  return Promise.race([promise, timeout]);
}

// Usage
const result = await withTimeout(fetchUser("123"), 5000);

// First successful result
async function fetchFirst<T>(promises: Promise<T>[]): Promise<T> {
  return Promise.race(promises);
}

Cancellation with AbortController

Basic Cancellation

async function fetchWithAbort(
  url: string,
  signal?: AbortSignal
): Promise<Response> {
  return fetch(url, { signal });
}

// Usage
const controller = new AbortController();

// Start fetch
const fetchPromise = fetchWithAbort("/data", controller.signal);

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetchPromise;
} catch (error) {
  if (error instanceof DOMException && error.name === "AbortError") {
    console.log("Request was cancelled");
  } else {
    throw error;
  }
}

Cancellation in Long Operations

async function processLargeDataset(
  items: Item[],
  signal?: AbortSignal
): Promise<ProcessedItem[]> {
  const results: ProcessedItem[] = [];

  for (const item of items) {
    // Check cancellation before each iteration
    if (signal?.aborted) {
      throw new DOMException("Operation cancelled", "AbortError");
    }

    const processed = await processItem(item);
    results.push(processed);
  }

  return results;
}

// Helper for cancellation check
function checkAborted(signal?: AbortSignal): void {
  if (signal?.aborted) {
    throw new DOMException("Operation cancelled", "AbortError");
  }
}

Linked Abort Signals

// Combine multiple abort signals
function linkAbortSignals(...signals: AbortSignal[]): AbortSignal {
  const controller = new AbortController();

  for (const signal of signals) {
    if (signal.aborted) {
      controller.abort();
      break;
    }

    signal.addEventListener("abort", () => controller.abort(), { once: true });
  }

  return controller.signal;
}

// Usage: Cancel on either timeout or user action
const timeoutController = new AbortController();
setTimeout(() => timeoutController.abort(), 30000);

const userController = new AbortController();
cancelButton.onclick = () => userController.abort();

const combinedSignal = linkAbortSignals(
  timeoutController.signal,
  userController.signal
);

await fetchWithAbort("/data", combinedSignal);

Retry Patterns

Simple Retry

async function retry<T>(
  fn: () => Promise<T>,
  maxAttempts: number
): Promise<T> {
  let lastError: unknown;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      if (attempt === maxAttempts) break;
    }
  }

  throw lastError;
}

// Usage
const result = await retry(() => fetchData(), 3);

Retry with Exponential Backoff

interface RetryOptions {
  maxAttempts: number;
  initialDelayMs: number;
  maxDelayMs: number;
  backoffMultiplier: number;
  signal?: AbortSignal;
}

async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  options: RetryOptions
): Promise<T> {
  const {
    maxAttempts,
    initialDelayMs,
    maxDelayMs,
    backoffMultiplier,
    signal,
  } = options;

  let delay = initialDelayMs;
  let lastError: unknown;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      if (attempt === maxAttempts) break;
      if (signal?.aborted) break;

      // Wait before retry
      await sleep(delay);

      // Increase delay for next attempt
      delay = Math.min(delay * backoffMultiplier, maxDelayMs);
    }
  }

  throw lastError;
}

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Usage
const result = await retryWithBackoff(fetchData, {
  maxAttempts: 5,
  initialDelayMs: 100,
  maxDelayMs: 10000,
  backoffMultiplier: 2,
});

Debouncing and Throttling

Async Debounce

function debounceAsync<T extends unknown[], R>(
  fn: (...args: T) => Promise<R>,
  delayMs: number
): (...args: T) => Promise<R> {
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
  let pendingPromise: Promise<R> | null = null;
  let resolve: ((value: R) => void) | null = null;
  let reject: ((error: unknown) => void) | null = null;

  return (...args: T): Promise<R> => {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    if (!pendingPromise) {
      pendingPromise = new Promise<R>((res, rej) => {
        resolve = res;
        reject = rej;
      });
    }

    timeoutId = setTimeout(async () => {
      try {
        const result = await fn(...args);
        resolve?.(result);
      } catch (error) {
        reject?.(error);
      } finally {
        pendingPromise = null;
        resolve = null;
        reject = null;
      }
    }, delayMs);

    return pendingPromise;
  };
}

// Usage
const debouncedSearch = debounceAsync(searchApi, 300);

Queue Patterns

Simple Async Queue

class AsyncQueue<T> {
  private queue: Array<() => Promise<T>> = [];
  private processing = false;
  private results: T[] = [];

  add(task: () => Promise<T>): void {
    this.queue.push(task);
    this.process();
  }

  private async process(): Promise<void> {
    if (this.processing) return;
    this.processing = true;

    while (this.queue.length > 0) {
      const task = this.queue.shift()!;
      const result = await task();
      this.results.push(result);
    }

    this.processing = false;
  }

  async drain(): Promise<T[]> {
    while (this.processing || this.queue.length > 0) {
      await new Promise(resolve => setTimeout(resolve, 10));
    }
    return this.results;
  }
}

Best Practices

  1. Always Handle Errors: Use try-catch or Result types
  2. Avoid Floating Promises: Always await or handle promise
  3. Use Parallel When Possible: Promise.all for independent operations
  4. Implement Timeouts: Prevent hanging operations
  5. Support Cancellation: Use AbortController for long operations
  6. Add Retries for Network: With exponential backoff
  7. Limit Concurrency: Prevent overwhelming servers
  8. Type Return Values: Explicit Promise<T> for public APIs

Source: SKILL.md on GitHub

1 warning14d5 checks · Risk SAFE
  • Gen Agent Trust Hub14d

    This skill provides educational documentation and Deno-based utility scripts for TypeScript development, including tools for code analysis, module scaffolding, and type generation. It operates entirely on local files and contains no malicious code or network operations.

  • Socket14d

    No alerts

  • Snyk14d

    Risk: LOW · No issues

  • Runlayer7mo

    18/18 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 99a8797. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Dormantupdated 8 months ago
compatibility
Requires Deno for analysis scripts. Applicable to any TypeScript codebase.
Other metadata
metadata
{
  "author": "agent-skills",
  "version": "1.0",
  "type": "utility",
  "mode": "assistive",
  "domain": "development"
}

README badge

README badge for jwynia/agent-skills/typescript-best-practices