Skip to content
Back to skills

CleanTypes

ASecurity

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

  • 666 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added February 8, 2026
developmenttypescriptgobashrefactoringapidocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add majiayu000/claude-skill-registry --skill cleanTypes --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of CleanTypes?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for CleanTypes
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-cleantypes/badge)](https://www.skillsdirectory.com/skills/majiayu000-cleantypes)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: cleanTypes
description: TypeScript type best practices - proper typing patterns, avoiding type complexity, meaningful type annotations
---

# 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:**
```typescript
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:**
```typescript
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:**
```typescript
function buildSymlinkPlan(): SymlinkPlan[] {
  const plan: SymlinkPlan[] = []

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

  return plan
}
```

**Good:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
// 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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
// 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:**
```typescript
// 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:**
```typescript
// links.ts
export { setupSymlinks, buildSymlinkPlan, safeLink, transformPath }
export type { SymlinkPlan, LinkResult }
```

Check if these are imported anywhere:
```bash
$ rg "import.*buildSymlinkPlan"
# No results

$ rg "import.*safeLink"
# No results
```

**Good:**
```typescript
// 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

- TypeScript Handbook: https://www.typescriptlang.org/docs/handbook/
- Effective TypeScript by Dan Vanderkam

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.