From fbac3e1a5574d42446b8f0f1f8ab8ec4a60523fd Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Thu, 2 Apr 2026 16:01:29 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=D1=81=D1=8F=20=D0=BE=D1=82=20FC=20=D0=B8=20interfa?= =?UTF-8?q?ce=20=D0=B2=20=D0=BF=D1=80=D0=BE=D0=BF=D1=81=D0=B0=D1=85=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - переписана типизация: type вместо interface, убран FC - введена система типов: Params + RootAttrs + Props - добавлены обоснования: почему type, почему не FC, почему types/ - обновлены примеры в components, page-level, templates, documentation - убраны упоминания FC из таблиц index.md и README - перегенерированы RULES.md --- README.md | 4 +- README_RU.md | 4 +- docs/en/index.md | 4 +- docs/ru/applied/components.md | 29 ++-- docs/ru/applied/page-level.md | 7 +- docs/ru/applied/templates-generation.md | 16 +- docs/ru/basics/documentation.md | 4 +- docs/ru/index.md | 4 +- generated/en/RULES.md | 4 +- generated/ru/RULES.md | 204 ++++++------------------ 10 files changed, 91 insertions(+), 189 deletions(-) diff --git a/README.md b/README.md index 4920ab6..cdd8f00 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing, | Code Style | How to format code: indentation, quotes, imports, early return? | | Naming | How to name files, variables, components, hooks? | | Documentation | How to write JSDoc: what to document and what not? | -| Typing | How to type: type vs interface, any/unknown, FC? | +| Typing | How to type: type vs interface, any/unknown? | ### Applied Sections @@ -39,7 +39,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing, | Section | Answers the question | |---------|---------------------| | Project Structure | How are folders and files organized by FSD? | -| Components | How is a component structured: files, props, clsx, FC? | +| Components | How is a component structured: files, props, clsx? | | Page-level Components | How to define layout, page, loading, error, not-found? | | Templates & Code Generation | How do templates work: syntax, variables, modifiers? | | Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? | diff --git a/README_RU.md b/README_RU.md index fb783fa..0ead4f8 100644 --- a/README_RU.md +++ b/README_RU.md @@ -35,7 +35,7 @@ | Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? | | Именование | Как называть файлы, переменные, компоненты, хуки? | | Документирование | Как писать JSDoc: что документировать, а что нет? | -| Типизация | Как типизировать: type vs interface, any/unknown, FC? | +| Типизация | Как типизировать: type vs interface, any/unknown? | ### Прикладные разделы @@ -45,7 +45,7 @@ |--------|-------------------| | Настройка VS Code | Как настроить редактор для проекта? | | Структура проекта | Как организованы папки и файлы по FSD? | -| Компоненты | Как устроен компонент: файлы, пропсы, clsx, FC? | +| Компоненты | Как устроен компонент: файлы, пропсы, clsx? | | Page-level компоненты | Как описывать layout, page, loading, error, not-found? | | Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? | | Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? | diff --git a/docs/en/index.md b/docs/en/index.md index 4920ab6..cdd8f00 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -30,7 +30,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing, | Code Style | How to format code: indentation, quotes, imports, early return? | | Naming | How to name files, variables, components, hooks? | | Documentation | How to write JSDoc: what to document and what not? | -| Typing | How to type: type vs interface, any/unknown, FC? | +| Typing | How to type: type vs interface, any/unknown? | ### Applied Sections @@ -39,7 +39,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing, | Section | Answers the question | |---------|---------------------| | Project Structure | How are folders and files organized by FSD? | -| Components | How is a component structured: files, props, clsx, FC? | +| Components | How is a component structured: files, props, clsx? | | Page-level Components | How to define layout, page, loading, error, not-found? | | Templates & Code Generation | How do templates work: syntax, variables, modifiers? | | Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? | diff --git a/docs/ru/applied/components.md b/docs/ru/applied/components.md index 56f7180..5ea67fd 100644 --- a/docs/ru/applied/components.md +++ b/docs/ru/applied/components.md @@ -25,7 +25,7 @@ container/ ├── styles/ │ └── container.module.css ├── types/ -│ └── container.interface.ts +│ └── container.type.ts ├── container.tsx └── index.ts ``` @@ -33,13 +33,17 @@ container/ ## Именования - Имя корневого css класса всегда `.root` -- Интерфейс именуется `{ComponentName}Props`. +- Тип пропсов именуется `{ComponentName}Props`. +- Тип пользовательских параметров именуется `{ComponentName}Params`. ## Типизация -- Компонент типизируется через `FC`. -- Интерфейс пропсов наследует HTML-атрибуты своего корневого элемента. -- `children` отдельно не объявляется — приходит из `HTMLAttributes`. +Структура типов компонента показана в [примере](#пример). Ниже — обоснования ключевых решений. + +- **`type` вместо `interface`** — гибче для пропсов: поддерживает union, intersection, mapped types. Declaration merging пропсам не нужно. +- **Без `FC`** — неявно добавляет `children`, усложняет дженерики, не даёт преимуществ перед аннотацией параметра. +- **Типы в `types/`, а не в `.tsx`** — предотвращает циклические зависимости (компонент импортирует хук, хук импортирует тип из компонента) и разделяет ответственность: `.tsx` для рендера, `.type.ts` для данных. +- **Без возвращаемого типа** — TypeScript выводит из JSX. Осознанное исключение из [базового правила](/basics/typing). ## Реализация @@ -50,7 +54,7 @@ container/ ## Пример -`container/types/container.interface.ts` +`container/types/container.type.ts` ```ts import type { HTMLAttributes } from 'react' @@ -58,7 +62,12 @@ import type { HTMLAttributes } from 'react' /** * Параметры компонента Container. */ -export interface ContainerProps extends HTMLAttributes {} +export type ContainerParams = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type ContainerProps = RootAttrs & ContainerParams ``` `container/styles/container.module.css` @@ -74,9 +83,8 @@ export interface ContainerProps extends HTMLAttributes {} `container/container.tsx` ```tsx -import type { FC } from 'react' import cl from 'clsx' -import type { ContainerProps } from './types/container.interface' +import type { ContainerProps } from './types/container.type' import styles from './styles/container.module.css' /** @@ -86,7 +94,7 @@ import styles from './styles/container.module.css' * - обёртки контента страниц с ограничением ширины * - центрирования блоков в лейауте */ -export const Container: FC = (props) => { +export const Container = (props: ContainerProps) => { const { children, className, ...htmlAttr } = props return ( @@ -102,4 +110,3 @@ export const Container: FC = (props) => { ```ts export { Container } from './container' ``` - diff --git a/docs/ru/applied/page-level.md b/docs/ru/applied/page-level.md index 2fd3ce4..e9a8585 100644 --- a/docs/ru/applied/page-level.md +++ b/docs/ru/applied/page-level.md @@ -31,7 +31,7 @@ export const metadata: Metadata = { description: 'Страница профиля пользователя', } -interface ProfilePageProps { +type ProfilePageProps = { params: Promise<{ id: string }> } @@ -47,15 +47,14 @@ export default async function ProfilePage({ params }: ProfilePageProps) { ```tsx 'use client' -import type { FC } from 'react' import { ErrorScreen } from '@/screens/error' -interface ErrorPageProps { +type ErrorPageProps = { error: Error & { digest?: string } reset: () => void } -const ErrorPage: FC = ({ error, reset }) => { +const ErrorPage = ({ error, reset }: ErrorPageProps) => { return } diff --git a/docs/ru/applied/templates-generation.md b/docs/ru/applied/templates-generation.md index 3ae013f..3e49a9b 100644 --- a/docs/ru/applied/templates-generation.md +++ b/docs/ru/applied/templates-generation.md @@ -20,7 +20,7 @@ title: Шаблоны и генерация кода │ ├── styles/ │ │ └── {{name.kebabCase}}.module.css │ ├── types/ -│ │ └── {{name.kebabCase}}.interface.ts +│ │ └── {{name.kebabCase}}.type.ts │ ├── {{name.kebabCase}}.tsx │ └── index.ts └── store/ # шаблон Zustand стора @@ -86,26 +86,30 @@ export { {{name.pascalCase}} } from './{{name.kebabCase}}' ``` ```ts -// .templates/component/types/{{name.kebabCase}}.interface.ts +// .templates/component/types/{{name.kebabCase}}.type.ts import type { HTMLAttributes } from 'react' /** * Параметры {{name.pascalCase}}. */ -export interface {{name.pascalCase}}Props extends HTMLAttributes {} +export type {{name.pascalCase}}Params = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type {{name.pascalCase}}Props = RootAttrs & {{name.pascalCase}}Params ``` ```tsx // .templates/component/{{name.kebabCase}}.tsx -import type { FC } from 'react' import cl from 'clsx' -import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.interface' +import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type' import styles from './styles/{{name.kebabCase}}.module.css' /** * {{name.pascalCase}}. */ -export const {{name.pascalCase}}: FC<{{name.pascalCase}}Props> = (props) => { +export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => { const { children, className, ...htmlAttr } = props return ( diff --git a/docs/ru/basics/documentation.md b/docs/ru/basics/documentation.md index d4e9c61..a70c209 100644 --- a/docs/ru/basics/documentation.md +++ b/docs/ru/basics/documentation.md @@ -79,7 +79,7 @@ export const buildCategoryTree = (categories: Category[]): CategoryTree[] => { . * - обёртки контента страниц с ограничением ширины * - центрирования блоков в лейауте */ -export const Container: FC = (props) => { ... } +export const Container = (props: ContainerProps) => { ... } ``` **Плохо** @@ -90,7 +90,7 @@ export const Container: FC = (props) => { ... } */ // Плохо: нет описания вообще. -export const Container: FC = (props) => { ... } +export const Container = (props: ContainerProps) => { ... } ``` ## Типы, интерфейсы, enum diff --git a/docs/ru/index.md b/docs/ru/index.md index fb783fa..0ead4f8 100644 --- a/docs/ru/index.md +++ b/docs/ru/index.md @@ -35,7 +35,7 @@ | Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? | | Именование | Как называть файлы, переменные, компоненты, хуки? | | Документирование | Как писать JSDoc: что документировать, а что нет? | -| Типизация | Как типизировать: type vs interface, any/unknown, FC? | +| Типизация | Как типизировать: type vs interface, any/unknown? | ### Прикладные разделы @@ -45,7 +45,7 @@ |--------|-------------------| | Настройка VS Code | Как настроить редактор для проекта? | | Структура проекта | Как организованы папки и файлы по FSD? | -| Компоненты | Как устроен компонент: файлы, пропсы, clsx, FC? | +| Компоненты | Как устроен компонент: файлы, пропсы, clsx? | | Page-level компоненты | Как описывать layout, page, loading, error, not-found? | | Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? | | Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? | diff --git a/generated/en/RULES.md b/generated/en/RULES.md index 09ff44f..af892fd 100644 --- a/generated/en/RULES.md +++ b/generated/en/RULES.md @@ -31,7 +31,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing, | Code Style | How to format code: indentation, quotes, imports, early return? | | Naming | How to name files, variables, components, hooks? | | Documentation | How to write JSDoc: what to document and what not? | -| Typing | How to type: type vs interface, any/unknown, FC? | +| Typing | How to type: type vs interface, any/unknown? | ### Applied Sections @@ -40,7 +40,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing, | Section | Answers the question | |---------|---------------------| | Project Structure | How are folders and files organized by FSD? | -| Components | How is a component structured: files, props, clsx, FC? | +| Components | How is a component structured: files, props, clsx? | | Page-level Components | How to define layout, page, loading, error, not-found? | | Templates & Code Generation | How do templates work: syntax, variables, modifiers? | | Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? | diff --git a/generated/ru/RULES.md b/generated/ru/RULES.md index dd9e444..fc375db 100644 --- a/generated/ru/RULES.md +++ b/generated/ru/RULES.md @@ -36,7 +36,7 @@ | Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? | | Именование | Как называть файлы, переменные, компоненты, хуки? | | Документирование | Как писать JSDoc: что документировать, а что нет? | -| Типизация | Как типизировать: type vs interface, any/unknown, FC? | +| Типизация | Как типизировать: type vs interface, any/unknown? | ### Прикладные разделы @@ -46,7 +46,7 @@ |--------|-------------------| | Настройка VS Code | Как настроить редактор для проекта? | | Структура проекта | Как организованы папки и файлы по FSD? | -| Компоненты | Как устроен компонент: файлы, пропсы, clsx, FC? | +| Компоненты | Как устроен компонент: файлы, пропсы, clsx? | | Page-level компоненты | Как описывать layout, page, loading, error, not-found? | | Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? | | Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? | @@ -616,7 +616,7 @@ export const buildCategoryTree = (categories: Category[]): CategoryTree[] => { . * - обёртки контента страниц с ограничением ширины * - центрирования блоков в лейауте */ -export const Container: FC = (props) => { ... } +export const Container = (props: ContainerProps) => { ... } ``` **Плохо** @@ -627,7 +627,7 @@ export const Container: FC = (props) => { ... } */ // Плохо: нет описания вообще. -export const Container: FC = (props) => { ... } +export const Container = (props: ContainerProps) => { ... } ``` ### Типы, интерфейсы, enum @@ -845,7 +845,7 @@ container/ ├── styles/ │ └── container.module.css ├── types/ -│ └── container.interface.ts +│ └── container.type.ts ├── container.tsx └── index.ts ``` @@ -853,13 +853,17 @@ container/ ### Именования - Имя корневого css класса всегда `.root` -- Интерфейс именуется `{ComponentName}Props`. +- Тип пропсов именуется `{ComponentName}Props`. +- Тип пользовательских параметров именуется `{ComponentName}Params`. ### Типизация -- Компонент типизируется через `FC`. -- Интерфейс пропсов наследует HTML-атрибуты своего корневого элемента. -- `children` отдельно не объявляется — приходит из `HTMLAttributes`. +Структура типов компонента показана в [примере](#пример). Ниже — обоснования ключевых решений. + +- **`type` вместо `interface`** — гибче для пропсов: поддерживает union, intersection, mapped types. Declaration merging пропсам не нужно. +- **Без `FC`** — неявно добавляет `children`, усложняет дженерики, не даёт преимуществ перед аннотацией параметра. +- **Типы в `types/`, а не в `.tsx`** — предотвращает циклические зависимости (компонент импортирует хук, хук импортирует тип из компонента) и разделяет ответственность: `.tsx` для рендера, `.type.ts` для данных. +- **Без возвращаемого типа** — TypeScript выводит из JSX. Осознанное исключение из [базового правила](/basics/typing). ### Реализация @@ -870,7 +874,7 @@ container/ ### Пример -`container/types/container.interface.ts` +`container/types/container.type.ts` ```ts import type { HTMLAttributes } from 'react' @@ -878,7 +882,12 @@ import type { HTMLAttributes } from 'react' /** * Параметры компонента Container. */ -export interface ContainerProps extends HTMLAttributes {} +export type ContainerParams = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type ContainerProps = RootAttrs & ContainerParams ``` `container/styles/container.module.css` @@ -894,9 +903,8 @@ export interface ContainerProps extends HTMLAttributes {} `container/container.tsx` ```tsx -import type { FC } from 'react' import cl from 'clsx' -import type { ContainerProps } from './types/container.interface' +import type { ContainerProps } from './types/container.type' import styles from './styles/container.module.css' /** @@ -906,7 +914,7 @@ import styles from './styles/container.module.css' * - обёртки контента страниц с ограничением ширины * - центрирования блоков в лейауте */ -export const Container: FC = (props) => { +export const Container = (props: ContainerProps) => { const { children, className, ...htmlAttr } = props return ( @@ -924,100 +932,25 @@ export { Container } from './container' ``` -## Страницы (App Router) +## Файлы роутинга -Специальные файлы Next.js App Router, которые фреймворк использует по соглашению: `layout.tsx`, `page.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`, `template.tsx`. +Правила для специальных файлов App Router (`page.tsx`, `layout.tsx`, `error.tsx`, `not-found.tsx` и др.) — чем наш подход отличается от дефолтного. -### Общие правила +### Организация -- Экспорт через `export default function` — конвенция Next.js. -- Типизация через `PropsWithChildren` или явный интерфейс. -- Каждая страница (`page.tsx`) должна содержать `metadata` с `title` и `description`. -- Минимум логики — page-level компоненты делегируют работу экранам, виджетам и фичам. -- Стили в page-level компонентах не используются — стилизация внутри вызываемых компонентов. +- `page.tsx` — тонкий файл: только `metadata` и рендер экрана. Логика, стили и зависимости живут в экране, не в `page.tsx`. +- `error.tsx` и `not-found.tsx` делегируют разметку экранам по тому же принципу. +- `layout.tsx` — точка подключения провайдеров и глобальных стилей. Вёрстка layout-обёрток выносится в слой `layouts/`. +- Стили в файлах роутинга не используются — стилизация только внутри вызываемых компонентов. -### layout.tsx +### Реализация -Корневой layout — точка подключения провайдеров, глобальных стилей и метаданных. +- Каждый `page.tsx` экспортирует `metadata` с `title` — он подставляется в шаблон корневого layout (`%s | App`). +- Корневой `layout.tsx` задаёт `metadata` с `title.template`, `description`, `metadataBase` и OpenGraph-настройками. -```tsx -import type { PropsWithChildren } from 'react' -import type { Metadata } from 'next' -import { Providers } from './providers' -import './styles/index.css' +### Примеры -export const metadata: Metadata = { - title: { - default: 'App', - template: '%s | App', - }, - description: 'Описание приложения', - metadataBase: new URL('https://example.com'), - openGraph: { - type: 'website', - locale: 'ru_RU', - siteName: 'App', - images: [ - { - url: '/og-image.png', - width: 1200, - height: 630, - alt: 'App', - }, - ], - }, - twitter: { - card: 'summary_large_image', - }, -} - -export default function RootLayout({ children }: PropsWithChildren) { - return ( - - - - {children} - - - - ) -} -``` - -Вложенный layout — для секции с общей обёрткой (sidebar, header): - -```tsx -import type { PropsWithChildren } from 'react' -import { DashboardLayout } from '@/shared/ui/dashboard-layout' - -export default function Layout({ children }: PropsWithChildren) { - return ( - - {children} - - ) -} -``` - -### page.tsx - -Тонкий файл — только импорт и рендер экрана. Логика, стили и зависимости размещаются в экране, не в `page.tsx`. - -```tsx -import type { Metadata } from 'next' -import { HomeScreen } from '@/screens/home' - -export const metadata: Metadata = { - title: 'Главная', - description: 'Главная страница приложения', -} - -export default function HomePage() { - return -} -``` - -С параметрами маршрута: +`src/app/profile/[id]/page.tsx` ```tsx import type { Metadata } from 'next' @@ -1028,7 +961,7 @@ export const metadata: Metadata = { description: 'Страница профиля пользователя', } -interface ProfilePageProps { +type ProfilePageProps = { params: Promise<{ id: string }> } @@ -1039,70 +972,25 @@ export default async function ProfilePage({ params }: ProfilePageProps) { } ``` -Каждая страница должна содержать `metadata` с `title` — он подставится в шаблон из корневого layout: `Профиль | App`. - -### loading.tsx - -Состояние загрузки. Показывается пока загружается контент страницы. - -```tsx -export default function Loading() { - return
Загрузка...
-} -``` - -### error.tsx - -Обработка ошибок. Обязательно `'use client'` — error boundary работает только на клиенте. Разметку выносим в экран. +`src/app/error.tsx` ```tsx 'use client' -import type { FC } from 'react' import { ErrorScreen } from '@/screens/error' -interface ErrorPageProps { +type ErrorPageProps = { error: Error & { digest?: string } reset: () => void } -const ErrorPage: FC = ({ error, reset }) => { +const ErrorPage = ({ error, reset }: ErrorPageProps) => { return } export default ErrorPage ``` -### not-found.tsx - -Страница 404. Показывается когда маршрут не найден. Разметку выносим в экран. - -```tsx -import type { Metadata } from 'next' -import { NotFoundScreen } from '@/screens/not-found' - -export const metadata: Metadata = { - title: 'Страница не найдена', - description: 'Запрашиваемая страница не существует', -} - -export default function NotFound() { - return -} -``` - -### template.tsx - -Аналог layout, но пересоздаётся при каждой навигации (не сохраняет состояние). Используется редко — для анимаций переходов между страницами. - -```tsx -import type { PropsWithChildren } from 'react' - -export default function Template({ children }: PropsWithChildren) { - return
{children}
-} -``` - ::: v-pre @@ -1122,7 +1010,7 @@ export default function Template({ children }: PropsWithChildren) { │ ├── styles/ │ │ └── {{name.kebabCase}}.module.css │ ├── types/ -│ │ └── {{name.kebabCase}}.interface.ts +│ │ └── {{name.kebabCase}}.type.ts │ ├── {{name.kebabCase}}.tsx │ └── index.ts └── store/ # шаблон Zustand стора @@ -1188,26 +1076,30 @@ export { {{name.pascalCase}} } from './{{name.kebabCase}}' ``` ```ts -// .templates/component/types/{{name.kebabCase}}.interface.ts +// .templates/component/types/{{name.kebabCase}}.type.ts import type { HTMLAttributes } from 'react' /** * Параметры {{name.pascalCase}}. */ -export interface {{name.pascalCase}}Props extends HTMLAttributes {} +export type {{name.pascalCase}}Params = {} + +/** HTML-атрибуты корневого элемента. */ +type RootAttrs = HTMLAttributes + +export type {{name.pascalCase}}Props = RootAttrs & {{name.pascalCase}}Params ``` ```tsx // .templates/component/{{name.kebabCase}}.tsx -import type { FC } from 'react' import cl from 'clsx' -import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.interface' +import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type' import styles from './styles/{{name.kebabCase}}.module.css' /** * {{name.pascalCase}}. */ -export const {{name.pascalCase}}: FC<{{name.pascalCase}}Props> = (props) => { +export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => { const { children, className, ...htmlAttr } = props return (