Skip to content

Commit 6ee77c2

Browse files
fix: ensure variance of types matches how values are used
The issue with specifying them as separate properties is that you get the wrong variance. Consider the case of: ```ts declare function runQueryA(q: TypedDocumentNode<{output: string}, {input: string | null}>): void; // valid declare const optionalInputRequiredOutput: TypedDocumentNode<{output: string}, {input: string | null}>; runQueryA(optionalInputRequiredOutput); // invalid: query might return {output: null} but runQueryA expects to get {output: string} declare const optionalInputOptionalOutput: TypedDocumentNode<{output: string | null}, {input: string | null}>; runQueryA(optionalInputOptionalOutput); // invalid: runQueryA might pass {input: null} but query expects {input: string} declare const requiredInputRequiredOutput: TypedDocumentNode<{output: string}, {input: string}>; runQueryA(requiredInputRequiredOutput); // invalid: runQueryA might pass {input: null} but query expects {input: string} AND // query might return {output: null} but runQueryA expects to get {output: string} declare const requiredInputOptionalOutput: TypedDocumentNode<{output: string | null}, {input: string}>; runQueryA(requiredInputOptionalOutput); ``` Because for the purposes of type checking, TypeScript assumes you will only read from queries (all properties are treated as "Covariant", it will correctly catch the inaccuracies in the Result type, but not in the Variables type. The purpose of specifying it as a function, is that it tells TypeScript that the Variables will be used as input (they are "contravariant") and the results will be read from (they are "covariant"). To see this variance in action, consider the following example, using the same queries as above, but a different runQuery definition that is more tollerant in both the input and output parameters: ```ts declare function runQueryB(q: TypedDocumentNode<{output: string | null}, {input: string}>): void; // still valid: We still accept {output: string} as a valid result. // We're now passing in {input: string} which is still assignable to {input: string | null} runQueryB(optionalInputRequiredOutput); // valid: we now accept {output: null} as a valid Result runQueryB(optionalInputOptionalOutput); // valid: we now only pass {input: string} to the query runQueryB(requiredInputRequiredOutput); // valid: we now accept {output: null} as a valid Result AND // we now only pass {input: string} to the query runQueryA(requiredInputOptionalOutput); ```
1 parent 27e8d02 commit 6ee77c2

File tree

1 file changed

+6
-2
lines changed

1 file changed

+6
-2
lines changed

packages/core/src/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { DocumentNode } from 'graphql';
22

33
export interface TypedDocumentNode<Result = { [key: string]: any }, Variables = { [key: string]: any }> extends DocumentNode {
4-
__resultType?: Result;
5-
__variablesType?: Variables;
4+
/**
5+
* This type is used to ensure that the variables you pass in to the query are assignable to Variables
6+
* and that the Result is assignable to whatever you pass your result to. The method is never actually
7+
* implemented, but the type is valid because we list it as optional
8+
*/
9+
__apiType?: (variables: Variables) => Result;
610
}
711

812
/**

0 commit comments

Comments
 (0)