arktype
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseArktype Discriminated Unions
Arktype 区分联合类型
Patterns for composing discriminated unions with arktype's and methods.
.merge().or()使用Arktype的和方法构建区分联合类型的模式。
.merge().or()When to Apply This Skill
何时应用此技巧
- Defining a discriminated union schema (e.g., commands, events, actions)
- Composing a base type with per-variant fields
- Working with schemas that use union types
defineTable()
- 定义区分联合类型模式(如命令、事件、操作)
- 将基础类型与各变体专属字段组合
- 处理使用联合类型的模式
defineTable()
base.merge(type.or(...))
Pattern (Recommended)
base.merge(type.or(...))base.merge(type.or(...))
模式(推荐)
base.merge(type.or(...))Use when you have shared base fields and per-variant payloads discriminated on a literal key. distributes over unions — it merges the base into each branch of the union automatically.
.merge()typescript
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'),
},
),
);当你拥有共享基础字段和基于字面量键区分的各变体负载时,可使用此模式。会在联合类型上分发——它会自动将基础类型合并到联合类型的每个分支中。
.merge()typescript
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'),
},
),
);How it works
工作原理
- creates a union of plain object definitions — each is a variant with its own fields.
type.or(...) - distributes the merge across each branch of the union. Internally, arktype calls
commandBase.merge(union)to apply the merge to each branch individually (source).rNode.distribute() - The result is a union where each branch has all fields plus its variant-specific fields.
commandBase - Arktype auto-detects the key as a discriminant because each branch has a distinct literal value.
action - in TypeScript narrows the full union — payload fields and result types are type-safe per branch.
switch (cmd.action)
- 创建普通对象定义的联合类型——每个对象都是带有自身字段的变体。
type.or(...) - 会将合并操作分发到联合类型的每个分支上。在内部,Arktype会调用
commandBase.merge(union)来单独对每个分支应用合并操作(源码)。rNode.distribute() - 结果是一个联合类型,每个分支都包含所有字段以及该变体的专属字段。
commandBase - Arktype会自动将键识别为区分符,因为每个分支都有不同的字面量值。
action - TypeScript中的会缩小联合类型的范围——每个分支的负载字段和结果类型都是类型安全的。
switch (cmd.action)
Why this pattern
为何选择此模式
| Property | Benefit |
|---|---|
Base is a real | Reusable, composable, inspectable at runtime |
| No need to repeat |
| All variants in one list — easy to read and add to |
| Base appears once | DRY — change base fields in one place |
| Auto-discrimination | No manual discriminant config needed |
| Flat payload | No nested |
| 属性 | 优势 |
|---|---|
基础是真实的 | 可复用、可组合、运行时可检查 |
| 无需为每个变体重复编写 |
| 所有变体在一个列表中——易于阅读和添加 |
| 基础仅出现一次 | 遵循DRY原则——只需在一处修改基础字段 |
| 自动识别区分符 | 无需手动配置区分符 |
| 负载结构扁平 | 无需嵌套 |
.merge().or()
Chaining Pattern (Good for 2-3 variants)
.merge().or().merge().or()
链式调用模式(适合2-3个变体)
.merge().or()Use when you have a small number of variants where chaining reads naturally.
typescript
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'),
}),
);For 4+ variants, prefer to avoid repeating per branch.
base.merge(type.or(...))commandBase.merge(...)当变体数量较少,链式调用可读性更好时,可使用此模式。
typescript
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(...)The "..."
Spread Key Pattern (Alternative)
"...""..."
扩展键模式(替代方案)
"..."Use when defining inline without a pre-declared base variable, or when you prefer a more compact syntax.
typescript
const User = type({ isAdmin: 'false', name: 'string' });
const Admin = type({
'...': User,
isAdmin: 'true',
permissions: 'string[]',
});The key spreads all properties from the referenced type into the new object definition. Conflicting keys in the outer object override the spread type (same as ).
"...".merge()当你需要内联定义而无需预先声明基础变量,或更偏好紧凑语法时,可使用此模式。
typescript
const User = type({ isAdmin: 'false', name: 'string' });
const Admin = type({
'...': User,
isAdmin: 'true',
permissions: 'string[]',
});"...".merge()Spread key in unions
联合类型中的扩展键
typescript
const Command = type({
'...': commandBase,
action: "'closeTabs'",
tabIds: 'string[]',
}).or({
'...': commandBase,
action: "'openTab'",
url: 'string',
});Functionally equivalent to . Choose based on readability preference.
.merge().or()typescript
const Command = type({
'...': commandBase,
action: "'closeTabs'",
tabIds: 'string[]',
}).or({
'...': commandBase,
action: "'openTab'",
url: 'string',
});功能上与等价,可根据可读性偏好选择。
.merge().or().or()
Chaining vs type.or()
Static
.or()type.or().or()
链式调用 vs type.or()
静态调用
.or()type.or()Chaining (preferred for 2-3 variants)
链式调用(适合2-3个变体)
typescript
const Command = variantA.or(variantB).or(variantC);typescript
const Command = variantA.or(variantB).or(variantC);Static type.or()
(preferred for 4+ variants)
type.or()静态type.or()
调用(适合4个及以上变体)
type.or()typescript
const Command = type.or(variantA, variantB, variantC, variantD, variantE);The static form avoids deeply nested chaining and creates the union in a single call.
typescript
const Command = type.or(variantA, variantB, variantC, variantD, variantE);静态调用形式避免了深层嵌套的链式调用,可在单次调用中创建联合类型。
.merge()
Distribution Over Unions
.merge().merge()
在联合类型上的分发行为
.merge().merge()typescript
// 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' }))Constraint: Each branch of the union must be an object type. If any branch is non-object (e.g., ), arktype will throw a :
'string'ParseErrortypescript
// ❌ 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' }));.merge()typescript
// base.merge(union) — 对每个分支应用合并操作
const Result = baseType.merge(type.or({ a: 'string' }, { b: 'number' }));
// 等价于: type.or(baseType.merge({ a: 'string' }), baseType.merge({ b: 'number' }))约束条件:联合类型的每个分支必须是对象类型。如果任何一个分支是非对象类型(如),Arktype会抛出:
'string'ParseErrortypescript
// ❌ 错误:'string'不是对象类型
commandBase.merge(type.or({ a: 'string' }, 'string'));
// ✅ 正确:所有分支均为对象类型
commandBase.merge(type.or({ a: 'string' }, { b: 'number' }));Optional Properties in Unions
联合类型中的可选属性
Use arktype's syntax for optional properties. Never use for optionals — it breaks JSON Schema conversion.
'key?'| undefinedtypescript
// 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
});The pattern is correct — the makes the key optional, and allows the value to be explicitly when present. This is the standard pattern for "pending = absent, done = has value" semantics.
'result?': type({...}).or('undefined')?.or('undefined')undefined使用Arktype的语法定义可选属性。切勿使用来定义可选属性——这会破坏JSON Schema转换。
'key?'| undefinedtypescript
// 正确:可选属性语法
commandBase.merge({
action: "'openTab'",
url: 'string',
'windowId?': 'string',
'result?': type({ tabId: 'string' }).or('undefined'),
});
// 错误:在必填键上显式定义undefined联合
commandBase.merge({
action: "'openTab'",
url: 'string',
windowId: 'string | undefined', // 破坏JSON Schema
});'result?': type({...}).or('undefined')?.or('undefined')undefinedMerge Behavior
合并行为
- Override: When both the base and merge argument define the same key, the merge argument wins
- Optional preservation: If a key is optional () in the base and required in the merge, the merge argument's optionality wins
'key?' - No deep merge: is shallow — it replaces top-level keys, not nested objects
.merge() - Distributes over unions: Both the base and the argument can be unions — merge is applied per-branch
- 覆盖:当基础类型和合并参数定义了同一个键时,合并参数的定义优先级更高
- 可选性保留:如果基础类型中的键是可选的(),而合并参数中该键是必填的,合并参数的可选性设置会生效
'key?' - 不支持深度合并:是浅合并——它会替换顶级键,而非嵌套对象
.merge() - 在联合类型上分发:基础类型和合并参数均可为联合类型——合并操作会应用到每个分支
Discriminant Detection
区分符检测
Arktype auto-detects discriminants when union branches have distinct literal values on the same key:
typescript
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 branchThis works with any literal type — string literals, number literals, or boolean literals.
当联合类型的各分支在同一个键上有不同的字面量值时,Arktype会自动识别该键为区分符:
typescript
const AorB = type({ kind: "'A'", value: 'number' }).or({
kind: "'B'",
label: 'string',
});
// Arktype内部使用`kind`作为区分符
// 校验会先检查`kind`,然后仅校验匹配的分支这适用于任何字面量类型——字符串字面量、数字字面量或布尔字面量。
Always Wrap Extracted Types with type()
type()始终用type()
包裹提取的类型
type()When extracting reusable arktype types into named constants, always wrap them with — even for simple string literal unions. This ensures the value is a proper arktype with , , , etc.
type()Type.infer.or().merge()typescript
// 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'";Both work when used as a value inside object literals (arktype coerces strings). But only the -wrapped version is a first-class that works in all positions.
type({...})type()Type当将可复用的Arktype类型提取为命名常量时,始终用包裹——即使是简单的字符串字面量联合类型。这能确保该值是一个完整的Arktype ,拥有、、等方法。
type()Type.infer.or().merge()typescript
// 正确:用type()包裹——可组合,拥有.infer,支持.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',
});
// 错误:普通字符串——不是Type类型,无法组合,没有.infer
const tabGroupColor =
"'grey' | 'blue' | 'red' | 'yellow' | 'green' | 'pink' | 'purple' | 'cyan' | 'orange'";两种方式在对象字面量中作为值使用时都能生效(Arktype会自动转换字符串)。但只有用包裹的版本是一等类型,可在所有场景中使用。
type({...})type()TypeAnti-Patterns
反模式
JS object spread (loses Type composition)
JS对象扩展(丢失Type组合性)
typescript
// 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'",
});This works but is not an arktype — you can't call , , or inspect it at runtime. Prefer when the base should be a proper type.
baseFieldsType.merge().or().merge()typescript
// 错误:基础是普通对象,不是Type类型
const baseFields = { id: 'string', deviceId: DeviceId, createdAt: 'number' };
const Command = type({ ...baseFields, action: "'closeTabs'" }).or({
...baseFields,
action: "'openTab'",
});这种方式虽然能运行,但不是Arktype ——你无法调用、,也无法在运行时检查它。当基础类型需要是完整类型时,建议使用。
baseFieldsType.merge().or().merge()Repeating base.merge(...)
per variant
base.merge(...)为每个变体重复base.merge(...)
base.merge(...)typescript
// 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' },
),
);typescript
// 错误:重复代码——每个变体都重复base.merge()
type.or(
commandBase.merge({ action: "'closeTabs'", tabIds: 'string[]' }),
commandBase.merge({ action: "'openTab'", url: 'string' }),
commandBase.merge({ action: "'activateTab'", tabId: 'string' }),
);
// 正确:合并一次,将变体转为联合类型
commandBase.merge(
type.or(
{ action: "'closeTabs'", tabIds: 'string[]' },
{ action: "'openTab'", url: 'string' },
{ action: "'activateTab'", tabId: 'string' },
),
);Forgetting 'key?'
syntax for optionals
'key?'忘记用'key?'
语法定义可选属性
'key?'typescript
// Bad: makes windowId required but accepting undefined
commandBase.merge({ windowId: 'string | undefined' });
// Good: makes windowId truly optional
commandBase.merge({ 'windowId?': 'string' });typescript
// 错误:将windowId设为必填但允许undefined
commandBase.merge({ windowId: 'string | undefined' });
// 正确:将windowId设为真正的可选属性
commandBase.merge({ 'windowId?': 'string' });References
参考资料
- — Commands table using
apps/tab-manager/src/lib/workspace.tscommandBase.merge(type.or(...)) - — Arktype optional properties section
.agents/skills/typescript/SKILL.md - —
.agents/skills/workspace-api/SKILL.mdaccepts union typesdefineTable() - arktype source: merge distributes — in merge implementation
rNode.distribute()
- — 使用
apps/tab-manager/src/lib/workspace.ts的命令表commandBase.merge(type.or(...)) - — Arktype可选属性章节
.agents/skills/typescript/SKILL.md - —
.agents/skills/workspace-api/SKILL.md支持联合类型defineTable() - Arktype源码:merge的分发逻辑 — merge实现中的
rNode.distribute()