Apollo 客户端的 TypeScript
随着您的应用程序的增长,一个类型系统可以成为早期发现错误并改善您的整体开发者体验的必要工具。
GraphQL使用类型系统来明确定义每种类型及其字段在GraphQL 架构中可供使用的数据。鉴于GraphQL 服务器的架构是严格类型的,我们可以使用像工具来自动生成 TypeScript 定义。GraphQL 代码生成器。我们将使用生成的类型来确保我们 GraphQL 操作的输入和结果类型的完整性。
以下我们将指导您安装和配置 GraphQL 代码生成器以为您的钩子和组件生成类型。
设置您的项目
要开始使用GraphQL代码生成器,请首先安装以下包(使用 Yarn 或 NPM):
yarn add -D typescript graphql @graphql-codegen/cli @graphql-codegen/client-preset @graphql-typed-document-node/core
接下来,我们将为GraphQL代码生成器创建一个配置文件,命名为codegen.ts
,并将其放在项目根目录下:
import { CodegenConfig } from '@graphql-codegen/cli';const config: CodegenConfig = {schema: '<URL_OF_YOUR_GRAPHQL_API>',// this assumes that all your source files are in a top-level `src/` directory - you might need to adjust this to your file structuredocuments: ['src/**/*.{ts,tsx}'],generates: {'./src/__generated__/': {preset: 'client',plugins: [],presetConfig: {gqlTagName: 'gql',}}},ignoreNoDocuments: true,};export default config;
在您的 codegen.ts
文件中有多种指定模式的方式,因此请选择最适合您项目设置的方法。
最后,我们将以下脚本添加到我们的 package.json
文件中:
{"scripts": {"compile": "graphql-codegen","watch": "graphql-codegen -w",}}
运行上述任一脚本都会根据您在 codegen.ts
文件中提供的模式文件或 GraphQL API 生成类型。
$ yarn run compile✔ Parse Configuration✔ Generate outputs
类型钩子
GraphQL 代码生成器会自动创建一个 gql
函数(来自 src/__generated__/gql.ts
文件),这个函数使我们能够为 React 钩子中的变量以及这些钩子返回的结果进行类型检查。
useQuery
以下,我们使用 gql
函数来定义我们的 查询,这将自动生成用于我们的 useQuery
钩子的类型:
import React from 'react';import { useQuery } from '@apollo/client';import { gql } from '../src/__generated__/gql';const GET_ROCKET_INVENTORY = gql(/* GraphQL */ `query GetRocketInventory($year: Int!) {rocketInventory(year: $year) {idmodelyearstock}}`);export function RocketInventoryList() {// our query's result, data, is typed!const { loading, data } = useQuery(GET_ROCKET_INVENTORY,// variables are also typed!{ variables: { year: 2019 } });return (<div><h3>Available Inventory</h3>{loading ? (<p>Loading ...</p>) : (<table><thead><tr><th>Model</th><th>Stock</th></tr></thead><tbody>{data && data.rocketInventory.map(inventory => (<tr><td>{inventory.model}</td><td>{inventory.stock}</td></tr>))}</tbody></table>)}</div>);}
fetchMore
和 subscribeToMore
该 useQuery
钩子返回一个 QueryResult
的实例,它包含了 fetchMore
和 subscribeToMore
函数。详见 查询中的详细类型信息。因为这两个函数执行 GraphQL 操作,它们接受类型参数。
默认情况下,fetchMore
的类型参数与 useQuery
相同。由于 fetchMore
和 useQuery
都封装了一个查询 operation,所以你不需要向 fetchMore
传递任何类型 参数。
在扩展我们之前的例子时,注意到我们没有显式地指定 fetchMore
的类型,因为它默认使用与 useQuery
相同的类型参数:
// ...export function RocketInventoryList() {const { fetchMore, loading, data } = useQuery(GET_ROCKET_INVENTORY,// variables are typed!{ variables: { year: 2019 } });return (//...<buttononClick={() => {// variables are typed!fetchMore({ variables: { year: 2020 } });}}>Add 2020 Inventory</button>//...);}
subscribeToMore 的类型参数和默认值与 fetchMore
相同。请注意,subscribeToMore
执行的是 订阅,而 fetchMore
执行后续查询。
使用 subscribeToMore
,你通常至少传递一个已类型化的 参数,如下所示:
// ...const ROCKET_STOCK_SUBSCRIPTION = gql(/* GraphQL */ `subscription OnRocketStockUpdated {rocketStockAdded {idstock}}`);export function RocketInventoryList() {const { subscribeToMore, loading, data } = useQuery(GET_ROCKET_INVENTORY,{ variables: { year: 2019 } });React.useEffect(() => {subscribeToMore(// variables are typed!{ document: ROCKET_STOCK_SUBSCRIPTION, variables: { year: 2019 } });}, [subscribeToMore])// ...}
useMutation
我们可以像类型化 useQuery
钩子一样类型化 useMutation
钩子。使用生成的 gql
函数来定义我们的 GraphQL mutations,我们确保我们为我们的突变变量和返回数据进行了类型化:
import React, { useState } from 'react';import { useMutation } from '@apollo/client';import { gql } from '../src/__generated__/gql';const SAVE_ROCKET = gql(/* GraphQL */ `mutation saveRocket($rocket: RocketInput!) {saveRocket(rocket: $rocket) {model}}`);export function NewRocketForm() {const [model, setModel] = useState('');const [year, setYear] = useState(0);const [stock, setStock] = useState(0);// our mutation's result, data, is typed!const [saveRocket, { error, data }] = useMutation(SAVE_ROCKET, {// variables are also typed!variables: { rocket: { model, year: +year, stock: +stock } }});return (<div><h3>Add a Rocket</h3>{error ? <p>Oh no! {error.message}</p> : null}{data && data.saveRocket ? <p>Saved!</p> : null}<form><p><label>Model</label><inputname="model"onChange={e => setModel(e.target.value)}/></p><p><label>Year</label><inputtype="number"name="year"onChange={e => setYear(+e.target.value)}/></p><p><label>Stock</label><inputtype="number"name="stock"onChange={e => setStock(e.target.value)}/></p><button onClick={() => model && year && stock && saveRocket()}>Add</button></form></div>);}
useSubscription
我们可以像类型化我们的 useQuery
和 useMutation
钩子一样类型化我们的 useSubscription
钩子。使用生成的 gql
函数来定义我们的 GraphQL subscriptions,我们确保我们对订阅变量和返回数据进行类型化:
import React from 'react';import { useSubscription } from '@apollo/client';import { gql } from '../src/gql';const LATEST_NEWS = gql(/* GraphQL */ `subscription getLatestNews {latestNews {content}}`);export function LatestNews() {// our returned data is typed!const { loading, data } = useSubscription(LATEST_NEWS);return (<div><h5>Latest News</h5><p>{loading ? 'Loading...' : data!.latestNews.content}</p></div>);}
对渲染属性组件进行类型化
为了对渲染属性组件进行类型化,你首先将使用生成的 gql
函数(来自 src/__generated__/gql
)定义一个 GraphQL query。
这将为该
import { gql, AllPeopleQuery, AllPeopleQueryVariables } from '../src/__generated__/gql';const ALL_PEOPLE_QUERY = gql(/* GraphQL */ `query All_People {allPeople {people {idname}}}`;const AllPeopleComponent = <Query<AllPeopleQuery, AllPeopleQueryVariables> query={ALL_PEOPLE_QUERY}>{({ loading, error, data }) => { ... }}</Query>
我们的
这种方法也适用于
扩展组件
在
class SomeQuery extends Query<SomeData, SomeVariables> {}
现在,基于类的渲染属性组件已被转换为功能组件,您无法再以这种方式扩展组件。
虽然我们建议您尽快切换到使用新的useQuery
、useMutation
和useSubscription
钩子,但在此期间,您可以替换您的类,使用包装并类型化组件:
export const SomeQuery = () => (<Query<SomeData, SomeVariables> query={SOME_QUERY} /* ... */>{({ loading, error, data }) => { ... }}</Query>);
类型化高阶组件
要类型化高阶组件,首先使用gql
函数(从
我们的包装组件接收我们的
以下是一个使用
import React from "react";import { ChildDataProps, graphql } from "@apollo/react-hoc";import { gql, GetCharacterQuery, GetCharacterQueryVariables } from '../src/gql';const HERO_QUERY = gql(/* GraphQL */ `query GetCharacter($episode: Episode!) {hero(episode: $episode) {nameidfriends {nameidappearsIn}}}`);type ChildProps = ChildDataProps<{}, GetCharacterQuery, GetCharacterQueryVariables>;// Note that the first parameter here is an empty Object, which means we're// not checking incoming props for type safety in this example. The next// example (in the "Options" section) shows how the type safety of incoming// props can be ensured.const withCharacter = graphql<{}, GetCharacterQuery, GetCharacterQueryVariables, ChildProps>(HERO_QUERY, {options: () => ({variables: { episode: "JEDI" }})});export default withCharacter(({ data: { loading, hero, error } }) => {if (loading) return <div>Loading</div>;if (error) return <h1>ERROR</h1>;return ...// actual component with data;});
以下逻辑也适用于
选项
通常,我们的包装组件的属性通过传递
以下是一个为组件的 props 设置类型的示例:
import React from "react";import { ChildDataProps, graphql } from "@apollo/react-hoc";import { gql, GetCharacterQuery, GetCharacterQueryVariables } from '../src/gql';const HERO_QUERY = gql(/* GraphQL */ `query GetCharacter($episode: Episode!) {hero(episode: $episode) {nameidfriends {nameidappearsIn}}}`);type ChildProps = ChildDataProps<GetCharacterQueryVariables, GetCharacterQuery, GetCharacterQueryVariables>;const withCharacter = graphql<GetCharacterQueryVariables,GetCharacterQuery,GetCharacterQueryVariables,ChildProps>(HERO_QUERY, {options: ({ episode }) => ({variables: { episode }}),});export default withCharacter(({ data: { loading, hero, error } }) => {if (loading) return <div>Loading</div>;if (error) return <h1>ERROR</h1>;return ...// actual component with data;});
当通过 props 将深层次对象传递给我们的组件时,这尤其有用。例如,当添加 prop 类型时,使用 TypeScript 的项目开始出现无效 props 的错误。
import React from "react";import {ApolloClient,createHttpLink,InMemoryCache,ApolloProvider} from "@apollo/client";import Character from "./Character";export const link = createHttpLink({uri: "https://mpjk0plp9.lp.gql.zone/graphql"});export const client = new ApolloClient({cache: new InMemoryCache(),link,});export default () =><ApolloProvider client={client}>// $ExpectError property `episode`. Property not found in. See: src/Character.js:43<Character /></ApolloProvider>;
Props
使用props
函数,您可以手动将
import React from "react";import { graphql, ChildDataProps } from "@apollo/react-hoc";import { gql, GetCharacterQuery, GetCharacterQueryVariables } from '../src/gql';const HERO_QUERY = gql(/* GraphQL */ `query GetCharacter($episode: Episode!) {hero(episode: $episode) {nameidfriends {nameidappearsIn}}}`);type ChildProps = ChildDataProps<GetCharacterQueryVariables, GetCharacterQuery, GetCharacterQueryVariables>;const withCharacter = graphql<GetCharacterQueryVariables,GetCharacterQuery,GetCharacterQueryVariables,ChildProps>(HERO_QUERY, {options: ({ episode }) => ({variables: { episode }}),props: ({ data }) => ({ ...data })});export default withCharacter(({ loading, hero, error }) => {if (loading) return <div>Loading</div>;if (error) return <h1>ERROR</h1>;return ...// actual component with data;});
在上面的示例中,我们为我们的响应、
export const withCharacter = graphql<GetCharacterQueryVariables,GetCharacterQuery,GetCharacterQueryVariables,Props>(HERO_QUERY, {options: ({ episode }) => ({variables: { episode }}),props: ({ data, ownProps }) => ({...data,// $ExpectError [string] This type cannot be compared to numberepisode: ownProps.episode > 1,// $ExpectError property `isHero`. Property not found on object typeisHero: data && data.hero && data.hero.isHero})});
类和函数
如果你在使用 React 类(而不是使用 graphql
包装器),你仍然可以像这样为你的类键入传入的属性:
import { ChildProps } from "@apollo/react-hoc";const withCharacter = graphql<GetCharacterQueryVariables, GetCharacterQuery>(HERO_QUERY, {options: ({ episode }) => ({variables: { episode }})});class Character extends React.Component<ChildProps<GetCharacterQueryVariables, GetCharacterQuery>, {}> {render(){const { loading, hero, error } = this.props.data;if (loading) return <div>Loading</div>;if (error) return <h1>ERROR</h1>;return ...// actual component with data;}}export default withCharacter(Character);
使用 name
属性
如果你在 graphql
包装器的配置中使用 name
属性,你需要手动将响应类型附加到 props
函数中,如下所示:
import { NamedProps, QueryProps } from '@apollo/react-hoc';export const withCharacter = graphql<GetCharacterQueryVariables, GetCharacterQuery, {}, Prop>(HERO_QUERY, {name: 'character',props: ({ character, ownProps }: NamedProps<{ character: QueryProps & GetCharacterQuery }, Props) => ({...character,// $ExpectError [string] This type cannot be compared to numberepisode: ownProps.episode > 1,// $ExpectError property `isHero`. Property not found on object typeisHero: character && character.hero && character.hero.isHero})});
使用 TypedDocumentNode
在 TypeScript 中,所有接受 DocumentNode
的 API 可以选择接受 TypedDocumentNode<Data, Variables>
。这个类型具有相同的 JavaScript 表示形式,但可以使 API 推断数据和变量类型(而不是在调用时指定类型)。
这种技术使我们能够修改上述 useQuery
示例 以使用类型推断:
import React from 'react';import { useQuery, gql, TypedDocumentNode } from '@apollo/client';interface RocketInventoryData {rocketInventory: RocketInventory[];}interface RocketInventoryVars {year: number;}const GET_ROCKET_INVENTORY: TypedDocumentNode<RocketInventoryData, RocketInventoryVars> = gql`query GetRocketInventory($year: Int!) {rocketInventory(year: $year) {idmodelyearstock}}`;export function RocketInventoryList() {const { loading, data } = useQuery(GET_ROCKET_INVENTORY,{ variables: { year: 2019 } });return (<div><h3>Available Inventory</h3>{loading ? (<p>Loading ...</p>) : (<table><thead><tr><th>Model</th><th>Stock</th></tr></thead><tbody>{data && data.rocketInventory.map(inventory => (<tr><td>{inventory.model}</td><td>{inventory.stock}</td></tr>))}</tbody></table>)}</div>);}