mcpField
M
meta-design-expressive-typescript

meta-design-expressive-typescript

github.com/agents-inc开发2664 次安装2 天前更新
代码仓库

关于 meta-design-expressive-typescript

meta-design-expressive-typescript 是一套用于编写和重构易读 TypeScript 的编码 Skill,面向希望让代码流程更清晰的开发者。它采用编排函数与纯函数分层的模式,并用命名和提取逻辑减少读者理解代码时的心智模拟。

主要功能

  1. 01以编排函数和纯函数分层
  2. 02用有意图的名称表达逻辑
  3. 03用卫语句扁平化条件分支
  4. 04使用判别联合与穷尽式 switch
  5. 05按可读性判断是否提取逻辑

适用场景

  1. 01重构混合验证与转换的函数
  2. 02简化深层嵌套的条件逻辑
  3. 03为谓词和转换逻辑设计清晰名称
  4. 04评估工具库是否比原生 JS 更清晰

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill meta-design-expressive-typescript

安装后在 Claude Code 中输入/meta-design-expressive-typescript确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
core.md
data-transforms.md

详细介绍

富有表现力的 TypeScript

快速指南: 编写能够传达意图的代码,让读者无需在脑海中模拟任何部分。应用双层模式:顶部的编排函数读起来像伪代码,底部的纯函数各自只做一件事。持续提取,直到代码读起来像散文。只有在确实比原生 JS 更易读时,才使用工具库。


<critical_requirements>

关键:使用此技能前

所有代码都必须遵循项目在 CLAUDE.md 中的约定(kebab-case、命名导出、导入顺序、import type、命名常量)

(每个非简单函数都必须采用双层编排模式:顶部放置守卫子句,中间调用具名函数,底部进行组装——编排函数中不得包含内联数据转换)

(必须将任何需要在脑海中模拟才能理解的表达式提取为具名函数或命名常量)

(函数命名必须体现其作用,而非实现方式——使用 isContentAddition(line),而不是 checkLineStartsWithPlusButNotTriplePlus(line))

(重构前必须阅读现有代码——先理解当前结构,再加以改进)

(当原生 JS 方法(.map()、.filter()、.reduce())已经清晰易读时,必须优先使用它们,而不是工具库)

</critical_requirements>


自动检测: 编排函数模式、双层函数、提取函数、命名谓词、命名常量、可读性重构、富有表现力的代码、函数分解、提取纯函数、可读的 TypeScript、守卫子句、提前返回、扁平化条件语句、判别联合、穷尽式 switch、as const satisfies、异步编排函数

适用场景:

  • 编写混合了验证、转换和组装逻辑的函数
  • 重构需要在脑海中模拟执行步骤才能理解流程的函数
  • 为谓词、常量或转换函数命名,以清晰传达意图
  • 决定使用工具库函数还是原生 JS
  • 将大型函数拆分为编排函数和纯辅助函数
  • 审查代码并找出需要在脑海中模拟才能理解的代码块
  • 使用守卫子句扁平化深层嵌套的 if/else 代码块
  • 编写编排多个独立操作的异步函数
  • 使用判别联合建模状态或事件,以实现穷尽式处理

不适用场景:

  • 编写本身已经清晰的简单单行函数
  • 学术性的函数式编程(monad、函子、Either/Option 类型)
  • 参数隐式的无点风格
  • 将过于简单的表达式过度提取为具名函数
  • 函数调用开销会产生影响的性能关键热路径

涵盖的关键模式:

  • 双层模式(编排函数 + 纯函数)
  • 可读性测试(“能否不经模拟就理解?”)
  • 命名谓词、常量和转换函数
  • 守卫子句:通过提前返回扁平化嵌套条件语句
  • 判别联合 + 穷尽式 switch,实现类型安全的控制流
  • 使用 Promise.all 并行执行独立操作的异步编排函数
  • 使用 as const satisfies 编写清晰表达意图的配置
  • 提取决策框架
  • 工具库用法:80/20 法则

详细资源


## 理念

Expressive TypeScript 是一种注重可读性的实用型 80/20 函数式编程。判断任意代码块的核心标准是:

“有人能否在不模拟其中任何部分的情况下理解代码的流程?”

如果不能,就把需要模拟的部分提取为具名函数或常量。如果可以,就保持原样——即使理论上还能让它“更整洁”。

这不是:

  • Monad、函子或 Either/Option 类型——这些属于另一项技能
  • 无点风格——隐式参数会让大多数读者难以理解意图
  • 教条式的函数纯洁性——编排代码中可以有副作用;真正重要的是其下方的纯函数
  • 过度提取——三行相似代码也胜过过早抽象

两个核心理念:

  1. 编排代码读起来像伪代码。 守卫子句、具名函数调用、组装。不要内联需要模拟的逻辑。
  2. 纯函数各司其职。 每个函数都有一个能传达其用途的名称,并且都可以独立测试。

适用场景:

  • 任何超过约 15 行且混合了多种关注点的函数
  • 任何需要读者追踪逻辑才能理解意图的表达式
  • 任何缺少描述性名称的重复逻辑模式

不适用场景:

  • 单个已经足够清晰的 .map() 或 .filter()
  • 已经处于单层抽象的函数
  • 提取后只会增加噪声的极简单代码

核心模式

模式 1:双层模式

每个非简单函数都遵循相同的结构:顶层是读起来像伪代码的编排函数,底层则是各司其职的纯函数。

编排函数(顶层)
// Orchestrator: reads like a step-by-step plan
function processUserImport(rawData: RawImportData): ImportResult {
  // 1. Guard clauses
  if (!rawData.users.length) {
    return { imported: 0, skipped: 0, errors: [] };
  }

  // 2. Named function calls for each step
  const validated = rawData.users.filter(isValidUser);
  const normalized = validated.map(normalizeUserRecord);
  const deduped = removeDuplicatesByEmail(normalized);
  const { existing, newUsers } = separateExistingUsers(deduped);

  // 3. Assembly
  return {
    imported: newUsers.length,
    skipped: existing.length,
    errors: rawData.users.length - validated.length,
  };
}

优点: 每一行都通过函数名称传达意图。读者无需了解每个步骤的实现方式,就能知道每一步会做什么。守卫子句位于顶部。没有内联 lambda 妨碍理解流程。

纯函数(底层)
// Pure function: one job, named for purpose
function isValidUser(user: RawUser): boolean {
  return user.email.includes("@") && user.name.trim().length > 0;
}

function normalizeUserRecord(user: RawUser): NormalizedUser {
  return {
    email: user.email.toLowerCase().trim(),
    name: user.name.trim(),
    role: user.role ?? DEFAULT_ROLE,
  };
}

function removeDuplicatesByEmail(users: NormalizedUser[]): NormalizedUser[] {
  const seen = new Set<string>();
  return users.filter((user) => {
    if (seen.has(user.email)) return false;
    seen.add(user.email);
    return true;
  });
}

优点: 每个函数都各司其职,名称清晰,并且无需 mock 或设置状态即可独立测试。

完整的改造前后示例: 请参阅 examples/core.md,了解完整的编排函数改造示例。


模式 2:可读性测试

提取或重构之前,请用此测试检查任意代码块:

“有人能否在不在脑海中模拟其中任何部分的情况下理解代码的流程?”

// Fails the readability test -- requires simulation
const result = items
  .filter(
    (item) =>
      item.status === "active" &&
      item.createdAt > cutoffDate &&
      !excludedIds.has(item.id),
  )
  .map((item) => ({
    ...item,
    displayName: item.firstName + " " + item.lastName,
    age: Math.floor((Date.now() - item.birthDate.getTime()) / MS_PER_YEAR),
  }));

缺点: 读者必须在脑海中执行筛选谓词和映射转换,才能理解这段代码会生成什么。意图被埋在实现细节中。

// Passes the readability test -- intent is clear from names
const activeItems = items.filter(isActiveAfterCutoff);
const displayRecords = activeItems.map(toDisplayRecord);

优点: 每个步骤都清晰地传达意图。读者无需查看两个函数的实现,就能知道筛选步骤会选出截止时间之后的活跃项,而映射步骤会创建展示记录。

测试结果为“保持原样”时
// Already passes -- don't over-extract
const names = users.map((user) => user.name);
const activeUsers = users.filter((user) => user.isActive);
const total = prices.reduce((sum, price) => sum + price, 0);

优点: 这些都是已经清晰的单表达式操作。提取 getName、isActive 或 sumPrices 只会增加间接性,不会提升清晰度。


模式 3:具名抽象

当某个表达式需要模拟才能理解时,为其命名,让名称传达其意图。

具名谓词
// Before: requires simulation to understand filter criteria
const lines = diff.filter(
  (line) => line.startsWith("+") && !line.startsWith("+++"),
);

// After: name communicates intent
const lines = diff.filter(isContentAddition);

function isContentAddition(line: string): boolean {
  return line.startsWith("+") && !line.startsWith("+++");
}

优点: isContentAddition 能让读者知道检查的内容。实现方式(startsWith 逻辑)可供查看,但理解流程时并非必需。

具名常量
// Before: magic setup with no context
const skill = createMockSkill("react", {
  conflictsWith: ["vue", "angular"],
  requires: ["typescript"],
});

// After: name communicates purpose
const REACT_WITH_FRAMEWORK_CONFLICTS = createMockSkill("react", {
  conflictsWith: ["vue", "angular"],
  requires: ["typescript"],
});

优点: 在测试中看到这个常量时,读者无需滚动到其定义处,就能知道它的用途。名称中包含了相关特征。

具名转换
// Before: inline formatting logic obscures orchestrator flow
const message = results
  .map((r) =>
    r.success ? `  Installed ${r.name}` : `  Failed ${r.name}: ${r.error}`,
  )
  .join("\n");

// After: name communicates intent
const message = results.map(formatInstallResult).join("\n");

function formatInstallResult(result: InstallResult): string {
  if (result.success) return `  Installed ${result.name}`;
  return `  Failed ${result.name}: ${result.error}`;
}

优点: 编排代码读起来就是“格式化每个结果,然后用换行符连接”。格式细节可以查看,但不会妨碍理解。

更多示例: 请参阅 examples/core.md,了解具名抽象模式的改造前后示例。


### 模式 4:提取决策框架

并非每个表达式都应该提取。请使用以下决策树:

Does this block require mental simulation to understand?
|-- NO -> Leave it inline. Don't over-extract.
+-- YES -> Does the same logic appear in 2+ places?
    |-- YES -> Extract to shared function (DRY + readability).
    +-- NO -> Would a named function communicate intent the expression doesn't?
        |-- YES -> Extract. The name adds value.
        +-- NO -> Leave it. Extraction would just move code without adding clarity.
提取:代码块需要模拟推演
// Before: simulation required to understand what this produces
const categories = Object.entries(skills).reduce<Record<string, Skill[]>>(
  (acc, [id, skill]) => {
    const cat = skill.category;
    if (!acc[cat]) acc[cat] = [];
    acc[cat].push(skill);
    return acc;
  },
  {},
);

// After: intent is clear from the name
const categories = groupSkillsByCategory(skills);
不提取:已经足够清晰
// Already clear -- extracting would just move one line
const ids = skills.map((skill) => skill.id);
const hasConflicts = conflicts.length > 0;
const displayName = `${firstName} ${lastName}`;

优点: 这些代码都很容易理解。名为 getIds、checkHasConflicts 或 buildDisplayName 的函数只会增加间接性,并不能提升可读性。

不提取:名称和实现一样复杂
// Don't extract this:
const isSelected = selectedIds.has(item.id);

// Because the extraction would be:
function isItemSelected(item: Item, selectedIds: Set<string>): boolean {
  return selectedIds.has(item.id);
}
// The name doesn't communicate more than the expression itself

模式 5:守卫子句(扁平化嵌套条件)

层层嵌套的 if/else 代码块会迫使读者在脑中维护条件栈。守卫子句会反转条件并提前返回,让正常执行路径无需嵌套。

// Flat: each guard exits early, happy path has zero nesting
function getDiscount(user: User, cart: Cart): number {
  if (!user.isActive) return 0;
  if (cart.items.length === 0) return 0;
  if (!user.membership) return STANDARD_DISCOUNT;

  return calculateMemberDiscount(user.membership, cart.total);
}

优点: 每个守卫子句只处理一个问题,然后退出。读者无需追踪自己处于哪个 else 分支中。最后一行就是正常执行路径,一目了然。

完整的转换前后对比: 请参阅 examples/core.md,了解如何将深层嵌套代码扁平化为守卫子句。


模式 6:判别联合类型 + 穷尽式 Switch

当一个值可能处于多种状态之一时,使用判别联合类型和穷尽式 switch,可以让类型系统确保每种情况都得到处理。添加新变体后,如果有遗漏的处理位置,就会出现编译错误。

type TaskStatus = "pending" | "running" | "completed" | "failed";

function getStatusMessage(status: TaskStatus): string {
  switch (status) {
    case "pending":
      return "Waiting to start";
    case "running":
      return "In progress...";
    case "completed":
      return "Done";
    case "failed":
      return "Something went wrong";
    default: {
      const _exhaustive: never = status;
      return _exhaustive;
    }
  }
}

优点: never 默认分支可确保在编译时检查穷尽性。如果向联合类型中添加第五种状态,在处理新情况之前,此 switch 就会报错。

包含判别对象联合类型的完整模式: 请参阅 examples/core.md,了解完整的判别联合类型模式。


模式 7:异步编排器

两层模式同样适用于异步代码。异步编排器会按顺序 await 命名函数来执行有依赖关系的步骤,并使用 Promise.all 执行互不依赖的步骤。

async function onboardNewUser(input: OnboardInput): Promise<OnboardResult> {
  const user = await createUserRecord(input);

  // Independent operations -- run in parallel
  const [profile, settings] = await Promise.all([
    initializeProfile(user.id, input.preferences),
    applyDefaultSettings(user.id),
  ]);

  await sendWelcomeEmail(user.email, profile.displayName);

  return { user, profile, settings };
}

优点: 编排器读起来就像分步计划。互不依赖的操作会归在 Promise.all 中,向读者表明它们之间没有依赖关系。每个被调用的函数都是纯异步操作,并以其用途命名。

完整的转换前后示例: 请参阅 examples/core.md,了解如何将包含多种关注点的代码拆分为异步函数。


模式 8:使用 as const satisfies 定义配置

定义静态配置对象时,as const satisfies Shape 会保留字面量类型(便于自动补全和使用 typeof/keyof),同时在编译时验证对象形状。

interface RouteConfig {
  path: string;
  auth: boolean;
}

const ROUTES = {
  home: { path: "/", auth: false },
  dashboard: { path: "/dashboard", auth: true },
  settings: { path: "/settings", auth: true },
} as const satisfies Record<string, RouteConfig>;

// Type of ROUTES.home.path is "/", not string
// typeof ROUTES gives you the full literal structure for keyof usage

优点: satisfies 运算符可以在编译时发现拼写错误和缺失字段。as const 会保留精确的字面量类型,让下游代码受益于更精确的类型推断。如果没有 satisfies,像 { pth: "/" } 这样的拼写错误就会被静默接受。


<decision_framework>

决策框架

何时应用两层模式
Is this function > ~15 lines?
|-- NO -> Is it mixing multiple concerns (validate + transform + assemble)?
|   |-- YES -> Apply two-tier pattern
|   +-- NO -> Leave it. Short single-concern functions are fine as-is.
+-- YES -> Does it have clear logical steps?
    |-- YES -> Apply two-tier pattern: orchestrator calls named functions
    +-- NO -> Consider breaking into separate functions by responsibility first
何时使用工具库,何时使用纯 JS
Is this a single .map(), .filter(), or .reduce()?
|-- YES -> Use plain JS. A utility library adds dependency without clarity.
+-- NO -> Is the operation a known concept (group-by, count-by, set-difference)?
    |-- YES -> Does the utility library name match the concept?
    |   |-- YES -> Use the library. groupBy(skills, s => s.category) is
    |   |          clearer than a manual reduce.
    |   +-- NO -> Use plain JS with a named function.
    +-- NO -> Are there 3+ chained transformations?
        |-- YES -> Consider pipe() if intermediate variables would obscure flow.
        +-- NO -> Use plain JS with named intermediate variables.
工具库的 80/20 法则

当函数名本身就是说明文档时,使用工具函数:

使用工具库使用纯 JS
groupBy(items, fn) —— 按键分组items.map(fn) —— 单次转换
countBy(items, fn) —— 统计各类别数量items.filter(fn) —— 单次筛选
difference(a, b) —— 集合差集items.find(fn) —— 单次查找
partition(items, fn) —— 拆分为两组items.some(fn) / items.every(fn)
indexBy(items, fn) —— 将数组转换为查找映射items.reduce(fn, init) —— 简单求和
pipe(data, ...fns) —— 串联 3 个或更多操作一到两个串联的 .map().filter() 调用

完整的数据转换示例: 请参阅 examples/data-transforms.md

</decision_framework>


<red_flags>

危险信号

高优先级问题:

  • 编排函数包含长于单个表达式的内联 lambda——应提取为具名函数
  • 一个函数混合了守卫子句、数据转换、副作用和返回值组装,却没有清晰区分
  • 变量名描述实现方式(filteredMappedItems),而非意图(activeDisplayRecords)
  • 使用 .reduce() 构建查找对象,而 groupBy 或 indexBy 能更直接地表达意图
  • if/else 嵌套过深(3 层或更多),而不是使用带提前返回的守卫子句
  • 对联合类型使用的 switch 缺少 default: never 穷尽性检查——新增变体时会悄无声息地落空

中优先级问题:

  • 对本身已经清晰的单个 .map() 或 .filter() 使用工具库
  • 将极其简单的表达式提取为具名函数(过度提取)
  • 只有一两个操作时使用 pipe(),而用变量会更清晰
  • 命名常量和它所替代的内容一样复杂

常见错误:

  • 当谓词需要的上下文会被无点风格掩盖时,仍使用无点风格(items.filter(isActive))——如果谓词需要闭包变量,请使用显式 lambda
  • 提取一个只使用一次的函数,而它的名称只是对其中单行代码的复述
  • 创建一个最终沦为杂物堆的“utils”文件——按领域对提取的函数分组,并放在调用它们的代码附近

陷阱与边缘情况:

  • 命名清晰的内联 lambda 本身就是一种具名抽象:.filter((user) => user.isActive) 已经足够清晰,因为 isActive 能自我说明。不要将它提取到单独的函数中。
  • 当读者不熟悉工具库时,工具库的 pipe() 可能会损害可读性——请考虑团队对它的熟悉程度
  • 守卫子句应提前返回,而不是用 if 块包住整个函数体
  • 提取纯函数时,将它们放在文件底部或共享模块中——不要夹杂在它们所服务的编排函数之间
  • as const satisfies Shape——顺序很重要。satisfies Shape as const 不可用。先锁定字面量,再验证结构。
  • 在异步编排函数中,对互不依赖的操作依次使用 await 是一个隐蔽的性能问题——如果步骤之间不依赖彼此的结果,请使用 Promise.all
  • never 穷尽性检查技巧需要 noUnusedLocals 才不会标记该变量——请使用 return _exhaustive,而不是只进行赋值,以避免 lint 警告

</red_flags>


<critical_reminders>

关键提醒

所有代码都必须遵循 CLAUDE.md 中的项目约定(kebab-case、命名导出、导入顺序、import type、命名常量)

(必须将每个非平凡函数组织为两层编排结构:顶部放置守卫子句,中间调用具名函数,底部组装返回值——编排函数中不得进行内联数据转换)

(必须将任何需要在脑中模拟才能理解的表达式提取为具名函数或命名常量)

(函数名称必须描述它们做什么,而不是如何做——使用 isContentAddition(line),而不是 checkLineStartsWithPlusButNotTriplePlus(line))

(重构前必须阅读现有代码——先理解当前结构,再进行改进)

(当普通 JS 方法(.map()、.filter()、.reduce())本身已经清晰时,必须优先使用它们,而不是工具库)

不遵循这些规则会导致代码需要在脑中模拟才能理解——这恰恰与富有表达力的 TypeScript 背道而驰。

</critical_reminders>

评论

支持 Markdown