Owner: Engineering Team | Last Updated: 2026-01-30 | Status: Current
TypeScript and JavaScript coding conventions for the WWAI web application. This document reflects the actual tooling configuration, enforced rules, and verified code patterns from the codebase. All examples shown are drawn from real project files.
any TypeKey compiler options:
| Option | Value | Notes |
|---|---|---|
target |
ES2017 |
Compile target |
module |
esnext |
ESM output |
strict |
true |
All strict checks enabled |
jsx |
react-jsx |
Automatic JSX transform (no React import needed) |
moduleResolution |
bundler |
Bundler-style resolution |
incremental |
true |
Faster rebuilds via .tsbuildinfo cache |
paths |
@/* -> project root |
Path alias for all internal imports |
Path alias usage -- always prefer the @/ alias over relative paths:
// Correct
import api from '@/utils/api'
import { HumanizerSelectOptions } from '@/types'
// Avoid
import api from '../../../utils/api'
import { HumanizerSelectOptions } from '../../types'
The project has two ESLint configurations:
Primary: eslint.config.mjs (flat config format)
@eslint/js recommended, typescript-eslint recommendedreact, tailwindcssreact/react-in-jsx-scope -- not needed with react-jsx transformreact/prop-types -- TypeScript interfaces replace prop-typesLegacy: .eslintrc.json
{ "extends": "next/core-web-vitals" }
This legacy config provides Next.js-specific linting rules (accessibility, Core Web Vitals compliance). It coexists with the primary flat config.
Full .prettierrc configuration:
{
"plugins": [
"@trivago/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss"
],
"importOrder": ["^react(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}
Key formatting rules enforced:
| Rule | Value | Effect |
|---|---|---|
semi |
false |
No semicolons |
singleQuote |
true |
Single quotes for strings |
trailingComma |
es5 |
Trailing commas in objects, arrays, parameters |
tabWidth |
2 |
2-space indentation |
| Import sorting | @trivago/prettier-plugin-sort-imports |
Automatic import grouping and ordering |
| Tailwind sorting | prettier-plugin-tailwindcss |
Automatic Tailwind class ordering |
Husky runs lint-staged on every commit. This executes Prettier on all staged files automatically, so formatting is enforced before code enters the repository. No manual formatting step is needed.
These rules are enforced by the tooling above. Do not override them in individual files.
prettier-plugin-tailwindcss. Do not manually reorder classes.// Correct -- matches enforced style
const config = {
name: 'humanizer',
version: 2,
enabled: true,
}
// Wrong -- semicolons, double quotes
const config = {
name: "humanizer";
version: 2;
enabled: true;
};
| Type | Convention | Real Examples |
|---|---|---|
| Components | PascalCase file and name | HumanizerLayout.tsx, HumanizeButton.tsx |
| Hooks | use prefix, camelCase file |
useWordsUsage.tsx |
| Utility files | camelCase | sentryLogger.ts, api.ts |
| Types / Interfaces | PascalCase | StatusProps, HumanizerSelectOptions |
| Constants | SCREAMING_SNAKE_CASE | CHARACTER_LIMIT, FILE_SIZE_LIMIT |
| Functions | camelCase | fetchDocuments(), handleSubmit() |
| Variables | camelCase | isLoggedIn, errorMessage |
| Boolean variables | is/has/should prefix |
is_scored, isLoggedIn |
Important: Utility files use camelCase, not kebab-case. This matches the existing codebase (sentryLogger.ts, api.ts).
Enforced automatically by @trivago/prettier-plugin-sort-imports with the following group order:
1. React and React-related imports (matches ^react(.*)$)
2. Third-party modules (everything from node_modules)
3. Local/relative imports (matches ^[./])
Each group is separated by a blank line (importOrderSeparation: true), and specifiers within each import are sorted alphabetically (importOrderSortSpecifiers: true).
Real example from the codebase:
'use client'
import { useSession } from 'next-auth/react'
import { useTranslation } from 'next-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { AppDispatch, RootState } from '@/lib/store'
import { HumanizerSelectOptions } from '@/types'
import api from '@/utils/api'
Rules:
'use client' directive always goes first, before any imports.next-auth/react, react-redux, etc.) appear in the first group.@/lib/store, @/types) fall into the local imports group.Components follow a consistent internal structure. This is the verified pattern from the codebase (e.g., HumanizeButton.tsx):
'use client'
import { useSession } from 'next-auth/react'
import { useDispatch } from 'react-redux'
import { AppDispatch } from '@/lib/store'
interface HumanizeButtonProps {
text: string
mode: string
disabled?: boolean
}
const HumanizeButton = ({ text, mode, disabled }: HumanizeButtonProps) => {
// 1. Hooks
const { data: session } = useSession()
const dispatch = useDispatch<AppDispatch>()
// 2. Derived state
const isReady = text.length > 0 && !disabled
// 3. Handlers
const handleClick = () => {
dispatch(/* action */)
}
// 4. Render
return (
<button onClick={handleClick} disabled={!isReady}>
{text}
</button>
)
}
export default HumanizeButton
Key points:
function declarations or React.FC.React.FC or FC wrapper. The codebase uses plain arrow functions with typed props.'use client' directive is required for components that use hooks or browser APIs (Next.js App Router convention).Shared interfaces and types live in types/index.ts. Import them using the path alias:
import { HumanizerSelectOptions, StatusProps } from '@/types'
Real interface examples from types/index.ts:
export interface HumanizerSelectOptions {
id: number
value?: string
icon: string
name: string
premium?: boolean
}
export interface StatusProps {
is_scored: string | boolean | null
total: string | boolean | null
gptZero: string | boolean | null
// ... additional detector fields
}
interface for object shapes (component props, API responses, data models).type for unions, intersections, and computed types.<ComponentName>Props (e.g., HumanizeButtonProps).types/index.ts. Keep component-specific interfaces co-located with the component file.? rather than using | undefined.// Correct -- interface for object shape
interface UserProfile {
id: number
name: string
email?: string
}
// Correct -- type for union
type DetectorResult = 'human' | 'ai' | 'mixed' | null
// Correct -- type for intersection
type AuthenticatedUser = UserProfile & { token: string }
The project uses Redux Toolkit with react-redux. Direct hooks are used -- there are no custom typed wrapper hooks.
import { AppDispatch, RootState } from '@/lib/store'
import { useDispatch, useSelector } from 'react-redux'
import { AppDispatch, RootState } from '@/lib/store'
// Always type dispatch with AppDispatch
const dispatch = useDispatch<AppDispatch>()
// Always type selectors with RootState
const selection = useSelector((state: RootState) => state.humanizerSelection)
Important: Do not create custom useAppDispatch / useAppSelector wrapper hooks. The codebase uses useDispatch<AppDispatch>() and useSelector((state: RootState) => ...) directly. Follow the established pattern.
The codebase uses two distinct patterns for API calls depending on context:
For server-side operations and general API utilities, use the shared axios instance from utils/api.ts:
import api from '@/utils/api'
const response = await api.post('/api/user/login/', data)
const result = response.data
For client-side calls to feature endpoints (e.g., humanizer), use native fetch with explicit headers:
const response = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/feature/humanizer/`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
}
)
| Context | Pattern | Import |
|---|---|---|
| Server-side utilities | Axios instance | import api from '@/utils/api' |
| Client-side feature calls | Native fetch |
None (global) |
When adding new API calls, follow whichever pattern matches the context. Do not mix patterns within the same module.
The codebase uses a cascading extraction pattern for API error messages:
const errorMessage =
errorData.detail ||
errorData.message ||
errorData.error ||
t('errorOccurred')
This accounts for different backend error response formats. Always check fields in this order: detail -> message -> error -> fallback translation string.
try {
const response = await api.post('/api/endpoint/', data)
// handle success
} catch (error: any) {
const errorMessage =
error?.response?.data?.detail ||
error?.response?.data?.message ||
error?.message ||
t('errorOccurred')
// handle error (display to user, log, etc.)
}
See the Usage of any Type section for guidance on catch (error: any).
any TypeThe strict TypeScript configuration discourages any, but some instances exist in the codebase. Here is the policy on when any is acceptable versus when it must be fixed.
| Pattern | Example | Rationale |
|---|---|---|
| Catch blocks | catch (error: any) |
TypeScript catch clauses default to unknown, but any avoids verbose type narrowing in legacy code. Acceptable in existing files. |
| Pattern | Example | Fix |
|---|---|---|
| API response generics | apiCall<any>() |
Define a response type interface and use apiCall<UserResponse>() |
| Event handlers | (e: any) => ... |
Use proper event types: React.ChangeEvent<HTMLInputElement>, React.MouseEvent<HTMLButtonElement>, etc. |
| Function parameters | function process(data: any) |
Define an interface or use unknown with type guards |
| State variables | useState<any>(null) |
Define the state type: useState<UserProfile | null>(null) |
any types. Use unknown with type guards if the type is truly unknown.types/index.ts if the response is used across modules.unknown with narrowing:// Preferred in new code
try {
await api.post('/api/endpoint/', data)
} catch (error: unknown) {
const message =
error instanceof Error ? error.message : t('errorOccurred')
// handle error
}
// Acceptable in existing/legacy code
try {
await api.post('/api/endpoint/', data)
} catch (error: any) {
const message = error?.response?.data?.detail || t('errorOccurred')
// handle error
}
any types in unrelated PRs. Address them in dedicated cleanup tickets to keep diffs focused.| Rule | Value |
|---|---|
| Semicolons | No |
| Quotes | Single |
| Trailing commas | ES5 (objects, arrays, params) |
| Indentation | 2 spaces |
| Component style | Arrow function, default export |
| Props typing | Dedicated interface, destructured in signature |
| Imports | @/ alias, auto-sorted by Prettier |
| Styling | Tailwind utility classes only |
| State management | Redux Toolkit, direct react-redux hooks |
| API (server) | Axios instance from @/utils/api |
| API (client) | Native fetch |
| Strict mode | Enabled |
any in new code |
Prohibited -- use unknown or proper types |
| Web App Overview | TypeScript in the web app |
| Chrome Extension | TypeScript in Chrome extension |
| Naming Conventions | General naming rules |
| Date | Author | Change |
|---|---|---|
| 2026-01-30 | Admin | Rewrote page with verified codebase configurations, real code examples, and expanded guidance on types, Redux, API patterns, and any usage policy |
| 2026-01-30 | Admin | Initial creation |
Prev: Documentation Standards | Next: Python Coding Standards | Up: General