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.

Views160
PublishedFeb 15, 2026

Loading actions...

5 minBeginnerpromptSingle file

Skill content

Main instructions and any bundled files for this skill.

markdown

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&#x3C;string, any>;
  attributes: Record&#x3C;string, any>;
}

// GOOD
interface MyEntity {
  metadata?: Record&#x3C;string, unknown>;
  attributes: Record&#x3C;string, unknown>;
}

Special cases where narrower types are appropriate:

// HTTP headers are always strings
request_headers?: Record&#x3C;string, string>;

// OpenTelemetry attributes
attributes: Record&#x3C;string, string | number | boolean>;

// Known key-value config
auth?: Record&#x3C;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 &#x26; { 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&#x3C;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&#x3C;string, Record&#x3C;string, string>>)[color];

When accessing window globals:

// BAD
(window as any).myGlobal = value;

// GOOD
(window as Window &#x26; { myGlobal?: string }).myGlobal = value;

5. Function Return Types - Use Typed Promises

// BAD
async function fetchData(): Promise&#x3C;any> { ... }

// GOOD
async function fetchData(): Promise&#x3C;Record&#x3C;string, unknown>> { ... }

// BETTER - define a response interface
interface FetchResponse {
  data: MyEntity[];
  total: number;
}
async function fetchData(): Promise&#x3C;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 &#x26;&#x26;
{test.metadata?.sources &#x26;&#x26; Array.isArray(test.metadata.sources) &#x26;&#x26; (
  &#x3C;Grid>{/* TS error: unknown is not ReactNode */}&#x3C;/Grid>
)}

// GOOD - extract and narrow before JSX
const sources: Array&#x3C;Record&#x3C;string, string>> = Array.isArray(test.metadata?.sources)
  ? test.metadata.sources
  : [];

// Then in JSX:
{sources.length > 0 &#x26;&#x26; (
  &#x3C;Grid>{/* works fine */}&#x3C;/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
&#x3C;span>{new Date(item.created_at).toLocaleDateString()}&#x3C;/span>

// GOOD
{typeof item.created_at === 'string' &#x26;&#x26; (
  &#x3C;span>{new Date(item.created_at).toLocaleDateString()}&#x3C;/span>
)}

3. Explicitly Type Boolean Conditions

// BAD - isMultiTurn could be unknown, leaks into JSX children
{test.metadata?.is_multi_turn &#x26;&#x26; &#x3C;MultiTurnView />}

// GOOD - explicitly typed boolean
const isMultiTurn: boolean = Boolean(test.metadata?.is_multi_turn);
{isMultiTurn &#x26;&#x26; &#x3C;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 &#x3C;li key={item.id} {...otherProps}>;

// GOOD
const { key: _key, ...otherProps } = props;
return &#x3C;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
  &#x3C;div key={item.id}>{item.name}&#x3C;/div>
));

// GOOD
items.map((item, _index) => (
  &#x3C;div key={item.id}>{item.name}&#x3C;/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) => (
  &#x3C;UserCard key={index} user={user} />
))}

// GOOD - use unique id from data
{users.map((user) => (
  &#x3C;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
  &#x3C;Box key={`${criterion.name}-${idx}`}>
    {criterion.value}
  &#x3C;/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

  1. Never use 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.
  2. Use proper library types - Import and use MUI types (GridRowParams, SxProps<Theme>) and Recharts types instead of any for callbacks and props.
  3. Prefer removal over underscore prefix - If a variable is truly not needed, remove it entirely.
  4. Consider if the variable should be used - Before prefixing with _, check if it should actually be used (e.g., error logging).
  5. Use unique IDs for dynamic lists - Add id field to data structures used in .map() with add/remove functionality.
  6. Run all three checks before committing - npm run format, npx tsc --noEmit, and npm run lint must all pass cleanly.
  7. Combine imports - Keep all imports from the same module in a single import statement.
  8. Extract unknown values before JSX - Never let Record<string, unknown> values flow into JSX conditionals. Extract, narrow, and type them first.
  9. Guard API values before use - Values from API responses typed as Record<string, unknown> may be {}. Use typeof checks before passing to new Date(), String.prototype methods, or JSX children.
  10. Test files may use any - Add /* eslint-disable @typescript-eslint/no-explicit-any */ at the top of test files where mocking requires any.
Share: