arktype

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Arktype Discriminated Unions

Arktype 区分联合类型

Patterns for composing discriminated unions with arktype's
.merge()
and
.or()
methods.
使用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
    defineTable()
    schemas that use union types
  • 定义区分联合类型模式(如命令、事件、操作)
  • 将基础类型与各变体专属字段组合
  • 处理使用联合类型的
    defineTable()
    模式

base.merge(type.or(...))
Pattern (Recommended)

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

Use when you have shared base fields and per-variant payloads discriminated on a literal key.
.merge()
distributes over unions — it merges the base into each branch of the union automatically.
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

工作原理

  1. type.or(...)
    creates a union of plain object definitions — each is a variant with its own fields.
  2. commandBase.merge(union)
    distributes the merge across each branch of the union. Internally, arktype calls
    rNode.distribute()
    to apply the merge to each branch individually (source).
  3. The result is a union where each branch has all
    commandBase
    fields plus its variant-specific fields.
  4. Arktype auto-detects the
    action
    key as a discriminant because each branch has a distinct literal value.
  5. switch (cmd.action)
    in TypeScript narrows the full union — payload fields and result types are type-safe per branch.
  1. type.or(...)
    创建普通对象定义的联合类型——每个对象都是带有自身字段的变体。
  2. commandBase.merge(union)
    会将合并操作分发到联合类型的每个分支上。在内部,Arktype会调用
    rNode.distribute()
    来单独对每个分支应用合并操作(源码)。
  3. 结果是一个联合类型,每个分支都包含所有
    commandBase
    字段以及该变体的专属字段。
  4. Arktype会自动将
    action
    键识别为区分符,因为每个分支都有不同的字面量值。
  5. TypeScript中的
    switch (cmd.action)
    会缩小联合类型的范围——每个分支的负载字段和结果类型都是类型安全的。

Why this pattern

为何选择此模式

PropertyBenefit
Base is a real
Type
Reusable, composable, inspectable at runtime
.merge()
distributes
No need to repeat
base.merge(...)
per variant
type.or()
is flat
All variants in one list — easy to read and add to
Base appears onceDRY — change base fields in one place
Auto-discriminationNo manual discriminant config needed
Flat payloadNo nested
payload
object — fields are top-level
属性优势
基础是真实的
Type
类型
可复用、可组合、运行时可检查
.merge()
支持分发
无需为每个变体重复编写
base.merge(...)
type.or()
结构扁平
所有变体在一个列表中——易于阅读和添加
基础仅出现一次遵循DRY原则——只需在一处修改基础字段
自动识别区分符无需手动配置区分符
负载结构扁平无需嵌套
payload
对象——所有字段均为顶级字段

.merge().or()
Chaining Pattern (Good for 2-3 variants)

.merge().or()
链式调用模式(适合2-3个变体)

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
base.merge(type.or(...))
to avoid repeating
commandBase.merge(...)
per branch.
当变体数量较少,链式调用可读性更好时,可使用此模式。
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
.merge().or()
. Choose based on readability preference.
typescript
const Command = type({
	'...': commandBase,
	action: "'closeTabs'",
	tabIds: 'string[]',
}).or({
	'...': commandBase,
	action: "'openTab'",
	url: 'string',
});
功能上与
.merge().or()
等价,可根据可读性偏好选择。

.or()
Chaining vs
type.or()
Static

.or()
链式调用 vs
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()
调用(适合4个及以上变体)

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()
distributes over unions on both sides. If you merge a union into an object type (or vice versa), the operation is applied to each branch individually:
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.,
'string'
), arktype will throw a
ParseError
:
typescript
// ❌ 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' }))
约束条件:联合类型的每个分支必须是对象类型。如果任何一个分支是非对象类型(如
'string'
),Arktype会抛出
ParseError
typescript
// ❌ 错误:'string'不是对象类型
commandBase.merge(type.or({ a: 'string' }, 'string'));

// ✅ 正确:所有分支均为对象类型
commandBase.merge(type.or({ a: 'string' }, { b: 'number' }));

Optional Properties in Unions

联合类型中的可选属性

Use arktype's
'key?'
syntax for optional properties. Never use
| undefined
for optionals — it breaks JSON Schema conversion.
typescript
// 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
'result?': type({...}).or('undefined')
pattern is correct — the
?
makes the key optional, and
.or('undefined')
allows the value to be explicitly
undefined
when present. This is the standard pattern for "pending = absent, done = has value" semantics.
使用Arktype的
'key?'
语法定义可选属性。切勿使用
| undefined
来定义可选属性——这会破坏JSON Schema转换。
typescript
// 正确:可选属性语法
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')
允许该键存在时的值为显式
undefined
。这是“待处理=不存在,完成=有值”语义的标准实现模式。

Merge Behavior

合并行为

  • Override: When both the base and merge argument define the same key, the merge argument wins
  • Optional preservation: If a key is optional (
    'key?'
    ) in the base and required in the merge, the merge argument's optionality wins
  • No deep merge:
    .merge()
    is shallow — it replaces top-level keys, not nested objects
  • 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 branch
This 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()
包裹提取的类型

When extracting reusable arktype types into named constants, always wrap them with
type()
— even for simple string literal unions. This ensures the value is a proper arktype
Type
with
.infer
,
.or()
,
.merge()
, etc.
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
type({...})
object literals (arktype coerces strings). But only the
type()
-wrapped version is a first-class
Type
that works in all positions.
当将可复用的Arktype类型提取为命名常量时,始终用
type()
包裹——即使是简单的字符串字面量联合类型。这能确保该值是一个完整的Arktype
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'";
两种方式在
type({...})
对象字面量中作为值使用时都能生效(Arktype会自动转换字符串)。但只有用
type()
包裹的版本是一等
Type
类型,可在所有场景中使用。

Anti-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
baseFields
is not an arktype
Type
— you can't call
.merge()
,
.or()
, or inspect it at runtime. Prefer
.merge()
when the base should be a proper type.
typescript
// 错误:基础是普通对象,不是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()

Repeating
base.merge(...)
per variant

为每个变体重复
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?'
语法定义可选属性

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

参考资料

  • apps/tab-manager/src/lib/workspace.ts
    — Commands table using
    commandBase.merge(type.or(...))
  • .agents/skills/typescript/SKILL.md
    — Arktype optional properties section
  • .agents/skills/workspace-api/SKILL.md
    defineTable()
    accepts union types
  • arktype source: merge distributes
    rNode.distribute()
    in merge implementation
  • apps/tab-manager/src/lib/workspace.ts
    — 使用
    commandBase.merge(type.or(...))
    的命令表
  • .agents/skills/typescript/SKILL.md
    — Arktype可选属性章节
  • .agents/skills/workspace-api/SKILL.md
    defineTable()
    支持联合类型
  • Arktype源码:merge的分发逻辑 — merge实现中的
    rNode.distribute()