Claude-skill-registry cleanTypes

TypeScript type best practices - proper typing patterns, avoiding type complexity, meaningful type annotations

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cleanTypes" ~/.claude/skills/majiayu000-claude-skill-registry-cleantypes && rm -rf "$T"
manifest: skills/data/cleanTypes/SKILL.md
source content

TypeScript Type Best Practices

1. Implicit is Better Than Explicit

Rule: If TypeScript can infer the type, don't annotate it.

TypeScript has powerful type inference. Let the compiler do the work.

Remove Return Type Annotations

Bad:

const extractExtension = (filename: string): string =>
  filename.replace(/.*symlink/, '')

function isSymlink(filePath: string): boolean {
  try {
    return fs.lstatSync(filePath).isSymbolicLink()
  } catch {
    return false
  }
}

function buildSymlinkPlan(): SymlinkPlan[] {
  return symlinkFiles.map(src => ({ from: src, to: transformPath(src) }))
}

Good:

const extractExtension = (filename: string) =>
  filename.replace(/.*symlink/, '')

function isSymlink(filePath: string) {
  try {
    return fs.lstatSync(filePath).isSymbolicLink()
  } catch {
    return false
  }
}

function buildSymlinkPlan() {
  return symlinkFiles.map(src => ({ from: src, to: transformPath(src) }))
}

Why:

  • .replace()
    returns
    string
    - TypeScript knows this
  • return true/false
    is clearly
    boolean
  • .map()
    with object literal - TypeScript infers the array type

Use .map() Instead of Imperative Loops

Bad:

function buildSymlinkPlan(): SymlinkPlan[] {
  const plan: SymlinkPlan[] = []

  for (const src of symlinkFiles) {
    plan.push({ from: src, to: transformPath(src) })
  }

  return plan
}

Good:

function buildSymlinkPlan() {
  return symlinkFiles.map(src => ({ from: src, to: transformPath(src) }))
}

Why:

  • No need for explicit array type annotation
  • No need for return type annotation
  • More declarative, less noise
  • Compiler infers everything correctly

Remove Variable Type Annotations

Bad:

interface Logger {
  info: (msg: string) => void
  success: (msg: string) => void
  warn: (msg: string) => void
  error: (msg: string) => void
}

const log: Logger = {
  info: createLogFunction('info'),
  success: createLogFunction('success'),
  warn: createLogFunction('warn'),
  error: createLogFunction('error')
}

Good:

const log = {
  info: createLogFunction('info'),
  success: createLogFunction('success'),
  warn: createLogFunction('warn'),
  error: createLogFunction('error')
}

Why:

  • TypeScript infers the shape from the object literal
  • If you remove the annotation and the interface becomes unused, delete it
  • Less noise, same type safety

When to Keep Type Annotations

Do annotate:

  1. Function parameters - Always annotate (TypeScript can't infer these)
  2. At the source - Type variables early in the chain to help inference flow
  3. Public APIs - Types are documentation for exported functions
  4. When inference fails - If compiler infers
    any
    , you MUST declare explicitly

Example - Type at the source to enable inference:

// Bad: No type annotation, chain can't infer properly
function findFiles(rootDir: string) {
  const output = execSync(`find . -name '*.txt'`, { cwd: rootDir, encoding: 'utf-8' })
  return output.trim().split('\n').filter(line => line.length > 0)
}
// Compiler may not infer that output is string, causing downstream issues

// Good: Type the source variable, inference flows through the chain
function findFiles(rootDir: string) {
  const output: string = execSync(`find . -name '*.txt'`, { cwd: rootDir, encoding: 'utf-8' })
  return output.trim().split('\n').filter(line => line.length > 0)
}
// Now .trim() knows it's working with string, .split() returns string[], etc.

Don't annotate:

  1. Obvious return types - String methods, boolean literals, void functions
  2. Intermediate transformations - Once you've typed the source, let inference flow
  3. Collection operations -
    .map()
    ,
    .filter()
    preserve and transform types correctly

Benefits

  • Less code - Fewer type annotations to write and maintain
  • Easier refactoring - Change implementation, types update automatically
  • Better signal-to-noise - Annotations that remain are truly meaningful
  • Finds dead code - Unused interfaces become obvious when you stop annotating

2. Type Aliases for Busy Function Signatures

Rule: Only use type aliases when the function signature gets "busy" with type noise.

Simple Signatures - Keep Inline

Good:

function isSymlink(filePath: string) {
  return fs.lstatSync(filePath).isSymbolicLink()
}

function findSymlinkFiles(rootDir: string): string[] {
  const output = execSync('find . -name "*.txt"', { cwd: rootDir })
  return output.trim().split('\n')
}

These are clean - minimal noise, easy to read.

Busy Signatures - Use Type Alias

Bad:

function processFiles(
  rootDir: string,
  filter: (file: string, stats: fs.Stats) => boolean,
  transform: (content: string, path: string) => Promise<string>,
  onError: (error: Error, file: string) => void
): Promise<ProcessResult[]> {
  // implementation
}

Good:

type FileFilter = (file: string, stats: fs.Stats) => boolean
type FileTransform = (content: string, path: string) => Promise<string>
type ErrorHandler = (error: Error, file: string) => void
type ProcessFiles = (
  rootDir: string,
  filter: FileFilter,
  transform: FileTransform,
  onError: ErrorHandler
) => Promise<ProcessResult[]>

const processFiles: ProcessFiles = (rootDir, filter, transform, onError) => {
  // implementation
}

Why: The signature is now scannable - you see the parameter names clearly, and can check the type alias definitions separately if needed.

When to Use Type Aliases

Use type aliases when:

  • Function has 4+ parameters
  • Parameters have complex types (callbacks, generic types)
  • The same signature is used multiple times
  • The inline signature makes the function definition hard to scan

Don't use type aliases when:

  • Function has 1-3 simple parameters
  • Types are obvious (string, number, boolean)
  • Compiler can infer the return type
  • The inline signature is still readable

3. Accept Nullable Types, Handle Explicitly

Rule: Accept

T | null
in your type signatures, handle the null case explicitly with clear failure semantics.

Bad:

// Filter nulls early, pass only valid values
function buildPlan() {
  return files.map(transformPath).filter(path => path !== null)
}

function executePlan(plan: string[]) {
  // Silent assumption: all paths are valid
  return plan.map(path => createLink(path))
}

Good:

// Accept nullable type, handle explicitly
function buildPlan() {
  return files.map(file => ({
    from: file,
    to: transformPath(file) // Returns string | null
  }))
}

function executePlan(plan: Array<{from: string, to: string | null}>) {
  return plan.map(({ from, to }) => safeLink(from, to))
}

function safeLink(src: string, dest: string | null) {
  if (!dest) {
    // Explicit failure with error result
    return { from: src, to: '<unparseable>', success: false }
  }
  // Continue with valid dest
}

Why:

  • Explicit over silent - Failures are visible in results, not hidden by filtering
  • Push responsibility down - Let
    safeLink
    decide what to do with nulls (it's about safety!)
  • Better error tracking - You can count and report failures, not silently skip them

4. Export Discipline

Rule: Only export what's actually used by other modules. Everything else is internal implementation.

Bad:

// links.ts
export { setupSymlinks, buildSymlinkPlan, safeLink, transformPath }
export type { SymlinkPlan, LinkResult }

Check if these are imported anywhere:

$ rg "import.*buildSymlinkPlan"
# No results

$ rg "import.*safeLink"
# No results

Good:

// links.ts - Only export the public API
export { setupSymlinks }
export type { LinkResult }  // Used by consumers

Why:

  • Clear API surface - What's exported is your public interface
  • Easier refactoring - Internal functions can be renamed/removed freely
  • Finds dead code - Unused exports highlight unnecessary abstractions

Process:

  1. After writing a module, search for usages:
    rg "import.*functionName"
  2. Remove exports that have zero imports
  3. Keep only: (a) main public API functions, (b) types used by consumers

References