inferと実例 UnArray<T> / TypeScript一人カレンダー
こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2022の3日目です。昨日は『Awaited<T>
』を紹介しました。
infer
ReturnType<T>
やAwaited<T>
を学ぶと、こういった型はTypeScriptの何か特殊な力で作られているのか?と気になってきます。答えはいいえ、別に特殊な力でも秘められた力でもなく、ちゃんとAPIとして公開されています。TypeScriptの提供するUtility Typesのほとんどの型は、基礎的なAPIをもとに組み合わせられた、いわば「TypeScript公式おすすめの組み合わせ」に過ぎません。そして我々もその基礎的なAPIを活用することで自作することができるのです。
今回紹介するのはinfer
、そしてinfer
を使った実例の2つです。
昨日までに紹介したReturnType<T>
やAwaited<T>
などは、TypeScript公式のlib.es5.d.ts
というファイルに記載があります。リンク先は本日付のmain
branchです。
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
このように、普通にtype
で宣言されていることがわかり、右辺を確認すればその中身を確認できます。Awaited<T>
も見てみましょう。
type Awaited<T> =
T extends null | undefined ? T : // special case for `null | undefined` when not in `--strictNullChecks` mode
T extends object & { then(onfulfilled: infer F, ...args: infer _): any } ? // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
F extends ((value: infer V, ...args: infer _) => any) ? // if the argument to `then` is callable, extracts the first argument
Awaited<V> : // recursively unwrap the value
never : // the argument to `then` was not callable
T; // non-object or non-thenable
ちょっとウッとなりますね…。でも実装がどうなっているかを確認できる点は同じです。この中でウッとならずに確認していただきたいワードがひとつあります。それがinfer
です。紹介した2つの型には、どちらもinfer
が出てくることに注目してください。
このinfer
がTypeScriptの提供するAPI、知らないと特殊な力のようですがちゃんとドキュメントもあります。
使い方についてですが、TypeScriptの公式のテストコードにinfer
をたくさん試すものがあるため、それを見るというのもよいです。本記事ではテストコードから引用してアレンジしたシンプルな例を紹介します。
type Example<T> = T extends { a: infer U } ? U : never;
type T1 = Example<{ a: string }>;
// ^? T1 = string
type T2 = Example<{ a: number }>;
// ^? T2 = number
慣れていないとこれだけでも難しそうに見えてしまうかもしれませんが、{a: string}
であればstring
、{a: number}
であればnumber
が得られていることはなんとなくわかります。
T extends { a: infer U } ? U : never;
を詳しく見ていきますと、これはECMAScriptにおける三項演算子を模していると思ってよいです。すなわちなんらかの条件に基づいて分岐処理を実施しているということです。このようにTypeScriptの型宣言内で?:
を使うものをConditional Typesといいます。
T extends { a: infer U } ? U : never;
は日本語だとこう言いかえられます。
もし T型 が { a: any } を満たす型であれば、
その a プロパティに割り当てられている型を U という型パラメータに束縛する
そして U型 を返す
そうでなければ never型 を返す
この理解でもう一度見てみましょう。今度は例を増やしています。
type Example<T> = T extends { a: infer U } ? U : never;
type T3 = Example<{ a: string; b: number }>;
// ^? T3 = string
type T4 = Example<{ a: { a1: string; a2: number } }>;
// ^? T4 = { a1: string; a2: number }
type T5 = Example<string>;
// ^? T5 = never
type T6 = Example<{ b: number }>;
// ^? T6 = never
{ a: any }
限定ではなく、{ a: any }
を満たすものであればよいため、T3
の例が成り立つことがわかります。a
プロパティが何型を持っていてもよいためT4
も成り立ちます。T5
, T6
はa
プロパティを持たないためnever
が返ってきていることがわかります。
注意点としてinfer
はConditional Typesの中でのみ使います。「もし〜のとき、それが満たされるならinfer U
としてU
に束縛し…」のように読める必要があるためtype Example<T> = infer T
のようなことはできません。なお、ここでU
としているものはA
でもSOMETHING
でもなんでもよいです。慣習的にTypeの大文字T
の次のアルファベットであることからT
, U
, V
が好まれがちというだけです。
ReturnType<T>
を読んでみる
おめでとうございます!infer
が読めるようになりました。それでは実戦です、ReturnType<T>
も読んでみましょう。
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
このままでは長いので分けてみていきましょう、右辺のみに注目します。改行も入れておきました。
T extends (...args: any) => infer R
? R
: any;
だいぶ読みやすくなりました。これを日本語的に読んでみるとこうなります。
もし T型 が (...args: any) => any を満たす型であれば、
その関数の戻り型として割り当てられている型を R という型パラメータに束縛する
そして R型 を返す
そうでなければ any型 を返す
読むことができました!infer
は何個でも使えるためこんなこともできます。
type Example2<T> = T extends [infer A, infer B] ? { a: A; b: B } : never;
type T21 = Example2<[string, number]>;
// ^? { a: string; b: number; }
type T22 = Example2<[0, 1]>;
// ^? { a: 0; b: 1; }
type T23 = Example2<[0, 1, 2]>;
// ^? never, length 2を満たしてないため
type T24 = Example2<string[]>;
// ^? never, length が定まらないため
infer
への抵抗をなくすと色々試してみることができそうです。Awaited<T>
の詳細は今回は紹介しませんが、ぜひ同じように読んでみてください。なぜPromise<Promise<T>>
がT
になるのか、再帰の理由がわかります。
extends がたくさん出てきて難しい?
type ReturnType<T extends (...args: any) => any> =
先ほどの左辺を注目すると、ここにもextends
が出てきています。TypeScriptではextends
がしばしば登場するため意味をひとつで考えると難しく見えがちですが、実は異なります。
型パラメータの<T extends U>
は、Generic Constraintsとして定められています。
T
はU
を満たしていないとだめですよ、という意味です。function getName(id: string)
という関数があったとしてid
はstring
を満たしていないとだめですよ、という表現とよく似ています。ジェネリクスの型パラメータに出てくるextends
は引数型アノテーションを表記するときの:
のようなものだと思ってください。
他にもextends
が出てくる箇所があります。classの継承をしたいときです。
このように、TypeScriptを書いているとしばしば「意味の異なるextends
」が出てきてしまいますので、今自分の扱っているextends
はどういう状況におけるものなのか注意してください。
実例 UnArray<T>
さっそくinfer
を使って業務に役立つちょっとした型を作ってみましょう。紹介するのはUnArray<T>
、これは筆者の案件でも活躍中の自作Utility Typeです。
type UnArray<T> = T extends Array<infer A> | ReadonlyArray<infer A> ? A : never;
type T1 = UnArray<string[]>;
// ^? string
type T2 = UnArray<number[][]>;
// ^? number[]
type T3 = UnArray<UnArray<number[][]>>;
// ^? number
type T4 = UnArray<Array<string>>;
// ^? string
infer
を読めるようになると、Array<T>
のT
だけを抜き出していることがわかると思います。再帰してしまうと逆に使い勝手が悪いため、配列の配列である場合は、その数だけUnArray<T>
をネストするようなデザインになっています。ReadonlyArray<T>
にも対応していますが、ReadonlyArray<T>
そのものについては後日紹介します。
余談ですが、Iterable<T>
に対応しようと思って名前をYielded<T>
にしてみたところ、開発チームの他の仲間にすこぶる打ちにくいと言われやめたという経緯もあったりします。あんまり日頃打たないですもんねyield
…。ということでわかりやすさ重視でUnArray<T>
と名付けました。
明日は『実例 runRenderHook()』
本日は以上です。Conditional Typesとinfer
を学ぶことで、TypeScriptをもっと活用してみてください。明日は自作関数runRenderHook()
について紹介します。「テストコードを書いていて、こんなときどう書けばいいの?」といったちょっとしたつまずきポイントを解消してみます。それではまた。
Discussion