Bài 25.3: Generating TypeScript types từ GraphQL

Bài 25.3: Generating TypeScript types từ GraphQL
Chào mừng trở lại với chuỗi bài viết về Lập trình Web Front-end!
Trong những bài trước, chúng ta đã khám phá sức mạnh của GraphQL như một ngôn ngữ truy vấn cho API của chúng ta. Chúng ta cũng đã thấy TypeScript giúp chúng ta xây dựng các ứng dụng Front-end mạnh mẽ và đáng tin cậy hơn nhờ hệ thống kiểu tĩnh.
Vậy điều gì sẽ xảy ra khi chúng ta kết hợp hai công nghệ tuyệt vời này? Làm thế nào để chúng ta đảm bảo rằng dữ liệu chúng ta nhận được từ GraphQL API luôn luôn khớp với kiểu dữ liệu mà chúng ta mong đợi trong code TypeScript của mình?
Một cách tiếp cận là tự tay định nghĩa các TypeScript interface hoặc type cho từng query, mutation, hoặc thậm chí toàn bộ schema của GraphQL. Tuy nhiên, cách này nhanh chóng trở thành một cơn ác mộng khi schema của bạn thay đổi hoặc trở nên phức tạp. Bạn sẽ phải liên tục cập nhật thủ công các definition type, dẫn đến sai sót, lãng phí thời gian và làm giảm đáng kể tốc độ phát triển.
Đây chính là lúc Generating TypeScript types từ GraphQL phát huy sức mạnh của mình! Thay vì làm thủ công, chúng ta sẽ sử dụng các công cụ để tự động đọc GraphQL schema và các query/mutation/subscription mà chúng ta viết ở phía client, sau đó sinh ra các file TypeScript chứa đựng toàn bộ các definition type cần thiết.
Tại Sao Phải Tự Động Sinh Types?
Đây không chỉ là một tiện ích nhỏ, mà là một yếu tố thiết yếu để phát triển ứng dụng Front-end hiện đại sử dụng GraphQL và TypeScript. Những lợi ích mang lại là cực kỳ to lớn:
- Type Safety Tuyệt Đối: Đảm bảo dữ liệu bạn nhận được từ API sẽ chính xác là kiểu mà code của bạn mong đợi. Bắt lỗi ngay tại thời điểm biên dịch (compile time) thay vì chờ đến lúc chạy (runtime).
- Trải Nghiệm Phát Triển Đỉnh Cao: Tận hưởng tính năng tự động hoàn thành (autocompletion) thông minh, thông tin về kiểu dữ liệu khi hover chuột, và khả năng refactoring code an toàn hơn trong trình soạn thảo code (VS Code, WebStorm...). Điều này giúp tăng năng suất và giảm thiểu sai sót đánh máy.
- Đồng Bộ Giữa Front-end và Back-end: Đảm bảo rằng Front-end của bạn luôn sử dụng đúng cấu trúc dữ liệu mà Back-end cung cấp thông qua GraphQL schema. Khi schema thay đổi, bạn sẽ ngay lập tức biết được phần nào của code Front-end cần được cập nhật.
- Giảm Thiểu Code Lặp: Không còn phải viết lại các definition type cho cùng một cấu trúc dữ liệu ở nhiều nơi.
- Duy Trì Codebase Dễ Dàng Hơn: Code có kiểu rõ ràng, nhất quán sẽ dễ đọc, dễ hiểu và dễ bảo trì hơn nhiều.
Nghe có vẻ như phép màu, đúng không? Thực ra đó là sức mạnh của tự động hóa!
Công Cụ Phổ Biến: GraphQL Code Generator
Có nhiều công cụ có thể giúp bạn làm điều này, nhưng GraphQL Code Generator (hay ngắn gọn là graphql-codegen
) là một trong những công cụ mạnh mẽ và phổ biến nhất trong cộng đồng. Nó hỗ trợ rất nhiều plugin khác nhau, cho phép bạn tạo ra không chỉ TypeScript types mà còn cả hooks, components, hay các helper function tùy thuộc vào thư viện GraphQL client bạn đang sử dụng (như Apollo, URQL, Relay...).
Trong bài viết này, chúng ta sẽ tập trung vào việc sử dụng graphql-codegen
để sinh ra các TypeScript types cơ bản dựa trên schema và các operations (query/mutation/subscription) của chúng ta.
Cài Đặt
Đầu tiên, chúng ta cần cài đặt graphql-codegen
và các plugin cần thiết vào project của mình.
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
# hoặc dùng yarn
yarn add --dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
@graphql-codegen/cli
: Giao diện dòng lệnh chính để chạy công cụ.@graphql-codegen/typescript
: Plugin cơ bản để sinh ra TypeScript types dựa trên toàn bộ schema GraphQL.@graphql-codegen/typescript-operations
: Plugin nâng cao hơn, sinh ra TypeScript types cụ thể cho từng query, mutation, subscription và fragment mà bạn định nghĩa trong các file.graphql
hoặc.gql
của mình. Đây là plugin quan trọng nhất cho việc gán kiểu cho dữ liệu bạn thực sự nhận được.
Cấu Hình (codegen.yml
)
Tiếp theo, chúng ta cần tạo một file cấu hình cho graphql-codegen
, thường có tên là codegen.yml
hoặc codegen.ts
(nếu muốn cấu hình bằng TypeScript). File này sẽ cho công cụ biết schema GraphQL của bạn ở đâu, các file chứa operations của bạn ở đâu, và bạn muốn sinh ra những gì và lưu ở đâu.
Đây là một ví dụ cấu hình cơ bản trong file codegen.yml
:
# codegen.yml
schema: "http://localhost:4000/graphql" # Hoặc đường dẫn đến file schema.graphql
documents: "src/**/*.graphql" # Đường dẫn đến các file chứa query/mutation/fragment của bạn
generates:
# Nơi bạn muốn lưu các file được sinh ra
./src/generated/graphql.ts:
# Các plugin mà bạn muốn sử dụng
plugins:
- typescript # Sinh ra types cho toàn bộ schema
- typescript-operations # Sinh ra types cho các operations (query, mutation,...)
config:
# Các tùy chọn cấu hình cho các plugin (tùy chọn)
skipTypename: true # Bỏ qua trường __typename trong các type được sinh ra
# Ví dụ: Dành cho React Apollo
# withHooks: true # Nếu bạn dùng @graphql-codegen/typescript-react-apollo
Giải thích:
schema
: Chỉ định nguồn của GraphQL schema. Có thể là một URL của endpoint GraphQL hoặc đường dẫn đến file.graphql
chứa schema definition.documents
: Chỉ định các file màgraphql-codegen
sẽ quét để tìm các query, mutation, subscription và fragment của bạn. Ở đây,src/**/*.graphql
có nghĩa là quét tất cả các file có đuôi.graphql
trong thư mụcsrc
và các thư mục con của nó.generates
: Đây là phần quan trọng nhất. Nó định nghĩa nơi các file output sẽ được tạo ra (./src/generated/graphql.ts
) và danh sách cácplugins
sẽ được sử dụng để sinh ra nội dung cho file đó.- Chúng ta dùng
typescript
để có các type cơ bản cho các object/enum/scalar... trong schema. - Chúng ta dùng
typescript-operations
để có các type chính xác khớp với shape dữ liệu mà các query/mutation của chúng ta trả về. - Phần
config
chứa các tùy chọn bổ sung cho các plugin.skipTypename: true
là một tùy chọn phổ biến để loại bỏ trường__typename
khỏi các type sinh ra, giúp code gọn gàng hơn nếu bạn không cần đến trường này.
- Chúng ta dùng
Chạy Công Cụ
Sau khi có file cấu hình, bạn chỉ cần chạy lệnh graphql-codegen
trong terminal:
npx graphql-codegen
# hoặc
yarn graphql-codegen
Lần đầu chạy có thể mất một chút thời gian. Sau khi chạy xong, bạn sẽ thấy file ./src/generated/graphql.ts
(hoặc tên file bạn cấu hình) được tạo ra hoặc cập nhật. File này chứa đựng toàn bộ các TypeScript types được sinh ra tự động dựa trên schema và các operations của bạn!
Tự Động Sinh Code Khi Có Thay Đổi
Để tiện lợi hơn trong quá trình phát triển, bạn nên thêm script vào package.json
để có thể chạy lại lệnh này dễ dàng, hoặc thậm chí cấu hình chế độ watch để công cụ tự động chạy lại mỗi khi schema hoặc các file .graphql
của bạn thay đổi.
// package.json
{
"scripts": {
"generate-graphql-types": "graphql-codegen",
"dev": "concurrently \"npm run generate-graphql-types --watch\" \"rest of your dev command\""
}
}
Ở đây, tôi dùng concurrently
(cần cài đặt npm install concurrently
) để chạy song song lệnh generate-graphql-types --watch
(theo dõi thay đổi và chạy lại) và lệnh phát triển ứng dụng Front-end thông thường của bạn.
Ví Dụ Minh Họa Sức Mạnh Của Types Được Sinh Ra
Hãy xem một ví dụ đơn giản về cách các type được sinh ra giúp ích cho chúng ta như thế nào.
Giả sử chúng ta có một schema GraphQL đơn giản định nghĩa kiểu User
:
# Một phần của schema.graphql type User { id: ID! name: String! email: String age: Int } type Query { user(id: ID!): User users: [User!]! }
Và bạn có một query trong file src/queries/userQuery.graphql
:
# src/queries/userQuery.graphql query GetUserById($id: ID!) { user(id: $id) { id name email } }
Sau khi chạy graphql-codegen
với cấu hình phù hợp (bao gồm cả typescript
và typescript-operations
), file src/generated/graphql.ts
sẽ được sinh ra. Nó sẽ chứa các type như User
(từ plugin typescript
) và quan trọng hơn là các type cụ thể cho query GetUserById
(từ plugin typescript-operations
).
Các type được sinh ra cho query GetUserById
sẽ có hình dạng (shape) chính xác với những trường bạn đã yêu cầu trong query:
// Một phần (được đơn giản hóa) của src/generated/graphql.ts
export type User = {
__typename?: 'User';
id: string;
name: string;
email?: string | null;
age?: number | null;
};
// Type được sinh ra cụ thể cho query GetUserById
export type GetUserByIdQueryVariables = {
id: string;
};
export type GetUserByIdQuery = {
__typename?: 'Query';
user?: {
__typename?: 'User';
id: string;
name: string;
email?: string | null;
} | null;
};
Giải thích:
User
: Type này được sinh ra từ schema bởi plugintypescript
. Nó chứa tất cả các trường được định nghĩa trong schema.GetUserByIdQueryVariables
: Type này được sinh ra bởi plugintypescript-operations
. Nó định nghĩa kiểu cho biến ($id
) mà queryGetUserById
yêu cầu. Điều này giúp bạn truyền đúng kiểu dữ liệu cho biến khi thực hiện query.GetUserByIdQuery
: Type này cũng được sinh ra bởitypescript-operations
. Nó định nghĩa kiểu cho kết quả trả về của queryGetUserById
. Lưu ý: Nó chỉ bao gồm các trườngid
,name
,email
bên trong đối tượnguser
, bởi vì đó là những gì bạn đã yêu cầu trong query GraphQL!
Bây giờ, khi bạn sử dụng query này trong code TypeScript/React của mình (ví dụ, sử dụng một thư viện client như Apollo Client hay URQL), bạn có thể gán kiểu cho dữ liệu nhận được một cách an toàn:
// Ví dụ sử dụng type được sinh ra trong code React/TypeScript (dùng Apollo Client hooks)
import { useQuery } from '@apollo/client';
import { GetUserByIdDocument } from './graphql/userQuery.graphql'; // Thường các tools sinh ra Document node
import { GetUserByIdQuery, GetUserByIdQueryVariables } from './generated/graphql'; // Import type được sinh ra
interface UserDetailProps {
userId: string;
}
function UserDetail({ userId }: UserDetailProps) {
// useQuery hook có thể nhận generic type cho dữ liệu và biến
const { data, loading, error } = useQuery<GetUserByIdQuery, GetUserByIdQueryVariables>(GetUserByIdDocument, {
variables: { id: userId },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
// 'data' bây giờ đã có kiểu GetUserByIdQuery
// Trình soạn thảo code sẽ biết data.user có kiểu { id: string; name: string; email?: string | null }
// và cung cấp autocompletion, kiểm tra lỗi
return (
<div>
<h2>User Details</h2>
<p>ID: {data?.user?.id}</p>
<p>Name: {data?.user?.name}</p>
{/* Trình soạn thảo code sẽ gợi ý 'email' nhưng không gợi ý 'age'
vì 'age' không có trong query GetUserById, phù hợp với kiểu GetUserByIdQuery */}
{data?.user?.email && <p>Email: {data.user.email}</p>}
</div>
);
}
export default UserDetail;
Lợi ích thấy rõ:
- Khi bạn gõ
data?.user?.
, trình soạn thảo code sẽ ngay lập tức gợi ýid
,name
, vàemail
. Nó sẽ không gợi ý trườngage
vì bạn không yêu cầu trường đó trong queryGetUserById
. - Nếu bạn cố gắng truy cập
data.user.age
, TypeScript sẽ báo lỗi ngay lập tức tại thời điểm biên dịch, giúp bạn phát hiện lỗi logic sớm. - Kiểu dữ liệu cho biến
variables
cũng được kiểm tra, đảm bảo bạn truyền đúng kiểu string choid
.
Đây chỉ là một ví dụ nhỏ. Với các ứng dụng lớn hơn với nhiều query, mutation và cấu trúc dữ liệu phức tạp, việc tự động sinh type sẽ giúp bạn tiết kiệm hàng giờ đồng hồ làm việc thủ công, loại bỏ hàng trăm lỗi tiềm ẩn và biến quá trình phát triển thành một trải nghiệm dễ chịu hơn rất nhiều.
Các Bước Tiếp Theo
Để áp dụng điều này vào project của bạn:
- Đảm bảo bạn đã cài đặt Node.js và npm/yarn.
- Cài đặt các package cần thiết (
@graphql-codegen/cli
,@graphql-codegen/typescript
,@graphql-codegen/typescript-operations
). - Tạo file
codegen.yml
và cấu hình nó trỏ đến schema GraphQL của bạn và các file chứa operations của bạn. - Chạy lệnh
npx graphql-codegen
lần đầu tiên. - Tích hợp lệnh này vào script
package.json
để chạy dễ dàng, hoặc cấu hình watch mode cho môi trường phát triển. - Import và sử dụng các type được sinh ra trong code Front-end của bạn.
Việc tự động sinh TypeScript types từ GraphQL schema và operations là một kỹ thuật không thể thiếu đối với bất kỳ dự án Front-end nghiêm túc nào sử dụng cả GraphQL và TypeScript. Nó là cầu nối vững chắc giữa thế giới dữ liệu của Back-end và thế giới kiểu của Front-end, mang lại sự an toàn, hiệu quả và niềm vui trong lập trình.
Hãy thử áp dụng nó vào project tiếp theo của bạn và cảm nhận sự khác biệt nhé!
Comments