diff --git a/docs/STYLE_GUIDE.md b/docs/STYLE_GUIDE.md new file mode 100644 index 00000000..2a438e63 --- /dev/null +++ b/docs/STYLE_GUIDE.md @@ -0,0 +1,1363 @@ +# TypeScript/React Style Guide + +## Table of Contents + +1. [File Naming](#file-naming) +2. [TypeScript](#typescript) +3. [Naming Conventions](#naming-conventions) +4. [Imports](#imports) +5. [Types](#types) +6. [Functions](#functions) +7. [React](#react) +8. [Comments and Documentation](#comments-and-documentation) +9. [Styling](#styling) + +## File Naming + +Use `PascalCase` for React component files and `camelCase` for all other TypeScript files: + +```typescript +// Good +components / Button.tsx; +hooks / useSchedules.ts; +types / Course.ts; +lib / getSiteSupport.ts; +utils / formatDate.ts; + +// Bad +components / button.tsx; +hooks / UseSchedules.ts; +types / course.ts; +``` + +## TypeScript Conventions + +## TypeScript + +### Strict Mode + +Enable strict TypeScript checking: + +```json +{ + "compilerOptions": { + "strict": true, + "noUncheckedIndexedAccess": true, + "noFallthroughCasesInSwitch": true + } +} +``` + +### Type Annotations + +Prefer explicit return types for exported functions: + +```typescript +// Good +export function getCourseById(id: number): Course | undefined { + return courses.find(c => c.id === id); +} + +// Acceptable for simple cases +export function getCourseById(id: number) { + return courses.find(c => c.id === id); +} +``` + +### Type Imports + +Use `type` keyword for type-only imports: + +```typescript +// Good +import type { Course } from './types/Course'; +import type { UserSchedule } from './types/UserSchedule'; + +// Bad +import { Course } from './types/Course'; +``` + +### No `any` + +Never use `any`. Use `unknown` for truly unknown types: + +```typescript +// Good +function parseData(data: unknown): Course { + if (typeof data === 'object' && data !== null) { + return data as Course; + } + throw new Error('Invalid data'); +} + +// Bad +function parseData(data: any): Course { + return data; +} +``` + +### Enums + +Avoid enums. Use const objects with `as const`: + +```typescript +// Good +export const Status = { + OPEN: 'OPEN', + CLOSED: 'CLOSED', + WAITLISTED: 'WAITLISTED', +} as const; + +export type StatusType = (typeof Status)[keyof typeof Status]; + +// Bad +enum Status { + OPEN = 'OPEN', + CLOSED = 'CLOSED', +} +``` + +### `null` vs `undefined` + +Prefer `undefined` for optional values. Use `null` for explicit absence: + +```typescript +// Good +function findCourse(id: number): Course | undefined { + return courses.find(c => c.id === id); +} + +interface Props { + description?: string; // undefined if not provided +} + +// Use null for explicit "no value" state +let selectedCourse: Course | null = null; +``` + +## Naming Conventions + +### General Rules + +- `camelCase` for variables, functions, methods, parameters +- `PascalCase` for classes, interfaces, types, React components +- `UPPER_SNAKE_CASE` for global constants +- Avoid abbreviations unless widely understood + +### Variables and Functions + +```typescript +// Good +const courseList = []; +const activeSchedule = schedules[0]; +function getCourseById(id: number) {} +async function fetchCourses() {} + +// Bad +const CourseList = []; +const active_schedule = schedules[0]; +function GetCourseById(id: number) {} +``` + +### Constants + +```typescript +// Good +const MAX_COURSES = 10; +const API_URL = 'https://api.example.com'; +const CACHE_TTL = 3600; + +// Bad +const maxCourses = 10; +const apiUrl = 'https://api.example.com'; +``` + +### Types and Interfaces + +```typescript +// Good +interface UserSchedule {} +type StatusType = 'OPEN' | 'CLOSED'; +class Course {} + +// Bad +interface userSchedule {} +type status_type = 'OPEN' | 'CLOSED'; +class course {} +``` + +### Private Fields + +Prefix private fields with `#` or use TypeScript's `private`: + +```typescript +// Good +class Course { + #internalId: string; + private metadata: object; + + public get id(): string { + return this.#internalId; + } +} +``` + +## Imports + +### Import Order + +Organize imports in this order: + +1. External libraries +2. Internal absolute imports +3. Relative imports + +```typescript +// Good +import { useState, useEffect } from 'react'; +import clsx from 'clsx'; + +import { Course } from '@shared/types/Course'; +import { Button } from '@views/components/Button'; + +import styles from './Component.module.scss'; +import { helper } from './helper'; + +// Bad - mixed order +import styles from './Component.module.scss'; +import { useState } from 'react'; +import { Course } from '@shared/types/Course'; +``` + +### Module vs Destructured Imports + +Use destructured imports for specific exports: + +```typescript +// Good +import { useState, useEffect } from 'react'; +import { getCourseById, formatCourseName } from './utils'; + +// Use default imports when appropriate +import React from 'react'; +import clsx from 'clsx'; + +// Avoid +import * as React from 'react'; +``` + +### Side-Effect Imports + +Place side-effect imports at the top: + +```typescript +// Good +import 'uno.css'; +import './global.scss'; + +import React from 'react'; +import { Button } from './components/Button'; +``` + +## Types + +### Type vs Interface + +Use `interface` for object shapes that may be extended. Use `type` for unions, intersections, and utility types: + +```typescript +// Good - Interface for object shapes +interface Course { + id: number; + name: string; +} + +interface AdvancedCourse extends Course { + prerequisites: string[]; +} + +// Good - Type for unions and utilities +type Status = 'OPEN' | 'CLOSED' | 'WAITLISTED'; +type CourseOrSchedule = Course | Schedule; +type PartialCourse = Partial; +``` + +### Optional vs `undefined` + +Use optional properties instead of explicit `undefined`: + +```typescript +// Good +interface Props { + name: string; + description?: string; +} + +// Bad +interface Props { + name: string; + description: string | undefined; +} +``` + +### Array Types + +Use `T[]` for simple arrays, `Array` for complex types: + +```typescript +// Good +const numbers: number[] = [1, 2, 3]; +const courses: Course[] = []; + +// Use Array for readability with complex types +const callbacks: Array<(data: unknown) => void> = []; +const tuples: Array<[string, number]> = []; +``` + +### Type Assertions + +Avoid type assertions. Use type guards instead: + +```typescript +// Good +function isCourse(obj: unknown): obj is Course { + return typeof obj === 'object' && obj !== null && 'id' in obj && 'name' in obj; +} + +if (isCourse(data)) { + console.log(data.name); +} + +// Bad +const course = data as Course; +console.log(course.name); +``` + +### Generic Types + +Use descriptive names for generic type parameters: + +```typescript +// Good +function useState(initial: TState): [TState, (value: TState) => void] {} +function map(items: TInput[], fn: (item: TInput) => TOutput): TOutput[] {} + +// Acceptable for simple cases +function identity(value: T): T {} + +// Bad +function map(items: A[], fn: (item: A) => B): B[] {} +``` + +## Functions + +### Function Declarations + +Use function declarations for top-level functions: + +```typescript +// Good +export function getCourseById(id: number): Course | undefined { + return courses.find(c => c.id === id); +} + +// Bad +export const getCourseById = (id: number): Course | undefined => { + return courses.find(c => c.id === id); +}; +``` + +### Arrow Functions + +Use arrow functions for callbacks and short functions: + +```typescript +// Good +const doubled = numbers.map(n => n * 2); +const filtered = courses.filter(c => c.status === 'OPEN'); + +const handleClick = useCallback((event: MouseEvent) => { + console.log('Clicked'); +}, []); + +// Use function declarations for named functions +function processData(data: string) { + return data.trim(); +} +``` + +### Async/Await + +Prefer async/await over promise chains: + +```typescript +// Good +async function fetchCourse(id: number): Promise { + const response = await fetch(`/api/courses/${id}`); + const data = await response.json(); + return data; +} + +// Bad +function fetchCourse(id: number): Promise { + return fetch(`/api/courses/${id}`) + .then(response => response.json()) + .then(data => data); +} +``` + +### Default Parameters + +Use default parameters instead of checking for undefined: + +```typescript +// Good +function greet(name: string, greeting: string = 'Hello'): string { + return `${greeting}, ${name}!`; +} + +// Bad +function greet(name: string, greeting?: string): string { + const finalGreeting = greeting || 'Hello'; + return `${finalGreeting}, ${name}!`; +} +``` + +### Rest Parameters + +Place rest parameters last: + +```typescript +// Good +function sum(initial: number, ...numbers: number[]): number { + return initial + numbers.reduce((a, b) => a + b, 0); +} + +// Bad +function sum(...numbers: number[], initial: number): number { + return initial + numbers.reduce((a, b) => a + b, 0); +} +``` + +## React + +### Function Components + +Always use function components: + +```typescript +// Good +export default function Button({ + variant = 'filled', + children +}: Props): JSX.Element { + return ; +} + +// Bad +const Button: React.FC = ({ variant, children }) => { + return ; +}; +``` + +### Props + +Define props with interfaces: + +```typescript +// Good +interface ButtonProps { + variant?: 'filled' | 'outline'; + onClick?: (event: React.MouseEvent) => void; + disabled?: boolean; +} + +export function Button({ + variant = 'filled', + onClick, + disabled, + children +}: React.PropsWithChildren): JSX.Element { + return ; +} +``` + +### Hooks + +Custom hooks must start with `use`: + +```typescript +// Good +export function useWindowSize() { + const [size, setSize] = useState({ width: 0, height: 0 }); + + useEffect(() => { + const handleResize = () => { + setSize({ width: window.innerWidth, height: window.innerHeight }); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return size; +} +``` + +### Event Handlers + +Prefix event handlers with `handle`: + +```typescript +// Good +const handleClick = () => {}; +const handleSubmit = (event: FormEvent) => {}; +const handleChange = (value: string) => {}; + +// Bad +const onClick = () => {}; +const submit = () => {}; +const changed = () => {}; +``` + +### JSX + +Use self-closing tags when there are no children: + +```typescript +// Good + +``` + +Use fragments instead of div wrappers when possible: + +```typescript +// Good +<> +
+
+ + +// Bad +
+
+
+
+``` + +## Comments and Documentation + +### JSDoc + +Document all exported functions, classes, and complex types: + +```typescript +/** + * Calculates the total credit hours for a schedule. + * + * @param courses - The list of courses in the schedule + * @returns The total number of credit hours + */ +export function calculateTotalHours(courses: Course[]): number { + return courses.reduce((total, course) => total + course.creditHours, 0); +} +``` + +```typescript +/** + * Represents a university course. + */ +export interface Course { + /** Unique identifier for the course */ + id: number; + /** Full course name including department */ + name: string; + /** Number of credit hours */ + creditHours: number; +} +``` + +### Inline Comments + +Use inline comments sparingly for complex logic: + +```typescript +// Good - Explains non-obvious behavior +// Remove duplicate courses based on unique ID +const uniqueCourses = courses.filter((course, index, arr) => arr.findIndex(c => c.id === course.id) === index); + +// Bad - Obvious comment +// Add 1 to count +count = count + 1; +``` + +### TODO/FIXME + +Format action comments consistently: + +```typescript +// TODO: Implement pagination +// FIXME: Handle edge case when array is empty +// NOTE: This workaround is needed for Safari compatibility +``` + +## Styling + +### CSS Modules + +Use CSS/SCSS modules for component styles: + +```typescript +// Component.tsx +import styles from './Component.module.scss'; + +export function Component() { + return
Content
; +} +``` + +```scss +// Component.module.scss +.container { + padding: 1rem; + background: white; +} +``` + +### Modern SCSS + +Use `@use` instead of `@import`: + +```scss +// Good +@use 'colors.module.scss'; + +.text { + color: colors.$primary; +} + +// Bad +@import 'colors.module.scss'; +``` + +### Conditional Classes + +Use `clsx` for conditional classes: + +```typescript +import clsx from 'clsx'; + +; +} + +// Bad +const Button = (props) => { + return ; +}; +``` + +### Props Definition + +Define props interfaces clearly with JSDoc comments: + +```typescript +interface Props { + /** The variant style of the button */ + variant?: 'filled' | 'outline' | 'minimal'; + /** The size of the button */ + size?: 'regular' | 'small' | 'mini'; + /** The theme color for the button */ + color: ThemeColor; + /** Additional CSS classes */ + className?: string; + /** Click handler */ + onClick?: (event: React.MouseEvent) => void; +} +``` + +### Component Structure + +```typescript +import type { Icon } from '@phosphor-icons/react'; +import type { ThemeColor } from '@shared/types/ThemeColors'; +import clsx from 'clsx'; +import React from 'react'; + +interface Props { + color: ThemeColor; + variant?: 'filled' | 'outline'; +} + +/** + * A reusable button component that follows the design system. + * + * @param props - The button props + * @returns The rendered button component + */ +export function Button({ + variant = 'filled', + color, + children +}: React.PropsWithChildren): JSX.Element { + return ( + + ); +} +``` + +### Hooks + +Custom hooks must start with `use` and follow React hooks rules: + +```typescript +// Good +export default function useSchedules(): [active: UserSchedule, schedules: UserSchedule[]] { + const [schedules, setSchedules] = useState([]); + const [activeIndex, setActiveIndex] = useState(0); + const [activeSchedule, setActiveSchedule] = useState(errorSchedule); + + useEffect(() => { + const l1 = UserScheduleStore.listen('schedules', newValue => { + setSchedules(newValue); + }); + return () => UserScheduleStore.removeListener(l1); + }, []); + + return [activeSchedule, schedules]; +} +``` + +### Event Handlers + +Prefix event handlers with `handle`: + +```typescript +// Good +const handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + // ... +}; + +const handleImportClick = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + // ... +}; + +// Bad +const onClick = () => { + /* ... */ +}; +const importClick = () => { + /* ... */ +}; +``` + +## Styling + +### SCSS Modules + +Use SCSS modules with the modern `@use` syntax: + +```scss +// Good - colors.module.scss +$burnt_orange: #bf5700; +$charcoal: #333f48; + +:export { + burnt_orange: $burnt_orange; + charcoal: $charcoal; +} +``` + +```scss +// Good - Component.module.scss +@use 'src/views/styles/colors.module.scss'; +@use 'src/views/styles/fonts.module.scss'; + +.text { + color: colors.$burnt_orange; + font-size: fonts.$medium_size; +} + +// Bad +@import 'src/views/styles/colors.module.scss'; +``` + +### TypeScript Definitions for SCSS + +Create `.d.ts` files for SCSS modules: + +```typescript +// colors.module.scss.d.ts +export interface ISassColors { + burnt_orange: string; + charcoal: string; + white: string; +} + +export type Color = keyof ISassColors; + +declare const colors: ISassColors; +export default colors; +``` + +### UnoCSS Utility Classes + +Use UnoCSS for utility classes alongside SCSS modules: + +```tsx +// Good - Combining UnoCSS with component classes +
+ + Title + +
+``` + +### clsx for Conditional Classes + +Use `clsx` for conditional className logic: + +```typescript +import clsx from 'clsx'; + +// Good + + + ); +} +```