This specification applies to the TypeScript, Vue script, composable, and shared helper implementations in this project.
Scope of application
- File matching:
**/*.{ts,tsx,js,jsx}, including.vuein files<script setup>TS logic within. - Applicable targets: business logic, composables, utility functions, type definitions, data transformations.
- Not applicable to: configuration files, build scripts, and other non-logic code unrelated to the above targets.
Goals
- Prefer functions, composition, and data transformations over classes and shared mutable state.
- Keep code short, small, and explicit to reduce reading cost.
- Ensure clarity first, then consider "more functional"; do not sacrifice readability for form.
Core rules
- Prefer pure functions
- Logic that does not depend on lifecycle, DOM, or I/O should preferably be extracted into pure functions.
- Inputs and outputs should be stable and predictable, avoiding hidden side effects.
- Pure functions that serve only the current module should be placed in the same-level
shared.ts。 - Pure functions with cross-module reuse value should be placed in the project's global shared directory.
- Prefer composition over imperative stacking
- Prefer small function composition, composables, and utility functions.
- Do not introduce classes unless an external API mandates it.
- Keep functions short and small
- A function should do only one thing.
- If a function handles filtering, mapping, branching, and side effects at the same time, it should be split.
- Reduce nesting levels
- Prefer guard clauses and early returns.
- When conditional nesting exceeds two levels, prefer extracting a helper function or inverting the condition.
- Prefer declarative data transformations
- When it can be expressed with
map、filter、find、some、every, prefer not to use handwritten loops. - Only retain local imperative style when it is clearly clearer or more efficient.
- Limit mutability
- Avoid shared mutable state.
- Prefer derived values over mirrored state.
- Local mutation is allowed only when the scope is very small and it is clearly more readable.
- Types must be precise
- Exported functions, public utilities, and complex return values should declare clear parameter and return types.
- Do not use
any, broad assertions, or type escapes to conceal modeling problems.
- Concise but not showy
- Do not introduce hard-to-read chained tricks for the sake of being "more functional".
- If a shorter form is harder to understand, choose the clearer version.
Vue context supplement
- Use
computedfor derived state; do not store it repeatedly in ref. - Separate side effects from data transformations.
- Pure logic unrelated to the template should preferably be extracted into a helper or composable.
- Pure logic used only by the current component family should preferably be placed in the same-level
shared.ts。 - Pure logic reusable across modules should preferably be placed in the project's global shared directory.
- Do not introduce a watcher when it is not necessary.
- Before adding a helper, composable, or type, first check whether the project's existing implementation already satisfies the need.
- If there is no suitable composable in the project, first check
@vueuse/core, and do not rewrite common capabilities from scratch.
Refactoring Checklist
- Can it be extracted into a pure function?
- After extraction, should it be placed at the same level as
shared.ts, or in the project's global shared directory? - Can early return be used to reduce indentation?
- Can duplicate state be eliminated?
- Can intermediate variables that are only passed through once be removed?
- Are the parameters and return values of the exported API sufficiently clear?
- Is the code still easier to read after being shorter?
Example
Worse:
export function collectEnabledIds(items: Item[]): string[] {
const result: string[] = [];
for (const item of items) {
if (item.enabled) {
result.push(item.id);
}
}
return result;
}
Better:
export function collectEnabledIds(items: Item[]): string[] {
return items.filter(item => item.enabled).map(item => item.id);
}
Worse:
export function resolveLabel(input?: string | null): string {
let label = '';
if (input) {
label = input.trim();
if (!label) {
label = 'unknown';
}
} else {
label = 'unknown';
}
return label;
}
Better:
export function resolveLabel(input?: string | null): string {
const label = input?.trim();
if (!label) {
return 'unknown';
}
return label;
}
Worse (imperative stacking, mixed responsibilities):
export function getActiveAdminNames(users: User[]): string[] {
const result: string[] = [];
for (const user of users) {
if (user.role === 'admin') {
if (user.active) {
if (!user.deleted) {
result.push(user.name);
}
}
}
}
return result;
}
Better (composing small functions + declarative transformation + guard clause):
const isActiveAdmin = (user: User): boolean =>
user.role === 'admin' && user.active && !user.deleted;
export function getActiveAdminNames(users: User[]): string[] {
return users.filter(isActiveAdmin).map(user => user.name);
}
Worse (type escape masking modeling problems):
export function parseConfig(raw: unknown): Config {
return raw as Config;
}
Better (precise types + explicit validation, clear return value types):
export function parseConfig(raw: unknown): Config {
if (!raw || typeof raw !== 'object') {
return { entries: [] };
}
const { entries } = raw as Record<string, unknown>;
return {
entries: Array.isArray(entries) ? entries.filter(isConfigEntry) : []
};
}