typescript-functional-style
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTypeScript 函数式风格
Functional Style for TypeScript
这份规范用于本项目的 TypeScript、Vue script、composable、shared helper 实现。
This guideline applies to the implementation of TypeScript, Vue script, composables, and shared helpers in this project.
适用范围
Scope
- 文件匹配:,包含
**/*.{ts,tsx,js,jsx}文件中.vue内的 TS 逻辑。<script setup> - 适用对象:业务逻辑、composable、工具函数、类型定义、数据转换。
- 不适用对象:与上述目标无关的配置文件、构建脚本等非逻辑代码。
- File matching: , including TS logic within
**/*.{ts,tsx,js,jsx}in<script setup>files..vue - Applicable scenarios: Business logic, composables, utility functions, type definitions, data transformation.
- Non-applicable scenarios: Configuration files, build scripts, and other non-logical code unrelated to the above objectives.
目标
Goals
- 优先函数、组合与数据变换,而不是 class 和共享可变状态。
- 保持代码短、小、显式,降低阅读成本。
- 先保证清晰,再考虑"更函数式";不要为了形式牺牲可读性。
- Prioritize functions, composition, and data transformation over classes and shared mutable state.
- Keep code short, concise, and explicit to reduce reading costs.
- Prioritize clarity before pursuing "more functional" approaches; do not sacrifice readability for form.
核心规则
Core Rules
- 优先纯函数
- 不依赖生命周期、DOM、I/O 的逻辑,优先提取为纯函数。
- 输入输出应稳定可预测,避免隐藏副作用。
- 只服务当前模块的纯函数,放到同级 。
shared.ts - 具备跨模块复用价值的纯函数,放到项目全局 shared 目录。
- 优先组合而非命令式堆叠
- 优先小函数组合、composable、工具函数。
- 除非外部 API 强制要求,否则不要引入 class。
- 保持函数短小
- 一个函数只做一件事。
- 如果一个函数同时处理筛选、映射、分支和副作用,应拆分。
- 降低嵌套层级
- 优先 guard clause 和 early return。
- 条件嵌套超过两层时,优先抽取辅助函数或反转条件。
- 优先声明式数据变换
- 能用 、
map、filter、find、some表达时,优先不用手写循环。every - 只有在明显更清楚或更高效时,才保留局部命令式写法。
- 限制可变性
- 避免共享可变状态。
- 优先派生值而不是镜像状态。
- 局部 mutation 只在作用域很小且明显更易读时允许。
- 类型必须精确
- 导出函数、公共工具、复杂返回值应声明清晰参数与返回类型。
- 不使用 、宽泛断言或类型逃逸来掩盖建模问题。
any
- 简洁但不炫技
- 不为了"更函数式"而引入难读的链式技巧。
- 如果更短的写法更难懂,选更清楚的版本。
- Prioritize Pure Functions
- Logic that does not depend on lifecycle, DOM, or I/O should be extracted as pure functions whenever possible.
- Input and output should be stable and predictable, avoiding hidden side effects.
- Pure functions only serving the current module should be placed in the sibling .
shared.ts - Pure functions with cross-module reuse value should be placed in the project's global shared directory.
- Prioritize Composition Over Imperative Stacking
- Prioritize small function composition, composables, and utility functions.
- Do not introduce classes unless required by external APIs.
- Keep Functions Short
- 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
- Prioritize guard clauses and early return.
- When conditional nesting exceeds two levels, prioritize extracting helper functions or reversing conditions.
- Prioritize Declarative Data Transformation
- When ,
map,filter,find,somecan be used, avoid writing manual loops.every - Retain local imperative code only when it is clearly more readable or efficient.
- Restrict Mutability
- Avoid shared mutable state.
- Prioritize derived values over mirrored state.
- Local mutation is only allowed when the scope is very small and it is clearly more readable.
- Ensure Precise Typing
- Exported functions, public utilities, and complex return values should declare clear parameter and return types.
- Do not use , broad assertions, or type escapes to cover up modeling issues.
any
- Be Concise But Not Showy
- Do not introduce hard-to-read chaining techniques just for being "more functional".
- If a shorter writing style is harder to understand, choose the clearer version.
Vue 语境补充
Vue Context Supplements
- 派生状态用 ,不要重复存储到 ref。
computed - 把副作用与数据转换分开。
- 与模板无关的纯逻辑,优先提取到 helper 或 composable。
- 仅当前组件族使用的纯逻辑,优先放同级 。
shared.ts - 跨模块可复用的纯逻辑,优先放项目全局 shared 目录。
- 没必要时不要引入 watcher。
- 在新增 helper、composable 或类型之前,先检查项目现有实现是否已满足。
- 如果项目里没有合适的 composable,优先检查 ,不要直接从 0 到 1 重写常见能力。
@vueuse/core
- Use for derived state instead of storing it repeatedly in ref.
computed - Separate side effects from data transformation.
- Pure logic unrelated to templates should be extracted to helpers or composables whenever possible.
- Pure logic only used by the current component family should be placed in the sibling .
shared.ts - Pure logic reusable across modules should be placed in the project's global shared directory.
- Do not introduce watchers unless necessary.
- Before adding new helpers, composables, or types, check if existing implementations in the project already meet the requirements.
- If there is no suitable composable in the project, prioritize checking instead of rewriting common capabilities from scratch.
@vueuse/core
重构检查清单
Refactoring Checklist
- 能否提取为纯函数?
- 提取后应该放同级 ,还是项目全局 shared 目录?
shared.ts - 能否用 early return 减少缩进?
- 能否消除重复状态?
- 能否删掉只转手一次的中间变量?
- 导出 API 的参数和返回值是否足够明确?
- 代码是否在更短之后仍然更容易读?
- Can it be extracted as a pure function?
- Should it be placed in the sibling or the project's global shared directory after extraction?
shared.ts - Can early return be used to reduce indentation?
- Can duplicate state be eliminated?
- Can intermediate variables that are only passed once be removed?
- Are the parameters and return values of the exported API clear enough?
- Is the code still easier to read after being shortened?
示例
Examples
较差:
ts
export function collectEnabledIds(items: Item[]): string[] {
const result: string[] = [];
for (const item of items) {
if (item.enabled) {
result.push(item.id);
}
}
return result;
}较好:
ts
export function collectEnabledIds(items: Item[]): string[] {
return items.filter(item => item.enabled).map(item => item.id);
}较差:
ts
export function resolveLabel(input?: string | null): string {
let label = '';
if (input) {
label = input.trim();
if (!label) {
label = 'unknown';
}
} else {
label = 'unknown';
}
return label;
}较好:
ts
export function resolveLabel(input?: string | null): string {
const label = input?.trim();
if (!label) {
return 'unknown';
}
return label;
}较差(命令式堆叠,职责混杂):
ts
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;
}较好(组合小函数 + 声明式变换 + guard clause):
ts
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);
}较差(类型逃逸掩盖建模问题):
ts
export function parseConfig(raw: unknown): Config {
return raw as Config;
}较好(精确类型 + 显式校验,返回值类型明确):
ts
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) : []
};
}Poor:
ts
export function collectEnabledIds(items: Item[]): string[] {
const result: string[] = [];
for (const item of items) {
if (item.enabled) {
result.push(item.id);
}
}
return result;
}Better:
ts
export function collectEnabledIds(items: Item[]): string[] {
return items.filter(item => item.enabled).map(item => item.id);
}Poor:
ts
export function resolveLabel(input?: string | null): string {
let label = '';
if (input) {
label = input.trim();
if (!label) {
label = 'unknown';
}
} else {
label = 'unknown';
}
return label;
}Better:
ts
export function resolveLabel(input?: string | null): string {
const label = input?.trim();
if (!label) {
return 'unknown';
}
return label;
}Poor (imperative stacking, mixed responsibilities):
ts
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 (combining small functions + declarative transformation + guard clause):
ts
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);
}Poor (type escape covers up modeling issues):
ts
export function parseConfig(raw: unknown): Config {
return raw as Config;
}Better (precise typing + explicit validation, clear return type):
ts
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) : []
};
}