TypeScriptで関数の戻り値の型をいい感じにunion型に推論してくれることはあるのに数値リテラル型は推論されない謎
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
のところに関数の戻り値の型(型注釈されたものか型推論されたもの)が入るだけだと気がついた。
なので関数の戻り値がどのように型推論されるのか実験してみたのが以下。
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になるのは当たり前のことだった。指摘してくれた同僚に感謝!