跳至内容

GraphQL Codegen:如何从 GraphQL 模式生成类型安全的代码

一篇动手实践的 GraphQL Code Generator 指南,涵盖搭建与配置、生成的 TypeScript 类型与带类型文档节点、与 Apollo、urql、graphql-request 的集成,以及通过监听模式与 CI 实现自动化。
已更新 2026年10月2日  · 12分钟 阅读

使用 AI 探索

ChatGPTClaudePerplexity

针对 GraphQL API 手写的 TypeScript 类型,一旦有人修改了模式,就会立刻过时。

设想一下,后端开发者把 Course 类型上的 title 字段重命名为 name。前端仍然可以编译通过,因为手写的 Course 接口并不知道发生了改变,结果只有在运行时才暴露出 bug。真实世界的应用通常有数十个查询、变更和片段,问题升级会非常快。

GraphQL Code Generator(简称 GraphQL Codegen)会读取您的模式和 GraphQL 操作,并将它们转换为类型、查询辅助函数、客户端集成以及其他原本需要手写的样板代码。它最常与 TypeScript 和 Apollo、urql 等主流客户端一起使用。每当模式发生变化,一个命令就能让您的类型重新保持同步。

本文将带您在一个小型 TypeScript 项目中从零开始设置 GraphQL Codegen,进行配置、使用生成的类型,并将其自动化到您的工作流程中。

GraphQL 与 REST 有何不同?阅读我们的《Graph QL vs REST:完整指南》了解差异与优势。

什么是 GraphQL Codegen?

GraphQL Codegen 是一个命令行工具,可将 GraphQL 定义转换为应用代码。

它是 The Guild 的一个开源项目,您将其作为开发依赖安装。它连接三样东西:

  • GraphQL 模式: 服务器与客户端之间的契约,包含 API 暴露的每个类型、字段和参数
  • GraphQL 操作: 您的应用发送给 API 的查询、变更、订阅和片段
  • 生成的代码: Codegen 基于模式和您的操作写出的类型和辅助工具

模式告诉 Codegen 能做什么,操作告诉它您的应用需要什么。

Codegen 将两者结合,因此每个生成的类型都与某个操作的响应结构完全匹配。它还会将每个操作与模式进行校验。前文提到将 title 重命名的例子,会令生成过程报错,例如 Cannot query field "title" on type "Course",您在代码运行前就能发现问题。

输出取决于插件和配置。Codegen 本身不直接写 TypeScript,插件才会。例如,typescript 插件会生成模式类型,而 typescript-operations 插件会为您的查询和变更生成类型。还有适用于其他语言的插件,如 Java 和 C#,但本文仅讨论 TypeScript。

需要记住的一点是,Codegen 并不会替代您的 GraphQL 服务器。

它不会解析查询或获取数据。它在开发期间读取 GraphQL 定义并写出文件。剩下的工作由您的服务器和 GraphQL 客户端完成。

GraphQL Codegen 的工作原理

每个 Codegen 的设置都遵循相同流程: 模式 + GraphQL 操作 -> Codegen 配置 -> 生成的代码。

Diagram of the GraphQL Codegen workflow

GraphQL Codegen 工作流示意图

大致思路如下:

  1. 提供模式: 将 Codegen 指向本地 .graphql 文件或正在运行的 GraphQL 端点,Codegen 会通过自省读取

  2. 指向您的操作: 使用 src/**/*.ts 这样的 glob 模式,使 Codegen 能在代码中找到查询、变更和片段

  3. 配置插件和输出目标: 告诉 Codegen 要写哪些文件,以及使用哪些插件来生成

  4. 运行 Codegen: 运行 npx graphql-codegen 以加载模式、校验操作并写出结果

  5. 使用生成的产物: 在应用代码中导入生成的类型和文档节点

第 1 到 3 步会放到一个配置文件中,通常是 codegen.ts。下面是课程目录项目的最小示例:

import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  // Step 1: Where the schema comes from
  schema: "schema.graphql",
  // Step 2: Where your operations are placed
  documents: ["src/**/*.ts"],
  // Step 3: What to generate and where to write it
  generates: {
    "./src/gql/": { preset: "client" },
  },
};

export default config;

client 预设是一个插件组合,可以一次性生成模式类型、操作类型和带类型的文档节点。稍后我会在搭建和配置部分分别介绍这些内容。

如何设置 GraphQL Codegen

理解 Codegen 的最佳方式是将其跑在一个真实项目上。

本节将从零开始构建一个小型课程目录项目。它包含课程和讲师的模式、一个查询,以及一个能将查询转换为 TypeScript 类型的 Codegen 配置。您只需要 Node.js 和 npm。

步骤 1:安装 GraphQL Code Generator

创建一个新文件夹并初始化 npm 项目:

mkdir course-catalog && cd course-catalog
npm init -y

Codegen 需要几个包。两个放进常规依赖,其余作为开发依赖,因为它们只在开发期间运行:

npm install graphql @graphql-typed-document-node/core
npm install -D typescript @graphql-codegen/cli @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node

每个包的作用如下:

  • graphql: JavaScript 的 GraphQL 参考实现,Codegen 用它解析模式和操作

  • @graphql-typed-document-node/core: 提供生成代码会导入的 TypedDocumentNode 类型

  • @graphql-codegen/cli: Codegen 命令行工具

  • @graphql-codegen/typescript-operations: 为查询、变更和片段生成类型的插件

  • @graphql-codegen/typed-document-node: 生成带类型的文档对象的插件

步骤 2:创建配置文件

Codegen 会从项目根目录的配置文件读取设置。先创建一个空配置的 codegen.ts:

import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {};

export default config;

您也可以运行 npx graphql-codegen init 来回答几个问题并获得一个生成的配置文件。这里手写是为了让您了解每个设置的作用。

步骤 3:指定模式与文档位置

Codegen 需要两个输入,我们来分别创建。

首先,将模式保存为项目根目录下的 schema.graphql:

enum Level {
  BEGINNER
  INTERMEDIATE
  ADVANCED
}

type Instructor {
  id: ID!
  name: String!
  bio: String
}

type Course {
  id: ID!
  title: String!
  level: Level!
  durationHours: Int!
  instructor: Instructor!
}

type Enrollment {
  id: ID!
  course: Course!
  studentEmail: String!
}

type Query {
  courses(level: Level, limit: Int): [Course!]!
  course(id: ID!): Course
}

type Mutation {
  enrollStudent(courseId: ID!, studentEmail: String!): Enrollment!
}

然后,新建 src/operations/courses.graphql,写一个获取课程及其讲师的查询:

query GetCourses($level: Level, $limit: Int) {
  courses(level: $level, limit: $limit) {
    id
    title
    level
    instructor {
      name
    }
  }
}

现在把两者都指给 Codegen。schema 键接收模式路径,documents 接收能匹配操作的 glob 模式:

const config: CodegenConfig = {
  schema: "schema.graphql",
  documents: ["src/**/*.graphql"],
};

步骤 4:选择输出文件

generates 键告诉 Codegen 要把输出写到哪里。它内部的每个键都是一个文件路径:

const config: CodegenConfig = {
  schema: "schema.graphql",
  documents: ["src/**/*.graphql"],
  generates: {
    "src/generated/graphql.ts": {},
  },
};

如果不存在,Codegen 会创建 src/generated/ 文件夹。

步骤 5:添加相关插件

没有插件的输出文件是空的。添加您安装的两个插件:

import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  schema: "schema.graphql",
  documents: ["src/**/*.graphql"],
  generates: {
    "src/generated/graphql.ts": {
      plugins: ["typescript-operations", "typed-document-node"],
    },
  },
};

export default config;

如果您看过较早的教程,可能也会在列表中看到 typescript 插件。现在可以移除它,因为 typescript-operations 可以单独工作,并且只生成实际使用到的输入、枚举和操作类型。如果在当前版本将两者放在同一个输出文件中,TypeScript 会抛出重复声明错误。

步骤 6:运行 Codegen

添加一个 npm 脚本,避免每次都记命令:

npm pkg set scripts.codegen="graphql-codegen"

然后运行:

npm run codegen

Codegen 会加载模式、根据它校验查询并写出输出:

Codegen generation results

Codegen 生成结果

如果某个操作引用的字段在模式中不存在,运行会在这里失败,并且不会写出任何文件。

步骤 7:检查生成的输出

打开 src/generated/graphql.ts。您会看到:

生成的输出

Codegen 生成了四样东西:

  • Level: 枚举对应的字符串联合类型,仅在查询使用到时才会生成

  • GetCoursesQueryVariables: 查询变量的类型,由于模式未标记为必填,因此二者都是可选的

  • GetCoursesQuery: 响应的形状,只包含查询请求的字段

  • GetCoursesDocument: 解析后的查询,作为带类型的文档节点,附带结果和变量类型

再看 GetCoursesQuery。它不包含 durationHours 或讲师的 bio,因为查询没有请求它们。如果您尝试在代码中读取 course.durationHours,会得到编译错误,而不是运行时的 undefined。

这就是全部搭建流程——您写 GraphQL,Codegen 写 TypeScript。

如何配置 GraphQL Codegen

大多数您见到的 Codegen 配置都比上面的更长,但它们都由相同的四个设置构成。

配置文件可以是 codegen.ts、codegen.yml 或 codegen.json。我使用 TypeScript,因为它能为配置本身提供自动补全和类型检查。不同格式下设置相同。

Schema(模式)

schema 键告诉 Codegen 模式的位置。它可以接受多种来源。

当模式在您的仓库中时,可以使用本地文件或 glob 模式:

schema: "schema.graphql",
// or, for a schema split across files
schema: "schema/**/*.graphql",

当模式在正在运行的服务器上时,可以使用 URL。Codegen 会向端点发送自省查询,您可以传入鉴权头:

schema: [
  {
    "https://api.example.com/graphql": {
      headers: { Authorization: Bearer ${process.env.API_TOKEN} },
    },
  },
],

URL 选项只在服务器启用自省时有效。许多生产 API 会关闭它,因此在 CI 中,提交一份模式文件到仓库通常更稳妥。

Documents(文档)

documents 键告诉 Codegen 去哪里找查询、变更、订阅和片段。

操作可以放在 .graphql 文件中,也可以内联在 TypeScript 代码中。若内联,Codegen 会在 gql 标签包裹或添加 /* GraphQL */ 注释时发现它们:

const EnrollStudent = /* GraphQL */ 
  mutation EnrollStudent($courseId: ID!, $studentEmail: String!) {
    enrollStudent(courseId: $courseId, studentEmail: $studentEmail) {
      id
      course {
        title
      }
    }
  }
;

要同时扫描两种文件类型,扩大 glob。再加一个否定模式,避免 Codegen 扫描自己的输出:

documents: ["src/**/*.{graphql,ts}", "!src/generated/**"],

请为每个操作起唯一名称。Codegen 会基于操作名构造类型名,比如 EnrollStudent 会变成 EnrollStudentMutation 和 EnrollStudentMutationVariables。

Generates(输出)

generates 键将输出路径映射到其中的内容。您可以按需添加多个输出目标,每个都有自己的插件。

比如,您想把操作类型放在一个文件里,把完整模式类型放在另一个文件里:

generates: {
  "src/generated/graphql.ts": {
    plugins: ["typescript-operations", "typed-document-node"],
    config: {
      useTypeImports: true,
    },
  },
  "src/generated/schema-types.ts": {
    plugins: ["typescript"],
  },
},

config 对象位于某个输出目标内时,仅对该文件生效。这里的 useTypeImports 会让生成文件使用 import type,某些构建环境需要它。如果将 config 放在文件根部,则会应用于每个输出。

输出路径也可以是以斜杠结尾的文件夹,如 "src/gql/"。这是预设常用的格式,因为它们会写多个文件。

Plugins(插件)

由插件决定生成什么。Codegen 加载模式和文档,每个插件将它们转换为某一部分输出。

在 TypeScript 项目中,您最常见到的插件有:

  • typescript-operations: 为查询、变更、订阅与片段生成类型,以及它们使用到的枚举和输入类型

  • typed-document-node: 生成附带结果与变量类型的文档对象,多数 GraphQL 客户端都能识别

  • typescript: 为模式中的每个对象、枚举和输入生成类型,无论您的操作是否使用它们

  • typescript-resolvers: 服务器端解析器函数的类型签名

一个预设是带有合理默认配置的插件集合。工作原理一节中提到的 client 预设是 The Guild 推荐给客户端应用的方案。它在底层基于改进的 typescript-operations,因此无需额外设置即可获得其优势。

每个插件都有自己的配置选项,数量不少。您不必预先掌握所有选项。先用默认值,当生成的输出不符合代码预期时再查找对应选项。

GraphQL Codegen 能生成什么?

Codegen 的产物取决于您选择的插件。

相同的模式与操作可以生成纯 TypeScript 类型,也可以生成现成的客户端代码。以下是您最常用的输出。

TypeScript 类型

typescript 插件会为模式中的每个对象、枚举和输入生成类型。对于课程目录,Course 类型大致如下:

export type Course = {
  __typename?: 'Course';
  durationHours: Scalars['Int']['output'];
  id: Scalars['ID']['output'];
  instructor: Instructor;
  level: Level;
  title: Scalars['String']['output'];
};

这些类型描述的是完整模式,因此在服务器端、测试夹具以及处理完整对象的工具函数中很实用。

但对客户端代码而言并不合适。Course 类型表示每个字段都存在,即使您的查询从未请求它们。

操作类型

typescript-operations 插件会为每个操作生成两个类型:一个用于结果,一个用于变量。对查询和订阅同样适用。

以下是对 EnrollStudent 变更生成的内容:

export type EnrollStudentMutationVariables = Exact<{
  courseId: string | number;
  studentEmail: string;
}>;

export type EnrollStudentMutation = { enrollStudent: { id: string, course: { title: string } } };

结果类型与选择集逐字段匹配。变量类型为 Exact,因此 TypeScript 会拒绝多余或拼写错误的变量。

courseId 变量接受 string 和 number,因为 GraphQL 的 ID 标量在输入端接受两者。自 v6 起,默认类型为 string | number,这对客户端用例是正确的。

带类型的文档节点

普通的 DocumentNode 是解析过的 GraphQL 查询。它了解查询的结构,但不知道查询返回什么或需要哪些变量。

一个带类型的文档节点同时拥有这两种类型。typed-document-node 插件会为每个操作生成一个:

export const GetCoursesDocument = { /* ... */ } as unknown as DocumentNode<GetCoursesQuery, GetCoursesQueryVariables>;

这样,任何接受 TypedDocumentNode 的函数都能仅凭文档推断结果和变量类型。您无需手动传入泛型,也无法把查询与错误的类型组合起来。

大多数现代 GraphQL 客户端都支持带类型的文档节点,包括 Apollo Client、urql 和 graphql-request。稍后您会看到三者的实际用法。

特定客户端的集成

一些插件更进一步,为特定客户端或框架生成代码。例如:

  • typescript-react-apollo: 为 Apollo Client 生成带类型的 React hooks

  • typescript-urql: 为 urql 生成带类型的 hooks 和组件

  • typescript-react-query: 为 TanStack Query 生成带类型的 hooks

  • typescript-graphql-request: 生成一个带类型的 SDK,每个操作对应一个函数

以上都是社区插件。The Guild 已将它们迁移到单独的社区仓库,因此其发版节奏与核心插件不同。

它们的必要性也比过去更低。只要客户端支持带类型的文档节点,生成的 hooks 对类型安全的提升并不大。例如 Apollo 的官方文档就推荐使用 typescript-operations 插件,它专注只生成类型,不包含额外的运行时代码。

通常,您应先从操作类型和带类型的文档节点开始。只有当团队确实需要生成的 hooks 或 SDK 函数时,再添加特定客户端的插件。

结合 TypeScript 使用 GraphQL Codegen

生成的类型只有在能捕捉错误时才有价值,那么就来故意犯些错。

这个示例在课程目录项目中添加第二个查询、一个将其发送到 API 的小型辅助函数,以及使用结果的代码。它假设实现了该模式的 GraphQL 服务器运行在 http://localhost:4000/graphql。

以下是本示例用到的模式部分:

type Instructor {
  id: ID!
  name: String!
  bio: String
}

type Course {
  id: ID!
  title: String!
  level: Level!
  durationHours: Int!
  instructor: Instructor!
}

type Query {
  course(id: ID!): Course
}

这里有两个关键信息。course 字段返回的 Course 没有 !,因此当没有匹配 ID 的课程时它为 null。讲师的 bio 也可为 null。

创建 src/operations/course.graphql,写一个获取单个课程的查询:

query GetCourse($id: ID!) {
  course(id: $id) {
    title
    durationHours
    instructor {
      name
      bio
    }
  }
}

再次运行 npm run codegen。Codegen 会在 src/generated/graphql.ts 中新增两个类型:

graphql.ts 文件的内容

模式中的可空性会被带入。course 可能为 null,bio 可能为 null,而 id 是必填的,因为查询声明它为 ID!。

现在编写应用代码。创建 src/execute.ts,写一个能将任意带类型文档发送到 API 的辅助函数:

import { print } from "graphql";
import type { TypedDocumentNode } from "@graphql-typed-document-node/core";

const API_URL = "http://localhost:4000/graphql";

export async function execute<TResult, TVariables>(
  document: TypedDocumentNode<TResult, TVariables>,
  variables: TVariables,
): Promise<TResult> {
  const response = await fetch(API_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query: print(document), variables }),
  });
  const { data, errors } = await response.json();
  if (errors?.length) {
    throw new Error(errors[0].message);
  }
  return data as TResult;
}

该辅助函数对课程一无所知。它从您传入的文档中获取两种类型,print() 函数会把文档重新转成请求所需的查询字符串。

然后在 src/course-details.ts 中使用它:

import { execute } from "./execute";
import { GetCourseDocument, type GetCourseQuery } from "./generated/graphql";

function formatCourse(course: NonNullable<GetCourseQuery["course"]>): string {
  const bio = course.instructor.bio ?? "No bio yet";
  return course.title({course.durationHours}h) by ${course.instructor.name} - ${bio};
}

const { course } = await execute(GetCourseDocument, { id: "c1" });

if (course) {
  console.log(formatCourse(course));
} else {
  console.log("Course not found");
}

formatCourse() 函数的参数类型来自生成的查询类型。NonNullable 会移除 null 情况,因为该函数只会在 if 判断之后运行。

示例使用了顶层 await,因此需要在 package.json 中设置 "type": "module",并用 npx tsx src/course-details.ts 运行:

Successful call to a GraphQL mock server

成功调用 GraphQL 模拟服务器

这里假设您已有一个 GraphQL 服务器在运行。以下是我使用的一个模拟服务器:

import { createServer } from "node:http";
import { readFileSync } from "node:fs";
import { createSchema, createYoga } from "graphql-yoga";

const instructors = [{ id: "i1", name: "Dario Radecic", bio: "Developer" }];

const courses = [
  { id: "c1", title: "Intro to GraphQL", level: "BEGINNER", durationHours: 4, instructor: instructors[0] },
  { id: "c2", title: "GraphQL Schema Design", level: "ADVANCED", durationHours: 6, instructor: instructors[0] },
];

const schema = createSchema({
  typeDefs: readFileSync("schema.graphql", "utf8"),
  resolvers: {
    Query: {
      courses: (_: unknown, args: { level?: string; limit?: number }) =>
        courses
          .filter((course) => !args.level || course.level === args.level)
          .slice(0, args.limit ?? undefined),
      course: (_: unknown, args: { id: string }) =>
        courses.find((course) => course.id === args.id) ?? null,
    },
    Mutation: {
      enrollStudent: (_: unknown, args: { courseId: string; studentEmail: string }) => ({
        id: "e1",
        course: courses.find((course) => course.id === args.courseId),
        studentEmail: args.studentEmail,
      }),
    },
  },
});

createServer(createYoga({ schema })).listen(4000, () => {
  console.log("Mock API running at http://localhost:4000/graphql");
});

现在试着把它搞坏。打开 src/course-details.ts,将 formatCourse() 函数以下的所有内容替换为三个常见的错误,手写类型经常会放过它们:

// Missing the required id variable
await execute(GetCourseDocument, {});

const { course } = await execute(GetCourseDocument, { id: "c1" });

// Reading a field the query doesn't request
console.log(course?.level);

// Forgetting that course can be null
console.log(course.title);

然后对项目进行类型检查:

npx tsc -p .

TypeScript 会在代码运行前捕捉到三处错误:

Errors catched by TypeScript

TypeScript 捕获的错误

这就是 Codegen 如何减少前端与 API 间的不匹配。类型来自与服务器相同的模式,因此不会描述不存在的字段,也不会遗漏服务器可能返回的 null。

并且这种保护并不会在第一次运行后停止。假设后端团队把 durationHours 改为可空。您重新生成后,类型会变为 number | null,TypeScript 会指出所有曾假设它为 number 的地方。

将 GraphQL Codegen 与 GraphQL 客户端结合

Codegen 本身不发送请求。这是 GraphQL 客户端的职责。

生成的带类型文档节点是二者之间的交接点。您将它们传给客户端,客户端据此推断类型。下面示例都使用相同的 src/generated/graphql.ts 文件和上一节的模拟 API,因此请在单独的终端中保持 npx tsx mock/server.ts 运行。

与 Apollo 搭配的 GraphQL Codegen

安装 Apollo Client 及其 rxjs 同级依赖:

npm install @apollo/client rxjs

示例还会运行 EnrollStudent 变更。将其保存为 src/operations/enroll.graphql:

mutation EnrollStudent($courseId: ID!, $studentEmail: String!) {
  enrollStudent(courseId: $courseId, studentEmail: $studentEmail) {
    id
    course {
      title
    }
  }
}

Apollo 的文档还推荐在 Codegen 中开启两个配置选项,因为 Apollo Client 总是包含 __typename 字段,但不会将它添加到根类型。将它们添加到 codegen.ts 的输出目标中:

"src/generated/graphql.ts": {
  plugins: ["typescript-operations", "typed-document-node"],
  config: {
    nonOptionalTypename: true,
    skipTypeNameForRoot: true,
  },
},

启用这些选项后,生成的结果类型中每个对象都会有必填的 __typename 字段,比如 __typename: 'Course'。这与 Apollo 缓存返回的内容一致。

再次运行 npm run codegen 以检测到新的变更和配置。

Apollo Client 在 query() 和 mutate() 中接受带类型文档节点。将以下保存为 src/apollo.ts:

import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import { EnrollStudentDocument, GetCoursesDocument } from "./generated/graphql";

const client = new ApolloClient({
  link: new HttpLink({ uri: "http://localhost:4000/graphql" }),
  cache: new InMemoryCache(),
});

const { data } = await client.query({
  query: GetCoursesDocument,
  variables: { level: "BEGINNER", limit: 5 },
});

data?.courses.forEach((course) => console.log(course.title));

const result = await client.mutate({
  mutation: EnrollStudentDocument,
  variables: { courseId: "c1", studentEmail: "student@example.com" },
});

console.log(result.data?.enrollStudent.course.title);

data 对象的类型是 GetCoursesQuery | undefined,TypeScript 会将 variables 与 GetCoursesQueryVariables 进行校验。变更同样适用于 EnrollStudentMutation 及其变量。

使用 npx tsx src/apollo.ts 运行:

Apollo output

Apollo 输出

第一行来自查询(它是唯一的初级课程)。第二行来自变更,返回的是学生所选课程的标题。

React hooks(如 useQuery() 和 useMutation())也会以同样方式从带类型文档节点中推断类型。

与 urql 搭配的 GraphQL Codegen

安装 urql 的核心包:

npm install @urql/core

urql 天生支持带类型文档节点。将文档作为 query() 的第一个参数传入,在 src/urql.ts 中如下:

import { Client, cacheExchange, fetchExchange } from "@urql/core";
import { GetCoursesDocument } from "./generated/graphql";

const client = new Client({
  url: "http://localhost:4000/graphql",
  exchanges: [cacheExchange, fetchExchange],
});

const result = await client
  .query(GetCoursesDocument, { level: "ADVANCED" })
  .toPromise();

if (result.error) {
  throw result.error;
}

result.data?.courses.forEach((course) => console.log(course.title));

第二个参数的类型是查询变量,result.data 的类型是查询结果。

使用 npx tsx src/urql.ts 运行:

Urql output

Urql 输出

各框架绑定(如 React 中的 urql 包里的 useQuery())同样支持带类型文档节点。

与其他客户端搭配的 GraphQL Codegen

您不需要功能齐全的客户端也能得到带类型的结果。

graphql-request 是一个最小化的客户端,它发送单个请求并返回数据。使用以下命令安装:

npm install graphql-request

它与 Apollo 和 urql 一样,从带类型文档节点中推断类型。将以下保存为 src/graphql-request.ts:

import { request } from "graphql-request";
import { GetCourseDocument } from "./generated/graphql";

const { course } = await request(
  "http://localhost:4000/graphql",
  GetCourseDocument,
  { id: "c2" },
);

console.log(course?.title);

使用 npx tsx src/graphql-request.ts 运行:

GraphQL request output

GraphQL request 输出

同样的思路也适用于 TanStack Query。它本身不发送 GraphQL 请求,因此您可以在查询函数中调用 graphql-request 或上一节的 execute() 辅助函数,结果类型会传递到您的代码中。

如果您的客户端完全不支持带类型文档节点,仍可使用纯操作类型。导入 GetCourseQuery 和 GetCourseQueryVariables 并将它们作为泛型传入。虽然更手动,但类型仍源自模式。

如何自动运行 GraphQL Codegen

只有当生成的类型与当前模式和操作一致时,它们才有用。

如果在修改后忘记重新运行 Codegen,类型描述的将是已经不存在的代码。这与手写类型的漂移问题本质相同,只是多了一步。解决方案是把这一手动步骤从工作流中移除。

npm 脚本

先在 package.json 中放几个脚本,让团队中的每个人以相同方式运行 Codegen:

{
  "scripts": {
    "codegen": "graphql-codegen",
    "codegen:watch": "graphql-codegen --watch",
    "codegen:check": "graphql-codegen --check",
    "prebuild": "npm run codegen",
    "build": "tsc -p ."
  }
}

本节其余部分会介绍每个脚本的用途。

监听模式

监听模式会在每次监控的文件发生变动时重跑 Codegen。它需要 @parcel/watcher 包,Codegen 不会替您安装:

npm install -D @parcel/watcher
npm run codegen:watch

如果跳过安装,Codegen 会运行一次并打印 Failed to import @parcel/watcher,而不会持续监听。

监听模式会追踪您的操作和本地模式文件。如果您在 course.graphql 中添加字段,新字段会在一秒后出现在 GetCourseQuery 中。如果您在 schema.graphql 中把 durationHours 改为可空,类型会变为 number | null,编辑器会标出所有曾假设它为 number 的位置。

在开发期间,让它在与开发服务器相邻的终端中持续运行。

在构建前生成

npm 会在 build 之前运行一个名为 prebuild 的脚本。借助上述脚本,npm run build 会先重新生成类型,然后才运行 TypeScript 编译器。

这样,即使有人在拉取更改后忘记运行 Codegen,构建也不会基于过时的类型。

CI/CD

--check 标志会以干跑模式运行 Codegen。它不会写任何文件,而是将“将要生成”的内容与磁盘上的内容进行比较,若有差异则以代码 1 退出。

以下是一个最小的 GitHub Actions 工作流示例:

name: CI

on: [push, pull_request]

jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
      - run: npm ci
      - run: npm run codegen:check
      - run: npm run build

codegen:check 这一步只在您将生成的文件提交到仓库时有意义。如果没有,请移除该步骤,因为 npm run build 会通过 prebuild 钩子重新生成所有内容。

在模式变化时再生成

操作的变化发生在您的仓库中,因此监听模式与 prebuild 就能覆盖它们。模式的变化更棘手,因为通常来自另一个团队。

如何处理取决于模式的位置:

  • 同一仓库中的模式文件: 监听模式和 CI 都能发现更改,这是最易用的设置
  • 来自 URL 的模式: Codegen 每次运行都会拉取最新版本,因此一旦服务器的模式不再与提交的输出匹配,CI 中的 codegen:check 会立即失败
  • 由其他仓库发布的模式: 将其在构建时复制或下载,然后对本地副本运行 Codegen

URL 选项有一个注意点。监听模式是基于文件变动触发的,因此它不会察觉远程模式的变化。您只能在下一次手动运行或 CI 任务中看到这些变化。

GraphQL Codegen 与手写类型对比

两种方式都能为 GraphQL 数据提供 TypeScript 类型,但仍有一些差异需要注意。

手写类型

使用手写类型时,您需要阅读模式并自行编写匹配的接口。

这样您拥有完全掌控力。您可以随意命名类型、把多个操作合并为一个共享类型,或者添加模式中没有的辅助类型。

但这是手工活。每个新增字段或重命名参数,都意味着您要手动找到并更新匹配的类型。而且没有人会在您遗漏时提醒您。

这就是“漂移”的根源。前文中的 Course 接口在后端重命名 title 后仍能编译,因为 TypeScript 只会将您的代码与您的类型进行校验,而不会与 API 对比。

生成的类型

生成的类型源自与服务器相同的模式,因此从设计上就与您的 GraphQL 定义保持同步。一次模式变更加一次 Codegen 运行就能同时更新所有受影响的类型。

您也会少写很多代码。课程目录项目有三个操作,但没有为它们手写任何类型。

此外,您几乎是“免费”地获得了以模式驱动的开发。您修改模式或操作,重新生成,编译器就会给出所有需要改动的地方列表。

但要记住,Codegen 只会反映您提供给它的内容。

如果模式把每个字段都标记为可空,那么生成的类型里就会充斥 | null 检查。如果某个操作获取了 40 个字段,而页面只需要 5 个,那么生成的类型也会有 40 个字段。

以下是差异的快速回顾:

  手写类型 生成的类型
维护 每次模式变更都需手动更新 每次变更后执行一次命令
模式漂移 可能且无声发生 在生成或编译时被捕获
样板代码 随操作数量增长 无需手写
自定义类型控制力 完全掌控 受限于插件配置选项
以模式驱动的开发 手动交叉核对 编译器展示所有受影响的代码行

Codegen 与手写类型对比

GraphQL Codegen 与 GraphQL 自省对比

两者常被混淆,因为 Codegen 经常使用自省。但它们解决的是不同问题。

自省(Introspection)是 GraphQL 规范中的一个特性。任何 GraphQL 服务器都可以通过 __schema 和 __type 等特殊字段回答关于自身模式的查询。结果是一个 JSON,描述了 API 暴露的每个类型、字段与参数。

通俗地说,自省回答的是“这个 API 长什么样?”的问题。

Codegen 回答的是“我的应用需要什么代码?”的问题。它获取模式信息和您的操作,并写出 TypeScript 类型和带类型的文档节点。

当您在 codegen.ts 中将 schema 设置为 URL 时,Codegen 会向服务器发送自省查询以获取模式。若使用本地 .graphql 文件,则不会发生自省。

Codegen 也可以生成自省结果作为输出。@graphql-codegen/introspection 插件会以自省 JSON 的形式写出模式,一些工具(如 urql 的 Graphcache)会用它实现基于模式的缓存。

  自省 GraphQL Codegen
是什么 内建的 GraphQL 查询系统 命令行工具与插件
输入 对运行中服务器的查询 模式与您的操作
输出 模式的 JSON 描述 类型、文档节点、客户端代码
何时运行 每当客户端向服务器请求 在开发、构建与 CI 期间

Codegen 与自省对比

常见的 GraphQL Codegen 问题

大多数 Codegen 错误可归结为以下四类。

缺少生成的类型

Codegen 运行没有报错,但您期望的类型不在输出文件中。

按以下顺序检查三点:

  • 模式: 如果该类型不在 Codegen 加载的模式中,就无法生成。请打开模式文件或自行运行自省查询确认它存在

  • 文档: 只有 Codegen 找到的操作才会生成类型,如果包含查询的文件不被 documents 的 glob 匹配到,就会缺少相应类型

  • 插件: 自 v6 起,typescript-operations 仅生成您的操作所使用到的枚举和输入类型,因此如果您需要像 Course 这样的完整模式类型,请在单独输出文件中添加 typescript 插件

如果您使用了未安装的插件,Codegen 会给出清晰的报错:

Unable to find template plugin matching 'typescript-react-apollo'
Install one of the following packages:
- @graphql-codegen/typescript-react-apollo

Codegen 找不到操作

该错误意味着 documents 的 glob 没有匹配到任何包含操作的文件:

Unable to find any GraphQL type definitions for the following pointers:
  - src/**/*.gql

检查 glob 的扩展名是否与您的文件一致。.gql 模式不会匹配 .graphql 文件,src/**/*.graphql 也不会匹配内联在 .ts 文件中的操作。

对内联操作,还要检查标签。Codegen 只会发现用 gql 包裹或带有 /* GraphQL */ 注释的模板字面量。仅包含查询的普通模板字符串对它是不可见的。

如果设置了 ignoreNoDocuments: true,Codegen 不会显示该错误,并会生成一个不包含操作类型的输出。在全新项目中这个选项有用,但后来可能会掩盖损坏的 glob。

生成的类型已过时

类型可编译,但未反映对操作或模式的最新更改。

Codegen 不会自行运行。每次修改 .graphql 文件、内联操作、模式或 codegen.ts 后都要重新运行。在开发期间,npm run codegen:watch 会替您完成。

如果在 CI 中类型仍然陈旧,请提交重新生成的文件,或让 prebuild 钩子在构建前重新生成。

生成的类型与 API 不匹配

类型可以编译,但服务器在运行时返回的数据形状不同。

这几乎总意味着 Codegen 与您的应用使用了不同的模式。例如,Codegen 读取的是一周前的本地 schema.graphql,而应用调用的生产 API 自那以后已经发生了改变。

检查 schema 键指向哪里。如果是文件,将它与应用调用的 API 进行比较并更新。如果是 URL,确保它与应用使用的是同一环境,而不是模式更新更快或更慢的预发服务器。

然后重新生成并查看差异。如果操作文件有变更而生成文件没有,操作很可能在 documents 的 glob 之外。

GraphQL Codegen 的最佳实践

Codegen 不需要大量配置也能良好运行。以下做法能在项目增长时保持这种状态:

  • 保持模式与生成文件同步: 开发期间运行监听模式,构建前运行 prebuild,避免任何人使用旧模式对应的类型

  • 生成面向操作的类型: 客户端代码使用 typescript-operations,因为它们与每个查询的获取内容精确匹配

  • 仅在工作流需要时提交生成文件: 提交生成文件可在 PR 中审阅类型变化,并在 CI 中运行 --check 捕捉过期输出;如果团队不需要,将输出文件夹加入 .gitignore 并在每次构建时生成

  • 保持配置简短: 一个输出文件配两个插件就能覆盖大多数客户端项目,每多一个选项,就多一件下一位开发者在修改前必须理解的事情

  • 在每次推送时在 CI 中运行 Codegen: --check 步骤或 prebuild 钩子可以捕捉有人在本地忘记重新生成的变更

  • 切勿手动编辑生成文件: 下一次 Codegen 运行会覆盖您的改动,请改动模式、操作或配置本身

结论

GraphQL Codegen 接管了 GraphQL 开发中最重复的部分——把模式和操作手动转成 TypeScript 类型的工作。

无论项目多大,工作流都保持一致。您定义模式和操作,配置 Codegen,生成类型和带类型的文档节点,然后将它们导入到应用代码中。

最大价值在于,您的类型始终描述的是您实际调用的 API,而不是您不必再手写的代码。当模式发生变化时,编译器会告诉您哪些地方出了问题,从而及早发现错误。

若您想了解 GraphQL 在现代 NoSQL 数据库中的用法,请阅读我们的MongoDB 与 GraphQL 博文。

GraphQL Codegen 常见问题

GraphQL Codegen 有什么用?

GraphQL Codegen 会根据 GraphQL 模式和您的 GraphQL 操作生成代码。多数团队用它为查询和变更生成 TypeScript 类型与带类型的文档节点,从而无需手写和更新这些类型。它还可以为服务器生成解析器类型,并为特定客户端生成现成的 hooks。

使用 Codegen 是否需要运行中的 GraphQL 服务器?

不需要。Codegen 可以从本地 .graphql 文件读取模式,因此在没有任何服务器的情况下也能工作。只有当您把 schema 键指向 URL 时才需要运行中的服务器,因为 Codegen 会通过自省来获取模式。

是否应将生成的文件提交到 Git?

取决于您团队的工作流。提交生成文件可以在拉取请求中审阅类型变更,并在 CI 中使用 graphql-codegen --check 捕捉过期输出。如果不需要这些,将输出文件夹加入 .gitignore,在每次构建前重新生成即可。

我应该使用 client 预设还是单独的插件?

取决于您团队的工作流。The Guild 推荐大多数客户端应用使用 client 预设,因为它将操作类型、带类型文档节点和片段屏蔽一次性打包。Apollo 则推荐使用 typescript-operations 插件,因为该预设会添加运行时代码和与 Apollo Client 冲突的特性。如果您使用 Apollo,请选择插件;否则该预设是不错的默认选项。

更新 Codegen 后为什么会出现重复声明错误?

自 v6 起,typescript-operations 插件会生成自己的枚举和输入类型。如果您的配置仍在同一输出文件中同时列出 typescript 和 typescript-operations,两者都会声明诸如 Level 的类型,TypeScript 会抛出重复声明错误。请将 typescript 插件从该输出中移除;若仍需要完整模式类型,请将它移动到单独文件。

主题

与 DataCamp 一起学习

课程

Software Development with Claude Code

4 小时
8.7K
Claude Code brings AI assistance to your terminal. Learn the workflows that turn it into a reliable tool for real software development.
查看详情Right Arrow
开始课程
查看更多Right Arrow