Export Context$ and Scenario$ interfaces for strongly-typed loadContext() in _.context.ts files#1822
Conversation
pmcelhaney
left a comment
There was a problem hiding this comment.
Create a BaseContext type and have the generated Context class extend that.
|
@copilot try again |
|
@copilot try again |
Done in c259be8. Replaced |
|
@copilot BaseContext needs to be an interface, not a class. It will not have an implementation. It just needs to define the signature of the constructor. |
Done in 8579779. |
Done in d6a7. The generated file is now |
pmcelhaney
left a comment
There was a problem hiding this comment.
The generated types/_.context.ts file should look like this:
// This file is generated by Counterfact. Do not edit manually.
import type { Context } from "../routes/_.context";
interface LoadContextDefinitions {
/* code generator adds additional signatures here */
loadContext(path: "/" | `/${string}`): Context;
loadContext(path: string): Record<string, unknown>;
}
export interface ApplyContext {
/** Root context, same as loadContext("/") */
readonly context: Context;
readonly loadContext: LoadContextDefinitions["loadContext"];
/** Named route builders stored in the REPL execution context */
readonly routes: Record<string, unknown>;
/** Create a new route builder for a given path */
readonly route: (path: string) => unknown;
}
/** A scenario function that receives the live REPL environment */
export type Scenario = ($: ApplyContext) => Promise<void> | void;
/** Interface for Context objects defined in _.context.ts files */
export interface Context$ {
/** Load a context object for a specific path */
readonly loadContext: LoadContextDefinitions["loadContext"];
/** Load a JSON file relative to this file's path */
readonly readJson: (relativePath: string) => Promise<unknown>;
}The generated routes/_.context.ts file should be exactly this:
import type { Context$ } from "../types/_.context.js";
/**
* This is the default context for Counterfact.
*
* It defines the context object in the REPL
* and the $.context object in the code.
*
* Add properties and methods to suit your needs.
*
* See https://github.com/counterfact/api-simulator/blob/main/docs/features/state.md
*/
export class Context {
constructor($: Context$) {
void $;
}
}…and Context$ interface Agent-Logs-Url: https://github.com/counterfact/api-simulator/sessions/3f62a0ad-a953-41da-a8c8-81665e27ddee
Done in 82d2824. The generated |
pmcelhaney
left a comment
There was a problem hiding this comment.
Rename ApplyContext to Scenario$
Done in 036e92f. |
Summary
_.context.tsconstructors receive{ loadContext, readJson }from the module loader with no type information, makingloadContexteffectively untyped. This exports aContext$interface and aScenario$interface from the generatedtypes/_.context.tsso users can type their_.context.tsconstructor arguments and get the same strongly-typedloadContextoverloads — with no runtime overhead and no implementation to inherit.The generated
types/_.context.tsuses a sharedLoadContextDefinitionsinterface to aggregate allloadContextoverloads, which is referenced by bothScenario$andContext$. AllScenario$members arereadonly. The defaultroutes/_.context.tsscaffold is simplified to accept aContext$argument without implementing any interface or assigning fields.Original Prompt
_.context.tsconstructors receive{ loadContext, readJson }from the module loader with no type information, makingloadContexteffectively untyped. Export a type from the generated types file so users can annotate their constructors and get strongly-typedloadContextoverloads.Manual acceptance tests
types/_.context.tscontains aLoadContextDefinitionsinterface with the generatedloadContextoverloads,export interface Scenario$with allreadonlymembers andloadContext: LoadContextDefinitions["loadContext"], andexport interface Context$withreadonly loadContext: LoadContextDefinitions["loadContext"]andreadonly readJsonexport type Scenario = ($: Scenario$) => Promise<void> | voidreferencesScenario$routes/_.context.tsimportsContext$and definesexport class Contextwithconstructor($: Context$) { void $; }— noimplements, no field assignment_.context.tsthat usesContext$to type its constructor, TypeScript provides typed overloads forloadContext(e.g.loadContext("/pets")resolves toPetsContextwhenroutes/pets/_.context.tsexists)_.context.tsfiles that do not useContext$continue to work unchangedScenario,Scenario$,Context$, and existingloadContextoverloads intypes/_.context.tsare unaffected by changes to unrelated routesTasks
generate.ts— RestructuredbuildApplyContextContentto extract aLoadContextDefinitionsinterface containing allloadContextoverloads (with a/* code generator adds additional signatures here */comment). RenamedApplyContext→Scenario$; all its members arereadonlyandloadContextreferencesLoadContextDefinitions["loadContext"]. TheScenariotype alias now usesScenario$. Addedexport interface Context$withreadonly loadContext: LoadContextDefinitions["loadContext"]andreadonly readJson. Updated the JSDoc onwriteApplyContextTypeto referenceScenario$.repository.ts— Simplified the defaultroutes/_.context.tsscaffold to importContext$and defineexport class Contextwithconstructor($: Context$) { void $; }— noimplements, no field declarations, no field assignment. Updated the doc link to point to the GitHub docs page.generate.test.ts— Updated test assertions to verifyexport interface Context$,export interface Scenario$,LoadContextDefinitions["loadContext"],readonly context, andScenario = ($: Scenario$)appear in the generated output.context-registry.ts— Updated a code comment that referenced the oldscenario-contextname.docs/features/repl.md— Updated prose to referenceScenario$instead ofApplyContext, and import examples to referencetypes/_.context.ts/types/_.context.js..changeset/strongly-typed-load-context.md— Minor bump changeset updated to reference the new filename.