API reference
The full public surface of tprompt is one factory, one default export, and one returned interface (Compiled) with four methods. Everything else is types.
Public exports
| Export | From | What |
|---|---|---|
prompt | '@nkwib/tprompt' | The default Compile call, pre-applied to {'{{var}}'} |
prompt | '@nkwib/tprompt/single-brace' | Pre-applied to {'{var}'} |
makePromptTag | '@nkwib/tprompt' | Factory; returns a prompt for any { open, close } pair |
MissingPlaceholderError | '@nkwib/tprompt' | Thrown by .with({...}) when a placeholder key is absent (default onMissing: 'throw') |
Compiled, PartialApplied, Validated, ValidatedSafe, etc. | '@nkwib/tprompt' | Type-only exports — covered below |
prompt
const prompt: <const S extends string>(template: S) => Compiled<readonly [S], '{{', '}}'>; The prompt import from '@nkwib/tprompt' is makePromptTag({ open: '{{', close: '}}' }). The <const S extends string> is what makes the wedge work — it preserves the literal type of the template through the call so ExtractPlaceholders can run at the type level.
import { prompt } from '@nkwib/tprompt';
const t = prompt('Hi {{name}}');
// ^? Compiled<readonly ['Hi {{name}}'], '{{', '}}'> makePromptTag(options)
function makePromptTag<O extends string, C extends string>(
options: ParserOptions<O, C>
): <const S extends string>(template: S) => Compiled<readonly [S], O, C>;
interface ParserOptions<Open extends string, Close extends string> {
readonly open: Open;
readonly close: Close;
readonly onMissing?: 'throw' | 'insert-undefined';
} Returns a prompt-shaped function specialised to a delimiter pair. Used at module top-level for project-wide custom delimiters or per call site for prompts that contain literal {'{{ }}'} content.
onMissing controls what happens when .with({...}) is called with a placeholder key absent from the supplied object (typically because TypeScript was bypassed via as or any). It defaults to 'throw', which raises MissingPlaceholderError; pass 'insert-undefined' to opt back into the legacy behavior of rendering the literal string "undefined". See Missing keys throw below.
import { makePromptTag } from '@nkwib/tprompt';
const angle = makePromptTag({ open: '<<', close: '>>' });
angle('Hi <<name>>').with({ name: 'world' }); // "Hi world" Compiled<Strings, Open, Close>
The return type of every Compile call. Carries the original template segments and exposes four methods.
interface Compiled<Strings, Open, Close> {
readonly strings: Strings;
readonly open: Open;
readonly close: Close;
readonly placeholders: ReadonlyArray<ExtractPlaceholders<Strings, Open, Close>>;
with(vars: VariablesOf<...>): string;
partial<const Bound extends ...>(vars: { readonly [K in Bound]: string }): PartialApplied<...>;
validate<TOutput>(schema: SchemaLike<TOutput>): Validated<...>;
validateSafe<TOutput>(schema: SchemaLike<TOutput>): ValidatedSafe<...>;
} .with({...})
Renders the template with all placeholders bound. The variables-object type is inferred from the template — any missing or extra key is a tsc error.
const t = prompt('Hi {{name}}, on {{plan}}');
t.with({ name: 'Alice', plan: 'pro' });
// "Hi Alice, on pro"
t.with({ name: 'Alice' });
// ^^^^^^^^^^^^^^^ Property 'plan' is missing .partial({...})
Pre-binds a subset of placeholders. Returns a PartialApplied whose .with({...}) requires only the remainder. The return type does not carry .partial — chaining .partial(...).partial(...) is a tsc error by design.
const t = prompt('You are a {{role}} agent for {{userName}}.');
const support = t.partial({ role: 'support' });
support.with({ userName: 'alice' });
// "You are a support agent for alice."
support.partial({ userName: 'alice' });
// ^^^^^^^ Property 'partial' does not exist on type 'PartialApplied<...>' .validate(schema)
Wraps the compiled template with a runtime validator that throws on invalid input.
import { z } from 'zod';
const greet = prompt('Hi {{name}}').validate(
z.object({ name: z.string().min(2) })
);
greet.with({ name: 'a' }); // throws ZodError The schema must accept an object whose required keys cover the placeholder set. Anything with .parse(value) and .safeParse(value) qualifies — Zod, Valibot, ArkType, or your own.
Ordering footgun —
.validate()after.partial(). When you call.validate(schema)on aPartialApplied(i.e. you bound keys with.partial({...})first), the schema validates only the remaining keys — the values you pre-bound via.partialbypass the schema entirely. This is intentional: the partial already committed those values. If you want every key validated, call.validate(schema)first and.partial({...})second (validate-then-partial still checks the merged input).
.validateSafe(schema)
Same shape, but .with({...}) returns a ValidationResult instead of throwing.
type ValidationResult =
| { readonly ok: true; readonly value: string }
| { readonly ok: false; readonly error: unknown }; const safe = prompt('Hi {{name}}').validateSafe(
z.object({ name: z.string().min(2) })
);
const r = safe.with({ name: 'a' });
if (r.ok) console.log(r.value); else console.error(r.error); .validate and .validateSafe produce mutually exclusive return shapes — pick one per call site. Mixing them produces dead branches.
Missing keys throw
If .with({...}) is called with a placeholder name absent from the supplied object (typically because TypeScript was bypassed via as or any), tprompt throws MissingPlaceholderError rather than silently rendering the literal string "undefined" into a prompt sent to a model.
import { prompt, MissingPlaceholderError } from '@nkwib/tprompt';
const t = prompt('Hi {{name}}');
const cast = t.with as (v: Record<string, unknown>) => string;
cast({}); // throws MissingPlaceholderError: missing placeholder value(s): "name" If you depend on the legacy behavior, opt back in via the factory:
import { makePromptTag } from '@nkwib/tprompt';
const lenient = makePromptTag({
open: '{{',
close: '}}',
onMissing: 'insert-undefined'
}); Explicit undefined values still render through String(undefined) in both modes; only absent keys trigger the throw.
Type helpers
These are all export type — zero runtime cost.
ExtractPlaceholders<Strings, Open, Close>
Recursive template-literal type that extracts placeholder names from a tuple of template segments. Identifiers must match [A-Za-z_][A-Za-z0-9_]* — non-identifier matches are skipped (so {'{{ user.name }}'} is not a placeholder).
type T = ExtractPlaceholders<readonly ['Hi {{name}}, on {{plan}}'], '{{', '}}'>;
// ^? "name" | "plan" VariablesOf<P>
Maps a placeholder union into the required variables-object type.
type V = VariablesOf<'name' | 'plan'>;
// ^? { readonly name: string; readonly plan: string } SchemaLike<TOutput>
The structural validator interface — what .validate and .validateSafe accept.
interface SchemaLike<TOutput> {
parse(value: unknown): TOutput;
safeParse(value: unknown):
| { readonly success: true; readonly data: TOutput }
| { readonly success: false; readonly error: unknown };
} ValidationResult
type ValidationResult =
| { readonly ok: true; readonly value: string }
| { readonly ok: false; readonly error: unknown }; Other interfaces
PartialApplied<Strings, Open, Close, Bound>— return of.partial({...}). Same asCompiledminus.partial.Validated<Strings, Open, Close>— return of.validate(schema).ValidatedSafe<Strings, Open, Close>— return of.validateSafe(schema).ValidatedPartial<Strings, Open, Close, Bound>— partial-then-validate or validate-then-partial.ValidatedSafePartial<Strings, Open, Close, Bound>— same, safe variant.
All five share .strings, .open, .close, .placeholders, and .with — the differences are visible only in the type-level set of methods that remain valid.
Module entry points
// Default — {{var}} (LangChain / BAML / OpenAI / Anthropic convention)
import { prompt, makePromptTag } from '@nkwib/tprompt';
// Pre-applied — {var} (Python f-string-style)
import { prompt } from '@nkwib/tprompt/single-brace';
// Type-only
import type {
Compiled,
PartialApplied,
Validated,
ValidatedSafe,
ExtractPlaceholders,
VariablesOf,
SchemaLike,
ValidationResult
} from '@nkwib/tprompt'; The subpath namespace is reserved for behaviour variants (delimiter, escape policy, future flavours). ESM/CJS interop is handled by conditional exports — there is no tprompt/compat. See ADR-0002 and ADR-0003.