Typescript:マップされたタイプのインデックス署名

Nov 23 2020

どのように私はタイプ取ることができます{ 'k': number, [s: string]: any }し、抽象以上'k'としますかnumber?そのタイプTT<'k', number>与えるようなタイプエイリアスが欲しいのですが。


次の例を考えてみましょう。

function f(x: { 'k': number, [s: string]: any }) {}                           // ok
type T_no_params = { 'k': number, [s: string]: any };                         // ok
type T_key_only<k extends string> = { [a in k]: number };                     // ok
type T_value_only<V> = { 'k': V, [s: string]: any};                           // ok
type T_key_and_index<k extends string, V> = { [a in k]: V, [s: string]: any };// ?
  • { 'k': number, [s: string]: any}関数のパラメータのタイプとして直接使用すると機能しfます。
  • -aliasで[s: string]: anyインデックス部分を使用すると機能しますtype
  • 使用k extends stringしてtypeも動作します-alias
  • を同じエイリアスで組み合わせるk extends stringと、解析エラーが発生します(セマンティックエラーでさえなく、有効な構文でさえないようです)。[s: string]: anytype

これはここでうまくいくようです:

type HasKeyValue<K extends string, V> = { [s: string]: any } & { [S in K]: V }

しかし、ここでは、なぜそれが追加のプロパティについて文句を言わないのか完全には理解できません(の右側のタイプ&は追加のプロパティを持つオブジェクトを許可しないはずです)。


編集

&、集合論的共通部分と同様に動作するはずの共通部分演算子であることが回答で何度か言及されています。ただし、次の例が示すように、追加のプロパティの処理に関してはそうではありません。

function f(x: {a: number}){};
function g(y: {b: number}){};
function h(z: {a: number} & {b: number}){};

f({a: 42, b: 58});  // does not compile. {a: 42, b: 58} is not of type {a: number}
g({a: 42, b: 58});  // does not compile. {a: 42, b: 58} is not of type {b: number}
h({a: 42, b: 58});  // compiles!

この例で{a: 42, b: 58}は、はタイプ{a: number}でもタイプでもないように見えます{b: number}が、どういうわけか交差点になり{a: number} & {b: number}ます。それは集合論的交差がどのように機能するかではありません。

それこそが、私自身の&提案が私にとって非常に疑わしいと思われた理由です。マップされた型を「交差」さ{ [s: string]: any }せることで、型を小さくするのではなく「大きく」する方法を誰かが詳しく説明していただければ幸いです。


私は質問を見ました

  • Typescriptでマップされたタイプのインデックス署名
  • マップされたタイプのインデックス署名を追加するにはどうすればよいですか

しかし、それらは似たような名前を持っていたとしても、直接関連しているようには見えませんでした。

回答

1 arturgrzesiak Dec 01 2020 at 18:58

type HasKeyValue<K extends string, V> = { [s: string]: any } & { [S in K]: V }目的のタイプを定義する正しい方法です。ただし、知っておくべきことが1つあります(非推奨のフラグを言い換えると:keyofStringsOnly):

keyof型演算子は文字列を返します| 文字列インデックスシグネチャを持つタイプに適用される場合、文字列ではなく数値。

インデックスをstringタイプだけに制限し、ではないようにする方法がわかりませんstring | number。実際には、インデックスnumberへのアクセスを許可するstringことは、Javascriptの動作と一致しているため、合理的なことのように思われます(常に数値を文字列化できます)。一方、文字列値を使用して数値インデックスに安全にアクセスすることはできません。


&それは常に可能な値のセットを制限(または葉、それらを変更せずに、しかし、拡張することはありません) -タイプ演算子は、集合論的な交差点と同様に動作します。あなたの場合、タイプは文字列に似ていないキーをインデックスとして除外します。正確にはunique symbol、インデックスとして除外します。

Typescriptが関数パラメータを処理する方法から混乱が生じるかもしれません。明示的に定義されたパラメーターを使用して関数を呼び出すと、パラメーターを変数として渡す場合とは動作が異なります。どちらの場合も、Typescriptはすべてのパラメーターが正しい構造/形状であることを確認しますが、後者の場合、追加の小道具は許可されません。


概念を説明するコード:

type HasKeyValue<K extends string, V> = { [s: string]: any } & { [S in K]: V };
type WithNumber = HasKeyValue<"n", number>;
const x: WithNumber = {
  n: 1
};

type T = keyof typeof x; // string | number
x[0] = 2; // ok - number is a string-like index
const s = Symbol("s");
x[s] = "2"; // error: cannot access via symbol

interface N {
  n: number;
}

function fn(p: N) {
  return p.n;
}

const p1 = {
  n: 1
};

const p2 = {
  n: 2,
  s: "2"
};

fn(p1); // ok - exact match
fn(p2); // ok - structural matching: { n: number } present;  additional props ignored
fn({ n: 0, s: "s" }); // error: additional props not ignore when called explictily
fn({}); // error: n is missing

編集

オブジェクトリテラル-のような形状のオブジェクトを明示的に作成するとconst p: { a: number} = { a: 42 }、Typescriptによって特別な方法で処理されます。通常の構造的推論とは異なり、型は正確に一致する必要があります。そして正直に言うと、これらの追加のプロパティは、安全でない可能性のあるキャストを追加せずに、とにかくアクセスできないため、理にかなっています。

[...]ただし、TypeScriptは、このコードにはおそらくバグがあるというスタンスを取っています。オブジェクトリテラルは特別な扱いを受け、他の変数に割り当てるとき、または引数として渡すときに過剰なプロパティチェックを受けます。オブジェクトリテラルに「ターゲットタイプ」にないプロパティがある場合、エラーが発生します。[...]これらのチェックを回避する最後の方法の1つは、少し意外かもしれませんが、オブジェクトを別の変数に割り当てることです。

TSハンドブック

このエラーを回避する他のオプションは、...と交差させること{ [prop: string]: any }です。

その他のコード:

function f(x: { a: number }) {}
function g(y: { b: number }) {}
function h(z: { a: number } & { b: number }) {}

f({ a: 42, b: 58 } as { a: number }); // compiles - cast possible, but `b` inaccessible anyway
g({ a: 42 } as { b: number }); // does not compile - incorrect cast; Conversion of type '{ a: number; }' to type '{ b: number; }' may be a mistake
h({ a: 42, b: 58 }); // compiles!

const p = {
  a: 42,
  b: 58
};

f(p); // compiles - regular structural typing
g(p); // compiles - regular structural typing
h(p); // compiles - regular structural typing

const i: { a: number } = { a: 42, b: 58 }; // error: not exact match
f(i); // compiles
g(i); // error
h(i); // error
1 artcorpse Nov 30 2020 at 18:05

交差演算子について推論する方法は次のとおりです。多分それは役立ちます:

type Intersection = { a: string } & { b: number }

あなたは読むことができるIntersection「プロパティを持つオブジェクトとしてaのタイプのstring プロパティbタイプのをnumber」。これは、この単純なタイプについても説明しています。

type Simple = { a: string; b: number }

そして、2つのタイプは互換性があります。ほとんどすべての目的で、一方を他方に置き換えることができます。

これが、なぜHasKeyValueあなたが定義しようとしていたタイプと実際に同じであるかを説明することを願っています。

なぜT_key_and_index機能しないのかというと、最初の部分で[a in k]: Vあるがマップされたタイプを定義しているためです。マップされたタイプの定義では、追加のプロパティを設定できません。マップされたタイプにプロパティを追加する必要がある場合は、とのタイプ交差を&作成できます。