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.

referencespatternsfunctional-patterns.md

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

Functional Patterns

Functional programming patterns in TypeScript: immutability, pure functions, composition, and higher-order functions.

Immutability

Readonly Types

// Immutable object
interface User {
  readonly id: string;
  readonly name: string;
  readonly email: string;
}

// Immutable array
type UserList = ReadonlyArray<User>;

// Deep readonly
type DeepReadonly<T> = T extends object
  ? { readonly [P in keyof T]: DeepReadonly<T[P]> }
  : T;

// Const assertion for literals
const CONFIG = {
  host: "localhost",
  port: 3000,
  features: ["auth", "logging"],
} as const;
// typeof CONFIG = { readonly host: "localhost"; readonly port: 3000; readonly features: readonly ["auth", "logging"] }

Immutable Updates

// Object updates with spread
function updateUser(user: User, name: string): User {
  return { ...user, name };
}

// Nested updates
interface State {
  readonly user: User;
  readonly settings: {
    readonly theme: string;
    readonly notifications: boolean;
  };
}

function updateTheme(state: State, theme: string): State {
  return {
    ...state,
    settings: {
      ...state.settings,
      theme,
    },
  };
}

// Array updates
function addItem<T>(items: ReadonlyArray<T>, item: T): ReadonlyArray<T> {
  return [...items, item];
}

function removeItem<T>(items: ReadonlyArray<T>, index: number): ReadonlyArray<T> {
  return [...items.slice(0, index), ...items.slice(index + 1)];
}

function updateItem<T>(
  items: ReadonlyArray<T>,
  index: number,
  updater: (item: T) => T
): ReadonlyArray<T> {
  return items.map((item, i) => (i === index ? updater(item) : item));
}

Pure Functions

Characteristics of Pure Functions

// Pure: Same input always produces same output, no side effects
function add(a: number, b: number): number {
  return a + b;
}

function formatUser(user: User): string {
  return `${user.name} <${user.email}>`;
}

// Impure: Uses external state
let counter = 0;
function increment(): number {
  return ++counter; // Side effect: modifies external state
}

// Impure: Non-deterministic
function getTimestamp(): number {
  return Date.now(); // Different result each call
}

// Impure: Side effects
function logUser(user: User): User {
  console.log(user); // Side effect: I/O
  return user;
}

Converting Impure to Pure

// Impure: Random selection
function pickRandomItem<T>(items: T[]): T {
  return items[Math.floor(Math.random() * items.length)];
}

// Pure: Inject randomness
function pickItem<T>(items: ReadonlyArray<T>, randomValue: number): T {
  const index = Math.floor(randomValue * items.length);
  return items[index];
}

// Impure: Uses current time
function isExpired(expiresAt: Date): boolean {
  return expiresAt < new Date();
}

// Pure: Inject current time
function isExpiredAt(expiresAt: Date, now: Date): boolean {
  return expiresAt < now;
}

Function Composition

Basic Composition

// Compose two functions
function compose<A, B, C>(
  f: (b: B) => C,
  g: (a: A) => B
): (a: A) => C {
  return (a: A) => f(g(a));
}

// Compose multiple functions (right to left)
function composeAll<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
  return (arg: T) => fns.reduceRight((acc, fn) => fn(acc), arg);
}

// Pipe (left to right, more readable)
function pipe<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
  return (arg: T) => fns.reduce((acc, fn) => fn(acc), arg);
}

// Usage
const processString = pipe(
  (s: string) => s.trim(),
  (s: string) => s.toLowerCase(),
  (s: string) => s.replace(/\s+/g, "-")
);

processString("  Hello World  "); // "hello-world"

Typed Pipe

// Type-safe pipe for different types
function pipe2<A, B, C>(
  ab: (a: A) => B,
  bc: (b: B) => C
): (a: A) => C {
  return (a: A) => bc(ab(a));
}

function pipe3<A, B, C, D>(
  ab: (a: A) => B,
  bc: (b: B) => C,
  cd: (c: C) => D
): (a: A) => D {
  return (a: A) => cd(bc(ab(a)));
}

// Usage with different types
const parseAndDouble = pipe2(
  (s: string) => parseInt(s, 10),
  (n: number) => n * 2
);

parseAndDouble("21"); // 42

Higher-Order Functions

Functions That Return Functions

// Factory function
function createMultiplier(factor: number): (value: number) => number {
  return (value: number) => value * factor;
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

double(5); // 10
triple(5); // 15

// Predicate factory
function createMatcher<T>(
  key: keyof T,
  value: T[keyof T]
): (item: T) => boolean {
  return (item: T) => item[key] === value;
}

const isActive = createMatcher<User>("status", "active");
const activeUsers = users.filter(isActive);

Functions That Accept Functions

// Map with index
function mapWithIndex<T, U>(
  items: ReadonlyArray<T>,
  fn: (item: T, index: number) => U
): U[] {
  return items.map(fn);
}

// Filter and map in one pass
function filterMap<T, U>(
  items: ReadonlyArray<T>,
  predicate: (item: T) => boolean,
  mapper: (item: T) => U
): U[] {
  const result: U[] = [];
  for (const item of items) {
    if (predicate(item)) {
      result.push(mapper(item));
    }
  }
  return result;
}

// Group by key
function groupBy<T, K extends string | number>(
  items: ReadonlyArray<T>,
  getKey: (item: T) => K
): Record<K, T[]> {
  const result = {} as Record<K, T[]>;

  for (const item of items) {
    const key = getKey(item);
    if (!result[key]) {
      result[key] = [];
    }
    result[key].push(item);
  }

  return result;
}

Currying and Partial Application

Currying

// Curried function
function curriedAdd(a: number): (b: number) => (c: number) => number {
  return (b: number) => (c: number) => a + b + c;
}

curriedAdd(1)(2)(3); // 6

// Generic curry helper (for 2 args)
function curry2<A, B, R>(fn: (a: A, b: B) => R): (a: A) => (b: B) => R {
  return (a: A) => (b: B) => fn(a, b);
}

// Usage
const curriedConcat = curry2((a: string, b: string) => a + b);
const greet = curriedConcat("Hello, ");
greet("World"); // "Hello, World"

Partial Application

// Partial application helper
function partial<T, U extends unknown[], R>(
  fn: (arg: T, ...rest: U) => R,
  arg: T
): (...rest: U) => R {
  return (...rest: U) => fn(arg, ...rest);
}

// Usage
function greet(greeting: string, name: string): string {
  return `${greeting}, ${name}!`;
}

const sayHello = partial(greet, "Hello");
sayHello("World"); // "Hello, World!"

// Partial with multiple args
function partialRight<T extends unknown[], U, R>(
  fn: (...args: [...T, U]) => R,
  arg: U
): (...args: T) => R {
  return (...args: T) => fn(...args, arg);
}

Option/Maybe Pattern

Option Type

type Some<T> = { readonly _tag: "some"; readonly value: T };
type None = { readonly _tag: "none" };
type Option<T> = Some<T> | None;

// Constructors
function some<T>(value: T): Option<T> {
  return { _tag: "some", value };
}

function none<T = never>(): Option<T> {
  return { _tag: "none" };
}

// Type guards
function isSome<T>(option: Option<T>): option is Some<T> {
  return option._tag === "some";
}

function isNone<T>(option: Option<T>): option is None {
  return option._tag === "none";
}

Option Operations

// Map
function mapOption<T, U>(
  option: Option<T>,
  fn: (value: T) => U
): Option<U> {
  return isSome(option) ? some(fn(option.value)) : none();
}

// FlatMap
function flatMapOption<T, U>(
  option: Option<T>,
  fn: (value: T) => Option<U>
): Option<U> {
  return isSome(option) ? fn(option.value) : none();
}

// Get with default
function getOrElse<T>(option: Option<T>, defaultValue: T): T {
  return isSome(option) ? option.value : defaultValue;
}

// Usage
function findUser(id: string): Option<User> {
  const user = users.get(id);
  return user ? some(user) : none();
}

const userName = pipe(
  findUser("123"),
  opt => mapOption(opt, user => user.name),
  opt => getOrElse(opt, "Unknown")
);

Lens Pattern

Simple Lens

interface Lens<S, A> {
  get: (s: S) => A;
  set: (a: A) => (s: S) => S;
}

// Create lens for a property
function lens<S, K extends keyof S>(key: K): Lens<S, S[K]> {
  return {
    get: (s: S) => s[key],
    set: (a: S[K]) => (s: S) => ({ ...s, [key]: a }),
  };
}

// Modify through lens
function over<S, A>(
  l: Lens<S, A>,
  fn: (a: A) => A
): (s: S) => S {
  return (s: S) => l.set(fn(l.get(s)))(s);
}

// Compose lenses
function composeLens<S, A, B>(
  outer: Lens<S, A>,
  inner: Lens<A, B>
): Lens<S, B> {
  return {
    get: (s: S) => inner.get(outer.get(s)),
    set: (b: B) => (s: S) => outer.set(inner.set(b)(outer.get(s)))(s),
  };
}

// Usage
interface Address {
  street: string;
  city: string;
}

interface Person {
  name: string;
  address: Address;
}

const addressLens = lens<Person, "address">("address");
const cityLens = lens<Address, "city">("city");
const personCityLens = composeLens(addressLens, cityLens);

const person: Person = { name: "John", address: { street: "Main", city: "NYC" } };
const updatedPerson = personCityLens.set("LA")(person);

Best Practices

  1. Prefer Immutability: Use readonly and spread operators
  2. Keep Functions Pure: Same input, same output, no side effects
  3. Use Small, Focused Functions: Each function does one thing
  4. Compose Functions: Build complex logic from simple pieces
  5. Use Higher-Order Functions: For reusable patterns
  6. Type Everything: Explicit types for function signatures
  7. Use Option for Missing Values: Instead of null/undefined
  8. Document Side Effects: When they're unavoidable

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