web-dev-qa-db-ja.com

Typescript、オブジェクトタイプのマージ?

2つの汎用オブジェクトタイプの小道具をマージすることは可能ですか?私はこれに似た機能を持っています:

function foo<A extends object, B extends object>(a: A, b: B) {
    return Object.assign({}, a, b);
}

タイプは、Bには存在しないAのすべてのプロパティ、およびBのすべてのプロパティにする必要があります。

merge({a: 42}, {b: "foo", a: "bar"});

かなり奇妙なタイプの{a: number} & {b: string, a: string}aは文字列ですが。実際の戻り値は正しいタイプを提供しますが、どのように明示的に記述するかわかりません。

10
Jomik

Object.assign()TypeScript標準ライブラリ定義)によって生成される交差タイプは---(近似 であり、後の引数にプロパティがある場合の動作を適切に表しません以前の引数と同じ名前です。しかし、ごく最近まで、これはTypeScriptの型システムで実行できる最高の方法でした。

ただし、TypeScript 2.8での 条件付きタイプ の導入からは、より近い近似が利用できます。そのような改善の1つは、タイプ関数Spread<L,R> defined here を次のように使用することです。

// Names of properties in T with types that include undefined
type OptionalPropertyNames<T> =
  { [K in keyof T]: undefined extends T[K] ? K : never }[keyof T];

// Common properties from L and R with undefined in R[K] replaced by type in L[K]
type SpreadProperties<L, R, K extends keyof L & keyof R> =
  { [P in K]: L[P] | Exclude<R[P], undefined> };

type Id<T> = {[K in keyof T]: T[K]} // see note at bottom*

// Type of { ...L, ...R }
type Spread<L, R> = Id<
  // Properties in L that don't exist in R
  & Pick<L, Exclude<keyof L, keyof R>>
  // Properties in R with types that exclude undefined
  & Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>>
  // Properties in R, with types that include undefined, that don't exist in L
  & Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>>
  // Properties in R, with types that include undefined, that exist in L
  & SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
  >;

(リンクされた定義を少し変更しました。Excludeの代わりに標準ライブラリのDiffを使用し、Spreadタイプをno-op Idタイプでラップして、検査されたタイプを交差点の束より扱いやすくします)。

試してみましょう:

function merge<A extends object, B extends object>(a: A, b: B) {
  return Object.assign({}, a, b) as Spread<A, B>;
}

const merged = merge({ a: 42 }, { b: "foo", a: "bar" });
// {a: string; b: string;} as desired

出力のastring & numberではなくstringとして正しく認識されるようになりました。わーい!


ただし、これはまだ概算であることに注意してください。

  • Object.assign()列挙可能、独自のプロパティ のみをコピーします。型システムは、フィルター処理するプロパティの列挙可能性と所有権を表す方法を提供しません。実行時にDateメソッドがコピーされず、出力が基本的に{}であっても、merge({},new Date())はTypeScriptに対して型Dateのように見えることを意味します。これは今のところハードリミットです。

  • さらに、Spreadの定義は、実際には distinguishmissingプロパティと未定義の値で存在します。そのため、merge({ a: 42}, {a: undefined})は、{a: number}であるはずの誤った{a: undefined}として入力されます。これはおそらくSpreadを再定義することで修正できますが、100%確実ではありません。そして、それはほとんどのユーザーにとって必要ないかもしれません。 (編集:これはtype OptionalPropertyNames<T> = { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T]を再定義することで修正できます)

  • 型システムは、知らないプロパティでは何もできません。 declare const whoKnows: {}; const notGreat = merge({a: 42}, whoKnows);は、コンパイル時に{a: number}の出力タイプになりますが、whoKnows{a: "bar"}{}に割り当て可能)である場合、notGreat.aは実行時の文字列ですが、コンパイル時の数値です。おっとっと。

ですから注意してください。 Object.assign()を交差点またはSpread<>として入力することは、「ベストエフォート」のようなものであり、Edgeのケースで迷う可能性があります。


とにかく、それが役立つことを願っています。幸運を!


*注意:誰かが識別マップタイプからId<T>の定義を編集して、Tに変更しました。このような変更は正確ではありませんが、交差を排除するためにキーを繰り返し処理するという目的に反します。比較:

type Id<T> = { [K in keyof T]: T[K] }

type Foo = { a: string } & { b: number };
type IdFoo = Id<Foo>; // {a: string, b: number }

IdFooを調べると、交差が削除され、2つの構成要素が1つのタイプにマージされていることがわかります。繰り返しになりますが、FooIdFooの間には割り当て可能性の点で実際の違いはありません。後者はある状況では読みやすいというだけです。確かに、コンパイラの型の文字列表現が不透明なId<Foo>になる場合があるため、完全ではありません。しかし、目的はありました。独自のコードでId<T>Tに置き換えたい場合は、ゲストになってください。

17
jcalz

union|)交差の代わりにタイプ(&)タイプ。それはあなたが望むものに近いです...

function merge<A, B>(a: A, b: B): A | B {
  return Object.assign({}, a, b)
}

merge({ a: "string" }, { a: 1 }).a // string | number
merge({ a: "string" }, { a: "1" }).a // string

tSを学ぶ私は多くの時間を使って このページ ...に戻ってきました。それは良い読み物であり(そのようなことに興味がある場合)、多くの有用な情報を提供します

3
Tyler Sebastian

プロパティの順序を維持したい場合は、次のソリューションを使用します。

実際にご覧ください こちら

export type Spread<L extends object, R extends object> = Id<
  // Merge the properties of L and R into a partial (preserving order).
  Partial<{ [P in keyof (L & R)]: SpreadProp<L, R, P> }> &
    // Restore any required L-exclusive properties.
    Pick<L, Exclude<keyof L, keyof R>> &
    // Restore any required R properties.
    Pick<R, RequiredProps<R>>
>

/** Merge a property from `R` to `L` like the spread operator. */
type SpreadProp<
  L extends object,
  R extends object,
  P extends keyof (L & R)
> = P extends keyof R
  ? (undefined extends R[P] ? L[Extract<P, keyof L>] | R[P] : R[P])
  : L[Extract<P, keyof L>]

/** Property names that are always defined */
type RequiredProps<T extends object> = {
  [P in keyof T]-?: undefined extends T[P] ? never : P
}[keyof T]

/** Eliminate intersections */
type Id<T> = { [P in keyof T]: T[P] }
2
aleclarson