Last active
May 15, 2023 20:41
-
-
Save BLamy/e9f953866e3a9f85d5024294642fa333 to your computer and use it in GitHub Desktop.
Typesafe prompt builder no dependencies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
> | |
> | |
]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
TS Playground