【TypeScript】発展的な型まとめ

TypeScriptには基本的なプリミティブ型や単純なオブジェクト型に加えて安全性を高めたり、
複雑な型を表現したりするための型・機能が用意されています。

この記事ではTypeScript標準の発展的な型をまとめます。


## 一覧
<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->

### タグ付き合併型(判別可能な合併型)

合併型は型の絞り込みが思ったように動作しない場合があります。
2種類のイベントとそれを処理するためのサンプルコードを見てみます。

```TypeScript

type UserTextEvent = {value: string, target: HTMLInputElement};
type UserMouseEvent = {value: [number,number], target: HTMLElement};

type UserEvent = UserTextEvent | UserMouseEvent;

function handle(event: UserEvent) {
    if (typeof event.value === "string") {
        event.value; // string
        event.target; // HTMLInputElement |  HTMLElement
        //省略
    } 
    event.value; // [number, number]
    event.target; // HTMLInputElement |  HTMLElement
}

```

event.targetは型の絞り込みが行われませんでした。UserEvent型は UserTextEvent | UserMouseEvent型を
とることができるため、targetの型が重複する可能性があります。

この問題を解決するために、合併型のそれぞれの型についてタグ付けをします。

タグ付け後のコードがこちらです。

```TypeScript

// UserTextEventとUserMouseEventにtypeプロパティを追加する。
// typeプロパティの型は一意のリテラル型
type UserTextEvent = {type: 'TextEvent',value: string, target: HTMLInputElement};
type UserMouseEvent = {type: 'MouseEvent', value: [number,number], target: HTMLElement};

type UserEvent = UserTextEvent | UserMouseEvent;

function handle(event: UserEvent) {
    if (event.type === "TextEvent") {
        event.value; // string
        event.target; // HTMLInputElement 
        //省略
    } 
    event.value; // [number, number]
    event.target; // HTMLElement
}

```

タグ付けされたプロパティ(type)の値を参照してeventの型を絞り込むにあたり、event.type === "TextEvent"の場合
TypeScriptはevent.targetがHTMLInputElementを推論します。if文を抜けた後はevent.targetはUserMouseEventだと推論されます。

タグはUserEvent型で一意であるため、このような推論が可能になります。

判別用のタグは以下の要件を満たしているのが望ましいです。

- 複数のオブジェクト型で共通のプロパティであること(今回の場合はtype)
- リテラル型として型付けされていること(通常は文字列リテラル型です)
- 合併型の中で一意であること


### ルックアップ型

オブジェクトのキーを指定して型を取得することができます。

```TypeScript

type APIResponse = {
    user: {
        userId: string
        friendList: {
            count: number
            friends: {
                firstName: string
                lastName: string
            }[]
        }
    }
}


// User型：count: numberとfriends: {firstName: string, lastName: string}[]を持つ
type User = APIResponse['user']['friendList'];

type Friends = User['friends']; //  type Friends = {firstName: string, lastName: string}[]
```

### keyof 演算子

オブジェクトの全てのキーを文字列リテラル型の合併型として取得することができます。

```TypeScript

type Job = {
    warrier: string
    witch: string
    martialArtist: string
    monk: string
}

type JobList = keyof Job; // "warrier" | "witch" | "martialArtist" | "monk"
```

keyof演算子には保守性向上というメリットがあります。オブジェクト型とオブジェクトのプロパティ名を文字列リテラルとして
持つ合併型を宣言している場合、プロパティが変更された時に合併型も変更する必要があります。

合併型をkeyof演算子を用いて定義している場合、オブジェクト型を修正すれば済みます。


### Record<T,U>型

2つの型のマップ(対応付け)をオブジェクトで表現するための型です。
Record型を使うとある型で定義されたキーが全て網羅されていることを強制することができます。

```TypeScript

// 職業の文字列リテラルを定義する合併型
type Job = "warrier" |  "martialArtist" | "witch" |"monk";

// 各職業の特技
type Skills = 
    "PhysicalAttack" |
    "SwordAttack" |
    "MagickAttack"|
    "Heal"

// 職業と特技をマッピングする
const JobSkills: Record<Job, Skills> = {
    warrier: "PhysicalAttack",
    martialArtist: "SwordAttack",
    witch: "MagickAttack",
    monk: "Heal"

};
```

上の例では定義されていないキーがあるとコンパイルエラーになります。
これにより未定義プロパティを実行前に検出することができます。

### マップ型

マップ型はオブジェクトのキーの型と値の型をマッピング(対応付け)します。
オブジェクト型にキーが存在することを保証できるため、安全性が向上します。

```TypeScript
// 構文
type MappedTypes = {
    [Key in UnionType | ObjectType]: ValueType
}

// 使用例
type SystemSupportLanguage = "en" | "fr" | "it" | "es";

type Language = {
  [key in SystemSupportLanguage]: string; // {en: string; fr: string; it: string; es: string}
};

```
### Partial<Object>型

Object型の全てのプロパティを省略可能にします。

```TypeScript

type SystemSupportLanguage = {
    en: string; 
    fr: string; 
    it: string; 
    es: string
};

type PartialLanguage = Partial<SystemSupportLanguage> // {en?: string; fr?: string; it?: string; es?: string}

```

### Required<Object>型

Object型の全てのプロパティを必須にします。

```TypeScript

type SystemSupportLanguage = {
    en: string; 
    fr: string; 
    it: string; 
    es: string
};

type RequiredLanguage = Required<SystemSupportLanguage> // {en: string; fr: string; it: string; es: string}

```

### ReadOnly<Object>型

Object型の全てのプロパティを読み取り専用にします。

```TypeScript

type SystemSupportLanguage = {
    en: string; 
    fr: string; 
    it: string; 
    es: string
};

type  ReadonlyLanguage = Readonly<SystemSupportLanguage> // {readonly en: string; readonly fr: string; readonly it: string; readonly es: string}

```

### Pick<Object, Keys>型

オブジェクトのプロパティから指定したKeysだけを持つサブタイプを生成します。

```TypeScript

type SystemSupportLanguage = {
    en: string; 
    fr: string; 
    it: string; 
    es: string
};

type  PickLanguage = Pick<SystemSupportLanguage, "en"> // {en: string}

// 合併型でキーを複数指定することも可能
type  PickLanguage2 = Pick<SystemSupportLanguage, "en"| "it" > // {en: string, it:string}
```

### 参考情報

[サバイバルTypeScript](https://typescriptbook.jp/)
[プログラミングTypeScript](https://www.oreilly.co.jp/books/9784873119045/)