n4nAI

TypeScript types for OpenAI function calling and tools

Learn how to build type-safe TypeScript OpenAI function calling types for tool schemas and handlers, with runnable code and verification steps.

n4n Team3 min read570 words

Audio narration

Coming soon — every post will get a voice note here.

When you wire LLM tool calls into a TypeScript service, the risk isn’t the network round-trip—it’s the untyped gap between the model’s emitted JSON and your function signatures. Solid typescript openai function calling types close that gap so a renamed parameter or missing field fails at compile time, not in a 3 a.m. page. This guide shows how to define tools once and infer both the API schema and the local handler types from that single source, then run a typed dispatch loop end to end.

Step 1: Install dependencies and create the client

Use the official SDK. It ships the request/response types you want for chat completions and tools.

npm install openai json-schema-to-ts
import OpenAI from 'openai';

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// If you route through n4n.ai, the same TypeScript types apply because its
// endpoint is OpenAI-compatible:
// const client = new OpenAI({ apiKey: process.env.N4N_KEY, baseURL: 'https://api.n4n.ai/v1' });

Keep the client thin. All tool-specific typing lives in your own module.

Step 2: Define tool schemas with const assertions

Write the JSON Schema for each tool exactly as the API expects. Use as const so TypeScript preserves literal strings like 'get_weather' and the enum values.

const toolSchemas = [
  {
    type: 'function',
    function: {
      name: 'get_weather',
      parameters: {
        type: 'object',
        properties: {
          location: { type: 'string' },
          unit: { type: 'string', enum: ['c', 'f'] },
        },
        required: ['location'],
        additionalProperties: false,
      },
    },
  },
] as const;

type ToolSchema = (typeof toolSchemas)[number];

The as const makes unit’s enum a tuple ['c', 'f'] rather than string[]. That matters when deriving types.

Step 3: Derive TypeScript types from the schemas

The OpenAI API consumes JSON Schema, but your handlers need TypeScript interfaces. Use json-schema-to-ts to map the schema to a static type without hand-writing interfaces.

import type { FromSchema } from 'json-schema-to-ts';

type GetWeatherSchema = (typeof toolSchemas)[0]['function']['parameters'];
type GetWeatherArgs = FromSchema<GetWeatherSchema>;
// Equivalent to:
// type GetWeatherArgs = {
//   location: string;
//   unit?: 'c' | 'f';
// };

Now typescript openai function calling types are generated, not duplicated. If you add a parameter to the schema, the handler argument type changes automatically.

For multiple tools, map them programmatically:

type ToolArgs = {
  [K in ToolSchema['function']['name']]: FromSchema<
    Extract<ToolSchema, { function: { name: K } }>['function']['parameters']
  >;
};

This yields a dictionary ToolArgs = { get_weather: GetWeatherArgs } (and more as you add entries).

Step 4: Build a typed handler registry

Define the local functions with the inferred argument types. Use a registry keyed by the exact tool name.

const handlers: {
  [K in keyof ToolArgs]: (args: ToolArgs[K]) => unknown;
} = {
  get_weather: (args) => {
    // args is GetWeatherArgs here
    const u = args.unit ?? 'c';
    return { location: args.location, temp: 21, unit: u };
  },
};

The mapped type forces every tool name in toolSchemas to have a matching handler with the correct signature. Forget one and tsc errors.

Step 5: Cast schemas for the SDK and send the request

The SDK expects ChatCompletionTool[]. Your as const object is narrower, so cast it once at the boundary.

import type { ChatCompletionTool } from 'openai/resources/chat/completions';

const tools = toolSchemas as unknown as ChatCompletionTool[];

const completion = await client.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: 'What is the weather in Berlin?' }],
  tools,
  tool_choice: 'auto',
});

The response message may contain tool_calls. Type that field as OpenAI.ChatCompletionMessageToolCall[].

Step 6: Dispatch tool calls with type narrowing

Iterate the tool calls and route to the registry. Parse arguments, then narrow by name.

const msg = completion.choices[0].message;
const toolCalls = msg.tool_calls ?? [];

for (const call of toolCalls) {
  if (call.type !== 'function') continue;
  const name = call.function.name as keyof ToolArgs;
  const raw = JSON.parse(call.function.arguments) as unknown;

  // Runtime guard (see Step 7) goes here
  const args = raw as ToolArgs[typeof name];
  const result = handlers[name](args);

  // Send result back to the model
  await client.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      { role: 'user', content: 'What is the weather in Berlin?' },
      msg,
      {
        role: 'tool',
        tool_call_id: call.id,
        content: JSON.stringify(result),
      },
    ],
  });
}

Because handlers is keyed by keyof ToolArgs, the call is type-checked. The cast as ToolArgs[typeof name] is safe only after validation.

Step 7: Add runtime validation

Types vanish at runtime. The model can emit malformed JSON or omit a required field. Add a minimal predicate or use Zod.

function isGetWeatherArgs(x: unknown): x is GetWeatherArgs {
  if (typeof x !== 'object' || x === null) return false;
  const o = x as Record<string, unknown>;
  return typeof o['location'] === 'string' &&
    (o['unit'] === undefined || o['unit'] === 'c' || o['unit'] === 'f');
}

In the loop:

if (name === 'get_weather' && !isGetWeatherArgs(raw)) {
  throw new Error(`Invalid args for ${name}`);
}

For larger surfaces, generate Zod schemas from the same JSON Schema with json-schema-to-zod to avoid drift.

Step 8: Verify the pipeline

Write a small script run.ts that exercises the flow without network flakiness by stubbing the client. Or run against the live API with a test key.

// run.ts
async function main() {
  const c = await client.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: 'Weather in Paris?' }],
    tools,
  });
  const tc = c.choices[0].message.tool_calls ?? [];
  console.assert(tc.length === 1, 'expected one tool call');
  console.assert(tc[0].function.name === 'get_weather', 'expected get_weather');
  const args = JSON.parse(tc[0].function.arguments);
  console.assert(typeof args.location === 'string', 'location must be string');
  console.log('OK', args);
}
main();

Run with npx ts-node run.ts. Success means the script prints OK { location: 'Paris' } (or similar) and the TypeScript compiler passes with tsc --noEmit. If you change required in the schema but not the handler, tsc fails before you run anything.

Pitfalls to avoid

  • Double-defining schemas. If you write both a JSON Schema and a hand-written interface, they will diverge. Generate one from the other.
  • Loose any on tools. Casting tools as any throws away the literal names; use as unknown as ChatCompletionTool[] at the edge only.
  • Ignoring additionalProperties. OpenAI rejects extra fields. Set additionalProperties: false in the schema and mirror it in your runtime check.
  • Enum widening. Without as const, 'c' | 'f' becomes string, and the model may pass 'kelvin'. Keep the const.

Strong typescript openai function calling types are not boilerplate. They are the contract that lets you refactor tools without reading every prompt. Do it once per project and the compiler becomes your integration test.

Tagstypescriptopenai-apifunction-callingtypes

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All typescript typed llm api clients posts →