Skip to main content

Style Guide

Pivot's style guide to ensure clean and readable code.

Note: This guide focuses on code style and conventions, not design patterns. Design patterns and architectural decisions should be documented close to where they are used (e.g., in a README within the relevant directory or inline comments). This keeps pattern documentation contextual and maintainable.


1. File Naming Conventions

1.1 Components

  • Use PascalCase for component files: TopMenu.tsx, AvatarInitials.tsx
  • Use .tsx extension for all React components
  • Use .ts for non-component TypeScript files

1.2 Utilities and Hooks

  • Use camelCase for utility files: api.ts, theme.js, handleError.ts
  • kebab-case is also acceptable for backend code (Cloud Functions, server): send-slack-notification.ts, check-maitred-data.ts
  • Prefix hook files with use: useDeviceHasNotch.ts, useGetServerTime.ts

1.3 Styles

  • Web: Styles can be in the same file as component (styled-components) or separate SCSS files
  • Mobile: Use styles.ts for StyleSheet exports

1.4 Tests

  • Use {filename}.test.ts
  • Place tests in __tests__/ directories alongside source files

1.5 File Naming Best Practices

Name files after their main export:

// Good - file name matches main export
// sendPushNotification.ts
export function sendPushNotification() { ... }

// Good - file name matches main export
// useGetServerTime.ts
export function useGetServerTime() { ... }

// Good - file name matches main type
// employee.ts (in types/)
export interface IEmployee { ... }

Avoid generic catch-all file names:

// Bad - vague, becomes a dumping ground
utils.ts;
helpers.ts;
common.ts;
misc.ts;

// Good - specific and descriptive
dateFormatters.ts;
validationHelpers.ts;
attendanceUtils.ts;
companyHelpers.ts;

When a utility file grows too large, split it into focused files named after their primary function.


2. Imports

2.1 Import Order

Import ordering is handled automatically by prettier. No manual sorting required.

2.2 Import Paths

Use path aliases configured in tsconfig.json rather than deep relative imports:

// Good
import { Symbol1 } from "components/ui/Button";
import { Company } from "types/company";

// Acceptable for same-directory or nearby files
import { Symbol2 } from "../parent/file";
import { Symbol3 } from "./sibling";

// Avoid deep relative paths
// Bad
import { Symbol4 } from "../../../../../components/ui/Button";

2.3 Default Imports

Use default imports only when necessary (e.g., React, third-party libraries that export defaults).

3. Exports

3.1 No Default Exports

Do not use default exports. Use named exports for everything.

// Good - named exports
export function ProfilePage() { ... }
export const Button = memo(({ ... }) => { ... })
export const findCompanyById = async (companyId: string) => { ... }
export interface Company { ... }

// Bad - default exports
export default function ProfilePage() { ... }
export default Button

Why: Named exports provide better refactoring support, clearer imports, and prevent inconsistent naming across the codebase. See Google's style guide reasoning.

Exception: Only use default exports when the framework explicitly requires it (e.g., Next.js pages, some lazy loading patterns).


4. Components

Always use functional components with hooks. Do not create new class components.


5. TypeScript

5.1 Type Definitions

Interfaces (preferred for objects):

interface User {
firstName: string;
lastName: string;
}

Types (for unions, primitives, utilities):

type IDay =
| "Monday"
| "Tuesday"
| "Wednesday"
| "Thursday"
| "Friday"
| "Saturday"
| "Sunday";

type AvailabilityMap = {
[K in IDay]?: IAvailability;
};

5.2 Naming Conventions

TypeConventionExample
Interfaces (props)Props suffixButtonProps, TopMenuProps
Type aliasesPascalCaseAvailabilityMap, DayStructure
EnumsPascalCaseDayOffType, SalaryRateUnit
Generic parametersSingle uppercase or PascalCaseT, TItem

5.3 Enums

Use plain objects declared with as const instead of TypeScript enums. This avoids the runtime overhead and quirks of TS enums while providing the same type safety.

Use PascalCase for both keys and values.

// Good - const objects with PascalCase keys matching values
export const DayOffType = {
AllDay: "AllDay",
Morning: "Morning",
Evening: "Evening",
Custom: "Custom",
} as const;

export type DayOffType = (typeof DayOffType)[keyof typeof DayOffType];

export const SalaryRateUnit = {
Yearly: "Yearly",
Hourly: "Hourly",
} as const;

export type SalaryRateUnit = (typeof SalaryRateUnit)[keyof typeof SalaryRateUnit];

// Bad - TypeScript enums
export enum DayOffType {
ALL_DAY = "allDay",
MORNING = "morning",
}

5.4 Avoid any

Use unknown instead of any when the type is truly unknown:

// Good
const value: unknown = getExternalData();
if (typeof value === "string") {
console.log(value.toUpperCase());
}

// Bad
const value: any = getExternalData();
value.anything(); // No type checking

Exception: any is acceptable when working with legacy code where adding proper types would require a large refactor. In these cases, add a // TODO comment indicating the intent to type it properly later:

// Acceptable in legacy code
// TODO: type this properly when refactoring the legacy module
const legacyData: any = getLegacyResponse();

5.5 Type Assertions

Avoid type assertions (as) and non-null assertions (!). Use type guards instead:

// Good
if (x instanceof Foo) {
x.foo();
}

if (y) {
y.bar();
}

// Avoid
(x as Foo).foo();
y!.bar();

6. Variables and Functions

6.1 Use const and let

Always use const by default. Use let only when reassignment is needed. Never use var.

6.2 Naming Conventions

TypeConventionExample
VariablescamelCaseuserName, isLoading
ConstantsSCREAMING_SNAKE_CASESALARY_TYPE_YEARLY, MAX_RETRIES
FunctionscamelCasefindCompanyById, handleSubmit
Custom hooksuse prefixuseIsOwnerOrHeadOffice, useGetServerTime
Boolean variablesis, has, should, can prefixisDisabled, hasError, shouldFetch, canEdit

6.3 Function Declarations

Prefer arrow functions over function declarations:

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

const handleClick: MouseEventHandler = (e) => {
// ...
};

6.4 Arrow Functions in Callbacks

Always use arrow functions that explicitly pass parameters:

// Good
const numbers = ["11", "5", "10"].map((n) => parseInt(n, 10));

// Bad - implicit parameter passing can cause bugs
const numbers = ["11", "5", "10"].map(parseInt);
// Results in [11, NaN, 2] due to radix parameter

7. Control Flow

7.1 Always Use Braces

// Good
if (x) {
doSomething();
}

for (let i = 0; i < x; i++) {
doSomething(i);
}

// Bad
if (x) doSomething();
for (let i = 0; i < x; i++) doSomething(i);

7.2 No Assignment in Conditions

// Good
const result = someFunction();
if (result) {
// ...
}

// Bad
if ((result = someFunction())) {
// ...
}

7.3 Iterating Arrays and Objects

Use for...of with Object.keys/values/entries, or Array/Object methods like forEach, map, filter, reduce, etc.:

// Good - for...of
for (const key of Object.keys(obj)) {
doWork(key, obj[key])
}

for (const [key, value] of Object.entries(obj)) {
doWork(key, value)
}

// Good - Array/Object methods
Object.entries(obj).forEach(([key, value]) => {
doWork(key, value)
})

items.map((item) => transform(item))
items.filter((item) => item.isActive)

7.4 Avoid for...in

Do not use for...in for arrays or objects. It iterates over the prototype chain and has unexpected behavior:

// Bad - for...in with arrays (i is string, not number)
for (const i in array) { ... }

// Bad - for...in with objects (includes inherited properties)
for (const key in obj) { ... }

// Good - use Object methods or for...of instead
for (const key of Object.keys(obj)) { ... }
Object.entries(obj).forEach(([key, value]) => { ... })

8. Error Handling

8.1 Only Throw Errors

// Good
throw new Error("Something went wrong");

// Bad
throw "Something went wrong";
throw { message: "error" };

8.2 Error Handling with Type Assertion

Use a type assertion helper to safely narrow caught errors:

function assertIsError(e: unknown): asserts e is Error {
if (!(e instanceof Error)) throw new Error("e is not an Error");
}

try {
doSomething();
} catch (e: unknown) {
// All thrown errors must be Error subtypes. Do not handle
// other possible values unless you know they are thrown.
assertIsError(e);
displayError(e.message);
// or rethrow:
throw e;
}

8.3 Error Tracking

  • Web: Use Rollbar error boundary
  • Mobile: Use rollbar.error() for error logging

9. Date and Time

9.1 Use dayjs, Not moment

import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";

dayjs.extend(utc);
dayjs.extend(timezone);

const now = dayjs();
const formatted = dayjs(timestamp).format("YYYY-MM-DD HH:mm");
const inTimezone = dayjs.tz(timestamp, "America/Toronto");

10. Async/Await

10.1 Prefer async/await Over .then()

// Good
async function fetchData() {
const response = await api.get("/endpoint");
const processed = await processData(response);
return processed;
}

// Avoid mixing styles
// Bad
async function fetchData() {
return api.get("/endpoint").then((response) => {
return processData(response);
});
}

11. Comments and Documentation

11.1 When to Comment

  • Add comments only when the logic isn't self-evident
  • Don't add comments that restate what the code does
  • Use JSDoc for public APIs and complex functions
// Good - explains why, not what
// Using setTimeout to ensure the modal animation completes before updating state
setTimeout(() => {
setIsOpen(false);
}, 300);

// Bad - restates the obvious
// Set isOpen to false
setIsOpen(false);

11.2 Deprecation

/**
* @deprecated Use `newFunction` instead
*/
function oldFunction() { ... }