as constについて整理してみる

TypeScriptの型はたくさんありますよね。

世界史みたいにカタカナがたくさん出てくる印象です。
そんでもって、こんな動きするのは覚えてるんだけど、これ名前なんだっけとなったりしています。（おそらく私だけ）

そんな中`as const`についてちょこちょこ見かける機会がありました。
なんとなく読み取り専用になるイメージでしたが、改めて調べてみました。

今回はそこで学んだことを整理してみました。

### as constとは
プロパティをすべてreadonlyにする型アサーションです。
もう少し詳しく見てみると、以下の特徴があります。
- 配列リテラルの型推論結果を配列型ではなくタプル型にする。
- オブジェクトリテラルから推論されるオブジェクト型はすべてのプロパティがreadonlyになる。配列リテラルから推論されるタプル型もreadonlyタプル型になる。
- 文字列・数値・BigInt・真偽値リテラルに対してつけられるリテラル型がwideningしないリテラル型になる。
- テンプレート文字列リテラルの型がstringではなくテンプレートリテラル型になる。

(鈴木 僚太. プロを目指す人のためのTypeScript入門　安全なコードの書き方から高度な型の使い方まで (Software Design plus) (Japanese Edition) (p.681)より引用） 

実際に`as const`をつけて型を作成してみました。
```tsx
const setting = {
  isDisabled: true,
  size: 'L',
  details: {
    text: "詳細"
  },
  attrs: ["button", "input"]
} as const

type Setting = typeof setting

// Settingの中身
{
    readonly isDisabled: true;
    readonly size: "L";
    readonly details: {
        readonly text: "詳細";
    };
    readonly attrs: readonly ["button", "input"];
}

// as constを外した場合
{
    isDisabled: boolean;
    size: string;
    details: {
        text: string;
    };
    attrs: string[];
}
```
型の中身をas constの有無によって比較してみると、
as constがある方は全ての項目に`readonly`がついており、リテラル型が使用されていることがわかります。
配列にもタプル型が適用されており、厳格な型になっています。

### readonlyとas constの違い
読み取り専用にする方法として、`readonly`や`Readonly<T>`を使用する方法があります。
```typescript
type Setting = {
	readonly isDisabled: Boolean
	readonly size: 'S' | 'M' | 'L'
}

const setting: Setting = {
	isDisabled: true,
	size: 'L'
}

setting.isDisabled = false //readonlyのためエラーが発生する
```
`readonly`と`as const`ではどんな違いがあるのでしょうか。
その違いは`readonly`にする範囲にあります。
- `as const`はすべてのプロパティを読み取り専用にします
- `readonly`はプロパティごとに読み取り専用できます

例えばプロパティの値がオブジェクトの場合、
オブジェクトのキーに`readonly`がついていなければ変更できてしまいます。

言葉だけだと難しいので、コードを書いてみました。
コードの中身はざっくり以下の通りです。
- Setting型があり、その中にはdetailsというプロパティがある。
- detailsはDetailsというオブジェクト型になっている。

```typescript
type Details = {
	text: string
}

type Setting = {
	readonly isDisabled: Boolean
	readonly size: 'S' | 'M' | 'L'
	readonly details: Details
}

const setting: Setting = {
	isDisabled: true,
	size: 'L',
	details: {
		text: '詳細'
	},
}
```

一見すべて読み取り専用に見えますが、
実は以下のような動作ができてしまいます。

```ts
//Details型のtextはreadonlyでないため、変更できてしまう。
setting.details.text = 'これは変えられる'

//detailsプロパティに対してreadonlyのため、detailsごと変更はできない。
setting.details = {
	text: 'これはできないけど'
} 
```

配列でも同様な動きとなります。
```ts
type Attrs = string[]

type Details = {
	text: string
}

type Setting = {
	readonly isDisabled: Boolean
	readonly size: 'S' | 'M' | 'L'
	readonly details: Details
	readonly attrs: Attrs
}

const setting: Setting = {
	isDisabled: true,
	size: 'L',
	details: {
		text: '詳細'
	},
	attrs: ['button', 'input']
}

// Attrs型にreadonlyがないため、pushできてしまう
setting.attrs.push('hoge')
```

`as const`は再帰的に見てくれるためプロパティがオブジェクトや配列の場合でも、すべての項目がreadonlyとなります。
```ts
const setting = {
	isDisabled: true,
	size: 'L',
	details: {
		text: '詳細'
	},
} as const

// 以下のことはできなくなる
setting.details.text = 'これも変えられないし'
setting.details = {
	text: 'こっちも変えられない'
}
setting.attrs.push('もちろんこれもできない')
```


### 参考
- プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで (Software Design plus)
- [サバイバルTypeScript constアサーション「as const」 (const assertion)](https://typescriptbook.jp/reference/values-types-variables/const-assertion)