mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
feat: add example
This commit is contained in:
1226
.opencode/skills/style-guide/SKILL.md
Normal file
1226
.opencode/skills/style-guide/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
4
.opencode/skills/style-guide/agents/openai.yaml
Normal file
4
.opencode/skills/style-guide/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Style Guide"
|
||||
short_description: "Frontend code style, naming and local consistency"
|
||||
default_prompt: "Use $style-guide to format or review frontend code according to the project style guide and local code conventions."
|
||||
@@ -0,0 +1,154 @@
|
||||
# Value Predicates
|
||||
|
||||
`value-predicates` — небольшая библиотека runtime-предикатов для безопасной работы с `unknown`, `null`, массивами и объектами.
|
||||
|
||||
Файл содержит два типа утилит:
|
||||
|
||||
- Type guards: возвращают `value is T` и сужают тип в TypeScript.
|
||||
- Boolean-предикаты: возвращают `boolean` и используются для читаемых условий без обязательного narrowing.
|
||||
|
||||
Проверки доменных DTO и API-ответов не размещаются здесь. Они должны жить рядом с владельцем данных: business-модулем, mapper/source или infra-адаптером.
|
||||
|
||||
## Группы
|
||||
|
||||
- `Nullish`: `isDefined`, `isNotDefined`.
|
||||
- `Primitives`: `isString`, `isNumber`, `isBoolean`.
|
||||
- `Strings`: `isNonEmptyString`.
|
||||
- `Arrays`: `isArray`, `isArrayOf`, `isEmptyArray`, `isNonEmptyArray`.
|
||||
- `Objects`: `isRecord`, `hasOwn`.
|
||||
- `Combinators`: `isOneOf`.
|
||||
|
||||
## Правило для TSX
|
||||
|
||||
В conditional rendering не пишем голые проверки длины массива:
|
||||
|
||||
```tsx
|
||||
items?.length
|
||||
items.length > 0
|
||||
items.length !== 0
|
||||
items.length === 0
|
||||
!items.length
|
||||
items && items.map(...)
|
||||
```
|
||||
|
||||
Для списков используем `isEmptyArray`, `isNonEmptyArray`, а для `unknown`-данных — `isArrayOf`.
|
||||
|
||||
Голый `.length` допустим, когда нужен именно числовой размер для текста, расчётов или атрибутов.
|
||||
|
||||
## Уже типизированный массив
|
||||
|
||||
```tsx
|
||||
const ordersData = orders.data
|
||||
|
||||
{isNonEmptyArray(ordersData) && (
|
||||
<OrdersList orders={ordersData} />
|
||||
)}
|
||||
```
|
||||
|
||||
В этом сценарии `ordersData` уже имеет тип вроде `Order[] | null | undefined`, поэтому достаточно проверить, что массив существует и не пуст.
|
||||
|
||||
## Empty state
|
||||
|
||||
```tsx
|
||||
const ordersData = orders.data
|
||||
const shouldShowEmptyState = isEmptyArray(ordersData) && !orders.isLoading
|
||||
|
||||
{shouldShowEmptyState && (
|
||||
<EmptyState />
|
||||
)}
|
||||
```
|
||||
|
||||
`isEmptyArray` считает `null` и `undefined` пустым списком. Это удобно для UI-состояний, где отсутствие данных и пустой список показывают один empty state.
|
||||
|
||||
## Map в JSX
|
||||
|
||||
```tsx
|
||||
const itemsData = items
|
||||
|
||||
{isNonEmptyArray(itemsData) && itemsData.map((item) => (
|
||||
<Card key={item.id} item={item} />
|
||||
))}
|
||||
```
|
||||
|
||||
Сначала сужаем локальную переменную, потом используем её в `map`. Не дублируем путь к данным внутри JSX.
|
||||
|
||||
## Unknown/API данные
|
||||
|
||||
```tsx
|
||||
type Order = {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
const isOrder = (value: unknown): value is Order => {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
hasOwn(value, 'id') &&
|
||||
isString(value.id) &&
|
||||
hasOwn(value, 'title') &&
|
||||
isString(value.title)
|
||||
)
|
||||
}
|
||||
|
||||
const ordersData = response.data
|
||||
|
||||
{isArrayOf(ordersData, isOrder) && (
|
||||
<OrdersList orders={ordersData} />
|
||||
)}
|
||||
```
|
||||
|
||||
`isArrayOf` нужен на границе с недоверенными данными: он проверяет не только массив, но и каждый элемент через item guard.
|
||||
|
||||
Если нужно одновременно проверить форму элементов и непустой массив:
|
||||
|
||||
```tsx
|
||||
const canRenderOrders = isArrayOf(ordersData, isOrder) && isNonEmptyArray(ordersData)
|
||||
|
||||
{canRenderOrders && (
|
||||
<OrdersList orders={ordersData} />
|
||||
)}
|
||||
```
|
||||
|
||||
Если такой паттерн повторится много раз, можно добавить отдельный `isNonEmptyArrayOf`, но заранее его не вводим.
|
||||
|
||||
## Object fields
|
||||
|
||||
```ts
|
||||
if (isRecord(value) && hasOwn(value, 'code') && isString(value.code)) {
|
||||
// value.code: string
|
||||
}
|
||||
```
|
||||
|
||||
`isRecord` проверяет только базовую форму объекта: не `null` и не массив. Конкретные поля всегда проверяются отдельно.
|
||||
|
||||
## Literal unions
|
||||
|
||||
```ts
|
||||
const statuses = ['draft', 'published'] as const
|
||||
|
||||
if (isOneOf(value, statuses)) {
|
||||
// value: 'draft' | 'published'
|
||||
}
|
||||
```
|
||||
|
||||
`isOneOf` удобен для runtime-проверки union-типов, собранных из `as const` массивов.
|
||||
|
||||
## Нормализованный массив
|
||||
|
||||
Если массив заранее нормализован, прямой `map` допустим:
|
||||
|
||||
```tsx
|
||||
const ordersData = orders.data ?? []
|
||||
|
||||
return ordersData.map((order) => (
|
||||
<OrderCard key={order.id} order={order} />
|
||||
))
|
||||
```
|
||||
|
||||
Для conditional rendering empty state всё равно используем predicate:
|
||||
|
||||
```tsx
|
||||
{isEmptyArray(ordersData) && (
|
||||
<EmptyState />
|
||||
)}
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
export * from './value-predicates'
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Value predicates для проверки runtime-значений.
|
||||
*
|
||||
* Type guards в этом файле сужают unknown-данные только до базовых типов.
|
||||
* Boolean-предикаты используются для читаемых условий и не обязаны сужать тип.
|
||||
* Проверки доменных DTO и API-ответов размещаются рядом с владельцем данных.
|
||||
*/
|
||||
|
||||
/* --- Nullish --- */
|
||||
|
||||
/**
|
||||
* Исключает только null и undefined из типа значения.
|
||||
*
|
||||
* `0`, `false` и пустая строка не считаются отсутствующими.
|
||||
* Удобно для `.filter(isDefined)`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const values = [0, null, false, undefined, '']
|
||||
* const definedValues = values.filter(isDefined)
|
||||
* // definedValues: Array<0 | false | ''>
|
||||
* ```
|
||||
*/
|
||||
export const isDefined = <T>(value: T | null | undefined): value is T => {
|
||||
return value != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, что значение отсутствует как null или undefined.
|
||||
*/
|
||||
export const isNotDefined = <T>(value: T | null | undefined): value is null | undefined => {
|
||||
return value == null
|
||||
}
|
||||
|
||||
/* --- Primitives --- */
|
||||
|
||||
/**
|
||||
* Сужает unknown-значение до string.
|
||||
*/
|
||||
export const isString = (value: unknown): value is string => {
|
||||
return typeof value === 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
* Сужает unknown-значение до конечного number.
|
||||
*
|
||||
* NaN и Infinity не проходят проверку.
|
||||
*/
|
||||
export const isNumber = (value: unknown): value is number => {
|
||||
return typeof value === 'number' && Number.isFinite(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Сужает unknown-значение до boolean.
|
||||
*/
|
||||
export const isBoolean = (value: unknown): value is boolean => {
|
||||
return typeof value === 'boolean'
|
||||
}
|
||||
|
||||
/* --- Strings --- */
|
||||
|
||||
/**
|
||||
* Проверяет, что значение является строкой с непустым содержимым.
|
||||
*
|
||||
* Пробельная строка считается пустой.
|
||||
*/
|
||||
export const isNonEmptyString = (value: unknown): value is string => {
|
||||
return typeof value === 'string' && value.trim().length > 0
|
||||
}
|
||||
|
||||
/* --- Arrays --- */
|
||||
|
||||
/**
|
||||
* Проверяет, что значение является массивом.
|
||||
*
|
||||
* Не проверяет тип элементов. Для проверки элементов используйте `isArrayOf`.
|
||||
*/
|
||||
export const isArray = (value: unknown): value is unknown[] => {
|
||||
return Array.isArray(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет массив и каждый его элемент через переданный предикат элемента.
|
||||
*
|
||||
* Используется на границах с unknown-данными, когда нужно получить `T[]`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* if (isArrayOf(value, isString)) {
|
||||
* // value: string[]
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const isArrayOf = <T>(value: unknown, isItem: (item: unknown) => item is T): value is T[] => {
|
||||
return Array.isArray(value) && value.every(isItem)
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, что массив отсутствует или не содержит элементов.
|
||||
*
|
||||
* Null и undefined считаются пустым списком для UI-условий.
|
||||
*/
|
||||
export const isEmptyArray = (value: readonly unknown[] | null | undefined): boolean => {
|
||||
return !Array.isArray(value) || value.length === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, что массив существует и содержит хотя бы один элемент.
|
||||
*
|
||||
* Сужает тип до non-empty tuple, чтобы TypeScript знал,
|
||||
* что обращение к первому элементу безопасно.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* if (isNonEmptyArray(items)) {
|
||||
* const firstItem = items[0]
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const isNonEmptyArray = <T>(value: readonly T[] | null | undefined): value is readonly [T, ...T[]] => {
|
||||
return Array.isArray(value) && value.length > 0
|
||||
}
|
||||
|
||||
/* --- Objects --- */
|
||||
|
||||
/**
|
||||
* Проверяет, что значение является объектом-записью.
|
||||
*
|
||||
* Исключает null и массивы, но не проверяет конкретную форму объекта.
|
||||
*/
|
||||
export const isRecord = (value: unknown): value is Record<PropertyKey, unknown> => {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет наличие собственного свойства объекта.
|
||||
*
|
||||
* Используйте вместе с `isRecord` перед чтением unknown-свойств.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* if (isRecord(value) && hasOwn(value, 'code') && isString(value.code)) {
|
||||
* // value.code: string
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const hasOwn = <K extends PropertyKey>(value: object, key: K): value is Record<K, unknown> => {
|
||||
return Object.prototype.hasOwnProperty.call(value, key)
|
||||
}
|
||||
|
||||
/* --- Combinators --- */
|
||||
|
||||
/**
|
||||
* Проверяет, что значение входит в список допустимых литералов.
|
||||
*
|
||||
* Удобно для runtime-проверки union-типов из `as const` массивов.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const statuses = ['draft', 'published'] as const
|
||||
*
|
||||
* if (isOneOf(value, statuses)) {
|
||||
* // value: 'draft' | 'published'
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const isOneOf = <T extends readonly unknown[]>(value: unknown, values: T): value is T[number] => {
|
||||
return values.some((item) => item === value)
|
||||
}
|
||||
Reference in New Issue
Block a user