Skip to content

Instantly share code, notes, and snippets.

@BLamy
Last active May 15, 2023 20:41
Show Gist options
  • Save BLamy/e9f953866e3a9f85d5024294642fa333 to your computer and use it in GitHub Desktop.
Save BLamy/e9f953866e3a9f85d5024294642fa333 to your computer and use it in GitHub Desktop.
Typesafe prompt builder no dependencies
class Prompt<
TPromptTemplate extends string,
SuppliedInputArgs extends ExtractArgs<TPromptTemplate, {}>
> {
constructor(
public template: TPromptTemplate,
public args: SuppliedInputArgs
) {}
get text() {
return Object.keys(this.args).reduce((acc, x) => {
return acc.replace(`{{${x}}}`, this.args[x as keyof typeof this.args]);
}, this.template) as ReplaceArgs<TPromptTemplate, SuppliedInputArgs>;
}
}
class PromptBuilder<
TPromptTemplate extends string,
TExpectedInput extends ExtractArgs<TPromptTemplate, {}>
> {
constructor(protected template: TPromptTemplate) {}
addInputValidation<
TSTypeValidator extends ExtractArgs<TPromptTemplate, TSTypeValidator>
>(): PromptBuilder<TPromptTemplate, TSTypeValidator> {
return new PromptBuilder(this.template) as any;
}
build<const SuppliedInputArgs extends TExpectedInput>(
args: SuppliedInputArgs
) {
return new Prompt<TPromptTemplate, SuppliedInputArgs>(this.template, args)
.text;
}
}
// Basic Example Usage (not type safe; will take arguments of any value)
const basicPromptBuilder = new PromptBuilder(
"Tell {{me}} {{num}} {{jokeType}} joke"
);
const basicPrompt = basicPromptBuilder.build({
// ^?
jokeType: "funny",
me: "Brett",
num: 1,
});
// Input Validation Example (Will throw type error if you call build without the right arguments)
const validatedPromptBuilder = basicPromptBuilder.addInputValidation<{
jokeType: "funny" | "silly";
me: "Brett" | "Liana";
num: number;
}>();
const validatedPrompt = validatedPromptBuilder.build({
// ^?
jokeType: "funny",
me: "Brett",
num: 1,
});
const invalidPrompt = validatedPromptBuilder.build({
// @ts-expect-error Type '"error"' is not assignable to type '"funny" | "silly"'.
jokeType: "error",
// @ts-expect-error Type '"error"' is not assignable to type '"Brett" | "Liana"'.
me: "error",
// @ts-expect-error Type 'string' is not assignable to type 'number'.
num: "error",
});
// TS-Helpers
type ReplaceArgs<
TPromptTemplate extends string,
TArgs extends Record<string, any>
> = TPromptTemplate extends `${infer TStart}{{${infer TDataType}}}${infer TRest}`
? TRest extends `${string}{{${string}}}` | `${string}{{${string}}}${string}`
? `${TStart}${TArgs[TDataType]}${ReplaceArgs<TRest, TArgs>}`
: `${TStart}${TArgs[TDataType]}${TRest}`
: "";
type ExtractArgsAsTuple<TPromptTemplate extends string> =
TPromptTemplate extends `${string}{{${infer TDataType}}}${infer TRest}`
? TRest extends `${string}{{${string}}}` | `${string}{{${string}}}${string}`
? [TDataType, ...ExtractArgsAsTuple<TRest>]
: [TDataType]
: [];
type ExtractArgs<
TPromptTemplate extends string,
TSTypeValidator = ExtractArgs<TPromptTemplate, {}>
> = {
[K in ExtractArgsAsTuple<TPromptTemplate>[number] as K]: K extends keyof TSTypeValidator
? TSTypeValidator[K]
: any;
};
type ExtractArgsAsUnion<
TPromptTemplate extends string,
TSTypeValidator = ExtractArgs<TPromptTemplate, {}>
> = keyof ExtractArgs<TPromptTemplate, TSTypeValidator>;
// TS-test
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
? 1
: 2
? true
: false;
type testReplaceArgs = [
Expect<
Equal<
ReplaceArgs<
"Tell {{person}} a {{jokeType}} joke",
{ jokeType: "funny"; person: "Brett" }
>,
"Tell Brett a funny joke"
>
>,
Expect<
Equal<
ReplaceArgs<"Tell me a {{jokeType}} joke", { jokeType: "funny" }>,
"Tell me a funny joke"
>
>,
Expect<
Equal<
ReplaceArgs<
"Tell me a {{jokeType}} {{joke}}",
{ jokeType: "funny"; joke: "poem" }
>,
"Tell me a funny poem"
>
>
];
type testExtractArgsAsTuple = [
Expect<
Equal<
ExtractArgsAsTuple<"Tell {{person}} a {{jokeType}} joke">,
["person", "jokeType"]
>
>,
Expect<
Equal<ExtractArgsAsTuple<"Tell me a {{jokeType}} joke">, ["jokeType"]>
>,
Expect<
Equal<
ExtractArgsAsTuple<"Tell me a {{jokeType}} {{joke}}">,
["jokeType", "joke"]
>
>
];
type fadsfsa = ExtractArgs<
"Tell {{person}} a {{jokeType}} joke",
{ jokeType: number }
>;
// ^?
type testExtractArgs = [
Expect<
Equal<
ExtractArgs<
"Tell {{person}} a {{jokeType}} joke",
{
person: string;
jokeType: string;
}
>,
{ jokeType: string; person: string }
>
>,
Expect<
Equal<
ExtractArgs<"Tell me a {{jokeType}} joke", { jokeType: "funny" | "dad" }>,
{ jokeType: "funny" | "dad" }
>
>,
Expect<
Equal<
ExtractArgs<
"Tell me a {{jokeType}} {{num}} {{joke}}",
{ jokeType: string; num: number; joke: string }
>,
{ jokeType: string; num: number; joke: string }
>
>
];
type testExtractArgsAsUnion = [
Expect<
Equal<
ExtractArgsAsUnion<"Tell {{person}} a {{jokeType}} joke">,
"jokeType" | "person"
>
>,
Expect<Equal<ExtractArgsAsUnion<"Tell me a {{jokeType}} joke">, "jokeType">>,
Expect<
Equal<
ExtractArgsAsUnion<"Tell me a {{jokeType}} {{joke}}">,
"jokeType" | "joke"
>
>
];
@BLamy
Copy link
Author

BLamy commented May 15, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment