mcpField
A
arktype

arktype

github.com/epicenterhq开发4.8k51 次安装2 周前更新
代码仓库

关于 arktype

这项开发技能介绍 arktype Schema 与推断类型的组合方式,重点涵盖可复用基础结构和判别联合,并说明可选字段及 workspace 字段表达式的限制。

主要功能

  1. 01用 merge 和 or 组合 Schema
  2. 02构建判别联合
  3. 03处理可选属性
  4. 04从 const 数组派生联合类型
  5. 05应用 workspace 字段限制

适用场景

  1. 01为命令建模并复用公共字段
  2. 02创建类型安全的变体负载
  3. 03使 Schema 枚举与常量保持一致
  4. 04校验 workspace 字段表达式

安装

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

$npx skills add https://github.com/epicenterhq/epicenter --skill arktype

安装后在 Claude Code 中输入/arktype确认已加载。

详细介绍

Arktype 模式

用于组合 arktype 架构、为运行时架构值命名并同时推导类型,以及使用 .merge() 和 .or() 构建可辨识联合的模式。

base.merge(type.or(...)) 模式(推荐)

当你有共享的基础字段,以及由字面量键区分的各变体负载时使用。.merge() 会分发到联合的各个分支:它会自动将基础类型合并到联合的每个分支中。

import { type } from 'arktype';

const commandBase = type({
	id: 'string',
	deviceId: DeviceId,
	createdAt: 'number',
	_v: '1',
});

const Command = commandBase.merge(
	type.or(
		{
			action: "'closeTabs'",
			tabIds: 'string[]',
			'result?': type({ closedCount: 'number' }).or('undefined'),
		},
		{
			action: "'openTab'",
			url: 'string',
			'windowId?': 'string',
			'result?': type({ tabId: 'string' }).or('undefined'),
		},
		{
			action: "'activateTab'",
			tabId: 'string',
			'result?': type({ activated: 'boolean' }).or('undefined'),
		},
	),
);
工作原理
  1. type.or(...) 会创建一个由普通对象定义组成的联合。每个对象都是一个拥有自身字段的变体。
  2. commandBase.merge(union) 会将合并操作分发到联合的每个分支。内部,arktype 会调用 rNode.distribute(),逐个将合并操作应用到各个分支(源代码)。
  3. 结果是一个联合,其中每个分支都包含所有 commandBase 字段以及该变体特有的字段。
  4. Arktype 会自动将 action 键识别为判别字段,因为每个分支都有不同的字面量值。
  5. TypeScript 中的 switch (cmd.action) 会缩窄整个联合类型。每个分支的负载字段和结果类型都具备类型安全性。
为什么采用这种模式
属性优点
基础类型是一个真实的 Type可复用、可组合,并可在运行时检查
.merge() 会分发无需针对每个变体重复调用 base.merge(...)
type.or() 是扁平的所有变体都在一个列表中,便于阅读和扩展
基础类型只出现一次DRY:在一个地方修改基础字段
自动判别无需手动配置判别字段
扁平负载没有嵌套的 payload 对象;字段都位于顶层

.merge().or() 链式模式(适用于 2~3 个变体)

当变体数量较少,链式写法更自然易读时使用。

const Command = commandBase
	.merge({
		action: "'closeTabs'",
		tabIds: 'string[]',
		'result?': type({ closedCount: 'number' }).or('undefined'),
	})
	.or(
		commandBase.merge({
			action: "'openTab'",
			url: 'string',
			'result?': type({ tabId: 'string' }).or('undefined'),
		}),
	);

对于 4 个或更多变体,建议使用 base.merge(type.or(...)),以避免针对每个分支重复调用 commandBase.merge(...)。

"..." 展开键模式(替代方案)

当你想直接内联定义而不预先声明基础变量,或者更喜欢精简语法时使用。

const User = type({ isAdmin: 'false', name: 'string' });

const Admin = type({
	'...': User,
	isAdmin: 'true',
	permissions: 'string[]',
});

"..." 键会将所引用类型的所有属性展开到新的对象定义中。外层对象中冲突的键会覆盖展开类型中的同名键(与 .merge() 一致)。

限制:"..." 键必须是对象中的第一个键。否则,Arktype 会抛出 ParseError: Spread operator may only be used as the first key。如果需要更灵活的写法,建议使用 .merge()。

联合中的展开键
const Command = type({
	'...': commandBase,
	action: "'closeTabs'",
	tabIds: 'string[]',
}).or({
	'...': commandBase,
	action: "'openTab'",
	url: 'string',
});

功能上等同于 .merge().or()。可根据可读性偏好选择。

.or() 链式写法与静态 type.or() 写法

链式写法(适用于 2~3 个变体)
const Command = variantA.or(variantB).or(variantC);
静态 type.or() 写法(适用于 4 个或更多变体)
const Command = type.or(variantA, variantB, variantC, variantD, variantE);

静态写法可避免深层嵌套的链式调用,并在一次调用中创建联合。

.merge() 在联合上的分发

.merge() 会对两侧的联合进行分发。如果将联合合并到对象类型中(或反过来),操作会分别应用于每个分支:

// base.merge(union): distributes merge across each branch
const Result = baseType.merge(type.or({ a: 'string' }, { b: 'number' }));
// Equivalent to: type.or(baseType.merge({ a: 'string' }), baseType.merge({ b: 'number' }))

限制:联合的每个分支都必须是对象类型。如果任何分支不是对象类型(例如 'string'),arktype 会抛出 ParseError:

// ❌ WRONG: 'string' is not an object type
commandBase.merge(type.or({ a: 'string' }, 'string'));

// ✅ CORRECT: all branches are object types
commandBase.merge(type.or({ a: 'string' }, { b: 'number' }));

联合中的可选属性

使用 arktype 的 'key?' 语法表示可选属性。不要使用 | undefined 表示可选项,因为这会导致 JSON Schema 转换出错。

// Good: optional property syntax
commandBase.merge({
	action: "'openTab'",
	url: 'string',
	'windowId?': 'string',
	'result?': type({ tabId: 'string' }).or('undefined'),
});

// Bad: explicit undefined union on a required key
commandBase.merge({
	action: "'openTab'",
	url: 'string',
	windowId: 'string | undefined', // Breaks JSON Schema
});

'result?': type({...}).or('undefined') 这种模式是正确的。? 会将键设为可选,而 .or('undefined') 则允许该键存在时其值显式为 undefined。这是实现“待处理 = 不存在,完成 = 有值”语义的标准模式。

合并行为

  • 覆盖:当基础类型和合并参数定义了相同的键时,以合并参数为准
  • 可选性优先:如果基础类型中的键是可选的('key?'),而合并参数中的键是必需的,则以合并参数的可选性为准
  • 不进行深度合并:.merge() 是浅层合并。它会替换顶层键,而不是嵌套对象
  • 对联合类型逐分支应用:基础类型和参数都可以是联合类型;合并会逐分支应用

判别字段检测

当联合类型的分支在同一个键上具有不同的字面值时,Arktype 会自动检测判别字段:

const AorB = type({ kind: "'A'", value: 'number' }).or({
	kind: "'B'",
	label: 'string',
});

// Arktype internally uses `kind` as the discriminant
// Validation checks `kind` first, then validates only the matching branch

这适用于任何字面量类型:字符串字面量、数字字面量或布尔字面量。

提取的类型始终用 type() 包装

将可复用的 arktype 类型提取为具名常量时,始终用 type() 包装,即使是简单的字符串字面量联合也不例外。这样可以确保该值是一个正规的 arktype Type,支持 .infer、.or()、.merge() 等操作。

// GOOD: wrapped with type(), composable, has .infer, works with .or()/.merge()
const tabGroupColor = type(
	"'grey' | 'blue' | 'red' | 'yellow' | 'green' | 'pink' | 'purple' | 'cyan' | 'orange'",
);

const commandBase = type({
	id: CommandId,
	deviceId: DeviceId,
	createdAt: 'number',
	_v: '1',
});

// BAD: plain string, not a Type, can't compose, no .infer
const tabGroupColor =
	"'grey' | 'blue' | 'red' | 'yellow' | 'green' | 'pink' | 'purple' | 'cyan' | 'orange'";

在 type({...}) 对象字面量中作为值使用时,两种写法都有效(arktype 会强制转换字符串)。但只有用 type() 包装的版本才是一级 Type,可以在所有位置使用。

让架构值与推断类型共用名称

当一个 arktype 架构以同一名称同时导出运行时值和推断类型时,只需导入一次该名称。TypeScript 将值空间和类型空间分开,因此同一标识符可以在运行时进行校验,也可以在编译时标注值。

// Good: one import covers both namespaces
import { WorkspaceId } from './workspace-id';

const Session = type({
	workspaceId: WorkspaceId,
});

type SessionResponse = {
	workspaceId: WorkspaceId;
};

不要仅仅为了给类型导入腾出空间,就给运行时架构起别名。

// Bad: duplicates the name with an artificial Schema suffix
import {
	WorkspaceId as WorkspaceIdSchema,
	type WorkspaceId,
} from './workspace-id';

const Session = type({
	workspaceId: WorkspaceIdSchema,
});

只有当两个导入的值确实在同一命名空间中冲突时,才使用别名。运行时架构与其推断类型不会冲突。

type.enumerated():从常量数组派生联合类型

使用 type.enumerated() 从现有的 as const 数组创建字符串字面量联合类型。这样可以自动让工作区架构与应用常量保持同步。

import { type } from 'arktype';

const RECORDING_MODES = ['manual', 'vad', 'upload'] as const;

// Spread the const array into type.enumerated()
const recordingMode = type.enumerated(...RECORDING_MODES);
// Equivalent to: type("'manual' | 'vad' | 'upload'")
从复杂对象数组中提取

当常量是包含 name 或 id 字段的对象时,先进行映射:

const OPENAI_TRANSCRIPTION_MODELS = [
	{ name: 'whisper-1', description: '...', cost: '$0.36/hour' },
	{ name: 'gpt-4o-transcribe', description: '...', cost: '$0.36/hour' },
] as const;

const openaiModel = type.enumerated(
	...OPENAI_TRANSCRIPTION_MODELS.map((m) => m.name),
);
在判别联合类型中使用

结合 base.merge(type.or(...)),构建每个变体的模型字段都派生自其常量数组的联合类型:

const transcriptionConfig = type.or(
	{ service: "'OpenAI'", model: type.enumerated(...OPENAI_MODELS.map((m) => m.name)) },
	{ service: "'Groq'", model: type.enumerated(...GROQ_MODELS.map((m) => m.name)) },
	{ service: "'whispercpp'" },  // local: no model field
);
从常量派生的原因
  • 单一事实来源:模型列表只在一个地方维护:常量数组
  • 自动同步:向数组中添加模型会自动更新工作区架构
  • 避免字符串偏差:架构不可能列出应用中不存在的模型

工作区字段表达式

工作区是纯 JSON 格式的 arktype 表达式字符串,因此有两种 arktype 特性无法在其中使用,而且它们导致的失败看起来毫不相关。

字段可以为 null,但不能是可选的。 工作区解析器会拒绝任何以 ? 结尾的字段键(packages/workspace/src/workspace.ts),因为可选性标记位于键中,而键标记在 JSON 往返转换后无法保留(ADR-0213)。

{ date: 'string|null = null' }  // Good
{ 'date?': 'string' }           // Refused: "declare it nullable instead"

数组不能带默认值。 'string[] = []' 会在解析时抛出错误 (Expected an expression before '[]'),因此应使用 null 表示空值。

{ tags: 'string[]|null = null' }  // Good
{ tags: 'string[] = []' }         // Throws

反模式

使用 JS 对象展开(会丢失 Type 组合能力)
// Bad: base is a plain object, not a Type
const baseFields = { id: 'string', deviceId: DeviceId, createdAt: 'number' };
const Command = type({ ...baseFields, action: "'closeTabs'" }).or({
	...baseFields,
	action: "'openTab'",
});

这种写法可行,但 baseFields 不是 arktype Type。你无法调用 .merge()、.or(),也无法在运行时检查它。如果基础类型应当是正规的类型,请优先使用 .merge()。

针对每个变体重复调用 base.merge(...)
// Bad: repetitive, base.merge repeated for every variant
type.or(
	commandBase.merge({ action: "'closeTabs'", tabIds: 'string[]' }),
	commandBase.merge({ action: "'openTab'", url: 'string' }),
	commandBase.merge({ action: "'activateTab'", tabId: 'string' }),
);

// Good: merge once, union the variants
commandBase.merge(
	type.or(
		{ action: "'closeTabs'", tabIds: 'string[]' },
		{ action: "'openTab'", url: 'string' },
		{ action: "'activateTab'", tabId: 'string' },
	),
);
忘记为可选项使用 'key?' 语法
// Bad: makes windowId required but accepting undefined
commandBase.merge({ windowId: 'string | undefined' });

// Good: makes windowId truly optional
commandBase.merge({ 'windowId?': 'string' });

参考资料

评论

支持 Markdown