@wangs-ui/skills 1.3.0-alpha.11 → 1.3.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -7
- package/dist/bin.js +18 -14
- package/dist/index.js +2 -2
- package/dist/rules/react19-checklist-and-reference.md +37 -0
- package/dist/rules/react19-compiler-render-patterns.md +13 -0
- package/dist/rules/react19-naming-conventions.md +16 -0
- package/dist/rules/react19-no-manual-memoization.md +26 -0
- package/dist/rules/react19-primitives-typing.md +85 -0
- package/dist/rules/react19-props-typing.md +25 -0
- package/dist/rules/react19-purity-and-immutability.md +41 -0
- package/dist/rules/react19-tooling-and-opt-out.md +68 -0
- package/dist/rules/typescript-assertions-last-resort.md +13 -0
- package/dist/rules/typescript-checklist-and-reference.md +34 -0
- package/dist/rules/typescript-discriminated-unions.md +40 -0
- package/dist/rules/typescript-explicit-return-types.md +22 -0
- package/dist/rules/typescript-interface-vs-type.md +40 -0
- package/dist/rules/typescript-literal-unions-vs-enums.md +22 -0
- package/dist/rules/typescript-naming-conventions.md +24 -0
- package/dist/rules/typescript-narrowing-over-casting.md +44 -0
- package/dist/rules/typescript-readonly-by-default.md +24 -0
- package/dist/rules/typescript-tsconfig-strictness.md +31 -0
- package/dist/rules/typescript-zero-any.md +37 -0
- package/dist/rules/wangs-ui/default-props-precedence.md +14 -0
- package/dist/rules/wangs-ui/forms-destructuring-lock.md +19 -0
- package/dist/rules/wangs-ui/no-raw-html.md +29 -0
- package/dist/rules/wangs-ui/no-redundant-wrappers.md +56 -0
- package/dist/rules/wangs-ui/theme-variable-override-breaks-engine.md +20 -0
- package/dist/skills/craft-theme/SKILL.md +2 -1
- package/dist/skills/create-form/SKILL.md +2 -1
- package/dist/skills/data-table/SKILL.md +2 -1
- package/dist/skills/dialog-modal/SKILL.md +2 -1
- package/dist/skills/i18n-usage/SKILL.md +2 -1
- package/dist/skills/layout-navigation/SKILL.md +2 -1
- package/dist/skills/responsive-design/SKILL.md +2 -1
- package/dist/skills/universal-layout/SKILL.md +2 -1
- package/dist/skills/wangs-ui-components/SKILL.md +2 -1
- package/dist/src-DwQOnl9b.js +640 -0
- package/package.json +3 -2
- package/rules/react19-checklist-and-reference.md +37 -0
- package/rules/react19-compiler-render-patterns.md +13 -0
- package/rules/react19-naming-conventions.md +16 -0
- package/rules/react19-no-manual-memoization.md +26 -0
- package/rules/react19-primitives-typing.md +85 -0
- package/rules/react19-props-typing.md +25 -0
- package/rules/react19-purity-and-immutability.md +41 -0
- package/rules/react19-tooling-and-opt-out.md +68 -0
- package/rules/typescript-assertions-last-resort.md +13 -0
- package/rules/typescript-checklist-and-reference.md +34 -0
- package/rules/typescript-discriminated-unions.md +40 -0
- package/rules/typescript-explicit-return-types.md +22 -0
- package/rules/typescript-interface-vs-type.md +40 -0
- package/rules/typescript-literal-unions-vs-enums.md +22 -0
- package/rules/typescript-naming-conventions.md +24 -0
- package/rules/typescript-narrowing-over-casting.md +44 -0
- package/rules/typescript-readonly-by-default.md +24 -0
- package/rules/typescript-tsconfig-strictness.md +31 -0
- package/rules/typescript-zero-any.md +37 -0
- package/rules/wangs-ui/default-props-precedence.md +14 -0
- package/rules/wangs-ui/forms-destructuring-lock.md +19 -0
- package/rules/wangs-ui/no-raw-html.md +29 -0
- package/rules/wangs-ui/no-redundant-wrappers.md +56 -0
- package/rules/wangs-ui/theme-variable-override-breaks-engine.md +20 -0
- package/skills/craft-theme/SKILL.md +2 -1
- package/skills/create-form/SKILL.md +2 -1
- package/skills/data-table/SKILL.md +2 -1
- package/skills/dialog-modal/SKILL.md +2 -1
- package/skills/i18n-usage/SKILL.md +2 -1
- package/skills/layout-navigation/SKILL.md +2 -1
- package/skills/responsive-design/SKILL.md +2 -1
- package/skills/universal-layout/SKILL.md +2 -1
- package/skills/wangs-ui-components/SKILL.md +2 -1
- package/dist/skills/react19-compiler-typescript/SKILL.md +0 -388
- package/dist/skills/typescript-strict-typing/SKILL.md +0 -317
- package/dist/src-DF1oWSLf.js +0 -258
- package/skills/react19-compiler-typescript/SKILL.md +0 -388
- package/skills/typescript-strict-typing/SKILL.md +0 -317
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wangs-ui/skills",
|
|
3
|
-
"version": "1.3.0-alpha.
|
|
3
|
+
"version": "1.3.0-alpha.12",
|
|
4
4
|
"description": "CLI to install, update, and manage modular AI agent skills for Wangs UI React applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
32
|
-
"skills"
|
|
32
|
+
"skills",
|
|
33
|
+
"rules"
|
|
33
34
|
],
|
|
34
35
|
"type": "module",
|
|
35
36
|
"sideEffects": false,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when doing a final review pass on React 19 component or hook code for compiler-optimization compliance."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: React 19 — Review Checklist & Quick Reference
|
|
9
|
+
|
|
10
|
+
## Checking Compiler Optimization
|
|
11
|
+
|
|
12
|
+
- **React DevTools** — an optimized component shows a "Memo ✨" badge next to its name in the component tree.
|
|
13
|
+
- **ESLint / Oxlint** — compiler's recommended rules flag Rules-of-React violations at lint time.
|
|
14
|
+
|
|
15
|
+
## Review Checklist
|
|
16
|
+
|
|
17
|
+
- [ ] Compiler confirmed active ("Memo ✨" badge) before removing any _existing_ manual memoization
|
|
18
|
+
- [ ] No new `useMemo`/`useCallback`/`React.memo` added without documented reason (confirmed bail-out, or external boundary)
|
|
19
|
+
- [ ] No prop/state/context mutation anywhere in render
|
|
20
|
+
- [ ] All hooks called unconditionally at top level, same order every render
|
|
21
|
+
- [ ] Side effects live in `useEffect`/event handlers, never during render
|
|
22
|
+
- [ ] Components are `PascalCase`; hooks are `camelCase` and prefixed `use`
|
|
23
|
+
- [ ] `ref` accepted as normal prop instead of `forwardRef`
|
|
24
|
+
- [ ] Action/optimistic-update state modeled as discriminated union, not optional fields
|
|
25
|
+
- [ ] Mutually exclusive prop combinations modeled as discriminated union `Props` type
|
|
26
|
+
- [ ] Any `"use no memo"` usage has a comment explaining why
|
|
27
|
+
|
|
28
|
+
## Quick Reference
|
|
29
|
+
|
|
30
|
+
| Situation | Do |
|
|
31
|
+
| ----------------------------------------------------------- | ------------------------------------------------------------- |
|
|
32
|
+
| Tempted to write `useMemo`/`useCallback` | Don't — write plain expression, let compiler decide |
|
|
33
|
+
| Need a ref on a function component | Accept `ref` as a prop, skip `forwardRef` |
|
|
34
|
+
| Form/async state with distinct outcomes | Discriminated union via `useActionState`, not optional fields |
|
|
35
|
+
| Callback needs latest props/state without re-running effect | `useEffectEvent` |
|
|
36
|
+
| A hook/library is known-incompatible with compiler | `"use no memo"` at top of that function, with comment |
|
|
37
|
+
| Checking if optimization is happening | React DevTools "Memo ✨" badge + compiler lint rules |
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when writing or reviewing render logic in a React 19 component."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: React 19 — Compiler-Friendly Render Patterns
|
|
9
|
+
|
|
10
|
+
- Creating new object/array/function literals inline in render (`style={{ color }}`, `onClick={() => ...}`) is fine — stop manually hoisting or `useMemo`-wrapping these preemptively; the compiler memoizes them if it determines it's worthwhile.
|
|
11
|
+
- Avoid module-level mutable variables read or written during render — that state is invisible to the compiler and breaks idempotence.
|
|
12
|
+
- Don't use `useRef` to store a value that should trigger a re-render when it changes — refs are an imperative escape hatch, not state, and the compiler treats them as such.
|
|
13
|
+
- Keep components small and composable. The compiler optimizes per component/hook boundary, so a single 300-line component gives it far less to work with than several focused ones.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when naming a React 19 component, hook, or prop."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: React 19 — Naming Conventions
|
|
9
|
+
|
|
10
|
+
The compiler identifies what to optimize by naming heuristics, same as the Rules of Hooks linter:
|
|
11
|
+
|
|
12
|
+
| Kind | Convention | Notes |
|
|
13
|
+
| ------------------------------------------------------------------------ | ------------------------------- | ----------------------------------------------------------------------------- |
|
|
14
|
+
| Components | `PascalCase`, returns JSX | Compiler treats it as a component to optimize |
|
|
15
|
+
| Custom hooks | `camelCase`, prefixed `use` | Required for both Rules-of-Hooks lint and compiler analysis |
|
|
16
|
+
| Plain helper functions that return JSX-like values but aren't components | Avoid `PascalCase`/`use` naming | Prevents the compiler (and other devs) from mistaking it for a component/hook |
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when writing or reviewing a React 19 component or hook that uses or is tempted to use useMemo/useCallback/React.memo."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: React 19 — Stop Hand-Rolling Memoization
|
|
9
|
+
|
|
10
|
+
Don't reach for `useMemo`, `useCallback`, or `React.memo` by default — the compiler adds this automatically wherever it determines it helps.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
// ❌ Old habit — noisy, and a mismatched dependency array is a whole class of bugs
|
|
14
|
+
const filteredUsers = useMemo(() => users.filter((u) => u.isActive), [users]);
|
|
15
|
+
const handleClick = useCallback(() => onSelect(user.id), [onSelect, user.id]);
|
|
16
|
+
|
|
17
|
+
// ✅ New default — just write the logic; the compiler memoizes what's worth memoizing
|
|
18
|
+
const filteredUsers = users.filter((u) => u.isActive);
|
|
19
|
+
const handleClick = () => onSelect(user.id);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## When manual memoization is still justified:
|
|
23
|
+
|
|
24
|
+
- You've **confirmed a compiler bail-out** on a genuine hot path via profiling, and fixing the underlying Rules-of-React violation isn't possible right now.
|
|
25
|
+
- A value must have **stable referential identity across a boundary the compiler can't see** — e.g. passed into a non-React library, a WebSocket subscription, or a third-party hook incompatible with the compiler (`react-hook-form`'s `useForm`, `@tanstack/react-table`'s `useReactTable` are known cases).
|
|
26
|
+
- Keep any manual memoization it produces isolated and commented with _why_, so it doesn't silently rot into a bail-out later when the code around it changes.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when typing React 19 primitives — ref props, useActionState, useOptimistic, use(), useEffectEvent."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: React 19 — Typing React 19 Primitives
|
|
9
|
+
|
|
10
|
+
## `ref` as a Normal Prop
|
|
11
|
+
|
|
12
|
+
`forwardRef` is no longer required for most cases; function components accept `ref` directly.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
type InputProps = {
|
|
16
|
+
ref?: React.Ref<HTMLInputElement>;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function TextInput({ ref, placeholder }: InputProps) {
|
|
21
|
+
return <input ref={ref} placeholder={placeholder} />;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Actions with `useActionState`
|
|
26
|
+
|
|
27
|
+
Type the state and payload as generics; model the result as a discriminated union rather than optional fields.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
type FormState = { status: 'idle' } | { status: 'error'; message: string } | { status: 'success' };
|
|
31
|
+
|
|
32
|
+
const [state, formAction, isPending] = useActionState<FormState, FormData>(
|
|
33
|
+
async (_previous, formData) => {
|
|
34
|
+
const email = formData.get('email');
|
|
35
|
+
if (typeof email !== 'string' || !email.includes('@')) {
|
|
36
|
+
return { status: 'error', message: 'Invalid email' };
|
|
37
|
+
}
|
|
38
|
+
await submit(email);
|
|
39
|
+
return { status: 'success' };
|
|
40
|
+
},
|
|
41
|
+
{ status: 'idle' },
|
|
42
|
+
);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Optimistic Updates with `useOptimistic`
|
|
46
|
+
|
|
47
|
+
Type both the state and the update shape.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
const [optimisticTodos, addOptimisticTodo] = useOptimistic<Todo[], Todo>(
|
|
51
|
+
todos,
|
|
52
|
+
(state, newTodo) => [...state, newTodo],
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Reading Promises or Context with `use()`
|
|
57
|
+
|
|
58
|
+
Type the resolved value, not the promise wrapper; `use()` is not a hook and may be called conditionally.
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
|
|
62
|
+
const comments = use(commentsPromise); // suspends until resolved
|
|
63
|
+
return (
|
|
64
|
+
<ul>
|
|
65
|
+
{comments.map((c) => (
|
|
66
|
+
<li key={c.id}>{c.text}</li>
|
|
67
|
+
))}
|
|
68
|
+
</ul>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Stable Event Callbacks with `useEffectEvent` (React 19.2+)
|
|
74
|
+
|
|
75
|
+
Separates "event" logic from "reactive" effect logic so the callback always sees latest props/state without being listed as an effect dependency.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
const onVisit = useEffectEvent((url: string) => {
|
|
79
|
+
logVisit(url, theme); // always fresh `theme`, never re-triggers the effect
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
onVisit(url);
|
|
84
|
+
}, [url]); // `theme` intentionally omitted — onVisit is stable
|
|
85
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when defining a component's Props type or interface."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: React 19 — Typing Props
|
|
9
|
+
|
|
10
|
+
- `interface` for a component's `Props` — it's an entity shape, often extended.
|
|
11
|
+
- A discriminated union when a component has mutually exclusive prop combinations, instead of a pile of optional props that can contradict each other.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// ❌ Bad — nothing stops passing both `href` and `onClick` incoherently
|
|
15
|
+
interface ButtonProps {
|
|
16
|
+
label: string;
|
|
17
|
+
href?: string;
|
|
18
|
+
onClick?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ✅ Good — the two variants can't be mixed
|
|
22
|
+
type ButtonProps =
|
|
23
|
+
| { variant: 'link'; label: string; href: string }
|
|
24
|
+
| { variant: 'action'; label: string; onClick: () => void };
|
|
25
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when writing or reviewing a React 19 component or hook body for purity and immutability."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: React 19 — Purity and Immutability (Load-Bearing)
|
|
9
|
+
|
|
10
|
+
The compiler assumes your components and hooks are pure. Violating these rules causes the compiler to silently skip optimizing that component:
|
|
11
|
+
|
|
12
|
+
- **Idempotent renders** — given the same props/state/context, a component must return the same output. No random values, no `Date.now()`, no side effects during render.
|
|
13
|
+
- **Immutability** — never mutate props, state, or context directly. Always create new objects/arrays for changes.
|
|
14
|
+
- **Side effects only in effects or event handlers** — never during render.
|
|
15
|
+
- **Hooks called unconditionally, top-level, same order every render** — no hooks inside conditionals, loops, or nested functions.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// ❌ Mutates a prop — breaks purity and the compiler can't safely memoize this
|
|
19
|
+
function TodoList({ todos }: { todos: Todo[] }) {
|
|
20
|
+
todos.sort((a, b) => a.priority - b.priority); // mutates caller's array
|
|
21
|
+
return (
|
|
22
|
+
<ul>
|
|
23
|
+
{todos.map((t) => (
|
|
24
|
+
<li key={t.id}>{t.title}</li>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ✅ Creates a new array — pure, compiler-safe
|
|
31
|
+
function TodoList({ todos }: { todos: Todo[] }) {
|
|
32
|
+
const sorted = [...todos].sort((a, b) => a.priority - b.priority);
|
|
33
|
+
return (
|
|
34
|
+
<ul>
|
|
35
|
+
{sorted.map((t) => (
|
|
36
|
+
<li key={t.id}>{t.title}</li>
|
|
37
|
+
))}
|
|
38
|
+
</ul>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: 'Apply when configuring React Compiler tooling or opting a component out via "use no memo".'
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: React 19 — Tooling Setup & Compiler Opt-Out
|
|
9
|
+
|
|
10
|
+
## Tooling Setup
|
|
11
|
+
|
|
12
|
+
**The compiler is opt-in — no default setup enables it automatically.** Plain `@vitejs/plugin-react` (`react()`), plain Next.js, plain Babel/webpack config, etc. do **not** run the compiler on their own. Verify it's actually wired up before assuming manual memoization can be dropped.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Compiler (build-time transform)
|
|
16
|
+
npm install --save-dev --save-exact babel-plugin-react-compiler@latest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Lint rules — oxlint
|
|
20
|
+
|
|
21
|
+
Oxlint ships a **native, Rust-based** `react/react-compiler` rule:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
// .oxlintrc.json
|
|
25
|
+
{
|
|
26
|
+
"plugins": ["react"],
|
|
27
|
+
"rules": {
|
|
28
|
+
"react/react-compiler": "error"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This single rule reports:
|
|
34
|
+
|
|
35
|
+
- **Rules-of-React violations** (conditional hooks, reading a ref during render, mutating props) — must-fix bugs.
|
|
36
|
+
- **Compiler bail-outs** — places compiler declined to optimize without rule violation.
|
|
37
|
+
|
|
38
|
+
### Wiring into Vite 8
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
// vite.config.js
|
|
42
|
+
import { defineConfig } from 'vite';
|
|
43
|
+
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
|
|
44
|
+
import babel from '@rolldown/plugin-babel';
|
|
45
|
+
|
|
46
|
+
export default defineConfig({
|
|
47
|
+
plugins: [
|
|
48
|
+
babel({ presets: [reactCompilerPreset()] }), // must run before react()
|
|
49
|
+
react(),
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install --save-dev @rolldown/plugin-babel @babel/core babel-plugin-react-compiler
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Incompatibility Escape Hatch (`"use no memo"`)
|
|
59
|
+
|
|
60
|
+
If a specific function is genuinely incompatible with compiler (e.g. calls `useForm` from `react-hook-form`), opt out with `"use no memo"` directive as **first line of function body** — leave a comment explaining why:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
function LegacyForm() {
|
|
64
|
+
'use no memo';
|
|
65
|
+
const form = useForm(); // incompatible with the compiler today
|
|
66
|
+
// ...
|
|
67
|
+
}
|
|
68
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when writing or reviewing a type assertion (`as X`) or non-null assertion (`x!`)."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: TypeScript — Type Assertions & Non-Null Assertions are a Last Resort
|
|
9
|
+
|
|
10
|
+
- `as X` and `x!` tell the compiler "trust me" — they produce zero runtime safety and actively hide bugs if wrong.
|
|
11
|
+
- Acceptable only when the compiler genuinely cannot know something you do (e.g. a DOM query you've already null-checked, or narrowing a third-party type at a well-tested boundary) — and even then, prefer a type guard or a runtime check over a bare assertion.
|
|
12
|
+
- Never use `as any` or `as unknown as X` to force an incompatible cast — that's `any` wearing a disguise.
|
|
13
|
+
- `x!` should almost always be replaceable by an actual null check or optional chaining (`x?.y`) plus a real fallback.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when doing a final TypeScript review pass on a file."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: TypeScript — Review Checklist & Quick Reference
|
|
9
|
+
|
|
10
|
+
## Review Checklist
|
|
11
|
+
|
|
12
|
+
Before considering TypeScript code "done," verify:
|
|
13
|
+
|
|
14
|
+
- [ ] No `any` anywhere (including implicit `any` from missing annotations)
|
|
15
|
+
- [ ] External/uncertain data enters as `unknown` and is narrowed before use
|
|
16
|
+
- [ ] Variant state is a discriminated union, not optional fields + booleans
|
|
17
|
+
- [ ] `interface` used for object/entity shapes; `type` used for unions/aliases/intersections
|
|
18
|
+
- [ ] No stray `I` prefixes on interfaces
|
|
19
|
+
- [ ] Naming follows casing conventions consistently
|
|
20
|
+
- [ ] `as` / `!` are rare, justified, and can't be replaced by a guard or null check
|
|
21
|
+
- [ ] Exported functions/methods have explicit return types
|
|
22
|
+
- [ ] Switch statements over unions have an exhaustiveness (`never`) check
|
|
23
|
+
- [ ] `tsconfig.json` includes the strictness baseline
|
|
24
|
+
|
|
25
|
+
## Quick Reference
|
|
26
|
+
|
|
27
|
+
| Situation | Use |
|
|
28
|
+
| ---------------------------------------------- | ------------------------------------------------------------- |
|
|
29
|
+
| External/uncertain data | `unknown` + narrowing |
|
|
30
|
+
| "This value is definitely one of these shapes" | Discriminated union (`type`) |
|
|
31
|
+
| Object with identity, may be extended | `interface` |
|
|
32
|
+
| Union, intersection, tuple, mapped type | `type` |
|
|
33
|
+
| Need to prove a type through logic | Type guard / narrowing |
|
|
34
|
+
| Tempted to write `any` | Stop — use `unknown`, a generic, or a local interface instead |
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when modeling a variant or multi-shape state in TypeScript."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: TypeScript — Discriminated Unions for Variant State
|
|
9
|
+
|
|
10
|
+
Whenever a value can be one of several distinct "shapes" (loading/success/error states, event types, API response variants), model it as a **discriminated union** with a literal tag field — never as a loose object with optional fields or boolean flags.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// ❌ Bad — booleans can contradict each other; unclear which fields are valid together
|
|
14
|
+
interface FetchState {
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
isError: boolean;
|
|
17
|
+
data?: User;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ✅ Good — only one shape is possible at a time, and the compiler enforces it
|
|
22
|
+
type FetchState =
|
|
23
|
+
| { status: 'idle' }
|
|
24
|
+
| { status: 'loading' }
|
|
25
|
+
| { status: 'success'; data: User }
|
|
26
|
+
| { status: 'error'; error: string };
|
|
27
|
+
|
|
28
|
+
function render(state: FetchState) {
|
|
29
|
+
switch (state.status) {
|
|
30
|
+
case 'success':
|
|
31
|
+
return state.data.name; // `data` is guaranteed to exist here
|
|
32
|
+
case 'error':
|
|
33
|
+
return state.error; // `error` is guaranteed to exist here
|
|
34
|
+
default:
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use a consistent tag field name across a codebase (`kind`, `type`, or `status` — pick one and stick with it) so narrowing patterns stay predictable.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when writing or reviewing an exported function, class method, or public API surface in TypeScript."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: TypeScript — Explicit Return Types on Exported Functions
|
|
9
|
+
|
|
10
|
+
Inference is fine for local, private helpers, but exported functions, class methods, and anything forming a public API should declare an explicit return type. This prevents an internal implementation change from silently widening/narrowing the public contract.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// ❌ Return type is inferred and can silently drift
|
|
14
|
+
export function getActiveUsers(users: User[]) {
|
|
15
|
+
return users.filter((u) => u.active);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ✅ Explicit, intentional contract
|
|
19
|
+
export function getActiveUsers(users: User[]): User[] {
|
|
20
|
+
return users.filter((u) => u.active);
|
|
21
|
+
}
|
|
22
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when deciding between `interface` and `type` for a new TypeScript declaration."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: TypeScript — `interface` vs `type`
|
|
9
|
+
|
|
10
|
+
Both can describe object shapes, but they signal different intent. Default rule:
|
|
11
|
+
|
|
12
|
+
| Use `interface` for... | Use `type` for... |
|
|
13
|
+
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
14
|
+
| Object / entity shapes (a `User`, a `Product`, a component's `Props`) | Unions (`"a" \| "b"`) and discriminated unions |
|
|
15
|
+
| Public API contracts meant to be `implements`-ed by classes | Intersections (`A & B`) |
|
|
16
|
+
| Shapes that consumers may want to **extend/augment** (declaration merging) | Tuples (`[string, number]`) |
|
|
17
|
+
| | Function types / callback signatures |
|
|
18
|
+
| | Mapped, conditional, or utility-derived types (`Partial<T>`, `Pick<T, K>`) |
|
|
19
|
+
| | Aliasing a primitive or another type for readability |
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// ✅ interface — an entity with identity, extendable
|
|
23
|
+
interface User {
|
|
24
|
+
id: string;
|
|
25
|
+
email: string;
|
|
26
|
+
role: UserRole;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface AdminUser extends User {
|
|
30
|
+
permissions: Permission[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ✅ type — union, alias, derived shape
|
|
34
|
+
type UserRole = 'admin' | 'editor' | 'viewer';
|
|
35
|
+
type UserId = User['id'];
|
|
36
|
+
type PartialUser = Partial<User>;
|
|
37
|
+
type Callback<T> = (value: T) => void;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Don't mix conventions arbitrarily within one file — if a shape is a plain data object that will never need a union/intersection, `interface` is the default; the moment it needs to express "one of several shapes," reach for `type`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when modeling a fixed set of string or numeric variants in TypeScript."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: TypeScript — Prefer Literal Unions Over Numeric Enums
|
|
9
|
+
|
|
10
|
+
String literal unions are simpler, tree-shake better, and produce clearer error messages than TypeScript `enum`. Reserve `enum` (or `as const` object maps) for cases that need reverse lookup or genuinely benefit from a namespaced runtime value.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
// ✅ Preferred
|
|
14
|
+
type OrderStatus = 'pending' | 'shipped' | 'delivered' | 'cancelled';
|
|
15
|
+
|
|
16
|
+
// Acceptable when a namespaced runtime object is actually needed
|
|
17
|
+
const OrderStatus = {
|
|
18
|
+
Pending: 'pending',
|
|
19
|
+
Shipped: 'shipped',
|
|
20
|
+
} as const;
|
|
21
|
+
type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus];
|
|
22
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when naming a TypeScript type, variable, or constant."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: TypeScript — Naming Conventions
|
|
9
|
+
|
|
10
|
+
| Kind | Convention | Example |
|
|
11
|
+
| ---------------------------------------------------------- | ------------------------------------------- | ----------------------------------- |
|
|
12
|
+
| Types, interfaces, classes, enums | `PascalCase` | `UserProfile`, `OrderStatus` |
|
|
13
|
+
| Interfaces | `PascalCase`, **no `I` prefix** | `User`, not `IUser` |
|
|
14
|
+
| Type aliases | `PascalCase` | `type ApiResponse<T> = ...` |
|
|
15
|
+
| Variables, functions, methods, properties | `camelCase` | `getUserById`, `isValid` |
|
|
16
|
+
| Booleans | `camelCase` with `is/has/should/can` prefix | `isLoading`, `hasPermission` |
|
|
17
|
+
| True constants (module-level, never reassigned, primitive) | `UPPER_SNAKE_CASE` | `MAX_RETRIES`, `DEFAULT_TIMEOUT_MS` |
|
|
18
|
+
| Enum members | `PascalCase` | `enum Status { Active, Archived }` |
|
|
19
|
+
| Generic type parameters (simple, single-purpose) | Single uppercase letter | `T`, `K`, `V`, `E` for errors |
|
|
20
|
+
| Generic type parameters (multiple / non-obvious) | Descriptive, prefixed with `T` | `TInput`, `TOutput`, `TContext` |
|
|
21
|
+
| Discriminated union tag field | Consistent across the codebase | `kind`, `type`, or `status` |
|
|
22
|
+
| Files with a single exported entity | Match the entity name | `UserProfile.ts`, `useAuth.ts` |
|
|
23
|
+
|
|
24
|
+
Naming should describe **intent**, not implementation — `fetchUser` not `getUserFromApiEndpoint`; `retryCount` not `numRetries2`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when narrowing an `unknown` or union-typed value in TypeScript."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: TypeScript — `unknown` + Narrowing, Not Casting
|
|
9
|
+
|
|
10
|
+
Prefer proving a type through control flow over asserting it with `as`.
|
|
11
|
+
|
|
12
|
+
## Narrowing techniques, in order of preference:
|
|
13
|
+
|
|
14
|
+
1. **`typeof`** — primitives (`string`, `number`, `boolean`, `undefined`, `function`)
|
|
15
|
+
2. **`instanceof`** — class instances, `Error`, `Date`, custom classes
|
|
16
|
+
3. **`in`** — checking a property exists before accessing it on a union/unknown
|
|
17
|
+
4. **User-defined type guards** — `function isUser(x: unknown): x is User`
|
|
18
|
+
5. **Discriminated union tag checks** — `switch (value.kind) { ... }`
|
|
19
|
+
6. **Exhaustiveness checks** — a `never`-typed default branch so adding a new variant is a compile error until every switch/if-chain handles it
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// ✅ Type guard
|
|
23
|
+
function isUser(value: unknown): value is User {
|
|
24
|
+
return typeof value === 'object' && value !== null && 'id' in value && 'email' in value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ✅ Exhaustiveness check
|
|
28
|
+
function assertNever(x: never): never {
|
|
29
|
+
throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function area(shape: Shape): number {
|
|
33
|
+
switch (shape.kind) {
|
|
34
|
+
case 'circle':
|
|
35
|
+
return Math.PI * shape.radius ** 2;
|
|
36
|
+
case 'square':
|
|
37
|
+
return shape.side ** 2;
|
|
38
|
+
default:
|
|
39
|
+
return assertNever(shape); // compile error if a variant is missed
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Type assertions (`as X`) and the non-null assertion (`!`) bypass this entirely — treat them as a last resort, not a shortcut.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when declaring a TypeScript collection or object shape."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: TypeScript — Readonly by Default
|
|
9
|
+
|
|
10
|
+
Prefer immutable shapes unless mutation is intentional and localized.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
interface Point {
|
|
14
|
+
readonly x: number;
|
|
15
|
+
readonly y: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function config(values: readonly string[]) {
|
|
19
|
+
/* ... */
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ROLES = ['admin', 'editor', 'viewer'] as const;
|
|
23
|
+
type UserRole = (typeof ROLES)[number];
|
|
24
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: model_decision
|
|
3
|
+
description: "Apply when configuring or reviewing tsconfig.json compiler strictness options."
|
|
4
|
+
owner: wangs-ui
|
|
5
|
+
metadata:
|
|
6
|
+
owner: wangs-ui
|
|
7
|
+
---
|
|
8
|
+
# Rule: TypeScript — Baseline `tsconfig.json` Strictness
|
|
9
|
+
|
|
10
|
+
Treat these as the non-negotiable floor for any project this rule touches:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"compilerOptions": {
|
|
15
|
+
"strict": true,
|
|
16
|
+
"noImplicitAny": true,
|
|
17
|
+
"strictNullChecks": true,
|
|
18
|
+
"strictFunctionTypes": true,
|
|
19
|
+
"strictPropertyInitialization": true,
|
|
20
|
+
"noUncheckedIndexedAccess": true,
|
|
21
|
+
"exactOptionalPropertyTypes": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
"noFallthroughCasesInSwitch": true,
|
|
24
|
+
"noUnusedLocals": true,
|
|
25
|
+
"noUnusedParameters": true,
|
|
26
|
+
"forceConsistentCasingInFileNames": true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`strict: true` alone enables the core group (`noImplicitAny`, `strictNullChecks`, etc.), but `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes` are commonly missed and close real gaps (array/object index access returning `T` instead of `T | undefined`; optional properties silently accepting `undefined` as an explicit value).
|