Q: TypeScript を使っているのに関数の引数のオブジェクトや配列に `readonly` を付与しないのは犯罪ですか? #Shorts
質問来てた👉 Q: TypeScript を使っているのに関数の引数のオブジェクトや配列に `readonly` を付与しないのは犯罪ですか? 結論:犯罪になる場合がある。 まず、配列の引数に `readonly` を付与しておけば以下の利点を得られます。 - うっかり関数の内部で引数の値を変更してしまうコードを書いてしまったときにコンパイルエラーが得られる - 関数の利用者が安心して関数を呼び出せる
質問来てた👉。
Q: TypeScript を使っているのに関数の引数のオブジェクトや配列に readonly
を付与しないのは犯罪ですか?
結論:犯罪になる場合がある。
まず、配列の引数に readonly
を付与しておけば以下の利点を得られます。
- うっかり関数の内部で引数の値を変更してしまうコードを書いてしまったときにコンパイルエラーが得られる
- 関数の利用者が安心して関数を呼び出せる
const getTop3 = (users: readonly User[]) => {
const sortedUsers = users.sort((a, b) => b.score - a.score) // Property 'sort' does not exist on type 'readonly User[]'.
return sortedUsers.slice(0, 3)
}
特に、関数の利用者視点では引数として渡したオブジェクトが変更されないことが保証されているので安心して利用できるという点が大きいでしょう。
逆に言えば readonly
ではない配列やオブジェクトを利用する場合はコピーを生成してから関数を利用する間に関数の実装を確認しなければいけないなど関数の利用者に負担を強いることになってしまします。
そのため関数の引数のオブジェクトや配列二¥に readonly
を付与しないのは副作用のある関数を定義してしまった罪 22 条によって犯罪となります。
ほかにも知りたいことがあったらコメント欄で教えて👇。
readonly とは
JavaScript では変数を宣言するときに const
を用いるとその変数に対して再代入をできなくなります。一般的にソースコードを読む際には値が変わらないことが保証されていると読み手の負荷が下がるので基本的に const
で変数を宣言することが推奨されています。
しかし const
で変数を宣言しても次のようにオブジェクトのプロパティを書き換えたり、配列の要素を増減させることはできてしまいます。
const user = {
id: 1,
name: 'alice'
}
user.id = 10
console.log(user)
// {
// "id": 10,
// "name": "alice"
// }
const colors = ['red', 'green', 'blue']
colors.push('yellow')
console.log(colors) // ["red", "green", "blue", "yellow"]
TypeScript においてオブジェクトやを完全に読み取り専用にしたい場合にはインターフェイス上のプロパティに対して readonly
を付与することできます。 readonly
が付与されたプロパティの値を変更しようとするコンパイルエラーを得られます。
interface User {
readonly id: number
readonly name: string
}
const user: User = {
id: 1,
name: 'alice'
}
user.id = 10 // Cannot assign to 'id' because it is a read-only property.
配列の場合も同様に readonly
を付与することで値の上書きだけでなく push
や sort
といった破壊的なメソッドの呼び出しについてもコンパイルエラーを得ることができます。
const colors: readonly string[] = ['red', 'green', 'blue']
colors.push('yellow')
関数の引数にオブジェクトや配列を渡すことの危うさ
ところで JavaScript において関数の引数にオブジェクトや配列を渡すときには 1 つの危険が発生する可能性があります。関数の引数として渡されたオブジェクトや関数を変更すると呼び出し元も変更されてしまいます。
interface User {
id: number
name: string
}
const user: User = { id: 1, name: 'alice' }
const someDangerousFunc = (argUser: User) => {
argUser.id = 99999999
}
someDangerousFunc(user)
console.log(user)
// {
// "id": 99999999,
// "name": "alice"
// }
基本的には引数のオブジェクトや配列の中身を変更しないように気をつければいいわけなのですが、 JavaScript の配列のメソッドは破壊的なものと非破壊的なものと一見区別がつかないのでその気がなくともうっかり変更してしまう可能性があります。
例えば array#sort
はメソッド名から配列の並べ替えを行うメソッドですがその名前からは元の配列を変更するかしないのかわかりません。
interface User {
id: number
name: string
score: number
}
const users: User[] = [
{
id: 1,
name: 'Aice',
score: 88
},
{
id: 2,
name: 'Bob',
score: 56
},
{
id: 3,
name: 'Carol',
score: 91
},
{
id: 4,
name: 'Dave',
score: 79
},
{
id: 5,
name: 'Eve',
score: 63
},
]
const getTop3 = (argsUsers: User[]) => {
const sortedUsers = argsUsers.sort((a, b) => b.score - a.score)
return sortedUsers.slice(0, 3)
}
getTop3(users)
console.log(users)
[LOG]: [{
"id": 3,
"name": "Carol",
"score": 91
}, {
"id": 1,
"name": "Aice",
"score": 88
}, {
"id": 4,
"name": "Dave",
"score": 79
}, {
"id": 5,
"name": "Eve",
"score": 63
}, {
"id": 2,
"name": "Bob",
"score": 56
}]
見てのとおり実際に array#sort
は元の配列を変更するメソッドなので引数に渡して元の変数の配列に思わぬ変更を与えてしまいます。
readonly で身を守る
上記の危険を回避するために、配列の引数の型を User[]
から readonly User[]
に変更しましょう。前述のとおり readonly
を付与した配列に対して破壊的なメソッドを呼び出すとコンパイルエラーが得られます。
User[]
型は readonly User[]
型のサブタイプなので問題なく代入をできます。
const users: User[] = [/** 省略 **/]
const getTop3 = (users: readonly User[]) => {
const sortedUsers = users.sort((a, b) => b.score - a.score) // Property 'sort' does not exist on type 'readonly User[]'.
return sortedUsers.slice(0, 3)
}
getTop3(users)
上記コンパイルエラーを解決するには配列のコピーを作成してから sort
メソッドを呼び出すと良いでしょう。
const getTop3 = (users: readonly User[]) => {
const sortedUsers = [...users].sort((a, b) => b.score - a.score)
return sortedUsers.slice(0, 3)
}
ちなみに上記実装にはまだ抜け穴が残っています。 User
オブジェクトが readonly
になっていないのでまだ変更可能になってしまっています。
const getTop3 = (users: readonly User[]) => {
users[1].score = 99999999 // Bobの点数をこっそり書き換える
const sortedUsers = [...users].sort((a, b) => b.score - a.score)
return sortedUsers.slice(0, 3)
}
抜け穴を塞ぐためには User
インターフェイスのプロパティをすべて readonly
にした新たなインターフェイスを用意して引数の型として利用しましょう。
interface User {
id: number
name: string
score: number
}
interface ReadonlyUser {
readonly id: number
readonly name: string
readonly score: number
}
const users: User[] = [/** 省略 **/]
const getTop3 = (users: readonly ReadonlyUser[]) => {
users[1].score = 99999999 // Cannot assign to 'score' because it is a read-only property.
const sortedUsers = [...users].sort((a, b) => b.score - a.score)
return sortedUsers.slice(0, 3)
}
実際にはわざわざ同じようなプロパティを持つインターフェイスを二重に定義するのは面倒なので組み込み型である Readonly<T>
を使いましょう。
const getTop3 = (users: readonly Readonly<User>[]) => {
users[1].score = 99999999 // Cannot assign to 'score' because it is a read-only property.
const sortedUsers = [...users].sort((a, b) => b.score - a.score)
return sortedUsers.slice(0, 3)
}