GraphQL接口推荐的用法
探索示例,避免常见陷阱
GraphQL接口允许模式字段返回多个对象类型之一,所有这些类型都必须实现该接口。为了实现接口,对象类型必须定义接口中包含的所有字段(否则,模式无效):
interface Media {title: String!}type Book implements Media {title: String!author: String!}
因为接口可以在实现 对象类型上强制执行此要求,所以纯粹为了强制字段定义来创建接口是很有诱惑力的。下面的方案要求所有的对象类型都通过声明这些类型实现 Nameable
接口来声明 name
字段:name
字段在其所有对象类型中通过声明这些类型实现 Nameable
接口来声明:
❌
interface Nameable {name: String}type Cat implements Nameable {name: StringmeowVolume: Int}type Dog implements Nameable {name: StringbarkVolume: Int}type Owner implements Nameable {name: Stringcats: [Cat]dogs: [Dog]}type Query {owners: [Owner]}
⚠️ 注意
这不是接口推荐的使用方式!注意,在这个方案中,Nameable
永远没有被用作字段的返回类型。这意味着客户端没有方法利用这种多态关系执行操作。
有效客户端用例的缺乏暗示了一种代码问题。Nameable
接口是不必要的,而且它可能会在你的方案在未来更改时引起问题。子图 分配它自己的挑战,所以去除不必要的接口通常会使流程变得更简单。
在下一个示例中,我们指定Cat
和 Dog
类型实现 Pet
接口,以便我们可以在 Owner.pets
字段中返回两种类型的 Fat 和 Dog 的聚合列表。现在存在一个有效的客户端用例进行多态,使用接口是合理的。
✅
interface Pet {name: String}type Cat implements Pet {name: StringmeowVolume: Int}type Dog implements Pet {name: StringbarkVolume: Int}type Owner {name: Stringpets: [Pet]}type Query {owners: [Owner]}
添加新实现类型的影响
当客户端在 操作 中包含返回抽象类型的字段时,他们通常会使用 "条件 片段," 列举他们感兴趣的来自具体类型的字段,如下所示:
query OwnersAndPets {owners {namepets {name... on Cat {meowVolume}... on Dog {barkVolume}}}}
ⓘ 注意
如果Pet
是一个联合体而不是接口(union Pet = Cat | Dog
),则所有请求的字段都需要包含在条件片段中。即使在两种类型都定义了该字段的情况下,也无法在name
字段外包含它。
如果您的API为抽象类型添加了额外的可能类型,则客户端必须更改其操作以从这些新类型中选择数据。虽然这不是一个破坏性的更改,但这种情况下需要一些防御性编程。
// Trigger a TypeScript error if a new type is introduced but// we haven't added a switch case for it.const assertUnknown = (x: never) =>console.log(`Unknown Pet type: ${(x as any).__typename}`);switch (pet.__typename) {case 'Cat':console.log(`Cat meows ${pet.meowVolume}`);break;case 'Dog':console.log(`Dog barks ${pet.barkVolume}`);break;default:assertUnknown(pet);console.log('Fallback behavior');}
ⓘ 注意
当使用 graphql-code-generator
时,访问已知的接口字段,如Pet.name
将触发TypeScript错误。请参阅问题8538以获取更多信息。
// `when` must be used as an expression to require exhaustivityval result = when (pet.__typename) {"Cat" -> println("Cat meows ${pet.onCat?.meowVolume}")"Dog" -> println("Dog barks ${pet.onDog?.barkVolume}")else -> println("Unknown animal ${pet.__typename}")}
switch pet.__typename {case "Cat":print("Cat meows \(pet.asCat?.meowVolume ?? 0)")case "Dog":print("Dog barks \(pet.asDog?.barkVolume ?? 0)")default:print("Unknown animal \(pet.__typename)")}