First Call and Streaming in TypeScript
On this page
Lesson updated November 3, 2026 after compiling examples in strict mode with SDK
@mistralai/mistralai3.5.0 and TypeScript 7.0.2. The SDK typescontentasstring | ContentChunk[]: multi-turn conversations and streaming loops failed to compile without atypeof content === 'string'check. The configuration example previously combinedtemperatureandtopP, which the API documentation advises against modifying together. Error handling now uses the SDK’sMistralErrorclass, and the Express route validates the question before calling the API. Sources: Mistral TypeScript SDK (README) · chat completion parameters.
Send Your First Request
Now that the SDK is installed, let’s get practical. The TypeScript SDK’s chat.complete() method is the exact equivalent of the Python SDK’s: same parameters, same response structure, aside from naming conventions. If you followed the Python lessons, you’re on familiar ground; only JavaScript’s asynchronous syntax is new.
import 'dotenv/config';
import { Mistral } from '@mistralai/mistralai';
const client = new Mistral({
apiKey: process.env.MISTRAL_API_KEY,
});
async function main() {
const response = await client.chat.complete({
model: 'mistral-small-latest',
messages: [
{
role: 'user',
content: 'Explain the concept of a closure in JavaScript in three sentences.',
},
],
});
console.log(response.choices[0]?.message?.content);
}
main();
The question marks in response.choices[0]?.message?.content aren’t just for show: in the SDK types, message and content are optional, and choices[0] doesn’t exist if the list comes back empty. Safe access returns undefined rather than throwing an exception. Also note that content isn’t always a string: the SDK types it as string | ContentChunk[] because a response can arrive as a list of fragments (text, thinking reasoning, references, etc.).
Understanding the Response
The response structure is identical to the Python SDK’s. Parsing it helps you understand what you’re paying for and why a response stopped. The following example extracts the text, stop reason, and token count one by one; typing it out will show your editor suggesting each field, proving the typing is doing its job.
async function analyserReponse() {
const response = await client.chat.complete({
model: 'mistral-small-latest',
messages: [
{ role: 'user', content: 'What is the capital of France?' },
],
maxTokens: 100,
});
// Response text
const texte = response.choices[0]?.message?.content;
// Stop reason
const finRaison = response.choices[0]?.finishReason;
// Token statistics
const tokensEntree = response.usage?.promptTokens;
const tokensSortie = response.usage?.completionTokens;
console.log(`Response: ${texte}`);
console.log(`Tokens: ${tokensEntree} input + ${tokensSortie} output`);
console.log(`Stop: ${finRaison}`);
}
Note the casing: where Python exposes max_tokens, finish_reason, and prompt_tokens, TypeScript uses maxTokens, finishReason, and promptTokens. This is the number one error when manually porting a Python example—and the compiler will flag it immediately.
Streaming
Streaming is essential for web interfaces: without it, the user stares at a frozen screen for several seconds; with it, text appears word by word and perceived latency collapses. The TypeScript SDK exposes the stream as an async generator, which you iterate with for await...of:
async function streamReponse() {
const stream = await client.chat.stream({
model: 'mistral-small-latest',
messages: [
{
role: 'user',
content: 'Describe the five main design patterns in JavaScript.',
},
],
});
for await (const chunk of stream) {
const content = chunk.data.choices[0]?.delta.content;
if (typeof content === 'string') {
process.stdout.write(content);
}
}
console.log();
}
Two details deserve your attention. The content lives in chunk.data, not directly in chunk: this is the SDK’s event wrapper since version 1.0. And the typeof content === 'string' check filters out both fragments without content, which would litter your output with undefined, and list-of-fragments content, which process.stdout.write doesn’t accept: without it, strict mode refuses to compile.
Exposing the Stream via Express
A stream that never leaves the terminal helps no one. Here’s how to relay it to a browser using Server-Sent Events from an Express API. The question is validated before any call to avoid sending an empty request to the API. The three headers then open the channel, each fragment is sent back as JSON in a data: event, and a [DONE] marker signals the client to close the connection.
import express from 'express';
import { Mistral } from '@mistralai/mistralai';
const app = express();
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
app.get('/api/chat', async (req, res) => {
const question = typeof req.query.q === 'string' ? req.query.q : '';
if (!question) {
res.status(400).json({ error: 'Missing q parameter' });
return;
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = await client.chat.stream({
model: 'mistral-small-latest',
messages: [{ role: 'user', content: question }],
});
for await (const chunk of stream) {
const content = chunk.data.choices[0]?.delta.content;
if (typeof content === 'string' && content) {
res.write(`data: ${JSON.stringify({ text: content })}\n\n`);
}
}
res.write('data: [DONE]\n\n');
res.end();
});
app.listen(3000, () => console.log('Server on port 3000'));
Multi-Turn Conversations
History management follows the same principle as in Python: the model has no memory, so you maintain an array of messages and resend the whole thing each turn. In TypeScript, declaring an explicit Message type prevents slipping in a non-existent role or a malformed object—an error the API would otherwise return as a 400.
import { Mistral } from '@mistralai/mistralai';
type Message = {
role: 'system' | 'user' | 'assistant';
content: string;
};
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
const historique: Message[] = [
{
role: 'system',
content: 'You are an expert TypeScript assistant. Respond in English.',
},
];
async function poserQuestion(question: string): Promise<string> {
historique.push({ role: 'user', content: question });
const response = await client.chat.complete({
model: 'mistral-small-latest',
messages: historique,
});
const contenu = response.choices[0]?.message?.content;
const reponse = typeof contenu === 'string' ? contenu : '';
historique.push({ role: 'assistant', content: reponse });
return reponse;
}
async function main() {
console.log(await poserQuestion('What is a generic type?'));
console.log(await poserQuestion('Show me a concrete example.'));
console.log(await poserQuestion('How do I add constraints?'));
}
main();
The typeof contenu === 'string' check isn’t a luxury: the response’s content field can be a list of fragments, whereas your Message type expects a string. Then observe the third question: “How do I add constraints?” only makes sense because the two previous exchanges are still in the array. Push the exercise by removing the line that adds the assistant’s response to the history, and you’ll see the model lose the thread by the second turn.
Tuning Generation and Handling Errors
Advanced parameters are identical to the Python SDK’s—only the casing changes. A low temperature for a structured deliverable and a maxTokens calibrated to the expected length yield more predictable responses. Set temperature or topP, not both: this is the API documentation’s recommendation, which advises a temperature between 0 and 0.7.
const response = await client.chat.complete({
model: 'mistral-large-latest',
messages: [
{ role: 'user', content: 'Propose an architecture for a B2B SaaS.' },
],
temperature: 0.5,
maxTokens: 1000,
});
Finally, any network call can fail: invalid key, exceeded quota, outage. Always wrap the call in a try/catch and check the error type before reading its properties, otherwise TypeScript will refuse to compile. All HTTP errors returned by the API inherit from MistralError, which exposes statusCode and body; network errors (ConnectionError, RequestTimeoutError, etc.) don’t inherit from it but remain Error instances.
import * as errors from '@mistralai/mistralai/models/errors';
try {
const response = await client.chat.complete({
model: 'mistral-small-latest',
messages: [{ role: 'user', content: 'Hello!' }],
});
console.log(response.choices[0]?.message?.content);
} catch (error) {
if (error instanceof errors.MistralError) {
// API error response: 401 key refused, 429 limit reached...
console.error(`Mistral API Error ${error.statusCode}: ${error.message}`);
} else if (error instanceof Error) {
// No API response: network down, timeout exceeded...
console.error(`Error: ${error.message}`);
}
}
Key Takeaways
client.chat.complete()is asynchronous—useawait- Streaming uses
for await...ofon the result ofclient.chat.stream() - TypeScript typing catches parameter errors at compile time
- SSE integration with Express is straightforward for real-time web interfaces
- Conversation history is managed with a
Message[]typed array contentis either a string or a list of fragments: testtypeof content === 'string'before using it as text- SDK HTTP errors inherit from
MistralError(statusCode,body)