Back to skills
extension
Category: Productivity & OfficeNo API key required

TypeScript

Provide best-practice coding conventions and generate standards-compliant TypeScript code.

personAuthor: wscatshubclawhub

TypeScript Style Guide Skill

Activation: This skill activates whenever the user says or implies TypeScript. It responds with standards-compliant TypeScript code and can explain any rule on demand.


Table of Contents

  1. General Principles
  2. Naming Conventions
  3. Types & Interfaces
  4. Enums
  5. Variables & Constants
  6. Functions
  7. Classes
  8. Modules & Imports
  9. Generics
  10. Error Handling
  11. Async / Await & Promises
  12. Comments & Documentation
  13. Formatting & Style
  14. Null & Undefined Handling
  15. Type Assertions & Guards
  16. React & JSX (when applicable)
  17. Testing Conventions
  18. Tooling & Configuration

1. General Principles

  • Strict mode always: Enable "strict": true in tsconfig.json. This turns on noImplicitAny, strictNullChecks, strictFunctionTypes, and more.
  • Prefer readability over cleverness: Code is read far more often than written. Favour explicit, self-documenting code.
  • Minimise any: Treat every use of any as tech debt. Prefer unknown when the type is truly not known, or use generics.
  • Immutability by default: Use const over let; prefer readonly properties and ReadonlyArray<T> / Readonly<T> utility types.
  • Single responsibility: Each file, class, and function should have one clear purpose.
  • Keep files small: A file should ideally stay under 400 lines. If it grows larger, consider splitting it.

2. Naming Conventions

| Construct | Convention | Example | | ----------------------- | ----------------------- | -------------------------------- | | Variable / Function | camelCase | getUserName, isActive | | Boolean variable | camelCase with prefix | isLoading, hasAccess, canEdit | | Constant (module) | UPPER_SNAKE_CASE | MAX_RETRY_COUNT, API_BASE_URL| | Constant (local) | camelCase | const defaultTimeout = 3000 | | Class | PascalCase | UserService, HttpClient | | Interface | PascalCase | UserProfile, ApiResponse | | Type alias | PascalCase | UserId, Theme | | Enum | PascalCase | Direction, HttpStatus | | Enum member | PascalCase | Direction.Up, HttpStatus.Ok | | Generic parameter | Single uppercase letter or descriptive PascalCase | T, TKey, TValue | | File name | kebab-case.ts | user-service.ts, api-client.ts| | Test file name | kebab-case.test.ts or kebab-case.spec.ts | user-service.test.ts | | React component file| PascalCase.tsx | UserProfile.tsx | | Private field | camelCase (no _ prefix) | private count: number |

Additional Naming Rules

  • Do NOT prefix interfaces with I (e.g., ~~IUser~~ → User).
  • Do NOT suffix types/interfaces with Type or Interface.
  • Use descriptive names. Avoid single-letter variables except for conventional usages like loop indices (i, j) or generic type parameters (T, K, V).
  • Acronyms of 2 characters stay uppercase (IO, ID); 3+ characters use PascalCase (Http, Xml, Api).

3. Types & Interfaces

Prefer interface for Object Shapes

// ✅ Good — use interface for object shapes
interface User {
  readonly id: string;
  name: string;
  email: string;
}

// ✅ Good — use type for unions, intersections, mapped types
type Status = 'active' | 'inactive' | 'suspended';
type Result<T> = Success<T> | Failure;

When to Use type vs interface

| Use interface when … | Use type when … | | ---------------------------------------------- | ------------------------------------------ | | Defining the shape of an object or class | Creating union or intersection types | | You want declaration merging | Using mapped / conditional types | | Extending other interfaces | Aliasing primitive or tuple types |

Rules

  • Always annotate function return types explicitly for public APIs.
  • Let TypeScript infer types for local variables when the type is obvious.
  • Prefer type over interface for function signatures:
    type Comparator<T> = (a: T, b: T) => number;
    
  • Avoid empty interfaces. If extending, include at least a comment explaining intent.
  • Use Record<K, V> instead of { [key: string]: V }.
  • Use utility types (Partial<T>, Pick<T, K>, Omit<T, K>, Required<T>) to derive types rather than duplicating.

4. Enums

Prefer String Enums

// ✅ Good — string enum for readability and debuggability
enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

When to Use const Enum or Union Types

// ✅ Good — const enum for zero-runtime-cost enums (no reverse mapping needed)
const enum HttpMethod {
  Get = 'GET',
  Post = 'POST',
  Put = 'PUT',
  Delete = 'DELETE',
}

// ✅ Also good — string literal union (simpler, tree-shakeable)
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

Rules

  • Do NOT use numeric enums without explicit values (auto-increment is fragile).
  • Prefer string literal union types over enums when the set is small and no namespace features are needed.
  • Never mix string and numeric members in the same enum.

5. Variables & Constants

// ✅ Good
const maxRetries = 3;
const baseUrl = 'https://api.example.com';
let currentAttempt = 0;

// ❌ Bad — using var
var legacyValue = 'old';

// ❌ Bad — using let when value never changes
let neverReassigned = 42;

Rules

  • Always use const unless the variable needs reassignment; then use let.
  • Never use var.
  • Declare variables as close to their first usage as possible.
  • One variable declaration per statement.
  • Prefer destructuring for extracting properties:
    // ✅ Good
    const { name, age } = user;
    
    // ❌ Bad
    const name = user.name;
    const age = user.age;
    
  • Use as const for immutable literal objects / arrays:
    const ROUTES = {
      home: '/',
      about: '/about',
      contact: '/contact',
    } as const;
    

6. Functions

Function Declarations

// ✅ Good — explicit return type for public functions
function calculateTotal(items: CartItem[]): number {
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}

// ✅ Good — arrow function for callbacks / short lambdas
const double = (n: number): number => n * 2;

// ✅ Good — use default parameters instead of optional + fallback
function createUser(name: string, role: Role = Role.Viewer): User {
  // ...
}

Rules

  • Annotate return types for all exported / public functions.
  • Prefer arrow functions for callbacks and inline functions.
  • Prefer function declarations for top-level named functions (they are hoisted and more readable in stack traces).
  • Maximum parameters: 3. If more are needed, group them into an options object:
    // ✅ Good
    interface CreateUserOptions {
      name: string;
      email: string;
      role?: Role;
      department?: string;
    }
    function createUser(options: CreateUserOptions): User { ... }
    
    // ❌ Bad — too many positional parameters
    function createUser(name: string, email: string, role: Role, dept: string): User { ... }
    
  • Do not use arguments; use rest parameters (...args) instead.
  • Avoid Function type. Use specific function signatures.
  • Keep functions small — ideally under 30 lines of logic.
  • Functions should do one thing.
  • Prefer early returns to reduce nesting:
    // ✅ Good
    function getDiscount(user: User): number {
      if (!user.isPremium) return 0;
      if (user.yearsActive < 2) return 5;
      return 10;
    }
    

7. Classes

// ✅ Good
class UserService {
  private readonly repository: UserRepository;

  constructor(repository: UserRepository) {
    this.repository = repository;
  }

  async findById(id: string): Promise<User | undefined> {
    return this.repository.get(id);
  }
}

Rules

  • Prefer composition over inheritance.
  • Use readonly for properties that should not change after construction.
  • Member ordering:
    1. Static fields
    2. Instance fields
    3. Constructor
    4. Static methods
    5. Public methods
    6. Protected methods
    7. Private methods
  • Use explicit access modifiers (public, protected, private) on every member.
  • Do NOT prefix private members with _. TypeScript's private keyword is sufficient.
  • Use # (ES private fields) when true runtime privacy is required.
  • Prefer parameter properties for simple constructor-only assignments:
    class Logger {
      constructor(private readonly prefix: string) {}
    }
    
  • Avoid classes with only static methods — use plain functions in a module instead.
  • Implement interfaces explicitly:
    class UserRepositoryImpl implements UserRepository { ... }
    

8. Modules & Imports

// ✅ Good — named exports
export function parseConfig(raw: string): Config { ... }
export interface Config { ... }

// ✅ Good — re-export barrel file (index.ts)
export { parseConfig } from './parse-config';
export type { Config } from './parse-config';

Rules

  • Prefer named exports over default exports (better refactoring, auto-import support).
  • Use default exports only for React components if the project convention requires it.
  • Use import type / export type for type-only imports to help bundlers tree-shake:
    import type { User } from './user';
    
  • Group and order imports:
    1. Node built-ins (node:fs, node:path)
    2. External packages (react, lodash)
    3. Internal aliases (@/utils, @/components)
    4. Relative parent (../)
    5. Relative sibling (./)
  • Separate each group with a blank line.
  • Do NOT use wildcard imports (import * as) unless re-exporting a module namespace.
  • Keep barrel files (index.ts) thin — do not add logic.

9. Generics

// ✅ Good — descriptive when intent is ambiguous
function merge<TTarget, TSource>(target: TTarget, source: TSource): TTarget & TSource {
  return { ...target, ...source };
}

// ✅ Good — single-letter when intent is obvious
function identity<T>(value: T): T {
  return value;
}

// ✅ Good — constrained generics
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

Rules

  • Use a single uppercase letter (T, U, K, V) for simple generics.
  • Use descriptive PascalCase prefixed with T (TKey, TValue, TResult) when there are multiple type parameters or the purpose is not obvious.
  • Always add constraints when possible (<T extends SomeType>).
  • Avoid unnecessary generics — if a type parameter is used only once, it's likely not needed.
  • Prefer generic utility types (Array<T>, Promise<T>, Map<K, V>) over their shorthand where readability benefits.

10. Error Handling

// ✅ Good — custom error class
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
  ) {
    super(message);
    this.name = 'AppError';
  }
}

// ✅ Good — typed error handling
function parseJson<T>(raw: string): T {
  try {
    return JSON.parse(raw) as T;
  } catch (error) {
    throw new AppError(
      `Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`,
      'PARSE_ERROR',
      400,
    );
  }
}

Rules

  • Never swallow errors silently (empty catch blocks).
  • Use custom error classes that extend Error for domain-specific errors.
  • Type the catch variable as unknown (default in TS 4.4+) and narrow before use.
  • Prefer Result / Either patterns for expected failures in libraries:
    type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
    
  • Always clean up resources in finally blocks or use disposable patterns.
  • Log errors with sufficient context (operation name, input summary, stack trace).

11. Async / Await & Promises

// ✅ Good — async/await
async function fetchUser(id: string): Promise<User> {
  const response = await httpClient.get<User>(`/users/${id}`);
  return response.data;
}

// ✅ Good — parallel execution
async function fetchDashboard(userId: string): Promise<Dashboard> {
  const [user, posts, notifications] = await Promise.all([
    fetchUser(userId),
    fetchPosts(userId),
    fetchNotifications(userId),
  ]);
  return { user, posts, notifications };
}

Rules

  • Prefer async/await over .then() chains for readability.
  • Always annotate return types as Promise<T>.
  • Use Promise.all() for independent concurrent operations.
  • Use Promise.allSettled() when failures should not abort sibling operations.
  • Never use new Promise() when an async function suffices (avoid the explicit-construction antipattern).
  • Avoid async void functions — they cannot be await-ed and swallow errors. Exception: event handlers where the framework requires void.
  • Prefer for...of with await for sequential async iteration, not forEach.

12. Comments & Documentation

/**
 * Calculate the compound interest for a principal amount.
 *
 * @param principal - The initial amount of money.
 * @param rate - The annual interest rate (decimal, e.g. 0.05 for 5%).
 * @param times - Number of times interest is compounded per year.
 * @param years - Number of years the money is invested.
 * @returns The total amount after compound interest.
 *
 * @example
 * ```typescript
 * const total = compoundInterest(1000, 0.05, 12, 10);
 * // total ≈ 1647.01
 * ```
 */
function compoundInterest(
  principal: number,
  rate: number,
  times: number,
  years: number,
): number {
  return principal * Math.pow(1 + rate / times, times * years);
}

Rules

  • Use TSDoc (/** */) for all public APIs, exported functions, classes, interfaces, and types.
  • Include @param, @returns, @throws, and @example tags where applicable.
  • Do NOT comment obvious code. The code should be self-documenting.
  • Use // TODO: with a ticket number for planned improvements.
  • Use // FIXME: for known issues that need resolution.
  • Use // HACK: for workarounds that should be revisited.
  • Never leave commented-out code in the main branch.
  • Place file-level comments at the top if the module's purpose is not obvious from its name.
  • Keep comments up-to-date with code changes.

13. Formatting & Style

General

  • Indentation: 2 spaces (no tabs).
  • Semicolons: Required at the end of every statement.
  • Quotes: Single quotes (') for strings; backticks (`) for template literals.
  • Trailing commas: Always use in multi-line constructs (arrays, objects, parameters, generics).
  • Max line length: 100 characters (soft limit), 120 characters (hard limit).
  • Braces: Required for all control structures, even single-line bodies.
  • Blank lines: One blank line between top-level declarations; no multiple consecutive blank lines.

Specific Patterns

// ✅ Good — braces always, even for single-line if
if (isValid) {
  process();
}

// ❌ Bad
if (isValid) process();

// ✅ Good — trailing comma
const config = {
  host: 'localhost',
  port: 3000,
  debug: true,
};

// ✅ Good — consistent object shorthand
const name = 'Alice';
const user = { name, age: 30 };

Tooling

  • Use Prettier for auto-formatting with the following baseline config:
    {
      "semi": true,
      "singleQuote": true,
      "trailingComma": "all",
      "printWidth": 100,
      "tabWidth": 2,
      "arrowParens": "always"
    }
    
  • Use ESLint with @typescript-eslint/eslint-plugin and @typescript-eslint/parser for linting.

14. Null & Undefined Handling

// ✅ Good — optional chaining
const city = user?.address?.city;

// ✅ Good — nullish coalescing
const displayName = user.nickname ?? user.name ?? 'Anonymous';

// ✅ Good — explicit null checks for critical paths
function getUser(id: string): User {
  const user = repository.findById(id);
  if (user === undefined) {
    throw new AppError(`User not found: ${id}`, 'USER_NOT_FOUND', 404);
  }
  return user;
}

Rules

  • Enable strictNullChecks (included in strict mode).
  • Prefer undefined over null as the "absence of value" indicator, unless interacting with external APIs that use null.
  • Use optional chaining (?.) and nullish coalescing (??) instead of manual checks.
  • Do NOT use non-null assertion (!) unless you can prove the value is always defined (add a comment explaining why).
  • Prefer the satisfies operator (TS 5.0+) to validate types without widening.

15. Type Assertions & Guards

// ✅ Good — type guard function
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'name' in value
  );
}

// ✅ Good — discriminated union
interface Success<T> {
  kind: 'success';
  data: T;
}
interface Failure {
  kind: 'failure';
  error: Error;
}
type Result<T> = Success<T> | Failure;

function handleResult<T>(result: Result<T>): T {
  switch (result.kind) {
    case 'success':
      return result.data;
    case 'failure':
      throw result.error;
  }
}

Rules

  • Prefer type guards (is keyword) over type assertions (as).
  • Use as const for literal narrowing, not as SpecificType.
  • Use discriminated unions with a kind / type field for state machines and result types.
  • Avoid as unknown as T double assertions — they are almost always a design smell.
  • When assertions are necessary, add a comment justifying them.
  • Use satisfies for type validation without assertion:
    const palette = {
      red: [255, 0, 0],
      green: '#00ff00',
    } satisfies Record<string, string | number[]>;
    

16. React & JSX (when applicable)

// ✅ Good — functional component with explicit props type
interface UserCardProps {
  readonly user: User;
  readonly onSelect?: (userId: string) => void;
}

export function UserCard({ user, onSelect }: UserCardProps): React.ReactElement {
  const handleClick = useCallback(() => {
    onSelect?.(user.id);
  }, [onSelect, user.id]);

  return (
    <div className="user-card" onClick={handleClick} role="button" tabIndex={0}>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}

Rules

  • Use function declarations or named function expressions for components (not anonymous).
  • Define props as an interface (e.g., UserCardProps), not inline.
  • Mark all props as readonly.
  • Do NOT use React.FC — it is discouraged since React 18.
  • Use React.ReactElement or React.ReactNode as return type annotation.
  • Colocate styles, tests, and types with the component when practical.
  • Prefer controlled components over uncontrolled.
  • Memoize expensive computations with useMemo, stable callbacks with useCallback.
  • Extract custom hooks when logic is reused across components.
  • Component file structure:
    1. Imports
    2. Types / Interfaces
    3. Constants
    4. Component function
    5. Helper functions (private to the module)

17. Testing Conventions

// ✅ Good — descriptive test structure
describe('UserService', () => {
  describe('findById', () => {
    it('should return the user when the id exists', async () => {
      const user = await service.findById('user-1');
      expect(user).toEqual(expect.objectContaining({ id: 'user-1' }));
    });

    it('should return undefined when the id does not exist', async () => {
      const user = await service.findById('non-existent');
      expect(user).toBeUndefined();
    });
  });
});

Rules

  • Test files are colocated or in a __tests__ directory.
  • Name test files *.test.ts or *.spec.ts.
  • Follow the Arrange → Act → Assert pattern.
  • One assertion concept per test (multiple expect calls are okay if they test one logical assertion).
  • Use descriptive test names that read like a sentence.
  • Mock external dependencies; do NOT mock the module under test.
  • Prefer dependency injection to make code testable.
  • Aim for high test coverage on business logic; don't chase 100% on boilerplate.
  • Use jest.fn() or equivalent for spies/mocks; type them properly:
    const mockFetch = jest.fn<Promise<User>, [string]>();
    

18. Tooling & Configuration

Recommended tsconfig.json (Baseline)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

Recommended ESLint Rules

| Rule | Setting | | ---------------------------------------- | --------- | | @typescript-eslint/no-explicit-any | error | | @typescript-eslint/explicit-function-return-type | warn (for exported functions) | | @typescript-eslint/no-unused-vars | error | | @typescript-eslint/consistent-type-imports | error | | @typescript-eslint/no-non-null-assertion | warn | | @typescript-eslint/prefer-nullish-coalescing | error | | @typescript-eslint/prefer-optional-chain | error | | @typescript-eslint/strict-boolean-expressions | warn | | @typescript-eslint/naming-convention | error (configured per construct) |


Usage

Natural Language Activation

Simply mention TypeScript in your conversation — the skill activates automatically.

Example prompts:

| Prompt | Skill Response | |---|---| | "Write a TypeScript function to merge two objects deeply" | Generates a fully typed, standards-compliant deepMerge<T, U>() function following all rules above. | | "Review my TypeScript code for style issues" | Analyses the provided code against this guide and suggests improvements. | | "Convert this JavaScript to TypeScript" | Converts code, adds strict types, interfaces, and follows all naming / formatting conventions. | | "What's the TypeScript best practice for error handling?" | Explains the Result pattern, custom error classes, and typed catch blocks per §10. | | "Create a TypeScript React component for a data table" | Generates a well-typed functional component following §16 React rules with proper props interface. | | "Set up a new TypeScript project" | Provides tsconfig.json, ESLint config, Prettier config, and project structure following §18. |

Inline Rule References

You can ask about any specific section:

  • "Explain TypeScript naming conventions" → References §2
  • "How should I handle null in TypeScript?" → References §14
  • "Show me TypeScript generic best practices" → References §9