I wasted three hours last month chasing a runtime bug in my AI agent config. The tool definitions looked fine. TypeScript wasn't complaining. Then at 2 AM the agent called a tool that didn't exist.
The problem: I was using as const where I should have been using satisfies.
Here's what changed everything.
The Problem With as const for Agent Config
When you define Claude tool schemas, you want two things simultaneously:
- TypeScript should enforce the shape is correct
- TypeScript should infer the specific literal values (not just
string)
as const gives you (2) but not (1). Type assertions give you (1) but lose (1). Until TypeScript 4.9, you had to pick.
// as const — infers literals but no shape validation
const tools = [
{
name: "search_web",
description: "Search the internet",
input_schema: {
type: "object",
properties: {
query: { type: "string" }
},
required: ["query"]
}
}
] as const;
// Type assertion — validates shape but widens literals
const tools: Tool[] = [
{
name: "search_web",
// TypeScript accepts this but loses literal inference
// tools[0].name is now `string`, not `"search_web"`
}
];
Why does losing literal inference matter? Because you can't write a discriminated union over your tool names. You can't have handleToolCall that TypeScript knows is exhaustive. You get runtime errors instead of compile-time errors.
The Fix: satisfies
import Anthropic from "@anthropic-ai/sdk";
type Tool = Anthropic.Messages.Tool;
const TOOLS = [
{
name: "search_web",
description: "Search the internet for current information",
input_schema: {
type: "object" as const,
properties: {
query: {
type: "string" as const,
description: "The search query"
}
},
required: ["query"]
}
},
{
name: "read_file",
description: "Read a file from the filesystem",
input_schema: {
type: "object" as const,
properties: {
path: {
type: "string" as const,
description: "Absolute path to the file"
}
},
required: ["path"]
}
}
] satisfies Tool[];
// Now TOOLS[0].name is literally "search_web", not string
type ToolName = typeof TOOLS[number]["name"];
// => "search_web" | "read_file"
With satisfies, TypeScript validates that every element matches Tool and preserves the literal types. You get both.
Building a Type-Safe Tool Dispatcher
This is where it pays off. You can now write a handleTool function that TypeScript proves handles every case:
type ToolInput<T extends ToolName> = T extends "search_web"
? { query: string }
: T extends "read_file"
? { path: string }
: never;
async function handleTool<T extends ToolName>(
name: T,
input: ToolInput<T>
): Promise<string> {
switch (name) {
case "search_web":
// TypeScript knows input.query is string here
return await searchWeb(input.query);
case "read_file":
// TypeScript knows input.path is string here
return await readFile(input.path);
// If you add a new tool to TOOLS and forget to handle it here,
// TypeScript will error at compile time
}
}
Add a new tool to TOOLS and forget to handle it in handleTool? TypeScript tells you immediately. No more 2 AM runtime surprises.
The Full Agent Loop
Here's how I wire this into a production Claude agent:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function runAgent(userMessage: string): Promise<string> {
const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: userMessage }
];
while (true) {
const response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 4096,
tools: TOOLS, // fully typed, validates against Tool[]
messages
});
if (response.stop_reason === "end_turn") {
const textBlock = response.content.find(b => b.type === "text");
return textBlock?.text ?? "";
}
if (response.stop_reason === "tool_use") {
const toolUses = response.content.filter(
(b): b is Anthropic.Messages.ToolUseBlock => b.type === "tool_use"
);
messages.push({ role: "assistant", content: response.content });
const toolResults: Anthropic.Messages.ToolResultBlockParam[] = [];
for (const toolUse of toolUses) {
// Cast is safe because Claude only returns tool names we gave it
const name = toolUse.name as ToolName;
const result = await handleTool(name, toolUse.input as ToolInput<typeof name>);
toolResults.push({
type: "tool_result",
tool_use_id: toolUse.id,
content: result
});
}
messages.push({ role: "user", content: toolResults });
}
}
}
Three More Places satisfies Saves You
1. Model configuration
type ModelId = "claude-opus-4-6" | "claude-sonnet-4-6" | "claude-haiku-4-5";
interface AgentConfig {
model: ModelId;
maxTokens: number;
temperature?: number;
}
const AGENT_CONFIGS = {
research: {
model: "claude-opus-4-6",
maxTokens: 8192,
temperature: 0.3
},
summarizer: {
model: "claude-haiku-4-5",
maxTokens: 1024
},
coder: {
model: "claude-sonnet-4-6",
maxTokens: 4096,
temperature: 0.1
}
} satisfies Record<string, AgentConfig>;
// AGENT_CONFIGS.research.model is "claude-opus-4-6", not ModelId
2. System prompt registry
const SYSTEM_PROMPTS = {
agent: "You are an autonomous AI agent...",
reviewer: "You are a code reviewer...",
planner: "You are a strategic planner..."
} satisfies Record<string, string>;
type PromptKey = keyof typeof SYSTEM_PROMPTS;
// => "agent" | "reviewer" | "planner"
3. MCP server definitions
interface McpServer {
command: string;
args: string[];
env?: Record<string, string>;
}
const MCP_SERVERS = {
filesystem: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
},
github: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-github"],
env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_TOKEN! }
}
} satisfies Record<string, McpServer>;
Quick Reference
| Pattern | Use when |
|---|---|
as const |
You only need literal inference, no shape validation |
| Type annotation | You need shape validation, don't care about literals |
satisfies |
You need both — production agent config, tool definitions, any enum-like registry |
The Bottom Line
If you're building AI agents in TypeScript and still using as const for your tool definitions, you're leaving compile-time safety on the table. The satisfies operator is a one-word fix that prevents entire categories of runtime errors.
I use this pattern across the full whoffagents.com automation stack — 20+ tools, 6 agents, zero runtime tool-name errors since switching.
The type system is smarter than you think. Let it do the work.
Building autonomous AI agents? Check out whoffagents.com for production-ready agent templates and tooling.