https://www.kabuku.co.jp/developers/learn-subtyping-of-function

こんにちは、バックエンドエンジニアのほとけちゃんです。社内では他にテキストチャット弁慶なども担当しています。Slackでわめくのが仕事です。

さて、現在開発中の新規サービスではバックエンドにTypeScriptを採用しているのですが、チーム内にTSに不慣れなメンバーもいたためThe TypeScript Handbookを輪読しています。TSのわけわからん機能を知ることができてなかなか面白いです。

Handbookは丁寧に書かれている一方、実は型システムや数学の前提知識を要求しているくせに詳しい説明も無くさらっと書かれている箇所もいくつかあります。その中でも関数型同士の部分型について口頭で説明するのに骨が折れたので、どうせならブログにまとめようと思い筆を取りました。なるべく小難しい単語(structural subtypingとかco-variantとか)を使わない説明を心がけたのでお付き合い頂けると嬉しいです。ただし、ユニオンやインターセクションの理解は前提とします。

なお、本稿の内容は『型システム入門』の15章を読めば大体わかるので、あれを理解している人は読まなくても大丈夫です。粗探しために読むのは歓迎します。

部分型の基本

大雑把に言ってしまえば、部分型のモチベーションは「安全な範囲ならばある型の値を別の型の値として扱いたい」というものです。こう書いてしまうと「そんなことあるの?」と思う向きもおられるかもしれませんが、実のところ日常茶飯事です。

function hello(arg: {name: string}): string {
  return 'Hello! ' + arg.name;
}

helloの引数の型は{name: string}なので、アサーション等の特殊な操作をしない限りhelloのボディの中ではarg.name以外のプロパティにはアクセスできません。だったら、helloの引数にはstring型のnameプロパティを持つ値だったら何でもいいと思いませんか?だってnameさえあれば危険な操作は起こり得ないですからね!

部分型はそれを実現するための仕組みです。実際、次のコードはコンパイルできます。

const arg = {name: 'John', age: 1}
hello(arg)

name: stringなプロパティを持つ変数であれば{name: string}型の部分型の値として扱われ、{name: string}を期待する関数の実引数にすることができるのです。ただしhello({name: 'John', age: 1})は型エラーになるので注意して下さい。部分型を与えていて安全であるにもかかわらず、オブジェクトリテラルの場合だと厳密な型の一致が求められます。理由はよく分からないというか調べていないのでエスパーすると、後からこの呼び出し部分のコードを読んだときに関数の型を誤解する可能性があるから、などでしょうか。

部分型の動きをよりシンプルに確認できるのが代入です。

const subUser: {name: string, age: number} = {name: 'John', age: 1};
const user: {name: string} = subUser;

もうお分かりでしょうが、2行目が怒られないのはsubUserの型が{name: string}の部分型だからです。

もう一つ具体例を見ましょう。人によってはこのユニオン型の例の方が理解しやすいかもしれません。

function stringify(arg: number | string): string {
  if (typeof arg === 'number') {
    return arg.toString()
  }
  return arg
}

stringify(1) // -> '1'
stringify('hoge') // -> 'hoge'

stringify(1)が通るのは、number型がnumber | string型の部分型だからです。stringifyのシグネチャは引数にnumberが来ることを想定しているので、実際にnumberの値を適用しても安全です。ユニオン型については一般に、「任意の型T, SについてTST | Sの部分型である」という命題が成り立つはずです1

ついでに言うと、インターセクションについても部分型関係にかんして一般的な命題が成り立ちそうです。例えば{name: string} & {age: number}という型は{name: string, age: number}と等価です。オブジェクトのインターセクションを取るとプロパティがどんどん増えることになります。ということは……予想がついたと思いますが、「任意の型T, Sについて、T & ST, Sそれぞれの部分型になる」と言えるはずです2