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
.tsxextension for all React components - Use
.tsfor 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.tsfor 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
| Type | Convention | Example |
|---|---|---|
| Interfaces (props) | Props suffix | ButtonProps, TopMenuProps |
| Type aliases | PascalCase | AvailabilityMap, DayStructure |
| Enums | PascalCase | DayOffType, SalaryRateUnit |
| Generic parameters | Single uppercase or PascalCase | T, 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
| Type | Convention | Example |
|---|---|---|
| Variables | camelCase | userName, isLoading |
| Constants | SCREAMING_SNAKE_CASE | SALARY_TYPE_YEARLY, MAX_RETRIES |
| Functions | camelCase | findCompanyById, handleSubmit |
| Custom hooks | use prefix | useIsOwnerOrHeadOffice, useGetServerTime |
| Boolean variables | is, has, should, can prefix | isDisabled, 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() { ... }