Type-Driven Prompting: Better Code Generation with LLMs

• By Javier López

Lately I find myself having so much fun with LLMs, they save me so much time, and I’ve also found they help me write better code.

This week I had a very simple task to introduce email sending into an app at work. This app is in TypeScript (Node.js), and I was happy with what the LLM produced.

Type-Driven Prompting

Type-Driven Prompting

Past me would have installed something like nodemailer and then configure SMTP transport using SendGrid.

But these days I’d rather avoid extra dependencies, and I’m confident I don’t even need to look up the SendGrid API it’s likely in the LLM’s training data.

Prompting with Types

So I prompted with this:

interface EmailService {
defaultFrom: string;
sendEmail(args: {
from?: string;
to: string;
subject: string;
body: string;
}): Promise<{ dispatched: true } | { dispatched: false; error: string }>;
}
// Help me write two implementations:
// 1. SendGrid API implementation using
// their HTTP API with fetch
// 2. NoOp implementation for dev/test
// this implementation should log the email
// being sent and also have a utility method
// to get the sent emails. To make testing easier.
// assert(emailService.getSentEmails()[0].from === '[email protected]')
declare class SendgridEmailService implements EmailService {}
declare class NoOpEmailService implements EmailService {}
// Factory function that returns the correct service
// based on environment
// If APP_ENV is not production return NoOp
// if production and SENDGRID_API_KEY return
// the sendgrid implementation.
let _emailService: EmailService;
export const getEmailService = (): EmailService => {};

What I got

I was very pleased with the result (see below), I like the characteristics of the code:

Conclusion: Even if this is a very simple and contrived example, this pattern really helps me write better code, and tame the LLM by providing a design upfront it can follow, and not start generating a lot of code I don’t need and don’t want to review!

There’s also something to be said about software engineering concepts being very useful in the world of LLMs, since now each of us are “Team Lead” of our own projects with a bunch of big brained bad decision makers (LLMs) at our disposal.

Results

And here is the result:

export class SendgridEmailService implements EmailService {
defaultFrom: string;
#apiKey: string;
#apiUrl = "https://api.sendgrid.com/v3/mail/send";
constructor(apiKey: string, defaultFrom: string) {
this.#apiKey = apiKey;
this.defaultFrom = defaultFrom;
}
async sendEmail(args: {
from?: string;
to: string;
subject: string;
body: string;
}): Promise<{ dispatched: true } | { dispatched: false; error: string }> {
try {
const response = await fetch(this.#apiUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${this.#apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
personalizations: [{ to: [{ email: args.to }] }],
from: { email: args.from || this.defaultFrom },
subject: args.subject,
content: [{ type: "text/plain", value: args.body }],
}),
});
if (response.ok) {
return { dispatched: true };
} else {
const errorData = await response.text();
return {
dispatched: false,
error: `SendGrid error: ${response.status} - ${errorData}`,
};
}
} catch (error) {
return {
dispatched: false,
error: `Network error: ${
error instanceof Error ? error.message : "Unknown error"
}`,
};
}
}
}
export class NoOpEmailService implements EmailService {
defaultFrom: string;
#sentEmails: Array<{
from: string;
to: string;
subject: string;
body: string;
timestamp: Date;
}> = [];
constructor(defaultFrom: string) {
this.defaultFrom = defaultFrom;
}
async sendEmail(args: {
from?: string;
to: string;
subject: string;
body: string;
}): Promise<{ dispatched: true } | { dispatched: false; error: string }> {
const email = {
from: args.from || this.defaultFrom,
to: args.to,
subject: args.subject,
body: args.body,
timestamp: new Date(),
};
this.#sentEmails.push(email);
console.log("=== NoOp Email Service ===");
console.log(`From: ${email.from}`);
console.log(`To: ${email.to}`);
console.log(`Subject: ${email.subject}`);
console.log(`Body: ${email.body}`);
console.log(`Timestamp: ${email.timestamp.toISOString()}`);
console.log("========================");
return { dispatched: true };
}
// Utility method to get sent emails
getSentEmails() {
return [...this.#sentEmails];
}
}
// Service factory
let _emailService: EmailService;
export const getEmailService = (): EmailService => {
if (!_emailService) {
const environment = process.env.NODE_ENV || "development";
const sendgridApiKey = process.env.SENDGRID_API_KEY;
const defaultFrom = process.env.EMAIL_DEFAULT_FROM || "[email protected]";
if (environment === "production" && sendgridApiKey) {
_emailService = new SendgridEmailService(sendgridApiKey, defaultFrom);
} else {
_emailService = new NoOpEmailService(defaultFrom);
}
}
return _emailService;
};