LoginSignup
47
24

More than 1 year has passed since last update.

最速TypeScript静的型付け不健全プログラミング~readonlyプロパティに値を再代入する~

Last updated at Posted at 2023-01-11

こんにちは。
最近Haskellを書いていない、型エンジニアのaiya000です!

皆さんは下記コードのreadonlyで、x.aは変更されることがないと思っていませんか?
残念ですが……完全にはそうはなりません……。

const x: { readonly a: number } = { a: 42 }

TypeScriptの静的型付けの方針(?)で、静的に代入を許すことができてしまいます。

しかもここで紹介する、x.aを変更する方法では、asなどのunsafeな操作は必要としません。

結論

結論からお話します。
下記コードで、x.a10に変更できます!

const x: { readonly a: number } = { a: 42 }
const y: { a: number } = x

// x.aが変更されている
y.a = 10

console.log(x) // { a: 10 }

// コンパイルエラーはない

{ a: number } extends { readonly a: number }になっているから

= { a: number }の変数に{ readonly a: number }の値を代入できるからです!

読み始める前に

本稿の主題は「静的型付き下でreadonlyの制約を破る」であり、
実行時(例えば下記コード)を考慮して制約を破ることは目標としていません。
(それは簡単すぎるので!)

const x: { readonly a: number } = { a: 42 }
const y: any = x
y.a = 10

constreadonlyとは?

const

上述のコードのx.aは、ある方法で変更され得ります。
しかし本来は変更されるべきではないです。

どうして上述のコードが不変であるべきなのか、改めて再確認してみましょう。
まずconstは「再代入」を許しません。
次の例で表せます。

const x: number = 42
x = 10  // コンパイルエラー

大丈夫です、このコードは正しくコンパイルエラーになります

readonly

次にreadonlyです。
下記に示すコードも、正しくコンパイルエラーになります

let x: { readonly a: number } = { a: 42 }

// ここはコンパイルエラーにならないので、
// 不変性を求める場合は通常、letではなくconstと組み合わせる。
x = { a: 10 }

// ここはコンパイルエラーになる。
x.a = 20

x.a(readonlyプロパティ)への再代入は、コンパイルエラーになります。

const + readonly

TypeScriptでは、上述の2つを組み合わせて、不変なオブジェクトの変数を作ります。

const x: { readonly a: number } = { a: 42 }
x = { a: 10 } // コンパイルエラー
x.a = 10      // コンパイルエラー

これでxはいかなる場合にも、プロパティの値が変わらないオブジェクトの変数になりました。
アプリケーションの設定など、アプリケーションの実行時に変更されたくない変数はこのテクニックを使うことで、実現することができます。

……

ごめんなさいというのは嘘です
実際は、このxのような変数は、変更され得ります

不変性と部分型付け

なぜconst x: { readonly a: number } = { a: 42 }のような変数は変更されうるのでしょうか。
その秘密にせまるために、通常あるべき、不変性の部分型付けを見てみます。

次のコードは、プログラミング意味論的に、通るべきです。
なぜなら「可変なオブジェクトは、一時的に不変にしても破綻しない」からです。

// ある不変なオブジェクトの型
type Immutable = {
  readonly a: number
}

// ある可変なオブジェクトの型
type Mutable = {
  a: number
}

function f(x: Immutable): void {
  // xを使って処理をする
  console.log(x)
}

const x: Mutable = { a: 42 }

f(x)

例えばKotlinでは上述と同様に、MutableListの変数を、(Immutable)Listの変数に代入することができます。

逆に次のコードは、プログラミング意味論を破綻させます。
なぜなら「不変なオブジェクトを一時的に可変にすると、不変性が破綻する」からです。

illegal.ts
// ある不変なオブジェクトの型
type Immutable = {
  readonly a: number
}

// ある不変でないオブジェクトの型
type Mutable = {
  a: number
}

function f(x: Mutable): void {
  // xを使って処理をする
  x.a = 10
}

const x: Immutable = { a: 42 }
f(x)
console.log(x)

具体的には{ readonly a: number } extends { a: number }であるべきで、
{ a: number } extends { readonly a: number }であるべきではありません。

……

勘のいい人は気づいてしまったかもしれません。

このコードillegal.tsは、現在のTypeScriptではコンパイルエラーになりません!
結論で申し上げた通り、{ a: number } extends { readonly a: number }だからです!


これは本来、そうあるべきではない挙動です。
KotlinがMutableListの変数に(Immutale)Listの値を代入できないように、
つまり{ a: number }の変数に{ readonly a: number }の値を代入できるべきではないのです。

まとめ

例をシンプルに書き直します。

これはx.aを書き換えます。
不変性が壊れているのが、容易にわかります。

const x: { readonly a: number } = { a: 42 }
const y: { a: number } = x

// x.aが変更されている
y.a = 10

console.log(x) // { a: 10 }

// コンパイルエラーはない

as constでも同様です。

const x = { a: 42 } as const
const y: { a: number } = x

y.a = 10

console.log(x)

// コンパイルエラーはない

おまけ

どうしてもTypeScriptで絶対の不変性が必要な場合には、Object.freeze()を使用して、実行時にエラーを送出させる必要があります。

const x: { readonly a: number } = Object.freeze({ a: 42 })
const y: { a: number } = x

// ここで例外が送出される
y.a = 10

TypeScriptの{readonly a: number}型では、aプロパティは変更不可になりません

47
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
24