GraphQL的类型安全优势在前后端分别独立工作时才能最大化。但这需要一套机制把Schema的类型信息同步到前端代码中。手动维护TypeScript类型定义是反模式——一旦Schema变更,前后端的类型定义就会不同步,类型安全变成虚假的安全感。
GraphQL Code Generator 是当前最成熟的代码生成工具,它从Schema文件或 introspection结果自动生成TypeScript类型、React hooks、Apollo操作签名等。
安装:
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
配置文件 codegen.ts:
import type { CodegenConfig } from '@graphql-codegen/cli'; const config: CodegenConfig = { schema: './schema.graphql', documents: 'src/**/*.{ts,tsx}', generates: { './src/generated/types.ts': { plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'], config: { withHooks: true, // 生成useQuery hooks withMutationFn: true, // 生成Mutation函数 withSubscriptionHooks: true, enumsAsTypes: true, // 枚举生成为联合类型 immutableTypes: true, // 生成readonly类型 skipTypename: true, // 不生成__typename字段 } } } }; export default config;
假设Schema中有:
type User { id: ID! name: String! email: String! posts(limit: Int = 10): [Post!]! }
Code Generator自动生成:
export type User = { __typename?: 'User'; id: string; name: string; email: string; posts: Array<Post>; }; export type Post = { __typename?: 'Post'; id: string; title: string; content: string; authorId: string; createdAt: string; };
当你写了一个查询:
query GetUser($id: ID!) { user(id: $id) { name posts(limit: 5) { title } } }
生成器会生成精确的响应类型:
export type GetUserQueryVariables = { id: string; }; export type GetUserQuery = { user?: { __typename?: 'User'; name: string; posts: Array<{ __typename?: 'Post'; title: string; }>; }; }; // 以及自动生成的hook export function useGetUserQuery( baseOptions?: ApolloHookOptions<GetUserQuery, GetUserQueryVariables> ) { return Apollo.useQuery<GetUserQuery, GetUserQueryVariables>(GetUserDocument, baseOptions); }
import { useGetUserQuery } from '../generated/types'; function UserProfile({ userId }: { userId: string }) { // data.user 的类型是 GetUserQuery['user'] // IDE自动补全 name, posts.title // 如果写了不存在的字段,TypeScript直接报错 const { data, loading, error } = useGetUserQuery({ variables: { id: userId } }); if (loading) return <Loading />; if (error || !data?.user) return <Error />; // 类型安全:data.user.name 一定是string // data.user.xxx TypeScript会报错 return ( <div> <h1>{data.user.name}</h1> {data.user.posts.map(p => ( <div key={p.id}>{p.title}</div> ))} </div> ); }
关键价值:IDE能根据Schema提供精确的自动补全和类型检查。你不需要查文档就知道一个字段叫什么、什么类型、是否可选——TypeScript类型已经告诉你了。
推荐把代码生成集成到开发工作流中:
{ "scripts": { "codegen": "graphql-codegen --config codegen.ts", "codegen:watch": "graphql-codegen --watch --config codegen.ts", "predev": "npm run codegen", "prebuild": "npm run codegen" } }
codegen:watch 在开发时监听Schema和查询文件的变更,自动重新生成。predev 和 prebuild 确保每次启动或构建前类型都是最新的。
代码生成的最大价值在于:Schema变更时,前端代码会自动得到类型错误提示。
假设后端把 User.name 改成了 User.displayName:
name 字段从类型中消失data.user.name 的代码报TypeScript错误这比"后端改了字段,前端不知道,上线后才出bug"要好太多了。代码生成充当了前后端类型同步的桥梁,让"Schema是单一可信源"这个理念在工程实践中落地。
除了GraphQL Code Generator,还有几个选择:
graphql-typed-document-node:轻量级方案,只生成document node类型,不生成hooks。适合不想依赖特定客户端库的项目。
urql/codegen:urql客户端的代码生成插件,原理类似。
graphql-codegen-hasura:针对Hasura后端的代码生成,自动生成CRUD操作的hooks。
Kayzen:Facebook内部的代码生成工具,功能强大但不开源。
选择GraphQL Code Generator作为默认方案——它支持最广泛的插件生态,社区最活跃,配置最灵活。
默认情况下,DateTime 标量在生成的 TypeScript 里是 string,JSON 标量是 any。想获得更精确的类型,可以在 codegen 配置里做标量映射:
const config: CodegenConfig = { generates: { './src/generated/types.ts': { config: { scalars: { DateTime: 'Date', JSON: 'unknown', UUID: 'string', } } } } };
映射后,DateTime 字段在代码里就是 Date 对象,配合前端日期库直接使用;JSON 用 unknown 强制开发者显式收窄类型,而不是悄悄用 any 逃过检查。这个配置值得每个项目做一遍——它让"类型安全"从字符串层面深入到了业务语义层面。
代码生成的另一项能力是 Fragment Masking:子组件拿到的 props 类型被"屏蔽",只暴露它在自己的 Fragment 里声明的字段。子组件无法访问父组件塞进来的多余字段,也无法意外读取它没声明过的数据。
// 生成的类型:PostCard 只能读到它声明的字段 type PostCard_post = { __typename?: 'Post'; title: string; likesCount: number; };
这强制团队遵守"组件只依赖自己声明的内容"的纪律——重构组件时,编译器会告诉你哪些字段不再需要,而不是让字段在组件树里悄悄泄漏。需要关闭 masking 时用 fragmentMasking: false,但建议默认开启。
代码生成的价值最终要靠流程保障。推荐的做法是:
这套流程把"前后端各改各的、上线才炸"变成"改 Schema 的当天前端就知道"。它是 GraphQL 相比 REST 的隐性红利:REST 的字段变更通常没有编译期检查,只有运行时才发现。