ウェブサイト検索

タイプガード - TypeScriptで型をマップし、thisベースのタイプガードを追加する方法は?


次のような任意のメンバーを持つインターフェースがあるとします:

interface A extends Record<string, any[]>{
  // Later used by interface merging
}

//maybe
declare module "./main"{
  interface A{
    func: [arg1:string]
  }
}

そして、私はこのように A を、このベースの型ガードを使ってマッピングしたいです:

interface Try1<T extends keyof A>{
  [K in keyof A]: (...args: A[K]) => this is Try1<K> 
  // Lots of errors
}
type Try2<T extends keyof A> = {
  [K in keyof A]: (...args: A[K]) => this is Try1<K> 
  // Error: A 'this' type is available only in a non-static member of a class or interface.ts(2526)
}

では、どうやってそれをすればいいですか?

実際の使用例:

interface Elements extends Record<string, [any[], any]>{
  // Later used by interface merging
}

interface Elements{
  button: [[text: string], MouseEvent];
}

class ImguiContext<CurrentEv extends keyof Elements>{
  // !!!
  [E in keyof Elements]: (...args: Elements[E][0]) => this is ImguiContext<E>;

  event: Elements[CurrentEv][1];
}


function main(_: ImguiContext){
  if(_.button("Click me")){
    // now `_.event` should be `MouseEvent`
    console.log("clicked at X:", _.event.screenX);
  }
}

もう一つの可能な解決策ですが、失敗しました

let a:string|number = 1;
function aIsNumber(/*no parameter*/)/*: ???*/ {
  return typeof a === "number";
}

??? になることができる

  1. アサーション関数 は asserts condition のようなもの
  2. 型予測子 のような a is number

しかし両方とも失敗した

回答 #1

TypeScriptのマップ型で「this」型述語を許可する機能は、元々 microsoft/TypeScript#5906 で実装されたものですが、明らかに欠けている機能です。これを試そうとすると、あなたが述べたエラーが発生します。これをサポートするための機能リクエストが microsoft/TypeScript#51958 で開かれていますが、現時点では公式に言語の一部ではありません。この問題は「さらなるフィードバック待ち」としてリストされていますので、これを実現したい場合は、そこに行って👍を付け、なぜあなたのユースケースが重要なのかを説明しても損はありません。

興味深いことに、その問題では、エラーにもかかわらず、コードは実際には期待通りに動作することが記載されています。

ですので、エラーを抑制するために //@ts-ignore コメントディレクティブを可能性として記述し、これを「サポートされていない機能」と見なすことができます。あなたの例では次のようになるかもしれません。

interface Elements {
  button: [[text: string], MouseEvent];
  other: [[age: number, isSquishy: boolean], Date]; 
}

type ImguiContext<K extends keyof Elements = keyof Elements> = {
  //@ts-ignore this is usually a bad idea
  [E in keyof Elements]: (...args: Elements[E][0]) => this is ImguiContext<E>;
} & { event: Elements[K][1]; }
    
function main(_: ImguiContext) {
  if (_.button("Click me")) {
    // now `_.event` should be `MouseEvent`
    console.log("clicked at X:", _.event.screenX); // okay
  } else if (_.other(2, false)) {
    console.log("otherThing", _.event.getFullYear()); // okay
  }
}

それは本当におすすめできません。//@ts-ignore ディレクティブはエラーの表示を防ぐだけで、コンパイラが抱えている根本的な問題を修正するわけではありません。エラーが報告される場所に問題が限定されている場合はそれで大丈夫かもしれませんが、他の部分で間接的に奇妙な問題に遭遇する可能性もあります。このディレクティブのドキュメントには、「非常に控えめに」使用すべきだと書かれています(強調は原文のまま)。つまり、使用する場合は自己責任となります。


代わりに、私はあなたのコンテキストをTypeScriptがエラーなく表現できるバージョンでラップする傾向があります。もし、ImguiContext がマップドタイプである代わりに、名前と引数を受け取る test() メソッドを持っていれば、次のように通常の interface として書き直すことができます:

interface ImprovedContext<T = unknown> {
  test<E extends keyof Elems>(
    name: E, ...args: Elems[E][0]
  ): this is ImprovedContext<Elems[E][1]> // no ts-ignore needed
  event: T
}

古いコンテキストを新しいコンテキストでラップするヘルパー関数を書く必要がある場合は書くことができます。これは型アサーションのみを必要とするため、//@ts-ignoreよりも危険性が低いです。

function improveContext(_: ImguiContext): ImprovedContext {
  return {
    test(n, ...args) { return (_[n] as any)(...args) },
    event: _.event
  }
}

そしてその後も物事はうまくいく:

function main2(_Orig: ImguiContext) {
  const _ = improveContext(_Orig);
  if (_.test("button", "Click me")) {
    console.log("clicked at X:", _.event.screenX); // okay
  } else if (_.test("other", 2, false)) {
    console.log("otherThing", _.event.getFullYear()); // okay
  }
}

もちろん、サポートされていない機能に依存する方が実現可能か、それともサポート可能なバージョンにリファクタリングする方が実現可能かは、使用ケースによります。

コードへのプレイグラウンドリンク