一键导入
frontend-coding-standards
TypeScript + React coding standards, architecture patterns, naming conventions, ESLint configuration
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
TypeScript + React coding standards, architecture patterns, naming conventions, ESLint configuration
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Backend testing patterns — API request construction, response verification, database state checks, error handling testing, and adaptive tool detection.
Frontend testing patterns using Playwright — navigation, interaction, assertions, screenshots on failure, and common UI testing scenarios.
Test report format with QA-XXX issue IDs compatible with code-review plugin. Defines report structure, severity levels, issue format with canonical fields, and detailed results.
Test plan structure, naming conventions, edge case generation rules, and file saving conventions for QA test plans.
Enforces AppVerk Swift coding standards across all code.
Structured concurrency and thread safety patterns in modern Swift.
| name | frontend-coding-standards |
| description | TypeScript + React coding standards, architecture patterns, naming conventions, ESLint configuration |
| activation | MUST load when writing or reviewing TypeScript/React code |
Foundational rules for TypeScript + React development. Enforced in all code:
any — use unknown + type guards insteadas type assertions except as const! non-null assertion — use optional chaining ?. and nullish coalescing ??enum — use as const objects + derived union types@ts-ignore — use @ts-expect-error with comment if absolutely unavoidableinterface for object shapes, type for unions/intersectionsI prefix on interfaces (User, not IUser)import type { } for type-only importsexport * or barrel exports >5 re-exportstsconfig.json MUST include:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"isolatedModules": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"paths": {
"@/*": ["./src/*"]
}
}
}
T | null instead of T | undefined unless specifically undefinedprop?: T (equivalent to prop: T | undefined)T | nullExample:
// ❌ BAD: Missing parameter and return types
function processUser(user) {
return user.name.toUpperCase();
}
// ✅ GOOD: All types explicit
function processUser(user: User): string {
return user.name.toUpperCase();
}
// ❌ BAD: Using type assertion
const id = userId as number;
// ✅ GOOD: Type guard or explicit context
function handleUser(userId: string | number): void {
if (typeof userId === 'number') {
// userId is number here
}
}
interface for object shapes (especially component props)type for unions, intersections, tuples, primitivesExample:
// ✅ GOOD: Interface for props
interface ButtonProps {
variant: 'primary' | 'secondary';
disabled?: boolean;
children: React.ReactNode;
}
// ✅ GOOD: Type for union
type Status = 'idle' | 'loading' | 'success' | 'error';
// ✅ GOOD: Type for intersection
type WithTimestamp = { createdAt: Date };
type User = { id: string; name: string } & WithTimestamp;
import *)import type { SomeType } from '...'Example:
// ✅ GOOD: Organized imports
import { useState } from 'react';
import type { ReactNode } from 'react';
import { QueryClient } from '@tanstack/react-query';
import type { User } from '@/types/user';
import { useAuth } from '@/features/auth';
React.FC — use named function with direct props annotationchildren implicitly — ALWAYS explicitly annotate children: ReactNode in propsfireEvent in tests — ALWAYS use userEventgetByText as first choice — prefer screen.getByRolegetByTestId without strong justificationonClick handlers without specific typesany for event handlers — use React-specific typesref.current! — use optional chaining or proper type narrowingundefined — throw in the hook insteadReact.FC<Props>children: React.ReactNode if component accepts childrenProps suffix for interfaces (ButtonProps, CardProps)Example:
// ❌ BAD: Using React.FC
const Button: React.FC<{ label: string; onClick: () => void }> = ({ label, onClick }) => (
<button onClick={onClick}>{label}</button>
);
// ✅ GOOD: Direct props annotation with children
interface ButtonProps {
label: string;
onClick: () => void;
children?: React.ReactNode;
disabled?: boolean;
}
function Button({ label, onClick, children, disabled }: ButtonProps): React.ReactElement {
return (
<button onClick={onClick} disabled={disabled}>
{label} {children}
</button>
);
}
React.MouseEvent<HTMLButtonElement>, React.ChangeEvent<HTMLInputElement>any for event parametersExample:
interface FormProps {
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
function Form({ onSubmit, onChange }: FormProps): React.ReactElement {
return (
<form onSubmit={onSubmit}>
<input onChange={onChange} />
</form>
);
}
use prefixExample:
// ❌ BAD: Hook inside condition
function Component({ shouldFetch }: { shouldFetch: boolean }): React.ReactElement {
if (shouldFetch) {
const data = useFetch(); // WRONG!
}
return <div />;
}
// ✅ GOOD: Hook at top level
function Component({ shouldFetch }: { shouldFetch: boolean }): React.ReactElement {
const data = useFetch();
if (!shouldFetch) return <div />;
return <div>{data}</div>;
}
createContext should be null or undefinedExample:
// ❌ BAD: Context without typed hook
const ThemeContext = React.createContext<Theme | undefined>(undefined);
// ✅ GOOD: Context with typed hook
const ThemeContext = React.createContext<Theme | null>(null);
function useTheme(): Theme {
const theme = React.useContext(ThemeContext);
if (theme === null) {
throw new Error('useTheme must be used within ThemeProvider');
}
return theme;
}
interface ThemeProviderProps {
children: React.ReactNode;
}
function ThemeProvider({ children }: ThemeProviderProps): React.ReactElement {
const [theme, setTheme] = React.useState<Theme>(defaultTheme);
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
src/
├── app/ # Shell: routes, providers, entry point
│ ├── App.tsx # Main route component
│ └── Root.tsx # Root with providers
├── features/ # Self-contained feature modules
│ ├── auth/
│ │ ├── api/ # API calls + TanStack Query hooks
│ │ ├── components/ # Feature-scoped UI
│ │ ├── hooks/ # Feature-scoped logic hooks
│ │ ├── stores/ # Feature-scoped Zustand stores
│ │ ├── types/ # Feature-scoped types
│ │ └── index.ts # Public API (max 5 exports)
│ ├── dashboard/
│ │ ├── api/
│ │ ├── components/
│ │ └── ...
│ └── users/
│ └── ...
├── components/ # Shared UI (Button, Modal, Card)
│ └── ui/ # Primitives (shadcn/ui style)
├── hooks/ # Shared hooks (useDebounce, useMediaQuery)
├── lib/ # Preconfigured libs (axios instance, cn utility)
├── stores/ # Global state stores
├── types/ # Shared TypeScript types
├── utils/ # Pure utility functions
└── test/ # Test setup, custom render, MSW handlers
Direction: shared -> features -> app
shared/ (hooks, utils, components, types, lib, stores)features/ and shared/Cross-feature composition happens ONLY in app/ (root routes/pages).
Example:
// ✅ GOOD: Auth feature imports shared utilities
import { formatDate } from '@/utils/format'; // from shared utils
import { Button } from '@/components/ui/button'; // from shared components
import type { User } from '@/types/user'; // from shared types
// ❌ BAD: Auth feature imports from orders feature
import { useOrders } from '@/features/orders/hooks'; // WRONG!
// ✅ GOOD: App composes features together
import { AuthPage } from '@/features/auth';
import { DashboardPage } from '@/features/dashboard';
Layer 1: API/Service Layer
features/<name>/api/<name>Api.tsfeatures/<name>/api/queries.ts for TanStack QueryLayer 2: Logic/Hook Layer
features/<name>/hooks/Layer 3: UI/Component Layer
features/<name>/components/Example:
// Layer 1: API service (pure function)
// features/users/api/usersApi.ts
export async function fetchUser(id: string): Promise<User> {
const response = await apiClient.get(`/users/${id}`);
return response.data;
}
// Layer 2: Hook (TanStack Query + Zustand)
// features/users/hooks/useUser.ts
export function useUser(id: string): {
user: User | undefined;
isLoading: boolean;
error: Error | null;
} {
const { data: user, isLoading, error } = useQuery({
queryKey: ['users', id],
queryFn: () => fetchUser(id),
});
return { user, isLoading, error };
}
// Layer 3: Component (UI only)
// features/users/components/UserCard.tsx
interface UserCardProps {
userId: string;
}
function UserCard({ userId }: UserCardProps): React.ReactElement {
const { user, isLoading } = useUser(userId);
if (isLoading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return <div>{user.name}</div>;
}
| Element | Convention | Example |
|---|---|---|
| Directories | kebab-case | user-profile/, auth-modal/ |
| Component files | PascalCase.tsx | UserProfile.tsx, LoginForm.tsx |
| Component exports | PascalCase function | export function UserProfile() {} |
| Hook files | camelCase with use prefix | useAuth.ts, useFormData.ts |
| Hook exports | camelCase with use prefix | export function useAuth() |
| Utility functions | camelCase | formatDate.ts, calculateTotal.ts |
| Types/Interfaces | PascalCase | User, ButtonProps, FormData |
| Constants | UPPER_SNAKE_CASE | API_BASE_URL, MAX_RETRIES |
| Const objects | PascalCase + as const | const OrderStatus = {...} as const |
| Test files | .test.tsx for components, .spec.ts for e2e | Button.test.tsx, auth.spec.ts |
Props suffix: ButtonProps, CardProps, ModalProps?: disabled?: booleanon: onClick, onChange, onSubmitExample:
interface UserProfileProps {
userId: string;
onLogout?: () => void;
disabled?: boolean;
}
function UserProfile({ userId, onLogout, disabled }: UserProfileProps): React.ReactElement {
// ...
}
// eslint.config.js
export default [
{
files: ['**/*.{ts,tsx}'],
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' }
],
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'@typescript-eslint/explicit-function-return-types': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-var': 'error',
'prefer-const': 'error',
},
},
];
@typescript-eslint/eslint-plugin with strict-type-checked + stylistic-type-checkedeslint-plugin-react-hookseslint-plugin-import for import orderingAll imports from src/ use @/ instead of relative paths.
tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
vite.config.ts:
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
Always use path aliases:
// ❌ BAD: Relative imports
import { Button } from '../../../../components/ui/button';
// ✅ GOOD: Path aliases
import { Button } from '@/components/ui/button';
Example:
import { useState } from 'react';
import type { ReactNode } from 'react';
import clsx from 'clsx';
import type { User } from '@/types/user';
import { Button } from '@/components/ui/button';
interface UserCardProps {
user: User;
onSelect: (id: string) => void;
}
export function UserCard({ user, onSelect }: UserCardProps): ReactNode {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={clsx('p-4', isHovered && 'bg-gray-100')}
>
<h3>{user.name}</h3>
<Button onClick={() => onSelect(user.id)}>Select</Button>
</div>
);
}
Example:
/**
* Formats a user's display name, handling cases where
* firstName or lastName might be missing.
*/
export function formatUserName(
firstName: string | null,
lastName: string | null,
): string {
// Prefer full name, fall back to firstName, then lastName
if (firstName && lastName) return `${firstName} ${lastName}`;
return firstName ?? lastName ?? 'Unknown User';
}
All code must follow these standards:
any, as, !React.FC