Languages and runtimes
A typed contact form in TypeScript
There is no SDK to install, which means there are no types shipped either, so here they are. Roughly thirty lines gives you a typed submit helper with a discriminated union response, so success, validation failure and network errors are all handled at compile time rather than discovered in production.
Setup
Copy your endpoint
Create a form in the dashboard.
Drop in the helper
One file, no dependencies. Use it from React, Vue, Svelte or plain TypeScript.
TypeScript examples
Swap in your own endpoint and these run as-is.
A typed client. Copy it into your project: there is nothing to install.
const ENDPOINT = "https://send.webforms.to/wft_your_key";
/** Directives are consumed by the API rather than stored as data. */
export type Directives = {
_subject?: string;
_replyto?: string;
/** Honoured on plans with custom sender names. */
_from_name?: string;
_next?: string;
};
export type SubmitSuccess = {
ok: true;
id: string;
message: string;
quarantined?: boolean;
tookMs: number;
};
export type SubmitFailure = {
ok: false;
error: string;
message: string;
issues?: Array<{ field: string; message: string }>;
};
export type SubmitResult = SubmitSuccess | SubmitFailure;
export async function submitForm(
values: Record<string, unknown> & Directives,
init?: { idempotencyKey?: string; signal?: AbortSignal },
): Promise<SubmitResult> {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
...(init?.idempotencyKey
? { "idempotency-key": init.idempotencyKey }
: {}),
},
body: JSON.stringify(values),
signal: init?.signal,
});
return (await res.json()) as SubmitResult;
}The discriminated union means TypeScript narrows the result for you.
import { submitForm } from "./lib/webforms";
const result = await submitForm({
name: "Ada Lovelace",
email: "ada@example.com",
message: "Hello",
_subject: "New enquiry",
});
if (result.ok) {
console.log(result.id); // string - narrowed to SubmitSuccess
} else {
for (const issue of result.issues ?? []) {
console.error(issue.field, issue.message);
}
}TypeScript form FAQ
The endpoint returns { ok: true, id } on success and { ok: false, error, message } on failure. Declaring that as a discriminated union on ok lets TypeScript narrow the error branch for you, so the error code is only reachable where it exists.
Yes. Define a type for your fields, build the FormData from a typed object, and the same type can validate the payload before it is sent. It keeps the form markup and the code that reads submissions from drifting apart.
No, and you do not need one. The API is a single POST, so a typed helper like the one above is smaller than any package you would install and never goes out of date.
Related guides
Where TypeScript submissions can go
The guide above gets the submission to your endpoint. From there it can land anywhere: one destination on Free, several on Pro.
Need the form markup too? The template gallery has complete forms for contact, waitlist, job application and more, and the form generator builds one field by field.
Ready to wire up your TypeScript form?
Free for 300 submissions a month. Endpoint in under a minute, no card required.
Start free