LoginSignup
25
10

More than 1 year has passed since last update.

TypeScript の型のエラーはどんなときに発生するのか?~部分型と変性で学ぶ TypeScript の型システム~

Last updated at Posted at 2022-08-24

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" などの文字列を値として持つ集合です。このとき、numbernumber | string の部分集合ですよね?

つまり、number 型は number | string 型の部分型であり、代入可能です!

const x: number = 1
const y: number | string = x
// => OK!

もう一つ例を見てみましょう。次はオブジェクト型です。
以下の2つのオブジェクト型があったとします。HogeHogeFuga どちらがどちらの部分型でしょうか?

type Hoge = { hoge: number }
type HogeFuga = { hoge: number, fuga: string }

見た目だけだと HogeHogeFuga の 部分型っぽいですが、実際はその逆で、 HogeFugaHoge の部分型です。
これは、使う場面を想像してみるとわかりやすいかと思います。

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

あらためて、 HogeHogeFuga をまとめると次のようになります。

  • Hoge: プロパティ hoge を持っているオブジェクトの型 (他にもプロパティを持っていてもOK!)
  • HogeFuga: プロパティ hogefuga の両方持っているオブジェクトの型

集合的に考えても Hoge の方が HogeFuga よりも広いの集合ですので、HogeFugaHoge の部分型です。

このように、型を集合のように考え、部分集合かどうかで部分型を判断すれば、ほとんどの場合型チェックが通ります。
次の章でもう少し複雑な場合を説明します。

型変数と変性

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

このように、AB の部分型とき、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

代入できました!
numbernumber | string の部分型ですが、逆に Func<number | string>Func<number> の部分型のようです。

ここまでをまとめると、次のようになります。

  • AB の 部分型のとき、Array<A>Array<B> の部分型
  • AB の 部分型のとき、Func<B>Func<A> の部分型

このように、型変数の使われ方によってその方の部分型関係が変わる性質のことを 変性 (variance)と呼びます。
変性の種類は以下の4種類です。

  • 共変 (covariant)
    • AB の 部分型のとき、Type<A>Type<B> の部分型
  • 反変 (contravariant)
    • BA の 部分型のとき、Type<A>Type<B> の部分型
  • 双変 (bivariant)
    • AB の 部分型または BA の 部分型のとき、Type<A>Type<B> の部分型
  • 不変 , 非変 (invariant, nonvariant)1
    • AB が同じ型のときのみ、Type<A>Type<B> の部分型

Array<T> は共変、 Func<T> は反変と表現することができます。
TypeScript では基本的に共変の場合が多く、関数の引数のような場合で反変です。

strictFunctionTypes

関数の引数は反変と説明しましたが、実は TypeScript のデフォルトでは、関数の引数は 双変 です。
TypeScript 2.6 で追加されたオプションの strictFunctionTypes によって、反変になります。
strict: truestrictFunctionTypes もオンになるので、この記事を読んでいるような型に関心のある方なら問題ないと思いますが、注意が必要です。

もし、関数の引数は双変だったらどうなるでしょうか?

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トークで直接お話しましょう!

参考文献

  1. 不変, 非変は同じ性質を指す単語だと思うのですが、サイトで表記異なるため両方書きました。参考: https://togetter.com/li/66427

25
10
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
25
10