🐴

ここではTypescriptの値渡しと参照渡しについて語られる

に公開1

Typescriptと少しのC言語しか学んでいなかった私には値渡し、参照渡しという概念がピンとこなかったので、検証を通じて理解を含めることとする。

値渡し

元の引数をコピーした値が新たに変数として作成され、元の引数に影響を及ぼさない渡し方。
Typescriptや元になったJavaでは基本的にこの渡し方が仕様となる。

function addTen(x: number): void {
  x += 10;          // ローカル変数 x を変更
  console.log("  inside:", x);  // => 15
}

let num = 5;
addTen(num);

console.log("outside:", num);   // => 5 (元の値は変わらない)

参照渡し

関数の引数に、変数そのものの参照を渡す方法のこと。
Typescriptでは参照を渡さないので、仕様上発生しない。
もしTypescriptが参照渡しの時、以下のような挙動になる。

type User = { name: string; age: number };

function replaceWithBob(ref u: User): void {
  u = { name: "Bob", age: 30 };  // 参照先を新オブジェクトへ
}

const alice: User = { name: "Alice", age: 25 };
replaceWithBob(alice);

console.log(alice); // **(想定) { name: "Bob", age: 30 }**

参照の値渡し

呼び出し側が持つ参照(ポインタ)の コピー が引数に渡される。
値渡しの一種で参照のコピーを渡している状態。

  • オブジェクト内部の変更 は同じ参照先を介して共有される。
type User = { name: string; age: number };

function birthday(u: User): void {
  u.age += 1;                      // プロパティを直接変更
  console.log("  inside:", u);     // => { name: 'Alice', age: 26 }
}

const alice: User = { name: "Alice", age: 25 };
birthday(alice);

console.log("outside:", alice);    // => { name: 'Alice', age: 26 }
  • 再代入 は呼び出し元に影響しない。
function replaceUser(u: User): void {
  u = { name: "Bob", age: 30 };    // 参照そのものを新しいオブジェクトに置き換え
  console.log("  inside:", u);     // => Bob
}

replaceUser(alice);
console.log("outside:", alice);    // => Alice(変化なし)

意図しない変更によるバグを回避するには?

引数を直接変更するのをやめ、新しいオブジェクトを作成する。

type User = { name: string; age: number };

  function birthday(u: User): User {
    const newUser = { ...u }; // 新しいオブジェクトを作成
    newUser.age += 1; // 新しいオブジェクトのプロパティを変更
    console.log("  inside:", u); // => { name: 'Alice', age: 26 }
    return newUser; // 新しいオブジェクトを返す
  }

  const alice: User = { name: "Alice", age: 25 };
  const newAlice = birthday(alice);

  console.log("outside:", alice); // => { name: 'Alice', age: 25 }
  console.log("newAlice:", newAlice); // => { name: 'Alice', age: 26 }

以下のルール設定によりビルド時などにエラーが発生するため、意図しない変更を防ぐことができる

  • ESLint no-param-reassign, functional/no-let など
  • TypeScript --noImplicitThis, --strict

まとめ

コードの可読性や変更容易性を高く保つためにも分かりやすいコードの書き方を心がけるのが大事。

YOSHINANI

Discussion

junerjuner

Typescriptでは参照を渡さないので、仕様上発生しない。

関数の引数として変数の参照を渡す手段はありませんが、 import した変数とかは export した変数 の参照 なので export 元での代入が出来たりします。(ちゃんと他の変数への代入と違って、代入が反映されます。