LoginSignup
9
1

More than 1 year has passed since last update.

Typescriptのスーパータイプとサブタイプについて

Last updated at Posted at 2021-12-06

はじめに

この記事は、JSL(日本システム技研) Advent Calendar 2021 の記事です。
Typescriptを書いていると、代入できるつもりが型エラーになったりしたことが何度かありました。この辺の「なんとなく」なところを解剖していこうという内容になります(なっているはずです)。

環境

  • https://www.typescriptlang.org/play v4.5.2
    • Typescriptを試したいときにオススメです
    • この記事内のコードはこちらのリンクを踏んでいただくとコピーされますので、気になるところを変更したりして試していただけると面白いかもしれません

本編

1. 「スーパータイプ」と「サブタイプ」

例として、 const me = "me"; を基準にして考えてみましょう。
変数 me は、文字列の"me"型として定義されていますね。(Typescriptのバージョンが低いとただのstring型として扱われるので注意)

SS 2021-12-05 20.29.37.png

で、これを別の型の変数として代入してみましょう。

SS 2021-12-05 20.37.37.png

上記では "me" | "you"stringany の三種類に me を代入しています。この3つはいずれも「 me < X」という関係性、つまり me の「スーパータイプ」となります。
当然、スーパータイプに対しては自身を代入することが可能なので型エラーは起きていませんね。
さらに言うと、 上の変数に対して下の変数が「スーパータイプ」になっているので、こんな代入も可能です。

SS 2021-12-05 20.50.34.png

じゃあ「サブタイプ」は何かと言えば逆の立ち位置のもの、上記に倣うなら下の変数に対して上の変数が「サブタイプ」となります。
当然、サブタイプに自身を代入することはできません。

SS 2021-12-05 21.03.14.png

"you"meOrYou に代入できる(スーパータイプ)なのに対し、 youmeOrYou は代入できませんね(サブタイプ)。エラーくんが「 "me" が割り当てられないヨ!」とおっしゃっている通りです。

このようにして、Typescriptは型の整合性を保っているわけです。

2. オブジェクト型の場合

1章ではリテラル型(stringやnumber)のみを例に取って説明しましたが、オブジェクト型が入ってくると少々複雑になります。
例として、 ab ( { a: string; b: string } 型)を自身として扱うことにしましょう。
まずは ab のスーパータイプについて考えてみます。

SS 2021-12-05 21.29.00.png

オブエジェクト型のスーパータイプになるのは大体この3パターンです。
なんとなく「プロパティ増やせばスーパータイプになりそう」な気がするかもしれません(当社比)が、プロパティが増えるということは制限がキツくなるということ なので、 ? を付けて任意プロパティにしない限り「サブタイプ」という扱いになってしまうのです。

SS 2021-12-05 21.35.21.png

d がないやんけ!」ってエラーくんも騒いでますね(そりゃそう)
逆に、自身のプロパティが余剰である場合はちゃんと代入できます。

SS 2021-12-05 22.11.48.png

関数の引数なんかは、必要最低限のプロパティを持ったオブジェクト型にしてあげると都合がいいので、すこし気を使ってあげると良いかもしれませんね。

SS 2021-12-05 22.24.30.png

直接渡す場合だけエラー出るのはナゼ…? :thinking:

3. アサーションについて

Typescriptは as で型アサーションをすることができますが、わりかし「アサーションしようとするとエラーになる」ことがありますよね?
スーパータイプ・サブタイプについて理解できると、これの謎が解けると思いますので一緒に紹介します。
結論から言えば、アサーションはサブタイプかスーパータイプにしかできません。

SS 2021-12-06 9.24.27.png

逆に言えば、どちらでもない赤の他人にはアサーションすることができないようになっています。

SS 2021-12-06 9.27.51.png

ただ、エラーくんも言っている通り、1度 unknownany といった最上位のスーパータイプへのアサーションを経由することですり抜ける事も可能です。…が、余程のことがない限りやめておいたほうがいいでしょう。

SS 2021-12-06 9.32.16.png

オブジェクト型の場合でもこの関係は同じですので、基本的には 自身と同じプロパティを1つでも持っている型 へならアサーション可能なわけです。

SS 2021-12-06 10.03.33.png

ですが例外もありまして、自身も持っているプロパティの型が赤の他人の場合は、オブジェクト型としても赤の他人扱いとなります。

SS 2021-12-06 10.07.38.png

ここだけややこしいので注意が必要ですね。

4. おまけ:「バベルの図書館 :books:

結構ガチガチに見えるTypescriptの型ですが、エラーをすり抜けてしまう例もあるので紹介します。
それが「キーが string 型のオブジェクト型」です。めっちゃよく見るやつですよね。

SS 2021-12-05 21.47.06.png

一見大丈夫そうに見えますが、空オブジェクトを代入できてしまっている(空オブジェクト型がサブタイプと扱われてしまっている)せいで d を参照した時に string 型と推論されてしまっています。本来は空オブジェクトも許容できてしまっているわけですから string | undefined と推論されるべきです。そうでないと、 undefined である d にアクセスした時、例外エラーが発生してしまうことに型エラーで気づくことができません…おぉ怖い怖い…。

ちなみにキーがstringのサブタイプであればちゃんと防げるんです。キーのプロパティが全て指定されてないとTypescriptが判断できてます。

SS 2021-12-05 21.55.54.png

が、キーがstringの場合プロパティが無限大 なのでTypescriptが正しく判断できない…というよりそもそも正しく定義することが不可能なのです。
こういうヤバイ型はなるべく使うのを避けましょう。
どうしても使わざるを得ない場合は Partial で全てのプロパティを任意にしちゃいましょう。こうすれば辻褄があって安全です。

SS 2021-12-05 22.06.44.png

おわりに

Typescriptで、「代入しようとしたら型エラーが出たけどよく分からない」「関数の引数を渡そうとすると型エラーになって詰み」みたいなものは、この辺りを理解しているとすんなり解決する場合があります。
面倒だからと as any する前にちょっとでも思い出していただければ幸いです(自戒)

追記

直接渡す場合だけエラー出るのはナゼ…? :thinking:

まず、変数を定義する時は余剰プロパティを許さないという仕様があります。直接渡すということは、その場で定義するのと同義らしくエラーが出るようです。

おまけ:「バベルの図書館 :books:

Partial にするだけでは配列などに対処できません。なので以下のコンパイラオプションを入れることを強くオススメします!

9
1
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
9
1