daisei-yoshino
@daisei-yoshino

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

ややこしい関数の型ですが、一般化したいです

解決したいこと

「ある変数がinterface Hogeを満たす前提の下、更にinterface Fuga(Fuga extends Hoge)である場合、Fugaに対応するclassインスタンスを生成する」という処理を書いていました。
生成するclassインスタンスですが、Hogeに対応するclass HogeClassは作成し、Fugaに対応するFugaClass extends HogeClassも作成しました。
FugaClass特有の処理やパラメータがあるので、FugaClass生成時にconstructorに渡す引数の型はFugaに限るようにしました。
そうすると、HogeであるからといってFugaであるとは限らないので、Fugaであるかどうかの判定が必要になります。
この判定の為に、(hoge: Hoge) => hoge is Fuga型の関数を生成し、利用するようにしました。
実際の対象はもっとややこしいですが、それらしく簡単に書くと以下のようになります。

interface Hoge {
  hoge: string
}

interface Fuga extends Hoge {
  fuga: string
}

function isFuga(hoge: Hoge):hoge is Fuga {
  return typeof hoge.fuga == 'string';
}

function createHogeClass(hoge: Hoge): HogeClass {
  if (isFuga(hoge)) {
    return new FugaClass(hoge);// FugaClass extends HogeClass
  } else {
    return new HogeClass(hoge);
  }
}

問題は、書いている処理内でFugaに相当する型が、それなりの数(20個以上?)存在している点です。
コードの構成の分かりやすさを考えると、isXX系の関数をまとめておく変数があった方が良いと考えました。これに型を付けようと考えた時、表題のややこしい関数の型が問題になりました。

// もしかしたらもっと良いデータ構造があるかも
// keyは特定値のいずれかという形式にできるとは思うものの、まだ網羅できていないので一旦stringにしています
const isInterfaceOf: {[key: string]: /* ここの型がどうなるのか知りたいです */} = {
  Fuga: (hoge: Hoge): hoge is Fuga => {/* ... */},
  Piyo: (hoge: Hoge): hoge is Piyo => {/* ... */},
  // ...
}

実際のコードでは、再帰的にcreateClassに相当する関数を実行することになるので、できれば関連する処理は容易にcreateClassから呼び出せるようにしたいです。

発生している問題・エラー

上記isInterfaceOfに指定した型と、isInterfaceOf.Fuga等の型のすり合わせが上手くいかないという状況にあります。

該当するソースコード

大凡上記「解決したいこと」の通りです。

自分で試したこと

型ジェネリクスを用いた記法(<T extends Hoge>(hoge: Hoge): hoge is T)なども試してみましたが、慣れていないのもあり、全体が上手く動作するように記述するのには失敗しました。

0

1Answer

質問者様のコードを実行したところ、isFuga

Property 'fuga' does not exist on type 'Hoge'.ts(2339)

エラーが発生するため、
別のアプローチから試しました。

function isFuga(hoge: Hoge): hoge is Fuga {
    if ("fuga" in hoge) {
        return true;
    }
    return false;
}

さて、修正したisFugaを元に、isInterfaceOfを作成します。
判定用のフィールドが常に1個と仮定しています。

// 毎回書くのが面倒なので、「HogeかHogeを拡張した型のいずれである」型を作成
type Extensions = Hoge | Fuga | Piyo;

// T型かどうかをチェックする関数
// keyは存在チェックに使われるフィールド名
function isInterfaceOf<T extends Hoge>(hoge: Extensions, key: string): hoge is T {
    if (key in hoge) {
        return true;
    }
    return false;
}

function createHogeClass(hoge: Extensions): HogeClass {
    // Fugaかどうかをチェックしたいなら、型ジェネリクスに<Fuga>、第二引数に"fuga"と記載
    if (isInterfaceOf<Fuga>(hoge, "fuga")) return new FugaClass(hoge);
    if (isInterfaceOf<Piyo>(hoge, "piyo")) return new PiyoClass(hoge);
    // ...
    // 最後まで残ったら、HogeClass
    return new HogeClass(hoge);
}

以上、答えになっていますでしょうか。

1Like

Comments

  1. @daisei-yoshino

    Questioner

    なるほど!
    この方式であれば、それぞれの型に対応する判定用関数の集合を予めを作らずに済みますし、判定条件の設定も簡単にできますね!
    幸いなことに、この質問の切っ掛けになっているプログラム内で、型の判定に必要な情報は特定フィールドの値だけだったので、ほぼそのまま使えそうです!
    ご回答ありがとうございます!

  2. お力になれてよかったです!
    よりスッキリした書き方を追記します。

    function isFuga(hoge: Hoge): hoge is Fuga {
        return "fuga" in hoge;
    }
    

    function isInterfaceOf<T extends Hoge>(hoge: Extensions, key: string): hoge is T {
        return key in hoge;
    }
    

Your answer might help someone💌