react-spa-template/docs/typescript-conventions.md
2025-04-19 19:24:27 +02:00

5.4 KiB

TypeScript Conventions

This document outlines our TypeScript conventions for the project.

Configuration

We use strict TypeScript settings to ensure type safety:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Naming Conventions

Files and Directories

  • Use kebab-case for file and directory names: product-list.tsx, view-model.ts
  • Exception: React components can use PascalCase: ProductCard.tsx

Variables and Functions

  • Use camelCase for variables and functions: productList, fetchProducts
  • Use PascalCase for React components and types: ProductList, ProductDto
  • Use ALL_CAPS for constants: MAX_ITEMS, API_URL

Effector Naming

  • Prefix store names with $: $products, $isLoading
  • Suffix effect names with Fx: fetchProductsFx, saveUserFx
  • Use descriptive names for events: setProductFilter, resetForm

Type Definitions

Interfaces vs. Types

  • Use interface for object shapes that might be extended
  • Use type for unions, intersections, and simple object shapes
// Interface for extendable objects
interface User {
  id: string;
  name: string;
}

interface AdminUser extends User {
  permissions: string[];
}

// Type for unions and simple objects
type Status = 'idle' | 'loading' | 'success' | 'error';

type ProductFilter = {
  category?: string;
  minPrice?: number;
  maxPrice?: number;
};

API Models

  • Use the generated types from the API client
  • Create wrapper types when needed for additional properties
import type { productCompositeDto } from '@lib/api/merchant/models/productCompositeDto';

// Extended type with UI-specific properties
type ProductWithUIState = productCompositeDto & {
  isSelected: boolean;
  isExpanded: boolean;
};

React Component Types

Functional Components

import React from 'react';

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
}

export const Button: React.FC<ButtonProps> = ({
  label,
  onClick,
  disabled = false
}) => {
  return (
    <button 
      onClick={onClick} 
      disabled={disabled}
      className="btn"
    >
      {label}
    </button>
  );
};

Event Handlers

// Form event
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  // ...
};

// Input change
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const { name, value } = e.target;
  // ...
};

// Click event
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  // ...
};

Effector Types

Events

import { createEvent } from 'effector';

// Event with no payload
export const resetFilter = createEvent();

// Event with payload
export const setSearchQuery = createEvent<string>();

// Event with complex payload
export const updateFilter = createEvent<{
  category?: string;
  minPrice?: number;
  maxPrice?: number;
}>();

Stores

import { createStore } from 'effector';

// Define the store state type
interface FilterState {
  searchQuery: string;
  category: string | null;
  minPrice: number | null;
  maxPrice: number | null;
}

// Initial state
const initialState: FilterState = {
  searchQuery: '',
  category: null,
  minPrice: null,
  maxPrice: null
};

// Create the store
export const $filter = createStore<FilterState>(initialState);

Effects

import { createEffect } from 'effector';
import { ProductsViewsService } from '@lib/api/merchant/services/ProductsViewsService';
import type { productQueryRequestDto } from '@lib/api/merchant/models/productQueryRequestDto';
import type { productCompositeDto } from '@lib/api/merchant/models/productCompositeDto';

// Define effect with explicit parameter and return types
export const fetchProductsFx = createEffect<
  productQueryRequestDto,
  productCompositeDto[],
  Error
>(async (filter) => {
  const response = await ProductsViewsService.postApiProductCompositeQueryByCanonical({
    requestBody: filter
  });
  return Array.isArray(response) ? response : [response];
});

Best Practices

  1. Avoid any: Never use the any type. Use unknown if the type is truly unknown.

  2. Use type inference: Let TypeScript infer types when possible, but add explicit types for function parameters and return values.

  3. Null vs. undefined: Use undefined for optional values, null for intentionally absent values.

  4. Non-null assertion: Avoid using the non-null assertion operator (!) when possible. Use optional chaining (?.) and nullish coalescing (??) instead.

  5. Type guards: Use type guards to narrow types:

function isProduct(item: unknown): item is productCompositeDto {
  return (
    typeof item === 'object' &&
    item !== null &&
    'id' in item
  );
}
  1. Readonly: Use readonly for immutable properties:
interface Config {
  readonly apiUrl: string;
  readonly timeout: number;
}
  1. Generics: Use generics for reusable components and functions:
function fetchData<T>(url: string): Promise<T> {
  return fetch(url).then(res => res.json());
}

const data = await fetchData<User[]>('/api/users');