4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ネストしたtypeにreadonlyを適用させる

Posted at

はじめに

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を例に。

  1. personalInfoはkeyを持つからfalse
  2. name, salary...とマッピングされて処理
  3. name,salaryのvalueはstringやnumberだからDeepReadonly,DeepReadonlyとなり、つまりstring or number
  4. girlFriendはkeyとvalueを持つ型だから、二週目のはじめの分岐もfalseに入る。
  5. 繰り返し

って感じで見事にマッピングされるような処理が入っていく。

他への応用

もちろんこれはreadonlyだけじゃなくてPartialとかにも使えそう。
こんな感じかな。

type DeepPartial<T> = keyof T entends never ? T : {[t in keyof T]?: DeepPartial<T[t]>}

終わりに

Typescriptの型は奥が深いので、これからもっと極めたい。

4
1
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
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?