🐧

【TypeScript】Object.assignに型をつける

2024/11/09に公開

objectを結合するjavascriptのメソッドObject.assignの型はtypescriptでどのように定義されているでしょうか?

最新のバージョン5.6.3で使ってみます。

const one = Object.assign({ a: 1 });
// any

const two = Object.assign({ a: 1 }, { b: 2 });
// { a: number, b: number }

const three = Object.assign({ a: 1 }, { b: 2 }, { c: 3 });
// { a: number, b: number, c: number }

const four = Object.assign({ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 });
// { a: number, b: number, c: number, d: number }

const five = Object.assign({ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }, { e: 5 });
// any

const six = Object.assign({ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 }, { e: 5 }, { f: 6 });
// any

objectを1個または5個以上渡したときにはany型になってしまいます。

型定義ファイルを確認すると以下のように定義されていました。

interface ObjectConstructor {
    /**
     * Copy the values of all of the enumerable own properties from one or more source objects to a
     * target object. Returns the target object.
     * @param target The target object to copy to.
     * @param source The source object from which to copy properties.
     */
    assign<T extends {}, U>(target: T, source: U): T & U;

    /**
     * Copy the values of all of the enumerable own properties from one or more source objects to a
     * target object. Returns the target object.
     * @param target The target object to copy to.
     * @param source1 The first source object from which to copy properties.
     * @param source2 The second source object from which to copy properties.
     */
    assign<T extends {}, U, V>(target: T, source1: U, source2: V): T & U & V;

    /**
     * Copy the values of all of the enumerable own properties from one or more source objects to a
     * target object. Returns the target object.
     * @param target The target object to copy to.
     * @param source1 The first source object from which to copy properties.
     * @param source2 The second source object from which to copy properties.
     * @param source3 The third source object from which to copy properties.
     */
    assign<T extends {}, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W;

    /**
     * Copy the values of all of the enumerable own properties from one or more source objects to a
     * target object. Returns the target object.
     * @param target The target object to copy to.
     * @param sources One or more source objects from which to copy properties
     */
    assign(target: object, ...sources: any[]): any;

...省略
}

objectが2~4個渡されたときには型定義がされていますが、それ以外のときにはany型になってしまっています。

また、Object.assignはプロパティ名に同じものが存在している場合は、後に指定したものが優先されるので、同じプロパティ名のobjectを渡してみます。

const two = Object.assign({ a: 1 }, { a: "1" });
// {
//   a: number;
// } & {
//   a: string;
// }
console.log(two);
// { a: '1' }

const three = Object.assign({ a: 1 }, { a: "1" }, { a: true });
// never
console.log(three);
// { a: true }

このときには、never型になってしまいます。異なる型同士(numberとstringなど)を交差型で結合しているのが原因です。

そこで、XでObject.assignの型定義を実装している投稿を見かけたので紹介します。

実装

コードは以下のようになります。

type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

type Merge<T1, T2> = Prettify<Omit<T1, keyof T2> & T2>;

type MergeArrayOfObjects<
  TArr extends readonly object[],
  T1 = {},
> = TArr extends [infer T2 extends object, ...infer TRest extends object[]]
  ? MergeArrayOfObjects<TRest, Merge<T1, T2>>
  : T1;

const merge = <TArr extends readonly object[]>(
  ...objects: TArr
): MergeArrayOfObjects<TArr> => {
  return Object.assign({}, ...objects);
};

一つずつ解説していきます。

Prettify

type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

交差型を定義したときの型は一つにまとめられず、&で結合されたものがホバー時に表示されると思います。

type A = { a: number } & { b: number }
// { a: number } & { b: number } としか表示されない

Prettifyを使用することで一つのobjectにまとめてくれます。

type PrettifyA = Prettify<A>;
// { a: number, b: number } とまとめられる

Merge

type Merge<T1, T2> = Prettify<Omit<T1, keyof T2> & T2>;

Prettifyは交差型を見やすくするために使ってるだけなので、以下の部分にだけ注目します。

Omit<T1, keyof T2> & T2

keyof T2でT2のプロパティ名のユニオン型が取れるので、Omit<T1, keyof T2>でT1からT2とプロパティ名が同じものを除外します。その後にT2との交差型を取るので、objectを結合しつつ、同じプロパティ名が存在すれば後ろのものを優先させます。

以下、例です。

type MergeExample = Merge<
  { a: number; b: string; c: boolean },
  { b: number; c: boolean; d: string }
>;

一個目のobjectから二個目のobjectと同じプロパティ名のものを除外した後に、二個目のobjectと結合させるので以下のようになります。

{ a: number } & { b: number, c: boolean, d: string }

これをPrettifyするので、MergeExampleは以下のようになります。

type MergeExample = {
    a: number;
    b: number;
    c: boolean;
    d: string;
}

MergeArrayOfObjects

type MergeArrayOfObjects<
  TArr extends readonly object[],
  T1 = {},
> = TArr extends [infer T2 extends object, ...infer TRest extends object[]]
  ? MergeArrayOfObjects<TRest, Merge<T1, T2>>
  : T1;

Object.assignでは任意の数のobjectを受け取れるので、先ほど定義したMergeを任意の数で使用できるように拡張します。

MergeArrayOfObjectsは型引数にobjectの配列を受け取り、T1は初期値に空のobjectを持ちます。次に返される型を見ていきます。

TArr extends [infer T2 extends object, ...infer TRest extends object[]]
  ? MergeArrayOfObjects<TRest, Merge<T1, T2>>
  : T1;

...infer TRest extends object[]は可変長のobjectを表すので、TArrに要素が存在するかどうかを判定します。要素が存在すれば最初の要素をT2としてT1とマージし、TRestに対して再度MergeArrayOfObjectsを呼び出します。例を見てみましょう。

type MergeArrayOfObjectsExample = MergeArrayOfObjects<
  [
    { a: number; b: string },
    { a: string; c: boolean },
    { b: number; c: boolean; d: string },
  ]
>;

T1は初期値の空のobject、T2は{ a: number; b: string }、TRestは[{ a: string; c: boolean; }, { b: number; c: boolean; d: string }となり、以下のようになります。

type MergeArrayOfObjectsExample2 = MergeArrayOfObjects<
  [{ a: string; c: boolean }, { b: number; c: boolean; d: string }],
  Merge<{}, { b: number; c: boolean; d: string }>
>;

Mergeはobjectの結合をするので、

type MergeArrayOfObjectsExample3 = MergeArrayOfObjects<
  [{ a: string; c: boolean }, { b: number; c: boolean; d: string }],
  { b: number; c: boolean; d: string }
>;

となります。同様に繰り返していきます。

type MergeArrayOfObjectsExample4 = MergeArrayOfObjects<
  [{ b: number; c: boolean; d: string }],
  Merge<{ a: string; c: boolean }, { b: number; c: boolean; d: string }>
>;

type MergeArrayOfObjectsExample5 = MergeArrayOfObjects<
  [{ b: number; c: boolean; d: string }],
  { a: string; b: number; c: boolean; d: string }
>;

type MergeArrayOfObjectsExample6 = MergeArrayOfObjects<
  [],
  Merge<
    { b: number; c: boolean; d: string },
    { a: string; b: number; c: boolean; d: string }
  >
>;

type MergeArrayOfObjectsExample7 = MergeArrayOfObjects<
  [],
  { a: string; b: number; c: boolean; d: string }
>;

MergeArrayOfObjectsExample7でTArrが空の配列となったので条件分岐がfalseとなり、T1が返され、MergeArrayOfObjectsExampleは以下のようになります。

type MergeArrayOfObjectsExample = {
  a: string;
  b: number;
  c: boolean;
  d: string;
};

merge

const merge = <TArr extends readonly object[]>(
  ...objects: TArr
): MergeArrayOfObjects<TArr> => {
  return Object.assign({}, ...objects);
};

MergeArrayOfObjectsを使用することで、型の付いたObject.assignを定義することができました。Object.assignは可変長引数なので、引数を...objects: TArrで定義しています。

Discussion