タイプマッピング時のジェネリックプロパティの問題


11

次のようなユーティリティタイプをエクスポートするライブラリがあります。

type Action<Model extends object> = (data: State<Model>) => State<Model>;

このユーティリティタイプを使用すると、「アクション」として実行される関数を宣言できます。Modelアクションが動作するという一般的な引数を受け取ります。

次にdata、「アクション」の引数に、エクスポートする別のユーティリティタイプを入力します。

type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;

Stateユーティリティ型は基本的に入ってくる取りModelジェネリックをして、タイプであるすべてのプロパティは、新しいタイプの作成Action削除されましたが。

たとえば、ここには上記の基本的なユーザー土地の実装があります。

interface MyModel {
  counter: number;
  increment: Action<Model>;
}

const myModel = {
  counter: 0,
  increment: (data) => {
    data.counter; // Exists and typed as `number`
    data.increment; // Does not exist, as stripped off by State utility 
    return data;
  }
}

上記は非常にうまく機能しています。👍

ただし、特に一般的なモデル定義が定義されているときに、一般的なモデルのインスタンスを生成するためのファクトリー関数と一緒に苦労しているケースがあります。

例えば;

interface MyModel<T> {
  value: T; // 👈 a generic property
  doSomething: Action<MyModel<T>>;
}

function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // Does not exist 😭
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

上記の例dataでは、doSomethingアクションが削除され、ジェネリックvalueプロパティがまだ存在する場所に引数が入力されることを期待しています。ただし、これは当てはまりません。valueプロパティもStateユーティリティによって削除されています。

これの原因は、T型の制限/絞り込みが適用されていない一般的なものであり、型システムはAction型と交差することを決定し、その後data引数の型から削除することです。

この制限を回避する方法はありますか?私はいくつかの研究を行っており、それを除いて、私がそれを述べることTができる何らかのメカニズムがあると期待していた。つまり、ネガティブ型の制限。Action

想像してみてください:

function modelFactory<T extends any except Action<any>>(value: T): UserDefinedModel<T> {

しかし、その機能はTypeScriptには存在しません。

これを期待どおりに機能させる方法を誰かが知っていますか?


ここでのデバッグを支援するために、完全なコードスニペットがあります。

// Returns the keys of an object that match the given type(s)
type KeysOfType<A extends object, B> = {
  [K in keyof A]-?: A[K] extends B ? K : never
}[keyof A];

// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;

// My utility function.
type Action<Model extends object> = (data: State<Model>) => State<Model>;

interface MyModel<T> {
  value: T; // 👈 a generic property
  doSomething: Action<MyModel<T>>;
}

function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // Does not exist 😭
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

このコード例は、https//codesandbox.io/s/reverent-star-m4sdb?fontsize = 14で試すことができ ます。

回答:


7

これは興味深い問題です。Typescriptは一般に、条件付き型のジェネリック型パラメーターに関してあまり機能しません。extends評価に型パラメーターが含まれていることが判明した場合は、評価を延期するだけです。

typescriptで特殊なタイプの関係、つまり等価関係(拡張関係ではない)を使用できる場合は、例外が適用されます。等式関係はコンパイラにとって理解しやすいので、条件付き型の評価を延期する必要はありません。ジェネリック制約は、型の等価性が使用されるコンパイラの数少ない場所の1つです。例を見てみましょう:

function m<T, K>() {
  type Bad = T extends T ? "YES" : "NO" // unresolvable in ts, still T extends T ? "YES" : "NO"

  // Generic type constrains are compared using type equality, so this can be resolved inside the function 
  type Good = (<U extends T>() => U) extends (<U extends T>() => U) ? "YES" : "NO" // "YES"

  // If the types are not equal it is still un-resolvable, as K may still be the same as T
  type Meh = (<U extends T>()=> U) extends (<U extends K>()=> U) ? "YES": "NO" 
}

遊び場リンク

この動作を利用して、特定のタイプを識別できます。現在、これは完全一致ではなく、完全一致です。完全一致は必ずしも適切ではありません。ただし、Actionは単なる関数シグネチャであるため、型の完全一致は十分に機能する可能性があります。

次のような、より単純な関数シグネチャに一致するタイプを抽出できるかどうかを見てみましょう(v: T) => void

interface Model<T> {
  value: T,
  other: string
  action: (v: T) => void
}

type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

function m<T>() {
  type M = Model<T>
  type KeysOfIdenticalType = {
    [K in keyof M]: Identical<M[K], (v: T) => void, never, K>
  }
  // Resolved to
  // type KeysOfIdenticalType = {
  //     value: Identical<T, (v: T) => void, never, "value">;
  //     other: "other";
  //     action: never;
  // }

}

遊び場リンク

上記のタイプKeysOfIdenticalTypeは、フィルタリングに必要なものに近いものです。の場合other、プロパティ名は保持されます。の場合、actionプロパティ名は消去されます。周りには厄介な問題が1つだけありvalueます。はのvalueタイプなのでT、は簡単には解決できません。また、同一T(v: T) => voidはありません(実際にはそうではない可能性があります)。

それでもvalue同じであると判断できTます。タイプのプロパティのT場合、このチェックと(v: T) => voidwithを交差させneverます。とのすべての交差neverは、に簡単に解決できneverます。次に、T別のIDチェックを使用して、タイプのプロパティを戻すことができます。

interface Model<T> {
  value: T,
  other: string
  action: (v: T) => void
}

type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

function m<T>() {
  type M = Model<T>
  type KeysOfIdenticalType = {
    [K in keyof M]:
      (Identical<M[K], (v: T) => void, never, K> & Identical<M[K], T, never, K>) // Identical<M[K], T, never, K> will be never is the type is T and this whole line will evaluate to never
      | Identical<M[K], T, K, never> // add back any properties of type T
  }
  // Resolved to
  // type KeysOfIdenticalType = {
  //     value: "value";
  //     other: "other";
  //     action: never;
  // }

}

遊び場リンク

最終的なソリューションは次のようになります。

// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object, G = unknown> = Pick<Model, {
    [P in keyof Model]:
      (Identical<Model[P], Action<Model, G>, never, P> & Identical<Model[P], G, never, P>)
    | Identical<Model[P], G, P, never>
  }[keyof Model]>;

// My utility function.
type Action<Model extends object, G = unknown> = (data: State<Model, G>) => State<Model, G>;


type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

interface MyModel<T> {
  value: T; // 👈 a generic property
  str: string;
  doSomething: Action<MyModel<T>, T>;
  method() : void
}


function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    str: "",
    method() {

    },
    doSomething: data => {
      data.value; // ok
      data.str //ok
      data.method() // ok 
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

/// Still works for simple types
interface MyModelSimple {
  value: string; 
  str: string;
  doSomething: Action<MyModelSimple>;
}


function modelFactory2(value: string): MyModelSimple {
  return {
    value,
    str: "",
    doSomething: data => {
      data.value; // Ok
      data.str
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

遊び場リンク

注:ここでの制限は、これが1つの型パラメーターでのみ機能することです(ただし、より多くに適応できる可能性があります)。また、APIはコンシューマーにとって少し混乱するため、これが最善の解決策ではない可能性があります。まだ特定していない問題があるかもしれません。見つけた場合はお知らせください😊


2
白人のガンダルフが現れたような気がします。🤯TBHコンパイラの制限としてこれを書き留める準備ができていました。これを試してみるのはとてもうれしいです。ありがとうございました!🙇
ctrlplusb

@ctrlplusb😂LOL、そのコメントで私の1日が終わりましたTi
Titian Cernicova-Dragomir

私はこの賞金をこの答えに当てはめるつもりでしたが、私は睡眠中の赤ちゃんの脳の深刻な欠如を続け、誤ってクリックしました。謝罪いたします!これは素晴らしい洞察に満ちた答えです。本質的にはかなり複雑ですが。answer時間を割いて回答いただきありがとうございます。
ctrlplusb

@ctrlplusb :(まあ..いくつか勝ついくつか失う:)
Titian Cernicova-Dragomir

2

TがAction型ではないことを私が表現できたら素晴らしいと思います。拡張の逆のソート

あなたが言った通り、問題はまだ負の制約がないことです。私はまた、彼らがそのような特徴を早く着陸させることを望みます。待っている間、私はこのような回避策を提案します:

type KeysOfNonType<A extends object, B> = {
  [K in keyof A]-?: A[K] extends B ? never : K
}[keyof A];

// CHANGE: use `Pick` instead of `Omit` here.
type State<Model extends object> = Pick<Model, KeysOfNonType<Model, Action<any>>>;

type Action<Model extends object> = (data: State<Model>) => State<Model>;

interface MyModel<T> {
  value: T;
  doSomething: Action<MyModel<T>>;
}

function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // Now it does exist 😉
      data.doSomething; // Does not exist 👍
      return data;
    }
  } as MyModel<any>; // <-- Magic!
                     // since `T` has yet to be known
                     // it literally can be anything
}

理想的ではありませんが、半回避策を知るのは素晴らしいです:)
ctrlplusb

1

countそしてvalue常にコンパイラを不幸にします。それを修正するには、次のようなことを試してください:

{
  value,
  count: 1,
  transform: (data: Partial<Thing<T>>) => {
   ...
  }
}

以来Partial、ユーティリティ型が使用されている場合には、あなたは大丈夫だろうtransform方法は存在しません。

Stackblitz


1
「カウントと値は常にコンパイラーを不幸にします」-なぜここにあるのかについての洞察をいただければ幸いです。xx
ctrlplusb

1

一般的に私はそれを2度読んで、あなたが何を達成したいのか完全には理解していません。私の理解から、あなたtransformは正確に与えられたタイプから省略したいと思いますtransform。これを簡単に実現するには、Omitを使用する必要があります。

interface Thing<T> {
  value: T; 
  count: number;
  transform: (data: Omit<Thing<T>, 'transform'>) => void; // here the argument type is Thing without transform
}

// 👇 the factory function accepting the generic
function makeThing<T>(value: T): Thing<T> {
  return {
    value,
    count: 1,
      transform: data => {
        data.count; // exist
        data.value; // exist
    },
  };
}

追加のユーティリティタイプで非常に複雑になっているため、これが目的のものかどうかはわかりません。それが役に立てば幸い。


ありがとう、そうです。しかし、これは私がサードパーティの消費のためにエクスポートしているユーティリティタイプです。オブジェクトの形状や性質がわかりません。すべての関数プロパティを取り除き、transform func data引数に対して結果を利用する必要があることを知っています。
ctrlplusb

問題の説明がわかりやすくなるように更新しました。
ctrlplusb

2
主な問題は、Tを除外するように定義されていないため、Tをアクションタイプにすることもできることです。いくつかの解決策を見つけることを願っています。しかし、私はカウントが大丈夫な場所にいますが、アクションとの交点なのでTはまだ省略されています
Maciej Sikora

TがAction型ではないことを私が表現できたら素晴らしいです。extendsの逆のソート。
ctrlplusb

相対ディスカッション:stackoverflow.com/questions/39328700/…–
ctrlplusb
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.