Frontend Typescript Linting.mdc
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...
Skill content
Main instructions and any bundled files for this skill.
Frontend TypeScript Linting Rules
No Explicit any
The 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.
1. Metadata and Generic Objects - Use 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>;
2. Catch Blocks - Use 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);
}
3. MUI DataGrid Callbacks - Use Library Types
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>;
4. Type Assertions - Avoid 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;
5. Function Return Types - Use Typed Promises
// 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> { ... }
6. Recharts and Chart Formatters
// BAD
tickFormatter?: (value: any) => string;
tooltipFormatter?: (value: any, name: any) => string;
// GOOD
tickFormatter?: (value: string | number) => string;
tooltipFormatter?: (value: string | number, name: string) => string;
7. Test Files - Disable Per-File
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
Handling unknown in JSX
When 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'.
1. Extract and Narrow Before JSX
// 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>
)}
2. Guard Against Empty Objects {}
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>
)}
3. Explicitly Type Boolean Conditions
// 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 />}
Non-Null Assertions
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
}
React Hooks Exhaustive Dependencies
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.
1. Add Missing Dependencies When Safe
// BAD
useEffect(() => {
fetchData(userId);
}, []); // missing userId
// GOOD
useEffect(() => {
fetchData(userId);
}, [userId]);
2. Disable with Justification When Dependencies Cause Loops
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
}, []);
Avoiding Unused Variable Warnings
The codebase enforces @typescript-eslint/no-unused-vars. Unused variables must be prefixed with underscore (_) or removed entirely.
1. Unused Imports - Remove Them
// BAD
import { Box, Typography, Button } from '@mui/material'; // Button not used
// GOOD
import { Box, Typography } from '@mui/material';
2. Unused Function Parameters - Prefix with Underscore
// BAD
const handleChange = (event, value) => {
console.log(value);
};
// GOOD
const handleChange = (_event, value) => {
console.log(value);
};
3. Unused Catch Block Errors
// BAD
try {
await fetchData();
} catch (error) {
showDefaultMessage();
}
// GOOD
try {
await fetchData();
} catch (_error) {
showDefaultMessage();
}
4. Unused Destructured Variables
// 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}>;
5. Unused useState Setters
// BAD
const [value, setValue] = useState(initialValue); // setValue never used
// GOOD
const [value, _setValue] = useState(initialValue);
6. Unused Component Props
// BAD
export default function MyComponent({
data,
unusedProp,
anotherProp,
}: Props) {
// GOOD
export default function MyComponent({
data,
unusedProp: _unusedProp,
anotherProp,
}: Props) {
7. Unused Map/Filter Callback Parameters
// 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>
));
React Keys - Avoiding Array Index as Key
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.
1. Use Unique Identifiers When Available
// 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} />
))}
2. For Dynamic Form Fields - Add ID to Data Structure
// 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: '' };
3. For Display-Only Static Lists - Use eslint-disable
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';
Import Organization
1. No Duplicate Imports
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';
2. Use Type Imports
Use import type for type-only imports (automatically removed during compilation):
import type { User, Organization } from './interfaces';
import { ApiClient } from './client';
Console Statements
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);
}
Verification Commands
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.
Best Practices
- Never use
anyin new code - Useunknownand narrow withinstanceof,typeof, orArray.isArray(). If stuck, useas unknown as TargetTypeas a last resort. - Use proper library types - Import and use MUI types (
GridRowParams,SxProps<Theme>) and Recharts types instead ofanyfor callbacks and props. - Prefer removal over underscore prefix - If a variable is truly not needed, remove it entirely.
- Consider if the variable should be used - Before prefixing with
_, check if it should actually be used (e.g., error logging). - Use unique IDs for dynamic lists - Add
idfield to data structures used in.map()with add/remove functionality. - Run all three checks before committing -
npm run format,npx tsc --noEmit, andnpm run lintmust all pass cleanly. - Combine imports - Keep all imports from the same module in a single import statement.
- Extract unknown values before JSX - Never let
Record<string, unknown>values flow into JSX conditionals. Extract, narrow, and type them first. - Guard API values before use - Values from API responses typed as
Record<string, unknown>may be{}. Usetypeofchecks before passing tonew Date(),String.prototypemethods, or JSX children. - Test files may use
any- Add/* eslint-disable @typescript-eslint/no-explicit-any */at the top of test files where mocking requiresany.