はじめに
typescriptの型にはreadonlyというパラメータがあります。
こいつをつけることでコードの途中でその値を変更することができなくなります。
type personalInfo = {
name: string
salary: number
readonly girlFriend: string
}
const updateInfo = (info: personalInfo) => {
info.salary += 10000000 // これはreadonlyついてないから実行できる
info.girlFriend = "広瀬すず" // ここでコンパイルエラー
return info
}
上の例では、readonlyのついていないsalaryは変更できますが、readonly属性の付されているgirlFriendは変更ができず、コンパイルエラーが起きます。いやしかし、給料は一千万円あげることができても、彼女を有名女優にすることはできません。なんと悲しい仕様でしょうか。
しかし、ほとんどのtypeにはreadonlyをつけるべきと言えます。immutableな実装を行う上では欠かせません。
readonlyをつけないのは、言ってしまえば直接変更しない変数の宣言をconstではなくあえてletを使うようなものです。
極力つけたほうがいいね。
Readnly
上記の例では要素の一つにつけるだけだったため、そのように実装しましたが、全要素に付したい場合はもっと楽に実装できます。
Readonlyというtypescriptに組み込まれたものを使います。
type personalInfo = {
name: string
salary: number
girlFriend: string
}
const updateInfo = (info: Readonly<personalInfo>) => {
info.salary += 10000000 // ここでエラー
info.girlFriend = "広瀬すず"
return info
}
Readonlyを使えば、型引数として与えられたtypeに対して、その全ての要素にreadonlyを付すことができます。
やってることは多分こんな感じ。
type Readonly<T> = {
readonly [t in keyof T]: T[t]
}
typeがネストするケース
だが、Readonlyだと、次のようにネストした要素にまでreadonlyをつけたいケースで問題になります。
type personalInfo = {
name: string
salary: number
girlFriend: {
name: string
job: {
jobName: string
position: string
workPlace: string
}
age: number
}
}
// type Readonly<personalInfo> は、
{
readonly name: string
readonly salary: number
readonly girlFriend: {
name: string // ネストした中身にはreadonlyが適用されない
job: {
jobName: string
position: string
workPlace: string
}
age: number
}
}
// 本当はこうしたい
{
readonly name: string
readonly salary: number
readonly girlFriend: {
readonly name: string
readonly job: {
readonly jobName: string
readonly position: string
readonly workPlace: string
}
readonly age: number
}
}
はいはい、どーせ別の組み込みのDeepReadonly的なやつあるんでしょ, ... ないんです。
だから自分で実装するしかない。
てことで実装。いろいろ自分で試行錯誤したり、調べていていくつでも方法はありそうだったが、あまりに美しい実装を見つけました。
type DeepReadonly<T> = keyof T entends never ? T : {readonly [t in keyof T]: DeepReadonly<T[t]>}
???て人のために。personalInfoを例に。
- personalInfoはkeyを持つからfalse
- name, salary...とマッピングされて処理
- name,salaryのvalueはstringやnumberだからDeepReadonly,DeepReadonlyとなり、つまりstring or number
- girlFriendはkeyとvalueを持つ型だから、二週目のはじめの分岐もfalseに入る。
- 繰り返し
って感じで見事にマッピングされるような処理が入っていく。
他への応用
もちろんこれはreadonlyだけじゃなくてPartialとかにも使えそう。
こんな感じかな。
type DeepPartial<T> = keyof T entends never ? T : {[t in keyof T]?: DeepPartial<T[t]>}
終わりに
Typescriptの型は奥が深いので、これからもっと極めたい。