近況
自分はちゃんと勉強せず雰囲気でググりながら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するのは微妙では…?という感情が芽生えたのでなんとかしたい
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 さんに聞いたところ、以下で議論はされているらしい)
と困っていたところ、上司のid:ma2sakaさんからなんか infer
というのを使えばできるらしいと聞いたのでとりあえず調べてやってみた、という次第である。
infer
TypeScriptには Conditional Typeというのがあり、ある型 T
が K
を継承している場合とそうでない場合に型を場合分けできるらしい。
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;
正直型引数の指定を飛ばしたいだけなのにここまで必要なのはどうかとも思うが…そのあたりは今後の機能拡充に期待したい。