<h1 align="center">
<a href="https://prompts.chat">
TypeScript and ESLint rules that MUST be followed when creating, modifying, or reviewing any file under apps/frontend/, including .ts, .tsx, .js, and .jsx files. Also apply when discussing frontend linting, type safety, or ESLint configuration.
Loading actions...
anyThe codebase enforces @typescript-eslint/no-explicit-any as a warning. Never use any in new code. Use unknown and narrow, or use the correct library/domain type.
Record<string, unknown>// BAD
interface MyEntity {
metadata?: Record<string, any>;
attributes: Record<string, any>;
}
// GOOD
interface MyEntity {
metadata?: Record<string, unknown>;
attributes: Record<string, unknown>;
}
Special cases where narrower types are appropriate:
// HTTP headers are always strings
request_headers?: Record<string, string>;
// OpenTelemetry attributes
attributes: Record<string, string | number | boolean>;
// Known key-value config
auth?: Record<string, string | boolean | number>;
unknown with Type Narrowing// BAD
try {
await api.fetch();
} catch (error: any) {
setError(error.message);
}
// GOOD
try {
await api.fetch();
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
setError(message);
}
For accessing non-standard properties like .status or .response:
} catch (error: unknown) {
const errObj = error as Error & { status?: number; response?: { data?: { detail?: string } } };
if (errObj.status === 404) {
// handle not found
}
const message = errObj instanceof Error ? errObj.message : String(error);
}
import type { GridRenderCellParams, GridRowParams, GridCellParams, GridRowModel, GridColDef } from '@mui/x-data-grid';
import type { SxProps, Theme } from '@mui/material';
// BAD
columns: any[];
rows: any[];
onRowClick?: (params: any) => void;
getRowId?: (row: any) => string;
sx?: any;
// GOOD
columns: GridColDef[];
rows: GridRowModel[];
onRowClick?: (params: GridRowParams) => void;
getRowId?: (row: GridRowModel) => string;
sx?: SxProps<Theme>;
as any// BAD
const result = response as any;
(theme.palette as any)[color];
// GOOD - use intermediate unknown when needed
const result = response as unknown as MyResponseType;
(theme.palette as unknown as Record<string, Record<string, string>>)[color];
When accessing window globals:
// BAD
(window as any).myGlobal = value;
// GOOD
(window as Window & { myGlobal?: string }).myGlobal = value;
// BAD
async function fetchData(): Promise<any> { ... }
// GOOD
async function fetchData(): Promise<Record<string, unknown>> { ... }
// BETTER - define a response interface
interface FetchResponse {
data: MyEntity[];
total: number;
}
async function fetchData(): Promise<FetchResponse> { ... }
// BAD
tickFormatter?: (value: any) => string;
tooltipFormatter?: (value: any, name: any) => string;
// GOOD
tickFormatter?: (value: string | number) => string;
tooltipFormatter?: (value: string | number, name: string) => string;
Using any in test files for mocks and partial objects is acceptable. Add a file-level disable:
/* eslint-disable @typescript-eslint/no-explicit-any */
import { render } from '@testing-library/react';
// ... test code using any for mocks
unknown in JSXWhen using Record<string, unknown> types, unknown values can leak into JSX children through && short-circuit operators, causing TS2769: Type 'unknown' is not assignable to type 'ReactNode'.
// BAD - unknown leaks into JSX via &&
{test.metadata?.sources && Array.isArray(test.metadata.sources) && (
<Grid>{/* TS error: unknown is not ReactNode */}</Grid>
)}
// GOOD - extract and narrow before JSX
const sources: Array<Record<string, string>> = Array.isArray(test.metadata?.sources)
? test.metadata.sources
: [];
// Then in JSX:
{sources.length > 0 && (
<Grid>{/* works fine */}</Grid>
)}
{}API responses typed as Record<string, unknown> may return {} where you expect a string or array. Always guard:
// BAD - created_at might be {} not string
<span>{new Date(item.created_at).toLocaleDateString()}</span>
// GOOD
{typeof item.created_at === 'string' && (
<span>{new Date(item.created_at).toLocaleDateString()}</span>
)}
// BAD - isMultiTurn could be unknown, leaks into JSX children
{test.metadata?.is_multi_turn && <MultiTurnView />}
// GOOD - explicitly typed boolean
const isMultiTurn: boolean = Boolean(test.metadata?.is_multi_turn);
{isMultiTurn && <MultiTurnView />}
The codebase enforces @typescript-eslint/no-non-null-assertion as a warning. Do not use the ! postfix operator.
// BAD
const name = user!.name;
const provider = providers.find(p => p.id === id)!;
// GOOD - use optional chaining or explicit checks
const name = user?.name;
const provider = providers.find(p => p.id === id);
if (provider) {
// use provider safely
}
The codebase enforces react-hooks/exhaustive-deps as a warning. All reactive values used inside useEffect, useCallback, and useMemo must be listed in the dependency array.
// BAD
useEffect(() => {
fetchData(userId);
}, []); // missing userId
// GOOD
useEffect(() => {
fetchData(userId);
}, [userId]);
When adding a dependency would cause an infinite re-render loop (e.g., a function that is recreated each render, or a state setter that triggers the effect), use an inline disable with a clear reason:
useEffect(() => {
loadInitialData();
// eslint-disable-next-line react-hooks/exhaustive-deps -- only run on mount
}, []);
The codebase enforces @typescript-eslint/no-unused-vars. Unused variables must be prefixed with underscore (_) or removed entirely.
// BAD
import { Box, Typography, Button } from '@mui/material'; // Button not used
// GOOD
import { Box, Typography } from '@mui/material';
// BAD
const handleChange = (event, value) => {
console.log(value);
};
// GOOD
const handleChange = (_event, value) => {
console.log(value);
};
// BAD
try {
await fetchData();
} catch (error) {
showDefaultMessage();
}
// GOOD
try {
await fetchData();
} catch (_error) {
showDefaultMessage();
}
// BAD - key is destructured but not used (common in MUI Autocomplete)
const { key, ...otherProps } = props;
return <li key={item.id} {...otherProps}>;
// GOOD
const { key: _key, ...otherProps } = props;
return <li key={item.id} {...otherProps}>;
// BAD
const [value, setValue] = useState(initialValue); // setValue never used
// GOOD
const [value, _setValue] = useState(initialValue);
// BAD
export default function MyComponent({
data,
unusedProp,
anotherProp,
}: Props) {
// GOOD
export default function MyComponent({
data,
unusedProp: _unusedProp,
anotherProp,
}: Props) {
// BAD
items.map((item, index) => ( // index not used
<div key={item.id}>{item.name}</div>
));
// GOOD
items.map((item, _index) => (
<div key={item.id}>{item.name}</div>
));
The codebase enforces react/no-array-index-key. Using array index as React key can cause rendering issues when items are reordered, added, or removed.
// BAD
{users.map((user, index) => (
<UserCard key={index} user={user} />
))}
// GOOD - use unique id from data
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
// BAD
interface Invite {
email: string;
}
// GOOD - include unique id for React keys
interface Invite {
id: string; // Use crypto.randomUUID() when creating
email: string;
}
// When adding new items:
const newInvite = { id: crypto.randomUUID(), email: '' };
When rendering parsed text, chart data, or other display-only content that will never be reordered:
// Acceptable with justification
{criteriaList.map((criterion, idx) => (
// eslint-disable-next-line react/no-array-index-key -- Display-only list
<Box key={`${criterion.name}-${idx}`}>
{criterion.value}
</Box>
))}
For files with many such cases, use file-level disable at the top:
'use client';
/* eslint-disable react/no-array-index-key -- This file renders parsed content */
import React from 'react';
Combine imports from the same module:
// BAD
import { Box, Typography } from '@mui/material';
import type { TypographyProps } from '@mui/material';
// GOOD
import { Box, Typography, type TypographyProps } from '@mui/material';
Use import type for type-only imports (automatically removed during compilation):
import type { User, Organization } from './interfaces';
import { ApiClient } from './client';
The codebase restricts console usage to console.warn and console.error only.
// BAD
console.log('Debug info');
// GOOD
console.warn('Warning message');
console.error('Error message');
// For debug logging that must stay, use explicit methods:
if (logLevel === 'error') {
console.error('Message', data);
} else {
console.warn('Message', data);
}
Before committing frontend changes, always run all three checks:
# Format code with Prettier
npm run format
# TypeScript type checking (catches type errors that ESLint misses)
npx tsc --noEmit
# ESLint (catches style and quality warnings)
npm run lint
Both tsc and lint must pass with zero errors and zero warnings before committing.
any in new code - Use unknown and narrow with instanceof, typeof, or Array.isArray(). If stuck, use as unknown as TargetType as a last resort.GridRowParams, SxProps<Theme>) and Recharts types instead of any for callbacks and props._, check if it should actually be used (e.g., error logging).id field to data structures used in .map() with add/remove functionality.npm run format, npx tsc --noEmit, and npm run lint must all pass cleanly.Record<string, unknown> values flow into JSX conditionals. Extract, narrow, and type them first.Record<string, unknown> may be {}. Use typeof checks before passing to new Date(), String.prototype methods, or JSX children.any - Add /* eslint-disable @typescript-eslint/no-explicit-any */ at the top of test files where mocking requires any.