GraphQLの型、インターフェース、そしてポリモーフィズム
このセクションでは、異なる種類のノードをどのように処理するかを見ていきます。サンプルアプリのニュースフィード記事の中には、個人によって投稿されたものと、組織によって投稿されたものがあることに気付くかもしれません。この例では、個人に関する個人固有の情報と、組織に関する組織固有の情報を選択するフラグメントを作成することで、ホバーカードを強化します。
GraphQLのノードは単なるフィールドのランダムな集合体ではなく、型を持っていることを示唆してきました。GraphQLスキーマは、各型が持つフィールドを定義します。例えば、`Story`型を次のように定義するかもしれません。
type Story {
id: ID!
title: String
summary: String
createdAt: Date
poster: Actor
image: Image
}
ここでは、いくつかのフィールドはスカラー型(`String`や`ID`など)です。他のものは、スキーマの他の場所で定義された型(`Image`など)であり、これらのフィールドは特定の型のノードへのエッジです。`ID!`の`!`は、そのフィールドが非NULL可能であることを意味します。GraphQLでは、フィールドは通常NULL可能であり、非NULL可能性は例外です。
フラグメントは常に特定の型に対して「on」で定義されます。上記の例では、`StoryFragment`は`on Story`で定義されています。これは、`Story`ノードが期待されるクエリの場所にのみ展開できることを意味します。また、フラグメントは`Story`型に存在するフィールドのみを選択できることを意味します。
`poster`フィールドで使用されている`Actor`型は特に注目に値します。この型はインターフェースです。つまり、ストーリーの`poster`は、Person、Page、Organization、または「Actor」であるという仕様を満たすその他のエンティティ型である可能性があります。
サンプルアプリのGraphQLスキーマでは、Actorを次のように定義しています。
interface Actor {
name: String
profilePicture: Image
joined: DateTime
}
偶然の一致ではありませんが、これはまさにここで表示している情報です。スキーマには、Actorを実装する2つの型があります。つまり、Actorで定義されているすべてのフィールドを含み、そのように宣言しています。
type Person implements Actor {
id: ID!
name: String
profilePicture: Image
joined: DateTime
email: String
location: Location
}
type Organization implements Actor {
id: ID!
name: String
profilePicture: Image
joined: DateTime
organizationKind: OrganizationKind
}
これら2つの型はどちらも`name`、`profilePicture`、`joined`を持っているので、どちらもActorを実装すると宣言でき、したがって、スキーマおよびフラグメントでActorが要求される場所でどこでも使用できます。また、各特定の型に固有の他のフィールドも持っています。
`Person`の場所または`Organization`の組織の種類を表示するように`PosterDetailsHovercardContentsBody`コンポーネントを拡張する方法を、インターフェースの使用方法について見てみましょう。これらは、`Actor`インターフェースではなく、これらの特定の型にのみ存在するフィールドです。
現時点では、これまで説明に従ってきた場合、次のようにフラグメントが定義されているはずです(`PosterDetailsHovercardContents.tsx`内)。
fragment PosterDetailsHovercardContentsBodyFragment on Actor {
name
joined
profilePicture {
...ImageFragment
}
}
`organizationKind`のようなフィールドをこのフラグメントに追加しようとすると、Relayコンパイラからエラーが発生します。
✖︎ The type `Actor` has no field organizationKind
これは、インターフェース上でフラグメントを定義する場合、そのインターフェースのフィールドしか使用できないためです。インターフェースを実装する特定の型のフィールドを使用するには、型絞り込みを使用して、GraphQLにその型からフィールドを選択していることを伝えます。
fragment PosterDetailsHovercardContentsBodyFragment on Actor {
name
joined
profilePicture {
...ImageFragment
}
... on Organization {
organizationKind
}
}
これを追加してみましょう。`Person`の型絞り込みも追加できます。
fragment PosterDetailsHovercardContentsBodyFragment on Actor {
name
joined
profilePicture {
...ImageFragment
}
... on Organization {
organizationKind
}
... on Person {
location {
name
}
}
}
インターフェースを実装する型のうち一部の型にのみ存在するフィールドを選択し、扱っているノードが異なる型の場合、そのフィールドの値を読み取ると、単に`null`が返されます。これを踏まえて、`PosterDetailsHovercardContentsBody`コンポーネントを修正して、個人の場所と組織の種類を表示することができます。
import OrganizationKind from './OrganizationKind';
function PosterDetailsHovercardContentsBody({ poster }: Props): React.ReactElement {
const data = useFragment(PosterDetailsHovercardContentsBodyFragment, poster);
return (
<>
<Image image={data.profilePicture} width={128} height={128} className="posterHovercard__image" />
<div className="posterHovercard__name">{data.name}</div>
<ul className="posterHovercard__details">
<li>Joined <Timestamp time={poster.joined} /></li>
{data.location != null && (
<li>{data.location.name}</li>
)}
{data.organizationKind != null && (
<li><OrganizationKind kind={data.organizationKind} /></li>
)}
</ul>
</>
);
}
これで、個人の場所と、組織の種類が組織に対して表示されるはずです。
ちなみに、なぜ以前の例で`... on Actor`があったのかが理解できるようになりました。`node`フィールドは、実行時に任意のIDを指定できるため、任意の型のノードを返すことができます。そのため、それが提供する型は`Node`であり、`id`フィールドを持つものなら何でも実装できる非常に汎用的なインターフェースです。`Actor`インターフェースのフィールドを使用するには、型絞り込みが必要でした。
GraphQL仕様書やその他のソースでは、型絞り込みはインラインフラグメントと呼ばれています。この用語はより記述的で分かりにくいので、「型絞り込み」と呼んでいます。
型によって全く異なる処理を行う必要がある場合は、`__typename`というフィールドを選択できます。これは、取得した具体的な型の名前(例:「Person」または「Organization」)を含む文字列を返します。これはGraphQLの組み込み機能です。
サマリー
`... on Type {}`構文を使用すると、インターフェースを実装する特定の型にのみ存在するフィールドを選択できます。