概要
Jestでテストケースを書く際変更漏れを起こさないよう、実際のAPIクラスの型をimplementsする形で実装しようとしました。
ところが、Promiseで返すべき型を与えないとunknownと推論されてしまうので、元の定義を引っ張ってくる型定義を作成しました。
環境
Typescript 3.7.2で確認
起こったこと
下のようなメソッドを持つAPIクラスを作成し、exportしています。
interface GetResponse {
id: number;
}
class OriginalApiClass {
public get(): Promise<AxiosResponse<GetResponse>> {
return null as any; // 実際のAPI処理
}
}
export const OriginalApi = new OriginalApiClass();
このファイルに対し、下のようなモックを定義しようとしたところ、TSの型チェックに引っかかりました。
type Api = {
[P in keyof typeof OriginalApi]: typeof OriginalApi[P];
}
// OriginalApiでpublicなメソッドをすべて定義させたいのでimplementsさせる
class Mock implements Api {
// 型 'unknown' を型 'AxiosResponse<GetResponse>' に割り当てることはできません。ts(2416)
public get() {
return new Promise(resolve => {
resolve({id: 1})
});
}
}
Promiseのジェネリクスパラメータに適切な型を渡してやれば解決する問題ですが、GetResponseをexportしてしまうと、他でサジェストに出てくるようになり邪魔なので、Promiseに渡すべき型を推論させる定義を作成しました。
作成した定義
export type PromiseParameter<T extends Record<string, any>, K extends keyof T> = (
T[K] extends (...args: any) => infer A // 1
? (A extends Promise<any> // 2
? (
A['then'] extends (resolve: (...args: infer B) => any, reject: any) => any // 3
? B[0] // 4
: never
)
: never
)
: never
)
type Result = PromiseParameter<typeof OriginalApi, 'get'>; // => AxiosResponse<GetResponse>
- 最初に、メソッドの戻り型
Aをinfer Aで取得しています。 - 後続でPromiseとして扱えるように、
AがPromiseのか判定します。 - Promiseだった場合、第1引数がresolve、第2引数がrejectなので、resolveの引数型
Bをinfer Bで取得しています。 - (自分の場合)第1引数以外不要なので、
B[0]でresolveの最初の引数の型を返しています。
これで、resolveで返されるべき型が取得できたので、あとはモックを以下のように書き直せば完成です。
class Mock implements Api {
// エラーが出なくなり、resolveで本来入れるべきパラメータがサジェストされるようになります。
public get() {
return new Promise<PromiseParameter<typeof OriginalApi, 'get'>(reject => {
resolve({
config: {},
data: {
id: 1,
},
headers: {},
status: 200,
statusText: 'OK',
}););
});
}
}