TypeScript の型のエラーはどんなときに発生するのか?
TypeScript は静的型付け言語です。
型のチェックをコンパイルやエディタの拡張機能などを通じて行い、実行前に型のエラーを示してくれます。
では、型のエラーはどんなときに発生するのでしょうか?
その手掛かりとして、TypeScript の実際の型エラーを見てみましょう。
const x: number = "number"
// ^
// Type 'string' is not assignable to type 'number'.
string
型はnumber
型に 代入 できません。
つまり、「右辺の値の型」が「左辺の変数の型」に代入できない場合に、エラーが発生するようです。
では、どんな場合に代入できて、どんな場合に代入できないのでしょうか?
型が同じ場合はもちろん代入できます。しかしそれだけではありません。
答えを言ってしまうと、「右辺の値の型」が「左辺の変数の型」の 部分型 である場合に代入できます。
そして、その場合に型のチェックを通過することができます。
この記事では 部分型 やそれに関連する 変性 そして、TypeScript 4.7 で追加された Optional Variance Annotations について説明します。
TypeScript の部分型は正確には 構造的部分型 (structural subtyping)です。
詳細はこちらの記事が参考になります: 構造的部分型 (structural subtyping) _ TypeScript入門『サバイバルTypeScript』
部分型
型を集合で説明することがよくあるので、ここでもそれに倣い集合で部分型の説明をするなら、部分型は部分集合です。
例えば、number | string
のようなユニオン型は、33
や -4
などの数値 と、"Hello World"
や "TypeScript"
などの文字列を値として持つ集合です。このとき、number
は number | string
の部分集合ですよね?
つまり、number
型は number | string
型の部分型であり、代入可能です!
const x: number = 1
const y: number | string = x
// => OK!
もう一つ例を見てみましょう。次はオブジェクト型です。
以下の2つのオブジェクト型があったとします。Hoge
と HogeFuga
どちらがどちらの部分型でしょうか?
type Hoge = { hoge: number }
type HogeFuga = { hoge: number, fuga: string }
見た目だけだと Hoge
は HogeFuga
の 部分型っぽいですが、実際はその逆で、 HogeFuga
が Hoge
の部分型です。
これは、使う場面を想像してみるとわかりやすいかと思います。
Hoge
型を使うとき、その値からプロパティ hoge
プロパティを取り出す場面が訪れるでしょう。
HogeFuga
型のプロパティは hoge
だけでなく fuga
も必要です。
もし、HogeFuga
の値を使おうとしたときに実際の中身が Hoge
だったら、プロパティ fuga
が存在しなくてエラーが発生してしまいます。逆に、 Hoge
の中身が HogeFuga
だったとしても、Hoge
はプロパティ hoge
しか使わないので問題なく動作します。
実際に、それぞれ代入してみると、前者はエラーになり、後者は問題ありません。
const h : Hoge = { hoge: 1 }
const hf: HogeFuga = { hoge: 1, fuga: "a" }
const x: HogeFuga = h // => Property 'fuga' is missing in type 'Hoge' but required in type 'HogeFuga'.
const y: Hoge = hf // => OK
あらためて、 Hoge
と HogeFuga
をまとめると次のようになります。
-
Hoge
: プロパティhoge
を持っているオブジェクトの型 (他にもプロパティを持っていてもOK!) -
HogeFuga
: プロパティhoge
とfuga
の両方持っているオブジェクトの型
集合的に考えても Hoge
の方が HogeFuga
よりも広いの集合ですので、HogeFuga
は Hoge
の部分型です。
このように、型を集合のように考え、部分集合かどうかで部分型を判断すれば、ほとんどの場合型チェックが通ります。
次の章でもう少し複雑な場合を説明します。
型変数と変性
TypeScript は Array<T>
のように型変数を持つ型があります。(Array<T>
の場合、T
が型変数)
このような型の場合、型変数の部分型になっているかどうかが重要になってきます。
例えば、Array<number | string>
と Array<number>
はどうでしょうか?
Array<number>
に Array<number | string>
は代入できません。
const arrNS: Array<number | string> = [1, "two", 3]
const arrN: Array<number> = arrNS // => Error
// ^^^^
// Type '(string | number)[]' is not assignable to type 'number[]'.
// Type 'string | number' is not assignable to type 'number'.
// Type 'string' is not assignable to type 'number'.
一方、Array<number | string>
に Array<number>
は代入できました。
const arrN: Array<number> = [1,2,3]
const arrNS: Array<number | string> = arrN // => OK
このように、A
が B
の部分型とき、Array<A>
は Array<B>
の部分型になりました。
ある型の型変数が部分型のとき、その型も必ず部分型になるのでしょうか?
答えは No です。もう一つの例を見てみましょう。
型変数として、関数の引数の型が与えられる Func<T>
型を考えてみましょう。
type Func<T> = (x: T) => number
Func<number | string>
に Func<number>
を代入してみましょう。
const funcN: Func<number> = (x: number) => 1
const funcNS: Func<number | string> = funcN
// ^^^^^^
// Type 'Func<number>' is not assignable to type 'Func<string | number>'.
// Type 'string | number' is not assignable to type 'number'.
// Type 'string' is not assignable to type 'number'.
エラーになりました。
では逆に、Func<number>
に Func<number | string>
を代入してみましょう。
const funcNS: Func<number | string> = (x: number | string) => 1
const funcN: Func<number> = funcNS // OK
代入できました!
number
は number | string
の部分型ですが、逆に Func<number | string>
が Func<number>
の部分型のようです。
ここまでをまとめると、次のようになります。
-
A
がB
の 部分型のとき、Array<A>
はArray<B>
の部分型 -
A
がB
の 部分型のとき、Func<B>
はFunc<A>
の部分型
このように、型変数の使われ方によってその方の部分型関係が変わる性質のことを 変性 (variance)と呼びます。
変性の種類は以下の4種類です。
-
共変 (covariant)
-
A
がB
の 部分型のとき、Type<A>
はType<B>
の部分型
-
-
反変 (contravariant)
-
B
がA
の 部分型のとき、Type<A>
はType<B>
の部分型
-
-
双変 (bivariant)
-
A
がB
の 部分型またはB
がA
の 部分型のとき、Type<A>
はType<B>
の部分型
-
-
不変 , 非変 (invariant, nonvariant)1
-
A
とB
が同じ型のときのみ、Type<A>
はType<B>
の部分型
-
Array<T>
は共変、 Func<T>
は反変と表現することができます。
TypeScript では基本的に共変の場合が多く、関数の引数のような場合で反変です。
strictFunctionTypes
関数の引数は反変と説明しましたが、実は TypeScript のデフォルトでは、関数の引数は 双変 です。
TypeScript 2.6 で追加されたオプションの strictFunctionTypes
によって、反変になります。
strict: true
で strictFunctionTypes
もオンになるので、この記事を読んでいるような型に関心のある方なら問題ないと思いますが、注意が必要です。
もし、関数の引数は双変だったらどうなるでしょうか?
const funcN: Func<number> = (x: number) => Math.abs(x)
const funcNS: Func<number | string> = funcN
funcNS("apple")
双変により、上記のような代入がエラーにならないため、Math.abs("apple")
を実行してしまいます。
このように、関数の引数が反変でないと実行時にエラーが発生してしまう可能性があるため、strictFunctionTypes
をオンにしておくことをオススメします。
Optional Variance Annotations
最後に、TypeScript 4.7 で追加された構文 Optional Variance Annotations を紹介します。
名前の通り、変性(variance)に関する注釈が可能となります。
以下のように関数の引数のように反変な場合は in
を、共変の場合は out
を記載します。
type Func<in T, out S> = (x: T) => S
Optional のため、あってもなくても型は変わりませんが、このように記載することで変性が明確になります。
また、誤った注釈をした場合にエラーを出してくれます。
type Func<out T, in S> = (x: T) => S // => Error
// ^^^^^ ^^^^
// Type 'Func<sub-T, S>' is not assignable to type 'Func<super-T, S>' as implied by variance annotation.
// Types of parameters 'x' and 'x' are incompatible.
// Type 'super-T' is not assignable to type 'sub-T'.
// Type 'Func<T, super-S>' is not assignable to type 'Func<T, sub-S>' as implied by variance annotation.
// Type 'super-S' is not assignable to type 'sub-S'.
さいごに
最後まで読んでいただきありがとうございます!
さらに詳しい内容を聞きたい方は、Devトークで直接お話しましょう!
参考文献
- 部分型
- 変性
- strictFunctionTypes
- Optional Variance Annotations
-
不変, 非変は同じ性質を指す単語だと思うのですが、サイトで表記異なるため両方書きました。参考: https://togetter.com/li/66427 ↩