diff --git a/packages/groq-builder/src/commands/conditional$.test.ts b/packages/groq-builder/src/commands/conditional$.test.ts index 7c6b07a..b866f3a 100644 --- a/packages/groq-builder/src/commands/conditional$.test.ts +++ b/packages/groq-builder/src/commands/conditional$.test.ts @@ -25,6 +25,7 @@ describe("conditional$", () => { msrp: true, }, }); + it("we should be able to extract the intersection of projection types", () => { expectType< Simplify> diff --git a/packages/groq-builder/src/commands/conditional$.ts b/packages/groq-builder/src/commands/conditional$.ts index a075ab1..08c8c67 100644 --- a/packages/groq-builder/src/commands/conditional$.ts +++ b/packages/groq-builder/src/commands/conditional$.ts @@ -5,7 +5,6 @@ import { ConditionalKey, ConditionalProjectionMap, ExtractConditionalProjectionResults, - SpreadableConditionals, } from "./conditional-types"; import { notNull } from "../types/utils"; import { ParserFunction } from "../types/public-types"; @@ -19,23 +18,28 @@ declare module "../groq-builder" { ResultItem, TRootConfig >, - TKey extends string = "[$]" + TKey extends string = "[$]", + TIsExhaustive extends boolean = false >( conditionalProjections: TConditionalProjections, - config?: ConditionalConfig + config?: Partial> ): ExtractConditionalProjectionResults< ResultItem, TConditionalProjections, - TKey + ConditionalConfig >; } } GroqBuilder.implement({ - conditional$( + conditional$< + TCP extends object, + TKey extends string, + TIsExhaustive extends boolean + >( this: GroqBuilder, conditionalProjections: TCP, - config?: ConditionalConfig + config?: Partial> ) { const root = this.root; const allConditionalProjections = Object.entries( @@ -58,20 +62,21 @@ GroqBuilder.implement({ .filter(notNull); const conditionalParser = !parsers.length ? null - : createConditionalParserUnion(parsers); + : createConditionalParserUnion(parsers, config?.isExhaustive ?? false); const conditionalQuery = root.chain(query, conditionalParser); - const uniqueKey: ConditionalKey = `[Conditional] ${ - config?.key ?? ("[$]" as TKey) - }`; - + const key = config?.key || ("[$]" as TKey); + const conditionalKey: ConditionalKey = `[Conditional] ${key}`; return { - [uniqueKey]: conditionalQuery, - } as unknown as SpreadableConditionals; + [conditionalKey]: conditionalQuery, + } as any; }, }); -function createConditionalParserUnion(parsers: ParserFunction[]) { +function createConditionalParserUnion( + parsers: ParserFunction[], + isExhaustive: boolean +) { return function parserUnion(input: unknown) { for (const parser of parsers) { try { @@ -82,6 +87,11 @@ function createConditionalParserUnion(parsers: ParserFunction[]) { // or if it errored due to not meeting the conditional check. } } + if (isExhaustive) { + throw new TypeError( + `The data did not match any of the ${parsers.length} conditional assertions` + ); + } return {}; }; } diff --git a/packages/groq-builder/src/commands/conditional-types.ts b/packages/groq-builder/src/commands/conditional-types.ts index 3225a4c..e5e7aac 100644 --- a/packages/groq-builder/src/commands/conditional-types.ts +++ b/packages/groq-builder/src/commands/conditional-types.ts @@ -30,10 +30,10 @@ export type ConditionalExpression = Tagged; export type ExtractConditionalProjectionResults< TResultItem, TConditionalProjectionMap extends ConditionalProjectionMap, - TKey extends string + TConfig extends ConditionalConfig > = SpreadableConditionals< - TKey, - | Empty + TConfig["key"], + | (TConfig["isExhaustive"] extends true ? never : Empty) | ValueOf<{ [P in keyof TConditionalProjectionMap]: ExtractProjectionResult< TResultItem, @@ -47,13 +47,12 @@ export type OmitConditionalProjections = { }; export type ExtractConditionalProjectionTypes = Simplify< - | Empty - | IntersectionOfValues<{ - [P in Extract< - keyof TProjectionMap, - ConditionalKey - >]: InferResultType>; - }> + IntersectionOfValues<{ + [P in Extract< + keyof TProjectionMap, + ConditionalKey + >]: InferResultType>; + }> >; export type ConditionalByTypeProjectionMap< @@ -72,10 +71,10 @@ export type ExtractConditionalByTypeProjectionResults< any, any >, - TKey extends string + TConfig extends ConditionalConfig > = SpreadableConditionals< - TKey, - | Empty + TConfig["key"], + | (TConfig["isExhaustive"] extends true ? never : Empty) | ValueOf<{ [_type in keyof TConditionalByTypeProjectionMap]: ExtractProjectionResult< Extract, @@ -99,4 +98,10 @@ export type SpreadableConditionals< [UniqueConditionalKey in ConditionalKey]: IGroqBuilder; }; -export type ConditionalConfig = { key: TKey }; +export type ConditionalConfig< + TKey extends string = string, + TIsExhaustive extends boolean = boolean +> = { + key: TKey; + isExhaustive: TIsExhaustive; +}; diff --git a/packages/groq-builder/src/commands/conditionalByType.test.ts b/packages/groq-builder/src/commands/conditionalByType.test.ts index 360c800..1ada883 100644 --- a/packages/groq-builder/src/commands/conditionalByType.test.ts +++ b/packages/groq-builder/src/commands/conditionalByType.test.ts @@ -103,6 +103,24 @@ describe("conditionalByType", () => { }); }); + it("types are correct when the conditions are exhaustive", () => { + const conditionsExhaustive = q.star + .filterByType("product", "variant") + .conditionalByType({ + product: { _type: true, name: true }, + variant: { _type: true, price: true }, + }); + + type ActualItem = ExtractConditionalProjectionTypes< + typeof conditionsExhaustive + >; + type ExpectedItem = + | { _type: "product"; name: string } + | { _type: "variant"; price: number }; + + expectType>().toStrictEqual(); + }); + it("should be able to extract the return types", () => { type ConditionalResults = ExtractConditionalProjectionTypes< typeof conditionalByType diff --git a/packages/groq-builder/src/commands/conditionalByType.ts b/packages/groq-builder/src/commands/conditionalByType.ts index e743ae8..1266418 100644 --- a/packages/groq-builder/src/commands/conditionalByType.ts +++ b/packages/groq-builder/src/commands/conditionalByType.ts @@ -5,10 +5,10 @@ import { ExtractConditionalByTypeProjectionResults, ConditionalByTypeProjectionMap, ConditionalKey, - SpreadableConditionals, ConditionalConfig, } from "./conditional-types"; import { ProjectionMap } from "./projection-types"; +import { IsEqual } from "../tests/expectType"; declare module "../groq-builder" { export interface GroqBuilder { @@ -17,14 +17,19 @@ declare module "../groq-builder" { ResultItem, TRootConfig >, - TKey extends string = "[ByType]" + TKey extends string = "[ByType]", + TIsExhaustive extends boolean = TConditionalProjections extends Required< + ConditionalByTypeProjectionMap, TRootConfig> + > + ? true + : false >( conditionalProjections: TConditionalProjections, - config?: ConditionalConfig + config?: Partial> ): ExtractConditionalByTypeProjectionResults< ResultItem, TConditionalProjections, - TKey + ConditionalConfig >; } } @@ -32,11 +37,12 @@ declare module "../groq-builder" { GroqBuilder.implement({ conditionalByType< TConditionalProjections extends object, - TKey extends string + TKey extends string, + TIsExhaustive extends boolean >( this: GroqBuilder, conditionalProjections: TConditionalProjections, - config?: ConditionalConfig + config?: Partial> ) { const typeNames = Object.keys(conditionalProjections); @@ -63,15 +69,20 @@ GroqBuilder.implement({ if (typeParser?.parser) { return typeParser.parser(input); } + if (!typeParser && config?.isExhaustive) { + throw new TypeError( + `Unexpected _type "${input._type}"; expected one of: ${typeNames}` + ); + } return {}; }; const conditionalQuery = this.root.chain(query, conditionalParser); - const uniqueKey: ConditionalKey = `[Conditional] ${ - config?.key ?? ("[ByType]" as TKey) - }`; + const key: TKey = config?.key || ("[ByType]" as TKey); + const conditionalKey: ConditionalKey = `[Conditional] ${key}`; return { - [uniqueKey]: conditionalQuery, - } as unknown as SpreadableConditionals; + _type: true, // Ensure we request the `_type` parameter + [conditionalKey]: conditionalQuery, + } as any; }, });