112
73

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【TypeScript】 inferに詳しくなろう

Last updated at Posted at 2020-08-25

目標

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ですので、間違えて本名を表示させないように下のように別の型としてRealNameDisplayNameを分けることができます。

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

思ったこと

ライブラリ側で実装して欲しい問題な気もするので、ライブラリ開発者向けかも

112
73
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
112
73

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?