TypeSpecを使ってみるメモ

2024年6月30日
TypeSpecを使ってみるメモ

使いたい背景

APIを実装する場合、個人的には、Protocol BuffersのようなDSLでAPIを定義して、それを元にコードを生成する方法が一番しっくりくる。 REST APIの場合は、OpenAPI(Swagger)で仕様を作成して、そこからドキュメントなり、コードを生成したりなどできるけど、 正直YAMLを書くのが辛くて、苦痛な作業だと感じることが多い。

OpenAPIの仕様をGUIで作成するツールも存在するので。それを使えば良いという意見もあると思う。 だけど、仕様を書くのにも読むのにも外部のGUIツールが必要になってしまうのは、何か間違ってるように感じる。 仕様はコードと同じようにテキストで書くべきものだと思う。

あるいは、バックエンドAPIから自動生成するようなツールを使う方法もある。 ただし、この方法はバックエンドの言語やフレームワークに依存してしまうし、 自分がフロントエンド担当でバックエンドを作成しない場合などは、この方法を採用しづらい。

フロントでTypeScriptでAPIの型定義を書いていると、YAML書くよりずっと良いAPI仕様書では?と思うこともしばしばあって、 TypeScript風にAPIの仕様を書ければ理想的だと思っていた。

そんな中、この課題にぴったりなTypeSpecの存在を知ったので、試してみることにした。

TypeSpecとは

TypeSpecは、Microsoft製のAPIの設計と文書化のためのDSLで、TypeScript風の構文を使用してAPIの仕様を記述できる。

公式サイト: https://typespec.io/

インストールとセットアップ

TypeSpecをグローバルにインストール

npm install -g @typespec/compiler

バージョンの確認

tsp --version

新規プロジェクトの作成。自動的にmain.tsp, package.json, tspconfig.yamlなどのファイルが作成される。

tsp init

依存関係のインストール

tsp install

基本的な使用方法

main.tspファイルを作成する。 以下は公式サイトの例として記載されている内容。

https://typespec.io/

import "@typespec/http";

using TypeSpec.Http;

model Store {
  name: string;
  address: Address;
}

model Address {
  street: string;
  city: string;
}

@route("/stores")
interface Stores {
  list(@query filter: string): Store[];
  read(@path id: Store): Store;
}

プログラミング経験がある人であれば、特に文法を知らなくてもストアの一覧と詳細取得を行うAPIが定義されていることが理解できると思う。 OpenAPIのYAMLだとこの程度でもかなりの行数を書かないといけないので、TypeSpecの方が断然読みやすい。

OpenAPIの生成

以下のパッケージをインストールする。

npm install @typespec/openapi3

TypeSpecファイルをコンパイルして、OpenAPI仕様を生成する。

tsp compile main.tsp --emit @typespec/openapi3

tspconfig.yamlに設定を記載できる。

上記の--emitオプションは、以下の記載で毎回コマンドラインで指定する必要がなくなる。

emit:
  - "@typespec/openapi3"

--watchでファイル変更を監視して自動再コンパイルできる

tsp compile main.tsp --emit @typespec/openapi3 --watch

デフォルトだと、tsp-output/@typespec/openapi3以下にopenapi.yamlが生成される。 ルートにopenapi.yamlを生成する場合はtspconfig.yamlに以下の設定を追加する。

options:
  "@typespec/openapi3":
    emitter-output-dir: "{output-dir}"
    output-file: "openapi.yml"

output-dir: "{project-root}"

文法について

モデル定義

modelキーワードを使用して、APIで仕様するデータモデルを定義することができる。 TypeScript風にデフォルト値やOptionalなプロパティも表現できる。

model User {
  id: string;
  name: string = "Anonymous";
  description?: string;
}

デコレーターを使って、ドキュメントやバリデーション情報を追加することができる。

model User {
  @doc("ユーザーID")
  @maxLength(50)
  username: string;

  @doc("ユーザーのメールアドレス")
  @pattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
  email: string;

  @doc("ユーザーの年齢(18歳以上)")
  @minValue(18)
  age: int32;
}

enumを使って、列挙型を定義することもできる。

enum UserRole {
  Admin,
  Editor,
  Viewer
}

model User {
  role: UserRole;
}

オペレーション (operation)

REST APIでなんらかのエンドポイントを定義したいときは、opキーワードを使用する。

例えば、ユーザー情報を取得するGETエンドポイントを定義する場合は、以下のようになる。

op getUser(userId: string): User;

デコレーターを使って、オペレーションの付加情報を追加できる。

@get
@route("/users/{userId}")
op getUser(userId: string): User;

インターフェース (interface)

interfaceキーワードを使用してインターフェースを定義できる。 インターフェースを使って、複数のオペレーションをグループ化することができる。

ユーザー情報のCRUD操作をまとめたUsersインターフェースを定義する例。

interface Users {
	@get
	@route("/users")
	listUsers(limit?: int32, role?: string): User[];

	@get
	@route("/users/{userId}")
	getUser(userId: string): User;

	@post
	@route("/users")
	createUser(
		user: {
			name: string;
			email: string;
		},
	): User;

	@patch
	@route("/users/{userId}")
	updateUser(
		userId: string,
		user: {
			name?: string;
			email?: string;
		},
	): User;

	@delete
	@route("/users/{userId}")
	deleteUser(userId: string): void;
}

まとめ

実際に、API仕様書を書く案件で使ってみたところ、 ほとんどTypeScriptコードを書くのと同じように、API仕様書を作ることができた。 VS Codeのエクステンションを使って、静的型チェックのサポートを受けることもできるので、 文法ミスなどもすぐに気づくことができるのが良かった。

プログラミングには「驚き最小の法則」という考え方があるけど、 TypeSpecは、APIの仕様書を書くという観点で、TypeScriptプログラマーにとっての驚き最小を実現しているように感じた。

今後、TypeSpecがどれくらい普及していくかはわからないけど、 個人的には、今後のプロジェクトで積極的に使っていきたいと思う。

次の記事はこちら。