Skip to content

Commit 4b1ad4d

Browse files
authored
feat: add typename and interface fields (typed-graphql-builder#48)
This PR adds several options that make working with unions/interfaces easier - Add `--includeTypename` argument to include `__typename` to all output types. This should make it easier to write queries for discriminated unions and interfaces. Note that the typename must be included in every object separately, e.g. ```typescript let addToCartMutation = mutation(m => [m.addToCart({itemId: $('id')}, res => [ res.message, res.$on('Success', s => [s.__typename]), res.$on('MissingItemError', s => [s.__typename]), res.$on('InsufficientQuantityError', s => [s.__typename, s.remaining]), ])]) function MyComponent() { let addToCart = useMutation(addToCartMutation) let handler = async (id) => { let res = (await addToCart({id})).addToCart; switch (res.__typename) { case 'Success': return `handle success ${res.message}`; case 'MissingItemError': return `handle missing item: ${res.message}` case 'InsufficientQuantityError': return `handle insufficient quantity of ${res.remaining}`; } } return <ui>...</ui> } ``` - Common fields in interfaces are now available at the toplevel interface type, so they don't have to be included in `$on` calls. See `message` in the above example. Note that this exlcudes the new `__typename` addition for now - while it can be added, it will unfortunately not be useful for discriminating between the types if added at the toplevel interface type. - Add `QueryOutputType` and `QueryInputType` to make it easier to declare the types of a query ```typescript import {query, QueryOutputType} from './my.api.ts'; let userQuery = query(q => [q.user({id: value}, m => [m.id, m.name])]) let MyPartialUser = QueryOutputType<typeof userQuery> // {id: string, name: string} ```
1 parent 865f250 commit 4b1ad4d

File tree

5 files changed

+83
-13
lines changed

5 files changed

+83
-13
lines changed

src/cli.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ async function main() {
3333
describe:
3434
'List of scalars in the format ScalarName=[./path/to/scalardefinition#ScalarExport]',
3535
},
36+
includeTypename: {
37+
type: 'boolean',
38+
boolean: true,
39+
default: false,
40+
describe: 'Include the __typename field in all objects',
41+
},
3642
}).argv
3743

3844
try {

src/compile.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export type Args = {
2626
* A list of scalars and paths to their type definitions
2727
*/
2828
scalar?: string[]
29+
30+
/**
31+
* Should we include __typename in the fields?
32+
*/
33+
includeTypename?: boolean
2934
}
3035

3136
/**
@@ -35,7 +40,10 @@ export async function compile(args: Args) {
3540
const schemaData = await fetchOrRead(args)
3641

3742
const scalars = args.scalar?.map(s => s.split('=') as [string, string])
38-
const outputScript = compileSchemas(schemaData, { scalars })
43+
const outputScript = compileSchemas(schemaData, {
44+
scalars,
45+
includeTypename: args.includeTypename,
46+
})
3947

4048
if (args.output === '') {
4149
console.log(outputScript)
@@ -96,6 +104,7 @@ type FieldOf<T extends SupportedExtensibleNodes> = T extends
96104

97105
export type Options = {
98106
scalars?: [string, string][]
107+
includeTypename?: boolean
99108
}
100109

101110
/**
@@ -152,6 +161,16 @@ export function compileSchemaDefinitions(
152161
f1.name.value < f2.name.value ? -1 : f1.name.value > f2.name.value ? 1 : 0
153162
)
154163

164+
if (options.includeTypename && sd.kind != gq.Kind.INTERFACE_TYPE_DEFINITION) {
165+
fieldList.push({
166+
kind: gq.Kind.FIELD_DEFINITION,
167+
name: { kind: gq.Kind.NAME, value: '__typename' },
168+
type: { kind: gq.Kind.NAMED_TYPE, name: { value: 'String', kind: gq.Kind.NAME } },
169+
description: { kind: gq.Kind.STRING, value: '' },
170+
directives: [],
171+
} as any)
172+
}
173+
155174
// Override duplicate fields
156175
return fieldList.filter((f, ix) => fieldList[ix + 1]?.name.value !== f.name.value)
157176
}
@@ -281,7 +300,7 @@ export class ${className} extends $Base<"${className}"> {
281300
}
282301
283302
${getExtendedFields(def)
284-
.map(f => printField(f, className))
303+
.map(f => printField(f, `"${className}"`))
285304
.join('\n')}
286305
}`
287306
}
@@ -321,7 +340,7 @@ export class ${className} extends $Base<"${className}"> {
321340
throw new Error('Attempting to generate function field definition for non-function field')
322341
}
323342
}
324-
function printField(field: gq.FieldDefinitionNode, _parentName: string) {
343+
function printField(field: gq.FieldDefinitionNode, typename: string) {
325344
const fieldTypeName = printTypeBase(field.type)
326345

327346
let hasArgs = !!field.arguments?.length,
@@ -365,9 +384,10 @@ export class ${className} extends $Base<"${className}"> {
365384
}
366385
`
367386
} else {
387+
let fieldType = field.name.value === '__typename' ? typename : printType(field.type)
368388
return `
369389
${printDocumentation(field.description)}
370-
get ${field.name.value}(): $Field<"${field.name.value}", ${printType(field.type)}> {
390+
get ${field.name.value}(): $Field<"${field.name.value}", ${fieldType}> {
371391
return this.$_select("${field.name.value}") as any
372392
}`
373393
}
@@ -377,13 +397,18 @@ export class ${className} extends $Base<"${className}"> {
377397
const className = def.name.value
378398

379399
const additionalTypes = reverseInheritanceMap.get(className) ?? []
400+
const typenameList = additionalTypes.map(t => `"${t}"`).join(' | ')
401+
380402
const InterfaceObject = `{${additionalTypes.map(t => `${t}: ${t}`)}}`
381403
return `
382404
${printDocumentation(def.description)}
383405
export class ${def.name.value} extends $Interface<${InterfaceObject}, "${def.name.value}"> {
384406
constructor() {
385407
super(${InterfaceObject}, "${def.name.value}")
386408
}
409+
${getExtendedFields(def)
410+
.map(f => printField(f, typenameList))
411+
.join('\n')}
387412
}`
388413
}
389414

@@ -436,7 +461,7 @@ export type ${def.name.value} = ${scalarMap.get(typeName) ?? 'unknown'}
436461
${printDocumentation(def.description)}
437462
export class ${def.name.value} extends $Union<${UnionObject}, "${def.name.value}"> {
438463
constructor() {
439-
super(${UnionObject})
464+
super(${UnionObject}, "${def.name.value}")
440465
}
441466
}`
442467
}

src/preamble.src.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,16 @@ class $Base<Name extends string> {
9393
}
9494

9595
// @ts-ignore
96-
class $Union<T, Name extends String> {
96+
class $Union<T, Name extends String> extends $Base<Name> {
9797
// @ts-ignore
98-
private type!: T
98+
private $$type!: T
9999
// @ts-ignore
100-
private name!: Name
100+
private $$name!: Name
101+
102+
constructor(private selectorClasses: { [K in keyof T]: { new (): T[K] } }, $$name: Name) {
103+
super($$name)
104+
}
101105

102-
constructor(private selectorClasses: { [K in keyof T]: { new (): T[K] } }) {}
103106
$on<Type extends keyof T, Sel extends Selection<T[Type]>>(
104107
alternative: Type,
105108
selectorFn: (selector: T[Type]) => [...Sel]
@@ -113,9 +116,9 @@ class $Union<T, Name extends String> {
113116
// @ts-ignore
114117
class $Interface<T, Name extends string> extends $Base<Name> {
115118
// @ts-ignore
116-
private type!: T
119+
private $$type!: T
117120
// @ts-ignore
118-
private name!: Name
121+
private $$name!: Name
119122

120123
constructor(private selectorClasses: { [K in keyof T]: { new (): T[K] } }, $$name: Name) {
121124
super($$name)
@@ -327,6 +330,19 @@ export type OutputTypeOf<T> = T extends $Base<any>
327330
? OutputTypeOf<Inner>
328331
: never
329332

333+
export type QueryOutputType<T extends TypedDocumentNode<any>> = T extends TypedDocumentNode<
334+
infer Out
335+
>
336+
? Out
337+
: never
338+
339+
export type QueryInputType<T extends TypedDocumentNode<any>> = T extends TypedDocumentNode<
340+
any,
341+
infer In
342+
>
343+
? In
344+
: never
345+
330346
export function fragment<T, Sel extends Selection<T>>(
331347
GQLType: { new (): T },
332348
selectFn: (selector: T) => [...Sel]

test/examples/test-sw.good.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { query, $ } from './sw.graphql.api'
1+
import { query, $, QueryOutputType } from './sw.graphql.api'
22
import { verify } from './verify'
33

44
let planetQuery = query(q => [
@@ -39,6 +39,28 @@ const nodeQueryString = `query {
3939
}
4040
}`
4141

42+
const multiChoiceQuery = query(q => [
43+
q.node({ id: 'x' }, n => [
44+
n.id,
45+
n.$on('Person', p => [p.birthYear, p.__typename]),
46+
n.$on('Planet', p => [p.climates, p.__typename]),
47+
]),
48+
])
49+
50+
type PersonOrPlanet = QueryOutputType<typeof multiChoiceQuery>
51+
52+
declare let v: PersonOrPlanet
53+
54+
export function test() {
55+
let personOrPlanet = v.node!
56+
switch (personOrPlanet.__typename) {
57+
case 'Person':
58+
return personOrPlanet.birthYear
59+
case 'Planet':
60+
return personOrPlanet.climates?.toString()
61+
}
62+
}
63+
4264
export default [
4365
verify({
4466
query: planetQuery,

test/index.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ for (let schema of glob.sync(`./examples/*.graphql`, { cwd: __dirname })) {
6969
await compile({
7070
schema: path.join(__dirname, schema),
7171
output: path.join(__dirname, 'examples', `${schemaName}.api.ts`),
72+
includeTypename: true,
7273
})
7374
})
7475

@@ -106,7 +107,7 @@ for (let schema of glob.sync(`./examples/*.graphql`, { cwd: __dirname })) {
106107
t.test(`compile fails with example ${exampleName}`, async t => {
107108
let res = compileTs(example)
108109
if (res.status) {
109-
t.pass('failed to compile ' + res.stdout)
110+
t.pass('failed to compile ' + res.stdout.toString().substring(0, 255))
110111
} else {
111112
t.fail('bad example compiled with no errors')
112113
}

0 commit comments

Comments
 (0)