microcms-js-sdkをtype-safeに扱いたい
普段私は、microCMSを頻繁に利用させていただいています。
そしてありがたいことに、コンテンツのやり取りを行う為に公式さんがmicrocms-js-sdkというSDKを提供してくれています!
公式からこういうSDKが出ているのはすごく嬉しいですね〜!
そんなSDKを日々使う中、私個人的にある不満が出てきました。
- endpontを誤ってtypoしてしまうことがある...
- 型定義を開発者側の裁量に任せられている。もっと楽したい!
今回はそんなお悩みがモチベーションです。
ざっくりまとめると、俺が考える最強のtype-safeなSDKにカスタマイズしてやるぜ! です。
今回の記事のソースコードはリポジトリにて管理していますので参考になれば幸いです。
🤔 課題の整理
では、具体的に何を解決したいのか課題化していきます。
課題.1 endpoint指定が開発者の裁量に任せられている。
microcms-js-sdk
の利用方法をあらためて確認してみましょう。
import { createClient } from 'microcms-js-sdk';
const client = createClient({
serviceDomain: 'foo',
apiKey: 'xxxxxxxx'
});
client
.getList({
endpoint: 'piyo'
})
.then((res) => console.log(res))
.catch((err) => console.log(err));
createClient
を用いてクライアントを作成して、getList
を用いてリストコンテンツを取得する。
これが一番基本の利用方法かと思います。
この際に、endpoint
を指定する必要があるのですが、これがmicroCMSのワークスペースでどのようなエンドポイントを作っているのか。というのを開発側で理解していないと404エラーなどになってしまう問題を抱えています。
例えば、aspida + microCMS という組み合わせでエンドポイントをいい感じにする方法があります。
aspidaを用いると実際の呼び出しは下記のようになります。
await client.piyo.$get();
オブジェクトを掘るような形でAPIを呼び出せるのは直感的ですね。
私も以前はよく利用していました。
しかし、microCMSのAPI数が増えた際にクライアントを作成するための似たようなファイルを大量に作成する必要があるなど、ファイル管理がちょっと億劫になってしまい、もやもやしてしまいました。
少し複雑なREST APIクライアントを作成する際にはaspidaは重宝しますが、シンプルなAPI呼び出し設計になっているmicroCMSには少しオーバースペックかなと感じました。
私の理想系としては、下記のようにendpointの指定がtype-safeになればなぁと思います。
client
.getList({
endpoint: 'piyo' // type-safe
})
.then((res) => console.log(res))
.catch((err) => console.log(err));
課題.2 型定義を開発者側の裁量に任せられている。
次の課題です。
microcms-js-sdk
は、TypeScriptのサポートもしています。
例えば、下記のような形で型定義を当てることができます。
type Piyo = {
title: string;
body: string;
}
const result = await client.getList<Piyo>({ endpoint: 'piyo' });
console.log(result.contents[0]);
// {
// id: string;
// title: string;
// body: string;
// createdAt: string;
// updatedAt: string;
// publishedAt?: string;
// revisedAt?: string;
// }
しかし、この型定義にもエンドポイントと同様に開発者の裁量が求められています。
ヒューマンエラーケースとして、エンドポイントと型定義が間違えて指定しまうなどが考えられるかなと思います。
client.getList<Piyo>({ endpoint: 'hoge' });
他にも裁量に任せられている課題として、microCMSのAPIのget
にはfields
というクエリパラメータがあります。
これは、APIのレスポンスに含まれるコンテンツの中で取得する要素を指定できるというものです。
実際の利用方法は下記のような形です。
client.getList({
endpoint: 'piyo',
queries: {
fields: ['id', 'title', 'publishedAt']
},
});
しかしながら、TypeScriptを扱う身としては、type-safeを身体が求めてしまいます。
まず、fields
の型定義ですがmicrocms-js-sdk
では、string[]|string
となっています。
これが、keyof(Piyo | MicroCMSListContent)
となっていて欲しいですね...
そうなっていることで、存在しないフィールドの指定の防止がtype-safeに行うことができます。
次にレスポンスについてです。
fields
に指定したパラメーターにより実際のレスポンスは指定した要素のみになっていますが、getList
の型定義を怠った場合には、残念なことが起こります。
client.getList<Piyo>({
endpoint: 'piyo',
queries: {
fields: ['id', 'title', 'publishedAt']
},
});
// こうなってしまう。。😭
// {
// id: string;
// title: string;
// body: string;
// createdAt: string;
// updatedAt: string;
// publishedAt?: string;
// revisedAt?: string;
// }
こうなってしまったら、型が定義されているから呼び出せるのに、実データが存在しない...なんてことが起きてしまいます。
ちゃんとしたレスポンスを求めるなら下記のようにしないといけませんね。
client.getList<Pick<Piyo, 'id'|'title'|'publishedAt'>>({
endpoint: 'piyo',
queries: {
fields: ['id', 'title', 'publishedAt']
},
});
// {
// id: string;
// title: string;
// publishedAt?: string;
// }
以上のように型定義全般が開発者の裁量に任せられています。
「型補完の恩恵をもっと楽に受けたい!!」という私の堕落した心が叫んでいます。
こうなるといいなぁと私は思います。
client.getList({
endpoint: 'piyo', // type-safe
queries: {
fields: ['id', 'title', 'publishedAt'] // type-safe
},
});
// Response Types:
// {
// contents: Pick<Piyo & MicroCMSContent, 'id'|'title'|'publishedAt'>[];
// totalcunt: number;
// offset: number;
// limit: number;
// }
さて、一旦不満の洗い出しが完了しました。
実装に移りましょう。
🔧 実装
実装.1 エンドポイントとレスポンスの型を定義する。
今回求めていたのは基本的には開発者に都度裁量を求められてしまうということでした。
ですので、まずはエンドポイントとそのエンドポイントと1対1になるように型定義を行います。
type Endpoints = {
list: {
piyo: Piyo;
}
object: {
hoge: Hoge;
}
}
microCMSのAPIにはリスト型とオブジェクト型、2種類のAPIを作成することができます。
それによってmicrocms-js-sdk
で呼び出しを行う関数が異なる為、棲み分けを行う意味でもlist
とobject
というkeyで分けました。
今回は主に、list
を取り扱っていきます。
実装.2 MicroCMSQueriesの型定義を改修する。
fields
の型定義がstring[]|string
であると前述しました。
これをどうにかする為に定義を改修しましょう。
import { MicroCMSQueries, MicroCMSListContent } from 'microcms-js-sdk'
type GetListQueries<F> = {
fields?: F[];
} & Omit<MicroCMSQueries, 'fields'>;
const queries: GetListQueries<keyof (Piyo & MicroCMSListContent)> = {
fields: ['id', 'title', 'publishedAt'] // type-safe
}
今回は、GetListQueries
という型名で進めます。
MicroCMSQueries
からfields
を除外して、新しく定義したfields
を当て込んでいます。
こうすることで、MicroCMSQueries
に更新があっても対応がしやすくなると踏んで今回はこのようにしました。
実装.3 GetListRequestの型定義を改修する。
getList
を実行する上で必要な型定義GetListRequest
というのがmicrocms-js-sdk
にはあります。
既存のものだと、endpointやqueriesなどの用途が合わないので改修を施します。
type ClientEndPoints = {
list?: {
[key: string]: any;
};
object?: {
[key: string]: any;
};
};
type GetListRequest<
T extends ClientEndPoints,
E extends keyof T['list'],
C extends T['list'][E],
F extends keyof C
> = {
endpoint: E;
queries?: GetListQueries<F>;
} & Omit<_GetListRequest, 'endpoint' | 'queries'>;
やたらとジェネリクスの引数が多くなってしまっていますが、クライアントを立ち上げる際に引数を渡したい都合上、継承を施したいがために渡しています。後で効いてきます。
実装.4 MicroCMSListResponseの型定義を改修する。
現在のMicroCMSListResponse
の型定義を見てみます。
contents: (T & MicroCMSListContent)[];
となっていますね。このままではせっかくfields
を型安全にしたけれどレスポンスがいい感じに帰ってきてくれません...改修します!
import { MicroCMSListResponse } from 'microcms-js-sdk'
type GetListResponse<C, F extends keyof C> = {
contents: Pick<C, F>[];
totalCount: number;
offset: number;
limit: number;
} & Omit<MicroCMSListResponse<C>, 'contents'>;
これにより、指定した要素をレスポンスとして返してもらえるような型になりました。
実装.5 microcms-js-sdkをラップしていく
ここまででgetList
を型安全に扱う上で必要な型定義が揃いました。
ここからは実際にmicrocms-js-sdk
をラッピングして型安全化していきます。
craeteClient
をラップしていきます。
import {
createClient as _createClient,
MicroCMSClient
} from 'microcms-js-sdk';
const createClient = (clientArg: MicroCMSClient) => {
const _client = _createClient(clientArg);
const getList = _client.getList;
return { getList };
};
const client = createClient({
serviceDomain: 'hoge',
apiKey: 'xxxxxx'
});
次に、エンドポイントの指定とレスポンスを型安全にカスタマイズしていきます。
import {
createClient as _createClient,
- MicroCMSClient
+ MicroCMSClient,
+ MicroCMSListContent
} from 'microcms-js-sdk';
+ type Endpoints = {
+ list: {
+ piyo: Piyo;
+ }
+ }
- export const createClient = (clientArg: MicroCMSClient) => {
+ export const createClient = <T extends Endpoints = Endpoints>(
+ clientArg: MicroCMSClient
+ ) => {
const _client = _createClient(clientArg);
- const getList = _client.getList;
+ const getList = <
+ E extends keyof T['list'],
+ C extends T['list'][E] & MicroCMSListContent
+ >({
+ endpoint
+ }: {
+ endpoint: E;
+ }) => {
+ return _client.getList<C>({
+ endpoint: String(endpoint)
+ });
+ };
return { getList };
};
const client = createClient({
serviceDomain: 'hoge',
apiKey: 'xxxxxx'
});
client.getList({
endpoint: 'piyo' // type-safe
})
// Response Types:
// {
// contents: (Piyo & MicroCMSListContent)[];
// totalcunt: number;
// offset: number;
// limit: number;
// }
無事エンドポイントと連動してレスポンスの値を取得することができました。
次に、queries.fields
に型保管を効かせて、レスポンスの型定義に連動されるようにします。
import {
createClient as _createClient,
MicroCMSClient,
MicroCMSListContent
} from 'microcms-js-sdk';
+ import {
+
+ } from './type'
type Endpoints = {
list: {
piyo: Piyo;
}
}
+ // GetListQueries を MicroCMSQueries にパースする。
+ const queriesParser = <T>(queries: GetListQueries<T>): MicroCMSQueries => {
+ return { ...queries, fields: queries.fields?.map((v) => String(v)) };
+ };
+
export const createClient = <T extends Endpoints = Endpoints>(
clientArg: MicroCMSClient
) => {
const _client = _createClient(clientArg);
const getList = <
E extends keyof T['list'],
- C extends T['list'][E] & MicroCMSListContent
+ C extends T['list'][E] & MicroCMSListContent,
+ F extends keyof C
>({
- endpoint
+ endpoint,
+ queries = {},
+ ...arg
- }: {
- endpoint: E;
- }) => {
- return _client.getList<C>({
+ }: GetListRequest<T, E, C, F>): Promise<GetListResponse<C, F>> => {
+ return _client.getList({
- endpoint: String(endpoint)
+ endpoint: String(endpoint),
+ queries: queriesParser(queries),
+ ...arg
});
};
return { getList };
};
const client = createClient({
serviceDomain: 'hoge',
apiKey: 'xxxxxx'
});
client.getList({
endpoint: 'piyo',
queries: {
fields: ['id', 'title', 'publishedAt'] // type-safe
}
});
// Response Types:
// {
// contents: Pick<Piyo & MicroCMSListContent, 'id'|'title'|'publishAt'>[];
// totalcunt: number;
// offset: number;
// limit: number;
// }
fields
が保管が効くようになり、指定した項目によってレスポンスが同期的に切り替わるようになりました。
あとは、残りのgetListDetail
やgetObject
などを同様にラップしていけば、型安全なmicrocms-js-sdk
の出来上がりです!
📢 microcms-ts-sdkの紹介
そして、今回実施した型安全な対応を施したSDKをnpmパッケージとして公開しました!!🎉
- getList
- getListDetail
- getObject
- create
- update
- delete
などの関数にも対応していますので、興味がある方...是非ご利用ください!!
(GitHubにstarつけてくれたらやる気につながります。。)
具体的な利用方法についてはREADMEを参照ください。
おわりに
今回は、個人的に感じていた課題感を無事解消することができました!!
皆さんの型安全ライフに貢献できたら幸いです。
Twitterに普段は生息しておりますのでよろしければこちらもお願いします!
それではこれにて
ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion