【TypeScript】Object.assignに型をつける
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