🎄

inferと実例 UnArray<T> / TypeScript一人カレンダー

2022/12/14に公開

こんにちは、クレスウェア株式会社の奥野賢太郎 (@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、知らないと特殊な力のようですがちゃんとドキュメントもあります。

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types

使い方についてですが、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といいます。

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html

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, T6aプロパティを持たないため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として定められています。

https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints

TUを満たしていないとだめですよ、という意味です。function getName(id: string)という関数があったとしてidstringを満たしていないとだめですよ、という表現とよく似ています。ジェネリクスの型パラメータに出てくるextendsは引数型アノテーションを表記するときの:のようなものだと思ってください。

他にもextendsが出てくる箇所があります。classの継承をしたいときです。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes/extends

このように、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