Skip to content

TypeScriptConventions

Allan Michay edited this page Dec 10, 2025 · 2 revisions

Discriminated Unions Over Conditional Props

Prefer discriminated unions to make prop relationships explicit and prevent optional prop ambiguity:

// ❌ Bad - Optional props create ambiguity
type Props = {
  authenticated: boolean;
  level?: "basic" | "admin";
};

// ✅ Good - Discriminated union makes the relationship clear
type Props =
  | { authenticated: true; level: "basic" | "admin" }
  | { authenticated: false };

This pattern:

  • Makes it clear that level is only relevant when authenticated is true
  • Provides better TypeScript inference and autocompletion
  • Prevents invalid prop combinations at compile time

Explicit Function Return Types

Always declare explicit return types for functions:

// ❌ Bad - Implicit return type
const calculateTotal = (items: Item[]) => {
  return items.reduce((sum, item) => sum + item.price, 0);
};

// ✅ Good - Explicit return type
const calculateTotal = (items: Item[]): number => {
  return items.reduce((sum, item) => sum + item.price, 0);
};

Benefits:

  • Catches errors early when implementation doesn't match expected type
  • Improves code readability and documentation
  • Makes refactoring safer

No Nested Ternaries

Avoid ternary expressions deeper than 2 levels:

// ❌ Bad - Nested ternary
const role = isAdmin ? (isManager ? "Manager" : "Admin") : "User";

// ✅ Good - Extract to function with early returns
function getRole(): string {
  if (!isAdmin) return "User";
  if (isManager) return "Manager";
  return "Admin";
}

const role = getRole();

Prefer Early Returns

Use early returns to reduce nesting and improve readability:

// ❌ Bad - Deeply nested
function getStatus(user: User): string {
  let status;
  if (user.isActive) {
    if (user.isAdmin) {
      status = "Admin";
    } else {
      status = "Active";
    }
  } else {
    status = "Inactive";
  }
  return status;
}

// ✅ Good - Early returns
function getStatus(user: User): string {
  if (!user.isActive) return "Inactive";
  if (user.isAdmin) return "Admin";
  return "Active";
}

Keep Logic Out of JSX

Extract complex logic above the JSX return:

// ❌ Bad - Logic embedded in JSX
return <div>{score > 80 ? "High" : score > 50 ? "Medium" : "Low"}</div>;

// ✅ Good - Logic extracted
let label: string;
if (score > 80) {
  label = "High";
} else if (score > 50) {
  label = "Medium";
} else {
  label = "Low";
}

return <div>{label}</div>;

Path Aliases

Use the ~/* path alias which maps to src/*:

// ✅ Good
import { Button } from "~/components/designSystem";
import { useCustomer } from "~/hooks/useCustomer";

// ❌ Avoid relative paths for deep imports
import { Button } from "../../../components/designSystem";

Type Imports

Use type imports when importing only types:

import type { Customer, Invoice } from "~/generated/graphql";
import { useGetCustomerQuery } from "~/generated/graphql";

Naming Conventions

Type Convention Example
Variables camelCase customerName
Functions camelCase getCustomer()
Components PascalCase CustomerCard
Types/Interfaces PascalCase CustomerProps
Constants SCREAMING_SNAKE_CASE MAX_RETRY_COUNT
Files (components) PascalCase CustomerCard.tsx
Files (utilities) camelCase formatCurrency.ts

Clone this wiki locally