Loading...
Loading...
Arktype patterns for discriminated unions using .merge() and .or(), spread key syntax, and type composition. Use when building union types, combining base schemas with variants, or defining command/event schemas with arktype.
npx skill4agent add epicenterhq/epicenter arktype.merge().or()defineTable()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'),
},
),
);type.or(...)commandBase.merge(union)rNode.distribute()commandBaseactionswitch (cmd.action)| 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 |
.merge().or()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'),
}),
);base.merge(type.or(...))commandBase.merge(...)"..."const User = type({ isAdmin: 'false', name: 'string' });
const Admin = type({
'...': User,
isAdmin: 'true',
permissions: 'string[]',
});"...".merge()const Command = type({
'...': commandBase,
action: "'closeTabs'",
tabIds: 'string[]',
}).or({
'...': commandBase,
action: "'openTab'",
url: 'string',
});.merge().or().or()type.or()const Command = variantA.or(variantB).or(variantC);type.or()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'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' }));'key?'| undefined// 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()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 branchtype()type()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({...})type()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'",
});baseFieldsType.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' });apps/tab-manager/src/lib/workspace.tscommandBase.merge(type.or(...)).agents/skills/typescript/SKILL.md.agents/skills/workspace-api/SKILL.mddefineTable()rNode.distribute()