typescript-strict

19
2
Source

TypeScript strict mode patterns. Use when writing any TypeScript code.

Install

mkdir -p .claude/skills/typescript-strict && curl -L -o skill.zip "https://mcp.directory/api/skills/download/1812" && unzip -o skill.zip -d .claude/skills/typescript-strict && rm skill.zip

Installs to .claude/skills/typescript-strict

About this skill

TypeScript Strict Mode

Core Rules

  1. No any - ever. Use unknown if type is truly unknown
  2. No type assertions (as Type) without justification
  3. Prefer type over interface for data structures
  4. Reserve interface for behavior contracts only

Schema Organization

Organize Schemas by Usage

Common patterns:

  • Centralized: src/schemas/ for shared schemas
  • Co-located: Near the modules that use them
  • Layered: Separate by architectural layer (if using layered/hexagonal architecture)

Key principle: Avoid duplicating the same validation logic across multiple files.

Gotcha: Schema Duplication

Common anti-pattern:

Defining the same schema in multiple places:

  • Validation logic duplicated across endpoints
  • Same business rules defined in multiple adapters
  • Type definitions not shared

Why This Is Wrong:

  • ❌ Duplication creates multiple sources of truth
  • ❌ Changes require updating multiple files
  • ❌ Breaks DRY principle at the knowledge level
  • ❌ Domain logic leaks into infrastructure code

Solution:

// ✅ CORRECT - Define schema once, import everywhere
// src/schemas/user-requests.ts
import { z } from 'zod';

export const CreateUserRequestSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
});

export type CreateUserRequest = z.infer<typeof CreateUserRequestSchema>;
// Use in multiple places
import { CreateUserRequestSchema } from '../schemas/user-requests.js';

// Express endpoint
app.post('/users', (req, res) => {
  const result = CreateUserRequestSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ error: result.error });
  }
  // Use result.data (validated)
});

// GraphQL resolver
const createUser = (input: unknown) => {
  const validated = CreateUserRequestSchema.parse(input);
  return userService.create(validated);
};

Key Benefits:

  • ✅ Single source of truth for validation
  • ✅ Schema changes propagate everywhere automatically
  • ✅ Type safety maintained across codebase
  • ✅ DRY principle at knowledge level

Remember: If validation logic is duplicated, extract it into a shared schema.


Dependency Injection Pattern

Inject Dependencies, Don't Create Them

The Rule:

  • Dependencies are always injected via parameters
  • Never use new to create dependencies inside functions
  • Factory functions accept dependencies as parameters

Why This Matters

Without dependency injection:

  • ❌ Only one implementation possible
  • ❌ Can't test with mocks (poor testability)
  • ❌ Tight coupling to specific implementations
  • ❌ Violates dependency inversion principle
  • ❌ Can't swap implementations

With dependency injection:

  • ✅ Any implementation works (in-memory, database, remote API)
  • ✅ Fully testable (inject mocks for testing)
  • ✅ Loose coupling
  • ✅ Follows dependency inversion principle
  • ✅ Runtime flexibility (configure implementation)

Example: Order Processor

❌ WRONG - Creating implementation internally

export const createOrderProcessor = ({
  paymentGateway,
}: {
  paymentGateway: PaymentGateway;
}): OrderProcessor => {
  // ❌ Hardcoded implementation!
  const orderRepository = new InMemoryOrderRepository();

  return {
    processOrder(order) {
      const payment = paymentGateway.charge(order.total);
      if (!payment.success) {
        return { success: false, error: payment.error };
      }

      orderRepository.save(order); // Using hardcoded repository
      return { success: true, data: order };
    },
  };
};

Why this is WRONG:

  • Only ONE repository implementation possible (in-memory)
  • Can't test with mock repository
  • Can't swap to database repository or remote API
  • Tight coupling to specific implementation

✅ CORRECT - Injecting all dependencies

export const createOrderProcessor = ({
  paymentGateway,  // ✅ Injected
  orderRepository, // ✅ Injected
}: {
  paymentGateway: PaymentGateway;
  orderRepository: OrderRepository;
}): OrderProcessor => {
  return {
    processOrder(order) {
      const payment = paymentGateway.charge(order.total);
      if (!payment.success) {
        return { success: false, error: payment.error };
      }

      orderRepository.save(order); // Delegate to injected dependency
      return { success: true, data: order };
    },
  };
};

Why this is CORRECT:

  • ✅ Any OrderRepository implementation works (in-memory, PostgreSQL, MongoDB)
  • ✅ Any PaymentGateway implementation works (Stripe, mock, testing)
  • ✅ Easy to test (inject mocks)
  • ✅ Loose coupling (depends on interfaces, not implementations)
  • ✅ Runtime flexibility (choose implementation at startup)

Type vs Interface - Understanding WHY

The choice between type and interface is architectural, not stylistic.

Behavior Contracts → Use interface

When to use: Interfaces define contracts that must be implemented.

Examples: UserRepository, PaymentGateway, EmailService, CacheProvider

Why interface for behavior contracts?

  1. Signals implementation contracts clearly

    • Interface communicates "this must be implemented elsewhere"
    • Type communicates "this is a data structure"
  2. Better TypeScript errors when implementing

    • class X implements UserRepository gives clear errors
    • Types don't have implements keyword
  3. Conventional for dependency injection

    • Standard pattern for dependency inversion
    • Clear separation between contract and implementation
  4. Class-friendly for implementations

    • Many libraries use classes for services
    • Classes naturally implement interfaces

Example:

// Behavior contract
export interface UserRepository {
  findById(id: string): Promise<User | undefined>;
  save(user: User): Promise<void>;
  delete(id: string): Promise<void>;
}

// Concrete implementation
export class PostgresUserRepository implements UserRepository {
  async findById(id: string): Promise<User | undefined> {
    // Implementation
  }
  // ... other methods
}

Data Structures → Use type

When to use: Types define immutable data structures.

Examples: User, Order, Config, ApiResponse

Why type for data?

  1. Emphasizes immutability

    • Types with readonly signal "don't mutate this"
    • Functional programming alignment
  2. Better for unions, intersections, mapped types

    • type Result<T, E> = Success<T> | Failure<E>
    • type Partial<T> = { [P in keyof T]?: T[P] }
  3. Prevents accidental mutations

    • readonly properties enforce immutability at type level
    • Compiler catches mutation attempts
  4. More flexible composition

    • Easier to compose with utility types
    • Better inference in complex scenarios

Example:

// Data structure
export type User = {
  readonly id: string;
  readonly email: string;
  readonly name: string;
  readonly roles: ReadonlyArray<string>;
};

export type Order = {
  readonly id: string;
  readonly userId: string;
  readonly items: ReadonlyArray<OrderItem>;
  readonly total: number;
};

Architectural Pattern

This pattern supports clean architecture:

  • Behavior contracts (interface) = Boundaries between layers
  • Data structures (type) = Data flowing through the system
  • Business logic depends on interfaces, not implementations
  • Data is immutable (types with readonly)

Strict Mode Configuration

tsconfig.json Settings

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noPropertyAccessFromIndexSignature": true,
    "forceConsistentCasingInFileNames": true,
    "allowUnusedLabels": false
  }
}

What Each Setting Does

Core strict flags:

  • strict: true - Enables all strict type checking options
  • noImplicitAny - Error on expressions/declarations with implied any type
  • strictNullChecks - null and undefined have their own types (not assignable to everything)
  • noUnusedLocals - Error on unused local variables
  • noUnusedParameters - Error on unused function parameters
  • noImplicitReturns - Error when not all code paths return a value
  • noFallthroughCasesInSwitch - Error on fallthrough cases in switch statements

Additional safety flags (CRITICAL):

  • noUncheckedIndexedAccess - Array/object access returns T | undefined (prevents runtime errors from assuming elements exist)
  • exactOptionalPropertyTypes - Distinguishes property?: T from property: T | undefined (more precise types)
  • noPropertyAccessFromIndexSignature - Requires bracket notation for index signature properties (forces awareness of dynamic access)
  • forceConsistentCasingInFileNames - Prevents case sensitivity issues across operating systems
  • allowUnusedLabels - Error on unused labels (catches accidental labels that do nothing)

Additional Rules

  • No @ts-ignore without explicit comments explaining why
  • These rules apply to test code as well as production code

Architectural Insight: noUnusedParameters Catches Design Issues

The noUnusedParameters rule can reveal architectural problems:

Example: A function with an unused parameter often indicates the parameter belongs in a different layer. Strict mode catches these design issues early.


Immutability Patterns

Use readonly on All Data Structures

// ✅ CORRECT - Immutable data structure
type ApiRequest = {
  readonly method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
  readonly url: string;
  readonly headers?: {
    readonly [key: string]: string;
  };
  readonly body?: unknown;
};

// ❌ WRONG - Mutable data structure
typ

---

*Content truncated.*

You might also like

flutter-development

aj-geddes

Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.

1,5701,369

ui-ux-pro-max

nextlevelbuilder

"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."

1,1161,190

drawio-diagrams-enhanced

jgtolentino

Create professional draw.io (diagrams.net) diagrams in XML format (.drawio files) with integrated PMP/PMBOK methodologies, extensive visual asset libraries, and industry-standard professional templates. Use this skill when users ask to create flowcharts, swimlane diagrams, cross-functional flowcharts, org charts, network diagrams, UML diagrams, BPMN, project management diagrams (WBS, Gantt, PERT, RACI), risk matrices, stakeholder maps, or any other visual diagram in draw.io format. This skill includes access to custom shape libraries for icons, clipart, and professional symbols.

1,4181,109

godot

bfollington

This skill should be used when working on Godot Engine projects. It provides specialized knowledge of Godot's file formats (.gd, .tscn, .tres), architecture patterns (component-based, signal-driven, resource-based), common pitfalls, validation tools, code templates, and CLI workflows. The `godot` command is available for running the game, validating scripts, importing resources, and exporting builds. Use this skill for tasks involving Godot game development, debugging scene/resource files, implementing game systems, or creating new Godot components.

1,193747

nano-banana-pro

garg-aayush

Generate and edit images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Use when the user asks to generate, create, edit, modify, change, alter, or update images. Also use when user references an existing image file and asks to modify it in any way (e.g., "modify this image", "change the background", "replace X with Y"). Supports both text-to-image generation and image-to-image editing with configurable resolution (1K default, 2K, or 4K for high resolution). DO NOT read the image file first - use this skill directly with the --input-image parameter.

1,153683

pdf-to-markdown

aliceisjustplaying

Convert entire PDF documents to clean, structured Markdown for full context loading. Use this skill when the user wants to extract ALL text from a PDF into context (not grep/search), when discussing or analyzing PDF content in full, when the user mentions "load the whole PDF", "bring the PDF into context", "read the entire PDF", or when partial extraction/grepping would miss important context. This is the preferred method for PDF text extraction over page-by-page or grep approaches.

1,311614

Stay ahead of the MCP ecosystem

Get weekly updates on new skills and servers.