LoginSignup
14
6

More than 3 years have passed since last update.

TypeScriptでのメソッドと関数プロパティの違い

Last updated at Posted at 2019-08-16

はじめに

この記事はすべて、strictFunctionTypesオプションをtrueにしている場合においての話である。TypeScriptは関数のSubtype, SupertypeについてBivariantであるという立場で、実行時エラー可能性を犠牲にしつつ実装者の直感的理解を助けてきた経緯があった。
(=> https://qiita.com/na-o-ys/items/aa56d678cdf0de2bdd79 )
しかし、近年 strictFunctionTypesのように、より論理に忠実な型システムを取り入れてきている。この流れのなかで、本記事のようなedge caseが生まれることも、無理からぬものである。

論旨

TypeScriptでは、インターフェイスに定義されたメソッドと、インターフェイスに定義された関数プロパティ(関数として定義されたプロパティ)は異なる解釈をされる。
この微妙な挙動の違いが、予期せぬ型のエラーを引き起こす場合がある。

playgroundリンク

実際に本記事に用いたコードは下記より確認できる。

現象の説明

導入

IMethodHolderは、fooというひとつのメソッドを持ったインターフェイスだ。

メソッド定義
interface IMethodHolder {
  foo(arg: string): void
}

IFuncPropHolderは、fooというひとつの関数プロパティを持ったインターフェイスだ。

関数プロパティ定義
interface IFuncPropHolder {
  foo: (arg: string) => void
}

このように、両者は微妙に表記が異なることを確認したい。

同じ挙動を示すケース

さて、まずは最初に出てきた IMethodHolderを実装したHoge01を定義してみた。

メソッドを定義通り実装=>OK
class Hoge01 implements IMethodHolder {
   foo(arg: string) {
       return;
    }
}

当然ながら、これは問題なくTypeScriptのコンパイルを通過する。
次いで、全く同じ定義ながら、IFuncPropHolderを実装したHoge02を定義してみた。

関数プロパティを定義通り実装=>OK
class Hoge02 implements IFuncPropHolder {
   foo(arg: string) {
       return;
    }
}

Hoge02内のfooは、関数プロパティでなくメソッドとして定義した。
この場合でも、TypeScriptのコンパイルを無事通過できる。

挙動が異なる場合

Hoge03は、fooメソッドの引数を、stringではなく、そのサブタイプである"abc"にして、IMethodHolderを実装した。

メソッドを実装して、引数にもとのメソッドのSubtypeを用いた場合=>OK
class Hoge03 implements IMethodHolder {
   foo(arg: "abc") {
       return;
    }
}

これは、うまくいく。実装したメソッドの引数が、もとの引数のSubtypeであっても、コンパイルは通る

一方Hoge04は、fooメソッドの引数を、stringではなく、そのサブタイプである"abc"にして、IFuncPropHolderを実装したものである。

関数プロパティを実装して、引数にもとの関数のSubtypeを用いた場合=>NG
class Hoge04 implements IFuncPropHolder {
   // コンパイルエラー
   foo(arg: "abc") {
       return;
    }
}

これは、エラーが起きる。実装したメソッドの引数が、もとの関数プロパティのSubtypeである場合、コンパイルは通らない

この挙動になる理由

まず断っておくが、私はこの現象に遭遇して自身が理解に時間を要したため、類似の事象に遭遇した方のために、発見した知見をシェアしたものであるため、TypeScriptのソースを読んで深い理解をするということはやっていない。

さて、現象を整理したい。

  1. Hoge03は通る: メソッドを実装した場合、その引数がもとのサブタイプであっても通る
  2. Hoge04は通らない: 関数プロパティを実装した場合、その引数がもとのサブタイプであると通らない

Hoge03が通るというのは、本来は型システムの厳密性としては、おかしなことなのだ。
例えば、このTypeScriptの特性を利用して、実行時エラーを起こすことだってできる。

Hoge03が通ることによる実行時エラー可能性

実行時エラーを起こすコード(Hoge03の実装にもよる)
function runFoo(hoge: IMethodHolder, str: string): void {
  hoge.foo(str);
}

runFoo(new Hoge03(), "xxxx");

runFooのような、IMethodHolderすべてを受け入れる関数によって、本来Hoge03に与えられるべきでない引数が与えられるのである。例えばHoge03のfooメソッド内で、"abc"という文字列を使い、"abc"を持つオブジェクトにアクセスしたりすると、エラーになる。

実行時エラーを起こすHoge03のfoo実装例
foo(arg: "abc"): void {
  obj[arg].foobar = 123;
}

しかし、実際にHoge03は通るし、そのほうが都合が良いケースも多い。
かつての名残で、Bivariance(関数の引数について、SupertypeまたはSubtypeであればよいとする考え方)を認めているのだろう。

一方で、関数プロパティを実装したHoge04の場合は、厳密な型システムの論理に従うためか、
コンパイルエラーとなる。実行時エラーの可能性を防ぐ意味もあるといえる。
実際、strictFunctionTypesをfalseにするとこのエラーはなくなった。

発展1 引数にSupertypeを用いた場合

メソッドを実装して、引数にもとのメソッドのSupertypeを用いた場合=>OK
class Hoge05 implements IMethodHolder {
   foo(arg: string | number) {
       return;
    }
}
関数プロパティを実装して、引数にもとの関数のSupertypeを用いた場合=>OK
class Hoge06 implements IFuncPropHolder {
   foo(arg: string | number) {
       return;
    }
}

この場合は両者ともコンパイルが通る

  • Hoge05はBivarianceという性質によって、通る
  • Hoge06はContravarianceという性質によって、通る

のであろう。

発展2 戻り値にSubtype, Supertypeを用いた場合

調査結果は、予想通りではあるが、下記の挙動となった。

  • 関数/メソッドの戻り値を、もとのSubtypeにすると、いずれも通る。
  • 関数/メソッドの戻り値を、もとのSupertypeにすると、いずれも通らない。

ここでは両者の挙動は同一である。戻り値の場合は、Subtypeのみが許容される。

結論

インターフェイスを実装したクラスを定義する際、

  1. 元がメソッドならば、その引数に対してBivariantな関係の型が利用可能
  2. 元が関数プロパティならば、その引数に対してContravarianctな関係の型が利用可能

より平易に言うと、

  1. 元がメソッドならば、その引数のSubtypeもSupertypeも、どちらも利用した実装ができる
  2. 元が関数プロパティならば、その引数に対してSupertype(同一も含む)を利用した実装しかできない

である。

14
6
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
14
6