Akihito Ikeda

TypeScriptで関数の戻り値の型をいい感じにunion型に推論してくれることはあるのに数値リテラル型は推論されない謎

posts/2021-04-06diary

type-challengesをやっていて、関数の戻り値の型推論の挙動がよくわからなかったのでメモ。解決はしてない。

わからなかったことだけ先に書くと、以下のコードで「fnの戻り値の型が数値リテラル型1ではなくてnumber型になるのはなぜ?」ということ。

const fn = () => 1 // 戻り値の型: number

疑問の発端となったtype-challengesの問題と答えは以下。

Get Return Type

type MyReturnType<T> = T extends (...args: any) => infer R ? R : T

これみて「いやこれ凄すぎか?戻り値をどうやって調べてるんだろう?リテラルじゃなくて式とかでもいけるの?」と思ったけど、infer Rのところに関数の戻り値の型(型注釈されたものか型推論されたもの)が入るだけだと気がついた。

なので関数の戻り値がどのように型推論されるのか実験してみたのが以下。

TypeScript Playground v4.2.3

const fn  = () => 1       // 戻り値の型: number
const fn2 = () => 1 + 1   // 戻り値の型: number

const fn3 = () => 1 | 1   // 戻り値の型: number (追記: これはただのビットORなのでnumberになるのは当たり前だった)
const fn4 = () => 1 | 2   // 戻り値の型: number (追記: これはただのビットORなのでnumberになるのは当たり前だった)

const fn5 = (v: boolean) => v ? 1 : 2       // 戻り値の型: 1 | 2
const fn6 = (v: boolean) => v ? 1 : 1       // 戻り値の型: number
const fn7 = (v: boolean) => v ? 1 : (1 + 1) // 戻り値の型: number

const fn8 = (a: boolean, b: boolean) => a ? b ? 1 : 2 : 3 // 戻り値の型: 1 | 2 | 3

// vがtrueであれfalseであれ常に1を返すが、型推論としては `1 | 2` となる
const fn9 = (v: boolean) => v ? ( v ? 1 : 2 ) : 1         // 戻り値の型: 1 | 2

この結果からいくつか気になることがある。

  • fn5, fn8, fn9とそれ以外では戻り値の型の推論結果が異なる

    • いい感じに推論されてunion型になってくれるのと、ただnumber型になるのがある
  • fnは戻り値の型を1(数値のリテラル型)としてくれてよさそうなのに、numberなのはなぜ?
  • fn5, fn8のときはわざわざ条件分岐を網羅して 1 | 2 , 1 | 2 | 3 としてくれているのに、fn6ではnumberなのはなぜ?(1としていいのでは?)
  • fn7をみると、いい感じに推論してくれるパターンでも式の評価まではやらないっぽい
  • fn9では、実際には常に1を返す関数であっても戻り値の型が 1 | 2 となっている

    • これは流石に推論するのが難しそうなので納得できる挙動で、構文解析(というのかわからないけど)してReturnValue的なノードが複数あればそれのunionをとるんだろうなと想像することはできる

色々書いたけど気になる(タイトルにもなってる部分)のは、fn5がunion型として推論されるならfn, fn2, fn3, fn4, fn6もそれぞれ数値のリテラル型, union型として推論されていいのでは?ということ。何か基本的なことがわかっていないか、勘違いしている…?

↑下部に追記した通り、fn3, fn4についてはシンプルに勘違いだった。

なにやらリテラル型にはWidening / NonWideningという2つがあるらしく、今回気になっているリテラル型がプリミティブ型に拡張されるような挙動となんとなく似ているような気がする。これが関係しているんだろうか?

試しにこう書いてみると

const fn  =  () => 1            // 戻り値の型: number
const fn2 =  () => (1 as const) // 戻り値の型: 1

const assertionと呼ばれる as const を戻り値につけた方は、戻り値の型が1つまり数値リテラル型になってる。ということはやっぱり Widening / NonWidening Literal Typesが関係してるんだろうか?うーんあんまり腑に落ちない。

結局今の時点では解決していない。何かわかったらまた書く予定。


2021/04/07 追記

const fn3 = () => 1 | 1   // 戻り値の型: number
const fn4 = () => 1 | 2   // 戻り値の型: number

これを書いているとき型のことしか頭になかったので勘違いしていたけど、この場合の|はただのビットORなので戻り値の型がnumberになるのは当たり前のことだった。指摘してくれた同僚に感謝!

© Akihito Ikeda - Last update 23.07.2021 17:02.