When you call the OpenAI API from a TypeScript service, relying on any or loosely typed objects invites silent regressions. Proper typescript openai chat completion types let the compiler catch malformed messages, wrong temperature ranges, and missing model fields before runtime. This guide walks through a strict, ergonomic typing setup for chat completion requests and responses using the official SDK and a few targeted extensions.
Step 1: Scaffold a strict TypeScript project
Create a minimal Node project and enable the strict family of compiler flags. You need TypeScript 5.0+ for the satisfies operator and improved union narrowing.
mkdir ts-chat-types && cd ts-chat-types
npm init -y
npm install openai
npm install -D typescript @types/node tsx
Create tsconfig.json with strict checks:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"verbatimModuleSyntax": true
}
}
strict alone catches most issues, but noUncheckedIndexedAccess forces you to handle undefined from array reads like choices[0], and exactOptionalPropertyTypes stops you from passing undefined to an optional field that should simply be omitted. Use NodeNext resolution so the OpenAI SDK’s subpath exports resolve cleanly.
Step 2: Import the official typescript openai chat completion types
The OpenAI SDK ships complete types for the chat endpoint. Import them directly from the subpath to avoid pulling the whole client into type space:
import type {
ChatCompletionCreateParams,
ChatCompletionCreateParamsNonStreaming,
ChatCompletionCreateParamsStreaming,
ChatCompletion,
ChatCompletionChunk,
} from 'openai/resources/chat/completions';
ChatCompletionCreateParams is a union of the streaming and non-streaming variants. The non-streaming type requires model and messages, and permits temperature, tools, response_format, seed, and more. The streaming type adds a literal stream: true. Using the specific variants prevents you from accidentally passing stream: true to a function that expects a ChatCompletion return.
A bare non-streaming call looks like this:
import OpenAI from 'openai';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const params: ChatCompletionCreateParamsNonStreaming = {
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Ping' }],
temperature: 0.2,
};
const completion: ChatCompletion = await client.chat.completions.create(params);
The compiler now verifies that role is the literal 'user' (not string) and that content matches the expected string | Array<...> shape. If you typo rol: 'user', the error is immediate.
Step 3: Build a typed request constructor
Hand-writing the full param object at every call site gets noisy and error-prone. Wrap it in a function that returns a precisely typed request. Use satisfies to keep literal inference while checking against the SDK type.
type UserMessage = { role: 'user'; content: string };
function makeChatRequest(
model: ChatCompletionCreateParams['model'],
userText: string,
opts?: Partial<Omit<ChatCompletionCreateParamsNonStreaming, 'model' | 'messages'>>
): ChatCompletionCreateParamsNonStreaming {
const messages = [{ role: 'user', content: userText }] satisfies UserMessage[];
return {
model,
messages,
temperature: 0.7,
...opts,
} satisfies ChatCompletionCreateParamsNonStreaming;
}
The satisfies operator confirms the shape without widening role to string. Under exactOptionalPropertyTypes, spreading opts is safe only because we typed it as Partial<Omit<...>>; if a caller passes temperature: undefined, the compiler rejects it unless the field is truly optional in the SDK type (it is, but the literal undefined assignment is blocked). Construct requests through this helper and you centralize the typescript openai chat completion types contract.
Step 4: Narrow streaming vs non-streaming responses
The create method returns Promise<ChatCompletion> when stream is omitted or false, and Promise<Stream<ChatCompletionChunk>> when stream: true. If you type the param as the base union, the return type is a union you must discriminate. Write a type guard for the async iterable:
import type { Stream } from 'openai/streaming';
import type { ChatCompletion, ChatCompletionChunk } from 'openai/resources/chat/completions';
function isStreaming(
res: ChatCompletion | Stream<ChatCompletionChunk>
): res is Stream<ChatCompletionChunk> {
return Symbol.asyncIterator in res;
}
Consume accordingly:
const res = await client.chat.completions.create({
...makeChatRequest('gpt-4o-mini', 'Stream me'),
stream: true,
});
if (isStreaming(res)) {
for await (const chunk of res) {
const delta = chunk.choices[0]?.delta?.content ?? '';
process.stdout.write(delta);
}
} else {
console.log(res.choices[0]?.message?.content ?? '');
}
With noUncheckedIndexedAccess, choices[0] is ChatCompletion.Choice | undefined, so the ?. is mandatory. The delta field on a chunk is also optional—another spot where the strict types force correct runtime handling.
Step 5: Extend types for provider routing and cache hints
If you proxy through a gateway that supports client routing directives, the base OpenAI type will reject extra fields. Augment the request type instead of using as any. For example, a gateway such as n4n.ai honors client routing directives and forwards provider cache-control hints, so you can add an optional routing block:
interface RoutedChatParams extends ChatCompletionCreateParamsNonStreaming {
route?: {
prefer?: string[];
exclude?: string[];
};
headers?: Record<string, string>;
}
const routed: RoutedChatParams = {
model: 'anthropic/claude-3.5-sonnet',
messages: [{ role: 'user', content: 'Summarize' }],
route: { prefer: ['anthropic'], exclude: ['meta'] },
headers: { 'x-cache-control': 'max-age=3600' },
};
At the call boundary, cast the extended object to the SDK param type so the client serializes it without complaint:
await client.chat.completions.create(routed as ChatCompletionCreateParamsNonStreaming);
This preserves local strictness for your typescript openai chat completion types while letting the gateway read the extra keys. Never cast to any—that defeats the entire exercise.
Step 6: Validate runtime shape with a thin schema
Types vanish at runtime. If you accept model names or messages from external input, back the types with a Zod schema that mirrors the SDK type. This is cheap insurance at a trust boundary.
import { z } from 'zod';
const MessageSchema = z.object({
role: z.enum(['system', 'user', 'assistant', 'tool']),
content: z.union([z.string(), z.array(z.any())]),
});
const RequestSchema = z.object({
model: z.string(),
messages: z.array(MessageSchema).min(1),
temperature: z.number().min(0).max(2).optional(),
});
export function parseChatInput(input: unknown) {
return RequestSchema.parse(input);
}
export type ValidatedChatRequest = z.infer<typeof RequestSchema>;
ValidatedChatRequest is structurally compatible with a subset of ChatCompletionCreateParamsNonStreaming. Run parseChatInput before constructing the SDK call. The inferred type matches the shape you expect, keeping both compile-time and runtime safety without duplicating field definitions by hand.
Step 7: Verify success
Type-check the project and run a minimal script against a fake key to confirm the request shape compiles and serializes.
npx tsc --noEmit
Create smoke.ts:
import { makeChatRequest } from './requests';
const req = makeChatRequest('gpt-4o-mini', 'Hello');
console.log(JSON.stringify(req));
Run with npx tsx smoke.ts. You should see a JSON object containing model, messages, and temperature with zero TypeScript errors. If you wired routing extensions in Step 5, serialize that object instead and confirm the route and headers fields appear. That proves your typescript openai chat completion types survive both compilation and runtime serialization.
For a live end-to-end check, set OPENAI_API_KEY (or point the client baseURL at your gateway) and invoke client.chat.completions.create with the constructed params. A 200 response with a parsed ChatCompletion object validates the full contract. If tsc passes and the smoke script prints the expected JSON, you have a strictly typed chat client ready for production refactors.