跳到主要内容

protoc-gen-ts_proto

protoc-gen-ts_proto 是从 Protocol Buffers 定义文件生成 TypeScript 类型定义的工具,适用于后端使用 gRPC 提供服务的场景。

工具说明

核心特性

  • 文件映射:每个 .proto 文件对应生成一个 TypeScript 文件
  • 类型精确:支持完整的 Protocol Buffers 特性
  • 性能优化:生成的代码经过性能优化
  • 依赖处理:通过 import 自动处理文件间的依赖关系

适用场景

  • 后端使用 gRPC 提供服务
  • 需要精确的类型定义
  • 希望按模块清晰组织代码(每个 proto 文件一个 TypeScript 文件)

安装

安装 protoc

# macOS
brew install protobuf

# Linux
apt-get install protobuf-compiler

# 或从源码编译
# https://github.com/protocolbuffers/protobuf/releases

安装 protoc-gen-ts_proto

ts-proto 是一个 npm 包,有多种安装方式:

方式1:在后端项目中安装(如果后端项目没有 package.json,需要先初始化)

# 如果后端项目是纯 Go 项目,没有 package.json,需要先初始化
npm init -y

# 然后安装 ts-proto
npm install -D ts-proto

安装后,protoc-gen-ts_proto 插件位于 ./node_modules/.bin/protoc-gen-ts_proto

方式2:全局安装(推荐,避免在后端项目中添加 npm 依赖)

npm install -g ts-proto

# 安装后,插件路径通常是:
# macOS/Linux: /usr/local/bin/protoc-gen-ts_proto 或 ~/.npm-global/bin/protoc-gen-ts_proto
# 可以通过 which protoc-gen-ts_proto 查看实际路径

方式3:使用 Docker(推荐,完全避免本地环境依赖)

无需安装,直接使用 Docker 运行,见下面的 Docker 方式示例。

提示

对于纯后端项目,推荐使用方式2(全局安装)方式3(Docker),避免在后端项目中添加 npm 依赖和 package.json 文件。

使用方式

后端 Makefile 配置

方式1:使用本地 node_modules(需要先 npm install)

FRONT_TYPES_DIR ?= ./frontend-types
PROTO_DIR := handler/grpc/proto
PROTO_FILES := $(shell find $(PROTO_DIR) -name "*.proto")

generate-front-types: ## 生成前端TypeScript类型定义
@echo "generating frontend types to $(FRONT_TYPES_DIR)..."
@mkdir -p $(FRONT_TYPES_DIR)
protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto \
--proto_path=$(PROTO_DIR) \
--ts_proto_out=$(FRONT_TYPES_DIR) \
--ts_proto_opt=exportCommonSymbols=false,outputClientImpl=false,esModuleInterop=true,onlyTypes=true \
$(PROTO_FILES)
@echo "Frontend types generated to $(FRONT_TYPES_DIR)"

方式2:使用全局安装的插件(推荐)

FRONT_TYPES_DIR ?= ./frontend-types
PROTO_DIR := handler/grpc/proto
PROTO_FILES := $(shell find $(PROTO_DIR) -name "*.proto")
PROTOC_GEN_TS_PROTO := $(shell which protoc-gen-ts_proto)

generate-front-types: ## 生成前端TypeScript类型定义
@mkdir -p $(FRONT_TYPES_DIR)
protoc --plugin=protoc-gen-ts_proto=$(PROTOC_GEN_TS_PROTO) \
--proto_path=$(PROTO_DIR) \
--ts_proto_out=$(FRONT_TYPES_DIR) \
--ts_proto_opt=exportCommonSymbols=false,outputClientImpl=false,esModuleInterop=true,onlyTypes=true \
$(PROTO_FILES)

说明

  • PROTOC_GEN_TS_PROTO := $(shell which protoc-gen-ts_proto):动态查找全局安装的插件路径
  • --ts_proto_opt=exportCommonSymbols=false,outputClientImpl=false,esModuleInterop=true,onlyTypes=true:只生成类型定义,不生成序列化方法和公共符号

命令行参数说明

  • --plugin=protoc-gen-ts_proto=路径:指定 TypeScript 生成器插件路径
    • 本地安装:./node_modules/.bin/protoc-gen-ts_proto
    • 全局安装:使用 $(shell which protoc-gen-ts_proto) 动态查找
  • --proto_path:指定 proto 文件的搜索路径,可以多次指定
  • --ts_proto_out:指定 TypeScript 文件输出目录
  • --ts_proto_opt:生成选项,多个选项用逗号分隔

方式3:使用 Docker(推荐,完全避免本地环境依赖)

FRONT_TYPES_DIR ?= ./frontend-types

generate-front-types: ## 生成前端TypeScript类型定义
@echo "generating frontend types to $(FRONT_TYPES_DIR)..."
@mkdir -p $(FRONT_TYPES_DIR)
docker run --rm \
-v $(PWD)/api:/api:ro \
-v $(PWD)/third_party:/third_party:ro \
-v $(PWD)/$(FRONT_TYPES_DIR):/output \
node:18-alpine sh -c "\
apk add --no-cache protobuf && \
npm install -g ts-proto && \
PROTOC_GEN_TS_PROTO=\$$(which protoc-gen-ts_proto) && \
protoc --plugin=protoc-gen-ts_proto=\$$PROTOC_GEN_TS_PROTO \
--proto_path=/api \
--proto_path=/third_party \
--ts_proto_out=/output \
--ts_proto_opt=exportCommonSymbols=false,outputClientImpl=false,esModuleInterop=true,onlyTypes=true \
/api/**/*.proto"
@echo "Frontend types generated to $(FRONT_TYPES_DIR)"

生成结果

文件结构

每个 .proto 文件对应生成一个 TypeScript 文件:

frontend-types/
├── user/
│ └── v1/
│ ├── common.ts # 从 common.proto 生成(公共类型)
│ ├── user.ts # 从 user.proto 生成(用户服务)
│ └── role.ts # 从 role.proto 生成(角色服务)

生成文件示例

common.ts(公共类型):

// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.10.1
// protoc v6.33.0
// source: user/v1/common.proto

export interface ResponseHeader {
code: number;
reason: string;
msg: string;
}

export interface PaginationRequest {
pageNum: number;
pageSize: number;
}

export interface PaginationReply {
pageNum: number;
pageSize: number;
total: number;
}

user.ts(用户服务,使用 onlyTypes=true 生成):

// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.10.1
// protoc v6.33.0
// source: user/v1/user.proto

/* eslint-disable */

import type { PaginationRequest, PaginationReply } from "./common";

export interface GetUserRequest {
uid: number;
}

export interface GetUserReply {
code: number;
msg: string;
data: UserInfo | undefined;
}

export interface ListUserRequest {
pageNum: number;
pageSize: number;
keyword?: string | undefined;
}

export interface ListUserReply {
code: number;
msg: string;
data: UserInfo[];
pagination: PaginationReply | undefined;
}

export interface UserInfo {
uid: number;
username: string;
email: string;
phone: string;
createdAt: string;
}

export interface User {
GetUser(request: GetUserRequest): Promise<GetUserReply>;
ListUser(request: ListUserRequest): Promise<ListUserReply>;
CreateUser(request: CreateUserRequest): Promise<CreateUserReply>;
UpdateUser(request: UpdateUserRequest): Promise<UpdateUserReply>;
DeleteUser(request: DeleteUserRequest): Promise<DeleteUserReply>;
}

前端集成

复制生成的代码

将后端项目的 frontend-types/ 目录复制到前端项目的 src/api/user/v1/ 目录。

使用生成的类型

import type { GetUserRequest, UserInfo } from '@/api/user/v1/user';
import type { PaginationRequest } from '@/api/user/v1/common';

// 使用生成的类型
const request: GetUserRequest = {
uid: 123,
};

// 使用类型定义
const userInfo: UserInfo = {
uid: 123,
username: 'john',
email: 'john@example.com',
phone: '13800138000',
createdAt: '2024-01-01T00:00:00Z',
};

自行封装 API 调用

由于 protoc-gen-ts_proto 只生成类型定义,需要自行封装 API 调用:

import axios from 'axios';
import type { GetUserRequest, GetUserReply, ListUserRequest, ListUserReply } from '@/api/user/v1/user';

// 获取用户信息
export async function getUser(request: GetUserRequest): Promise<GetUserReply> {
const response = await axios.get<GetUserReply>(
`/api/v1/user/${request.uid}`
);
return response.data;
}

// 获取用户列表
export async function listUser(request: ListUserRequest): Promise<ListUserReply> {
const response = await axios.get<ListUserReply>('/api/v1/user', {
params: {
pageNum: request.pageNum,
pageSize: request.pageSize,
keyword: request.keyword,
},
});
return response.data;
}

项目实践示例

项目结构

user-service/
├── api/
│ └── user/
│ └── v1/
│ ├── common.proto # 公共类型定义
│ ├── user.proto # 用户服务定义
│ └── role.proto # 角色服务定义
├── third_party/ # 第三方 proto 文件(如 google/api)
│ └── google/
│ └── api/
│ └── annotations.proto
├── Makefile
└── package.json # 需要安装 ts-proto 依赖

proto 文件示例

api/user/v1/common.proto

syntax = "proto3";

package api.user.v1;

message PaginationRequest {
int32 page_num = 1;
int32 page_size = 2;
}

message PaginationReply {
int32 page_num = 1;
int32 page_size = 2;
int32 total = 3;
}

api/user/v1/user.proto

syntax = "proto3";

package api.user.v1;

import "user/v1/common.proto";
import "google/api/annotations.proto";

service User {
rpc GetUser(GetUserRequest) returns (GetUserReply) {
option (google.api.http) = {
get: "/api/v1/user/{uid}"
};
}

rpc ListUser(ListUserRequest) returns (ListUserReply) {
option (google.api.http) = {
get: "/api/v1/user"
};
}
}

message GetUserRequest {
int32 uid = 1;
}

message GetUserReply {
int32 code = 1;
string msg = 2;
UserInfo data = 3;
}

message ListUserRequest {
int32 page_num = 1;
int32 page_size = 2;
string keyword = 3;
}

message ListUserReply {
int32 code = 1;
string msg = 2;
repeated UserInfo data = 3;
PaginationReply pagination = 4;
}

message UserInfo {
int32 uid = 1;
string username = 2;
string email = 3;
string phone = 4;
string created_at = 5;
}

编译流程

安装依赖

在后端项目中安装 ts-proto

npm install -D ts-proto

执行生成命令

make generate-front-types

或直接使用 protoc 命令:

protoc --plugin=protoc-gen-ts_proto=$(which protoc-gen-ts_proto) \
--proto_path=./api \
--proto_path=./third_party \
--ts_proto_out=./frontend-types \
--ts_proto_opt=exportCommonSymbols=false,outputClientImpl=false,esModuleInterop=true,onlyTypes=true \
api/user/v1/*.proto

生成的文件

后端生成的文件(使用 --go_out--go-grpc_out 等):

  • *.pb.go:Protocol Buffers 消息类型
  • *_grpc.pb.go:gRPC 服务定义
  • *_http.pb.go:HTTP 网关代码(如果使用 Kratos 等框架)

前端生成的文件(使用 --ts_proto_out):

  • common.ts:公共类型定义
  • user.ts:用户服务类型定义(包含消息类型和服务接口)
  • role.ts:角色服务类型定义

ts_proto_opt 参数详解

--ts_proto_opt 用于控制生成的 TypeScript 代码的格式和内容。多个选项用逗号分隔,格式为 key1=value1,key2=value2

核心选项

esModuleInterop=true

启用 ES 模块互操作,生成的代码使用 ES6 import/export 语法,兼容 TypeScript 的模块系统。

示例

// 启用后生成的代码
import type { UserInfo } from "./common";
export interface GetUserRequest { ... }

onlyTypes=true(推荐用于前端类型定义)

只生成类型定义(interfacetypeenum),不生成序列化方法(encodedecodefromJSONtoJSON 等)。

适用场景

  • 前端项目只需要类型定义,不需要序列化功能
  • 前端通过 HTTP REST API 调用,使用 JSON 格式,由 axios 等库自动处理序列化

生成内容对比

不使用 onlyTypes=true(默认):

export interface UserInfo {
uid: number;
username: string;
}

// 会生成序列化方法
export function encodeUserInfo(message: UserInfo, writer: Writer): Writer;
export function decodeUserInfo(input: Reader | Uint8Array, length?: number): UserInfo;
export function fromJSONUserInfo(object: any): UserInfo;
export function toJSONUserInfo(message: UserInfo): unknown;

使用 onlyTypes=true

export interface UserInfo {
uid: number;
username: string;
}
// 不生成序列化方法,只保留类型定义

服务生成选项

outputServices=generic-definitions

生成通用的服务接口定义,格式为 export interface ServiceName { ... }

生成示例

export interface UserService {
GetUser(request: GetUserRequest): Promise<GetUserReply>;
ListUser(request: ListUserRequest): Promise<ListUserReply>;
}

outputServices=grpc-js

生成 gRPC-JS 客户端代码,适用于前端直接使用 gRPC-Web 调用后端服务。

outputServices=grpc-web

生成 gRPC-Web 客户端代码。

类型选项

useExactTypes=false

使用更宽松的类型定义,允许对象包含额外属性(不严格匹配接口定义)。

示例

// useExactTypes=false(默认)
export interface UserInfo {
uid: number;
username: string;
// 允许额外属性,如 email、phone 等
}

// useExactTypes=true
export interface UserInfo {
uid: number;
username: string;
// 严格匹配,不允许额外属性
}

outputEncodeMethods=false

不生成 encode 方法(Protocol Buffers 二进制编码)。

outputJsonMethods=false

不生成 fromJSONtoJSON 方法。

outputPartialMethods=false

不生成 Partial 类型相关的方法。

outputType=type

指定输出类型为 type 而不是 interface(较少使用)。

推荐配置

前端类型定义(推荐)

--ts_proto_opt=exportCommonSymbols=false,outputClientImpl=false,esModuleInterop=true,onlyTypes=true

说明

  • exportCommonSymbols=false:不生成 protobufPackage 等公共符号常量
  • outputClientImpl=false:不生成客户端实现代码,只保留服务接口定义
  • esModuleInterop=true:使用标准 ES 模块语法
  • onlyTypes=true:只生成类型定义,不生成序列化方法,减少生成文件大小

完整示例

FRONT_TYPES_DIR ?= ./frontend-types
PROTO_DIR := handler/grpc/proto
PROTO_FILES := $(shell find $(PROTO_DIR) -name "*.proto")
PROTOC_GEN_TS_PROTO := $(shell which protoc-gen-ts_proto)

generate-front-types:
@mkdir -p $(FRONT_TYPES_DIR)
protoc --plugin=protoc-gen-ts_proto=$(PROTOC_GEN_TS_PROTO) \
--proto_path=$(PROTO_DIR) \
--ts_proto_out=$(FRONT_TYPES_DIR) \
--ts_proto_opt=exportCommonSymbols=false,outputClientImpl=false,esModuleInterop=true,onlyTypes=true \
$(PROTO_FILES)

完整选项列表

参考 ts-proto 官方文档 查看所有可用选项及其详细说明。

参考