Skip to content

Feature Proposal: typescript-best-practices #105

@jtannas

Description

@jtannas

TL;DR

Proposing typescript-best-practices skill to complement react-best-practices. Would maintainers be open to this?

Overview

A new skill category focused on TypeScript type system best practices. This would complement existing skills like react-best-practices by providing comprehensive guidance on TypeScript-specific patterns, with emphasis on types, interfaces, utility types, and type safety.

Skill Name: typescript-best-practices

Description: Review TypeScript code for type system correctness, safety, and best practices. Focuses on proper usage of types, interfaces, utility types, generics, and type narrowing to catch type-related bugs before runtime.

Trigger Phrases:

  • "Review my TypeScript types"
  • "Check TypeScript type safety"
  • "Audit TypeScript code for type issues"
  • "Improve my TypeScript types"

Scope

Targets TypeScript's type system and type-level programming only - not runtime behavior, frameworks, or tooling. Emphasizes rules impractical for standard linters.

Sample Rules

1. Avoid `any`, Prefer `unknown`

Severity: CRITICAL
Category: Type Safety

Issue: Using any completely disables type checking and undermines TypeScript's safety guarantees.

// ❌ BAD: Type checking disabled
function processData(data: any) {
  console.log(data.name.toUpperCase()); // No error even if data.name doesn't exist
}

// ✅ GOOD: Use unknown with type guards
function processData(data: unknown) {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    const record = data as { name: unknown };
    if (typeof record.name === 'string') {
      console.log(record.name.toUpperCase());
    }
  }
}

Why: unknown forces explicit type checking, catching runtime errors at compile time. Use any only during JS migration.

2. Use Interfaces for Object Inheritance

Severity: HIGH
Category: Type Definitions

Issue: Type intersections with & are less performant than interface extension.

// ❌ AVOID: Type intersection (less performant)
type User = { id: string; name: string };
type UserWithEmail = User & { email: string };

// ✅ PREFERRED: Interface extension (better performance)
interface User {
  id: string;
  name: string;
}
interface UserWithEmail extends User {
  email: string;
}

Why: TypeScript caches interface relationships, improving performance. Use interface for inheritance, type for unions/intersections.

3. Leverage Utility Types Instead of Manual Duplication

Severity: MEDIUM
Category: Type Reusability

Issue: Manually duplicating or partially redefining types is error-prone and harder to maintain.

// ❌ BAD: Manual duplication
interface User {
  id: string;
  name: string;
  email: string;
  role: string;
}
interface UserUpdate {
  id: string;
  name?: string;
  email?: string;
  role?: string;
}

// ✅ GOOD: Use utility types
interface User {
  id: string;
  name: string;
  email: string;
  role: string;
}
type UserUpdate = Required<Pick<User, 'id'>> & Partial<Omit<User, 'id'>>;

Why: Utility types maintain consistency with source types. Changes automatically propagate.

4. Enable Strict Null Checks

Severity: CRITICAL
Category: Compiler Configuration

Issue: Without strictNullChecks, null and undefined can be assigned to any type, causing runtime errors.

// With strictNullChecks: off (dangerous)
function getLength(str: string): number {
  return str.length; // Runtime error if str is null!
}

// With strictNullChecks: on (safe)
function getLength(str: string | null): number {
  if (str === null) {
    return 0;
  }
  return str.length; // TypeScript ensures str is not null here
}

Why: Forces explicit null/undefined handling, preventing null reference errors.

5. Use Type Guards for Runtime Type Narrowing

Severity: HIGH
Category: Type Narrowing

Issue: Type assertions (as) bypass type checking and can lead to runtime errors.

// ❌ BAD: Type assertion without validation
function processUser(data: unknown) {
  const user = data as User; // Dangerous - no runtime check
  console.log(user.name.toUpperCase());
}

// ✅ GOOD: Type guard with validation
function isUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    'name' in data &&
    typeof (data as any).id === 'string' &&
    typeof (data as any).name === 'string'
  );
}

function processUser(data: unknown) {
  if (isUser(data)) {
    console.log(data.name.toUpperCase()); // Type-safe
  }
}

Why: Provides runtime validation with safe type narrowing.

References

Next Steps

If this proposal is accepted, I'd be happy to submit a PR to start building out the rule set. I'm not a TypeScript expert but there's enough low hanging fruit to get a lot going.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions