TypeScriptのGenerics Typeで一部の型引数のみを指定する

近況

自分はちゃんと勉強せず雰囲気でググりながらTypeScriptを使っているのに、人にTypeScriptを教えている状況。

Generic Type

TypeScriptにおいてはある型Tに対し、別の型Kを使って型を定義したい場合以下のように記述できる。

interface T<K> = {
  P1: K
};

type TNumber = T<number>; // { P1: number }

このとき型引数にはデフォルトとなる型を指定できる

interface T<K = number> {
  P1: K
};

課題

今回問題になったのが、Generic Typeにおいて複数の型引数を持ち、かつそれぞれの型引数にデフォルト型が設定されている型に対し、一部の型のみ指定するような型を定義したいというところ。

具体的には、express.jsにおいて、req.body.prop にアクセスする際、no-unsafe-member-access でESLintから怒られてしまう、これを回避したいという課題があった。

これ自体は DefinitlyTypedで定義されているように、req の型に Request<core.ParamDictionary, any, { prop: SomeType }>というように指定すれば回避できるが、これだけのために使いもしない core.ParamDictionary をimportするのは微妙では…?という感情が芽生えたのでなんとかしたい

github.com

app.get("/", (req: Request<core.ParamDictionary, any, { prop: SomeType }>, res: Response) => {
    const a: SomeType = req.body.prop;
});

(こういう感じで定義はできる)

Rustでは以下のように指定をスキップする型引数に _ を指定することで実現できるが、TypeScriptにはそういう機能は今はないらしい。

let a: SomeType<_, _, String> = ...;

(id:mizdra さんに聞いたところ、以下で議論はされているらしい)

github.com

と困っていたところ、上司のid:ma2sakaさんからなんか infer というのを使えばできるらしいと聞いたのでとりあえず調べてやってみた、という次第である。

infer

TypeScriptには Conditional Typeというのがあり、ある型 TK を継承している場合とそうでない場合に型を場合分けできるらしい。

type A<T> = T extends K ? B : C;

この際、KがGeneric Typeである場合には以下のようにinferを使うことで、型を推論して取り出すことが可能らしい。

type A<T> = T extends K<infer F, infer S> ? B<F, S> : C;

これを利用すると、express.Request<P, ResBody, ReqBody, ReqQuery, Locals> という型の ReqBody だけ指定したい場合以下の型を定義することで可能となる。

type CustomRequest<T> = Request extends Request<infer P, infer ResBody, infer ReqBody, infer ReqQuery, infer Locals> ? Request<P, ResBody, T, ReqQuery, Locals> : never;

Request という型は何も指定しなければデフォルト型が設定されるので infer で推論してもらえば各デフォルト型を取り出して使える、これを使ってデフォルト引数の指定を実質的に飛ばすことができる。

一応 ReqBody 以降の型引数も記述したが、 ReqBody 以降の型引数は省略できるので、実用上は以下みたいになるだろうか

type CustomRequest<ReqBody> = Reqeust extends Request<infer P, infer ResBody> ? Request<P, ResBody, ReqBody> : never;

正直型引数の指定を飛ばしたいだけなのにここまで必要なのはどうかとも思うが…そのあたりは今後の機能拡充に期待したい。