TypeScriptでオブジェクト同士の同じプロパティ値のみをコピーする方法

TypeScriptを使っていて、コピー先のオブジェクトに存在するプロパティのみを、コピー元からコピーしたいということがありました。
「Object.assignじゃダメなの?」と思われるかもしれないですが、Object.assignだとコピー先にないプロパティまでコピーされてしまいます。

const target = { a: 1, b: 2 }
const source = { b: 4, c: 5 }

Object.assign(target, source)

console.log(target) // { a: 1, b: 4, c: 5 }となるが、{ a: 1, b: 4 }となってほしい

これをTypeScriptで実現する方法について調べたのでメモとして残しておきます。

ちなみに、JavaScriptならすぐに書けます。

// 存在するプロパティの値のみコピーする
function copySameProperties(target, source){
  for(const key in source){
    if(key in target){
      target[key] = source[key]
    }
  }
  return target
}

const target = { a: 1, b: 2 }
const source = { b: 4, c: 5 }
copySameProperties(target, source)

console.log(target) // {a: 1, b: 4}

ここまで書けるなら後は、TypeScriptで型を指定すればいいだけかと思ったのですが、これがなかなか難しい…。最終的に、下記のように書くことでうまくいきました。

function copySameProperties<T extends object, U extends object>(
  target: T,
  source: U
): T {
  for (const key in source) {
    if (key in target) {
      target[key as unknown as keyof T] = source[key] as unknown as T[keyof T];
    }
  }

  return target;
}

const target = { a: 1, b: 2 }
const source = { b: 4, c: 5 }
copySameProperties(target, source)

console.log(target) // {a: 1, b: 4}

ポイントとして下記の3つです。

1.引数のtargetとsourceがオブジェクトということを明示するため、ジェネリクスに「extends object」という制約をいれる
参考:型引数の制約 | TypeScript入門『サバイバルTypeScript』

2.keyの型が「Extract」となっている。このままだと、targetのプロパティに存在するか判断できないので、「key as unknown as keyof T」という記述をいれて、「keyof T」型と伝えている。なお、unknown型を経由せず、直接「keyof T」型に変換すると「Conversion of type ‘Extract‘ to type ‘keyof T’ may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to ‘unknown’ first.」というエラーメッセージが表示される。
参考:型アサーション「as」(type assertion) | TypeScript入門『サバイバルTypeScript』

3.2と同様に、source[key]の型がtarget[key]か判別できないため、「T[keyof T]」と型指定をいれている。

「extends object」とか、「as unknown as」とか、TypeScriptに慣れていないとなかなかでてこない書き方で、迷いました…。これぐらい、すんなり書けるようになりたいです。

ちなみに、ChatGPTでJavaScriptのコードを渡して、TypeScriptに変換してと伝えると、第2引数のsourceは「Partial」型にした書き方で提案されたので、下記のような記述でもいいかもしれないです。

function copySameProperties<T extends object>(
  target: T,
  source: Partial<T>
): T {
  for (const key in source) {
    if (key in target) {
      target[key as keyof T] = source[key]!;
    }
  }

  return target;
}

const target = { a: 1, b: 2 }
const source = { b: 4, c: 5 }
copySameProperties(target, source)

console.log(target) // {a: 1, b: 4}

※ChatGPTの提案では「extends object」が無かったり、「key as any」とされてしまったので少し書き換えてます。

コメント

タイトルとURLをコピーしました