目標
TypeScript のドキュメントにある上級者向けの型たち https://www.typescriptlang.org/docs/handbook/advanced-types.htmlに出てくる
type Unpacked<T> = T extends (infer U)[]
? U
: T extends (...args: any[]) => infer U
? U
: T extends Promise<infer U>
? U
: T;
が何しているのかを読めるようになる。
また実際にinfer
を用いて実装できるようになるのが目的。
inferとは
https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-inference-in-conditional-types の抄訳
inferは日本語に表すと「推論」です。
TypeScriptのextendsを使うと、型での条件分岐が可能になります。(extendsについてもまとめたい)
inferはその条件分岐で推論された型を指すときに用いることができます。
ジェネリック型を関数でいうところの引数(props)と呼ぶならば、
inferは引数によって動的に値が変化する変数のようなもので、infer U
と記述したら、U型を型情報に含めることができます。
例題1
ドキュメントの最初に紹介されている型を見てみましょう。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
これは関数の返り値を表す型です。
例えば
// 数値を文字列に変換する関数
type ToString = (num: number) => string;
// 数値を文字列に変換する関数の返り値
type ReturnTypeToString = ReturnType<ToString>; // (1)
// ^ == string
// そもそも関数でない
type ReturnTypeString = ReturnType<string>; // (2)
// ^ Type 'string' does not satisfy the constraint '(...args: any) => any'.(2344)
といった挙動をします。
(1) はまずTの型はToString
でありextends (...args: any[]) => infer R
を満たします。
(...args: any[]) => infer R
// ↓
(num: number) => string
そして**infer R
に対応する箇所は今回の場合string
になる**のでRの型はstring
型になります。
なので結局の型はstring
になりました。
ここで再掲例題2を見てみましょう。
type Unpacked<T> = T extends (infer U)[]
? U
: T extends (...args: any[]) => infer U
? U
: T extends Promise<infer U>
? U
: T;
これは配列、関数、Promiseの中の型を取り出す(unpackする)型です。
つまり
-
string[]
は string -
(a: any) => boolean
は boolean -
Promise<number>
は number
が取り出せるというわけです。
実践
実際にどこで使うのかといった疑問が発生します。
たしかに普段は使わないはずですし、できればそのあたりを考える必要のない言語であって欲しいものです。
TypeScriptの型(Rustとかいろいろな言語も)はたくさんの<>
で内包される記述を行います。
たくさんの<>
で安全になった型たちからもとの値を取り出すときにinfer
を用いると便利な場合があります。
今回型情報は同じだが、違う型として認識させたいと、newtype-tsを使ったとします。
例えば本名とサービスでの表示名の型はどちらもstringですので、間違えて本名を表示させないように下のように別の型としてRealName
とDisplayName
を分けることができます。
newtype-tsの説明コード
type RealName = Newtype<{ readonly RealName: unique symbol }, string>;
type DisplayName = Newtype<{ readonly DisplayName: unique symbol }, string>;
const realName = iso<RealName>().wrap("HikaruEgashira");
const userName = iso<DisplayName>().wrap("ehika");
// ログイン時に挨拶する
const Greet = (name: DisplayName) = {
console.log(`ようこそ ${iso<DisplayName>().unwrap(name)}`);
}
Greet(userName)
// log: "ようこそ ehika"
Greet(realName);
// ^ 型が違うのでエラーに
ここでstringを取り出そうと思ったときinfer
が役に立ちます。
// NewTypeされる前の型を表す
type BaseType<T> = T extends NewType<unknown, infer U> ? U : T;
// BaseType<RealName> == string
// BaseType<DisplayName> == string
思ったこと
ライブラリ側で実装して欲しい問題な気もするので、ライブラリ開発者向けかも