一键导入
arktype
Arktype: runtime validation, discriminated unions with .merge()/.or(), spread keys. Use when mentioning arktype, type(), union types, command/event schemas.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Arktype: runtime validation, discriminated unions with .merge()/.or(), spread keys. Use when mentioning arktype, type(), union types, command/event schemas.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Run a continuous collapse-and-simplify pass that surgically removes indirection failing to earn its boundary. Use when the user says 'collapse pass', 'simplify pass', 'reduce indirection', 'shrink the surface', 'find what to delete', when asking to audit a package for dead abstractions, when reviewing a pull request, branch, or recent merged change for simplification (isolated in a worktree), or when the goal is a sequence of small refactor commits that delete more than they add. Pairs with code-audit (smell catalog), refactoring (per-change mechanics), one-sentence-test (cohesion gate), cohesive-clean-breaks (deep redesigns), approachability-audit (first-read sanity), and post-implementation-review (second-read after each commit).
How a workspace-backed app under `apps/*` is composed: the isomorphic doc factory (`create<App>`), the environment factories (`open<App>Browser` / `open<App>Extension` / tauri), the `#platform/*` build-time platform DI for multi-platform (Tauri) apps, the `session` singleton, daemon/script placement under per-project `workspaces/<app>/`, and the file layout itself. Use when creating a new app, naming or placing the iso/browser/extension factory, wiring `#platform/*` subpath imports for a Tauri seam, choosing between auth-gated (Shape A) vs module-singleton (Shape B), placing the session singleton, or registering daemon/script bindings.
Query/RPC layer with TanStack Query, defineKeys, service composition, runtime DI. Use for createQuery, createMutation, queries/mutations, reactive data management.
TanStack AI patterns for @tanstack/ai, @tanstack/ai-svelte, chat state, streamed responses, UIMessage parts, tool calling, tool approvals, and provider model adapters. Use when working on AI chat, createChat, fetchServerSentEvents, UIMessage conversion, or TanStack AI tools.
Create a slash-command `/goal` for long-running Codex or Claude Code work when the user explicitly asks for a `/goal`, agent goal, or completion condition. Outputs one goal line with the objective, starting context, validation evidence, and stop condition.
List 3-5 comparable apps when planning a UX surface to test category fit and surface high-leverage refusals. Use for "what do other apps do", identity, sync state, local-first design.
| name | arktype |
| description | Arktype: runtime validation, discriminated unions with .merge()/.or(), spread keys. Use when mentioning arktype, type(), union types, command/event schemas. |
| metadata | {"author":"epicenter","version":"1.0"} |
Patterns for composing arktype schemas, naming runtime schema values alongside inferred types, and building discriminated unions with .merge() and .or().
defineTable() schemas that use union typesbase.merge(type.or(...)) Pattern (Recommended)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.
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(...) creates a union of plain object definitions. Each is a variant with its own fields.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).commandBase fields plus its variant-specific fields.action key as a discriminant because each branch has a distinct literal value.switch (cmd.action) in TypeScript narrows the full union. Payload fields and result types are type-safe per branch.| Property | Benefit |
|---|---|
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 once | DRY: change base fields in one place |
| Auto-discrimination | No manual discriminant config needed |
| Flat payload | No nested payload object; fields are top-level |
.merge().or() Chaining Pattern (Good for 2-3 variants)Use when you have a small number of variants where chaining reads naturally.
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.
"..." Spread Key Pattern (Alternative)Use when defining inline without a pre-declared base variable, or when you prefer a more compact syntax.
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()).
Constraint: The "..." key must be the first key in the object. Arktype throws ParseError: Spread operator may only be used as the first key otherwise. Prefer .merge() when you need more flexibility.
const Command = type({
'...': commandBase,
action: "'closeTabs'",
tabIds: 'string[]',
}).or({
'...': commandBase,
action: "'openTab'",
url: 'string',
});
Functionally equivalent to .merge().or(). Choose based on readability preference.
.or() Chaining vs type.or() Staticconst Command = variantA.or(variantB).or(variantC);
type.or() (preferred for 4+ variants)const Command = type.or(variantA, variantB, variantC, variantD, variantE);
The static form avoids deeply nested chaining and creates the union in a single call.
.merge() Distribution Over Unions.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:
// 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:
// ❌ 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' }));
Use arktype's 'key?' syntax for optional properties. Never use | undefined for optionals because it breaks JSON Schema conversion.
// 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.
'key?') in the base and required in the merge, the merge argument's optionality wins.merge() is shallow. It replaces top-level keys, not nested objectsArktype auto-detects discriminants when union branches have distinct literal values on the same key:
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.
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.
// 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.
When an arktype schema exports both a runtime value and its inferred type with the same name, import that name once. TypeScript keeps value space and type space separate, so the same identifier can validate at runtime and annotate values at compile time.
// Good: one import covers both namespaces
import { WorkspaceId } from './workspace-id';
const Session = type({
workspaceId: WorkspaceId,
});
type SessionResponse = {
workspaceId: WorkspaceId;
};
Avoid aliasing the runtime schema just to make room for the type import.
// Bad: duplicates the name with an artificial Schema suffix
import {
WorkspaceId as WorkspaceIdSchema,
type WorkspaceId,
} from './workspace-id';
const Session = type({
workspaceId: WorkspaceIdSchema,
});
Reach for an alias only when two imported values genuinely collide in the same namespace. A runtime schema and its inferred type do not collide.
type.enumerated(): Derive Unions from Const ArraysUse type.enumerated() to create string literal unions from existing as const arrays. This keeps the workspace schema in sync with app constants automatically.
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'")
When constants are objects with a name or id field, map first:
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),
);
Combine with base.merge(type.or(...)) to build unions where each variant's model field derives from its constant array:
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
);
// 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.
base.merge(...) per variant// 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?' syntax for optionals// 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.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 typesrNode.distribute() in merge implementation