はじめに
この記事は、JSL(日本システム技研) Advent Calendar 2021 の記事です。
Typescriptを書いていると、代入できるつもりが型エラーになったりしたことが何度かありました。この辺の「なんとなく」なところを解剖していこうという内容になります(なっているはずです)。
環境
-
https://www.typescriptlang.org/play
v4.5.2
- Typescriptを試したいときにオススメです
- この記事内のコードはこちらのリンクを踏んでいただくとコピーされますので、気になるところを変更したりして試していただけると面白いかもしれません
本編
1. 「スーパータイプ」と「サブタイプ」
例として、 const me = "me";
を基準にして考えてみましょう。
変数 me
は、文字列の"me"型として定義されていますね。(Typescriptのバージョンが低いとただのstring型として扱われるので注意)
で、これを別の型の変数として代入してみましょう。
上記では "me" | "you"
・ string
・ any
の三種類に me
を代入しています。この3つはいずれも「 me
< X
」という関係性、つまり me
の「スーパータイプ」となります。
当然、スーパータイプに対しては自身を代入することが可能なので型エラーは起きていませんね。
さらに言うと、 上の変数に対して下の変数が「スーパータイプ」になっているので、こんな代入も可能です。
じゃあ「サブタイプ」は何かと言えば逆の立ち位置のもの、上記に倣うなら下の変数に対して上の変数が「サブタイプ」となります。
当然、サブタイプに自身を代入することはできません。
"you"
は meOrYou
に代入できる(スーパータイプ)なのに対し、 you
に meOrYou
は代入できませんね(サブタイプ)。エラーくんが「 "me"
が割り当てられないヨ!」とおっしゃっている通りです。
このようにして、Typescriptは型の整合性を保っているわけです。
2. オブジェクト型の場合
1章ではリテラル型(stringやnumber)のみを例に取って説明しましたが、オブジェクト型が入ってくると少々複雑になります。
例として、 ab
( { a: string; b: string }
型)を自身として扱うことにしましょう。
まずは ab
のスーパータイプについて考えてみます。
オブエジェクト型のスーパータイプになるのは大体この3パターンです。
なんとなく「プロパティ増やせばスーパータイプになりそう」な気がするかもしれません(当社比)が、プロパティが増えるということは制限がキツくなるということ なので、 ?
を付けて任意プロパティにしない限り「サブタイプ」という扱いになってしまうのです。
「d
がないやんけ!」ってエラーくんも騒いでますね(そりゃそう)
逆に、自身のプロパティが余剰である場合はちゃんと代入できます。
関数の引数なんかは、必要最低限のプロパティを持ったオブジェクト型にしてあげると都合がいいので、すこし気を使ってあげると良いかもしれませんね。
直接渡す場合だけエラー出るのはナゼ…?
3. アサーションについて
Typescriptは as
で型アサーションをすることができますが、わりかし「アサーションしようとするとエラーになる」ことがありますよね?
スーパータイプ・サブタイプについて理解できると、これの謎が解けると思いますので一緒に紹介します。
結論から言えば、アサーションはサブタイプかスーパータイプにしかできません。
逆に言えば、どちらでもない赤の他人にはアサーションすることができないようになっています。
ただ、エラーくんも言っている通り、1度 unknown
や any
といった最上位のスーパータイプへのアサーションを経由することですり抜ける事も可能です。…が、余程のことがない限りやめておいたほうがいいでしょう。
オブジェクト型の場合でもこの関係は同じですので、基本的には 自身と同じプロパティを1つでも持っている型 へならアサーション可能なわけです。
ですが例外もありまして、自身も持っているプロパティの型が赤の他人の場合は、オブジェクト型としても赤の他人扱いとなります。
ここだけややこしいので注意が必要ですね。
4. おまけ:「バベルの図書館 」
結構ガチガチに見えるTypescriptの型ですが、エラーをすり抜けてしまう例もあるので紹介します。
それが「キーが string
型のオブジェクト型」です。めっちゃよく見るやつですよね。
一見大丈夫そうに見えますが、空オブジェクトを代入できてしまっている(空オブジェクト型がサブタイプと扱われてしまっている)せいで d
を参照した時に string
型と推論されてしまっています。本来は空オブジェクトも許容できてしまっているわけですから string | undefined
と推論されるべきです。そうでないと、 undefined
である d
にアクセスした時、例外エラーが発生してしまうことに型エラーで気づくことができません…おぉ怖い怖い…。
ちなみにキーがstringのサブタイプであればちゃんと防げるんです。キーのプロパティが全て指定されてないとTypescriptが判断できてます。
が、キーがstringの場合 は プロパティが無限大 なのでTypescriptが正しく判断できない…というよりそもそも正しく定義することが不可能なのです。
こういうヤバイ型はなるべく使うのを避けましょう。
どうしても使わざるを得ない場合は Partial
で全てのプロパティを任意にしちゃいましょう。こうすれば辻褄があって安全です。
おわりに
Typescriptで、「代入しようとしたら型エラーが出たけどよく分からない」「関数の引数を渡そうとすると型エラーになって詰み」みたいなものは、この辺りを理解しているとすんなり解決する場合があります。
面倒だからと as any
する前にちょっとでも思い出していただければ幸いです(自戒)
追記
直接渡す場合だけエラー出るのはナゼ…?
まず、変数を定義する時は余剰プロパティを許さないという仕様があります。直接渡すということは、その場で定義するのと同義らしくエラーが出るようです。
おまけ:「バベルの図書館 」
Partial にするだけでは配列などに対処できません。なので以下のコンパイラオプションを入れることを強くオススメします!