Break user code sdk code cyclic dep demo 3#3871
Conversation
Lazy Types with Declaration MergingHow we fetch user typesWasp uses TypeScript module augmentation to bring user-defined types into the SDK. Empty registry interfaces are declared in // SDK: empty registry
export interface CrudOverridesRegistry {}
// Generated augmentation (from user's code):
declare module 'wasp/types' {
interface CrudOverridesRegistry {
'tasks': {
GetAll: typeof import('./tasks').getAllOverride
}
}
}Conditional types resolve the override or fall back to a default: type GetAllQueryResolved = FromCrudOverridesRegistry<'tasks', 'GetAll', DefaultGetAllQuery>
// → user's override if augmented, DefaultGetAllQuery otherwiseWhy types must stay lazyThe SDK is compiled with With explicit annotation — the emitted // Source
const _getAllQuery: QueryFor<GetAllQueryResolved> = createQuery<GetAllQueryResolved>(...)
// Emitted .d.ts
declare const _getAllQuery: QueryFor<GetAllQueryResolved>;The consumer resolves Without annotation — the emitter expands the type into its full structural form: // Source
const _getAllQuery = createQuery<GetAllQueryResolved>(...)
// Emitted .d.ts (conditionals already resolved to defaults)
declare const _getAllQuery: {
(args: {}): Promise<{ id: number; description: string; userId: number }[]>;
queryCacheKey: string[];
};The declaration emitter asks the type checker for the fully inferred type of the variable. Since the variable is unannotated, the checker evaluates the conditional using only the types visible during SDK compilation. The override is lost — the consumer sees only the default Prisma types. Note: source type-checking ( Two approaches to lazy typesGeneric function defaults (TanStack Router's approach): function useLoaderData<TRouter = RegisteredRouter>() { ... }Generics must remain abstract in emitted declarations, so the emitter preserves them rather than resolving them. Each call site resolves the default against its own augmentation context. This pattern works naturally for APIs that resolve types at function call sites, but not for stored values. Explicit type annotations (Wasp's approach): Wasp exposes value-based APIs — const _getQuery: QueryFor<GetQueryResolved> = createQuery<GetQueryResolved>(...)
export const env: ClientEnv = _env
export const dbClient: UserPrismaClient = setupFn()Even when the annotation appears redundant (e.g., Since all annotations live in generated template code, this cost is invisible to users. |
# Conflicts: # waspc/data/Generator/templates/sdk/wasp/client/env.ts # waspc/data/Generator/templates/sdk/wasp/client/env/schema.ts # waspc/data/Generator/templates/sdk/wasp/client/vite/plugins/validateEnv.ts # waspc/data/Generator/templates/sdk/wasp/server/env.ts # waspc/src/Wasp/Generator/Valid/TsConfig.hs # waspc/src/Wasp/Util/StrongPath.hs
Description
Questions:
cwdlocatiosn).server/src/plugins/?Type of change
Checklist
I tested my change in a Wasp app to verify that it works as intended.
🧪 Tests and apps:
examples/kitchen-sink/e2e-tests.waspc/data/Cli/templates, as needed.examples/, as needed.examples/tutorials) I updated the tutorial in the docs (and vice versa).📜 Documentation:
web/docs/.🆕 Changelog: (if change is more than just code/docs improvement)
waspc/ChangeLog.mdwith a user-friendly description of the change.web/docs/migration-guides/.versioninwaspc/waspc.cabalto reflect the changes I introduced.