PythonのTypedDictでkeyをOptional(undefinedかもしれない)にする

# やりたいこと
Pythonで「一部のキーは必ずあって、一部のキーはないかもしれない辞書」をtypingしたい。
TypeScriptで書くなら以下のような型を、Pythonで表現したい

```typescript
interface IMember {
  lastName: string
  firstName: string
  middleName?: string // optional
  age?: number // optional
}
```

# TL;DR
## コード
```python
from typing import TypedDict

class IMemberRequired(TypedDict):
    """
    Required keys
    """
    last_name: str
    first_name: str


class IMember(IMemberRequired, total=False):
    """
    Optional keys
    """
    middle_name: str
    age: int

member: IMember = get_member()

print(member["last_name"]) # => アラートなし
print(member["middle_name"]) # => アラートあり
```

## エディタ上での表記
※ [Pylance拡張機能](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance)をインストールしたVSCodeを使っています

アラート
![スクリーンショット 2021-07-27 0.13.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326593/3b815c0c-13e5-e9fc-e2dd-1ca8988d4efd.png)


# 説明
## TypedDictについて
Pythonで辞書を型定義するときには、まず`dict[x,y]`が検討されます。
※ python3.9からリストや辞書の型定義には、typingからimportされる`List`や`Dict`ではなく、組み込みの`list`や`dict`を使うことが推奨されています（[参照](https://qiita.com/papi_tokei/items/bf652696d6b98f23565a#%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF%E5%9E%8Blistdicttuple)）

```python
my_dict: dict[str,int] = get_dict()
my_age = my_dict["age"] # my_ageはintでtypingされる
```

ただ`dict`だと、

- my_dictはこういうkey一覧をもっている
- valueは単一の型じゃない（int以外にもある）

ということが表現できません。

TypedDictを使うと、より柔軟に辞書の型を表現することができます。

```python
from typing import TypedDict

class IMyDict(TypedDict):
    age: int
    name: str
    has_PC: bool

my_dict: IMyDict = get_dict()

my_age = my_dict["hogefuga"] # 存在しないkeyへのアクセス
# Could not access item in TypedDict "hogefuga" is not a defined key in "IMyDict"

my_dict.update({"hoge":111}) # 存在しないkeyを持つ辞書でのupdate
# Argument of type "dict[str, int]" cannot be assigned to parameter "__m" of type "IMyDict" in function "update" "hoge" is an undefined field in type "IMyDict"
```

加えて適切な拡張機能をエディタにインストールしていれば、keyの自動補完もしてくれます（筆者はVSCodeにPylanceを導入しています）
![スクリーンショット 2021-07-27 0.58.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326593/8abbe343-1c30-03cf-ff17-de7fc4673c6f.png)

## Optional (Not required) なkeyの表現
「一部のキーは必ずあって、一部のキーはないかもしれない辞書」を表現したいことがあります。「クライアントからJSONでリクエストが渡ってくるが、requiredなキーとnot requiredなキーが混在している」ケースなど。公開APIを作っていたらいくらでもありそうなケースですね。

`Optional`というそれっぽい型がPythonにはありますが、TypeScriptの[Optional Property](https://www.typescriptlang.org/docs/handbook/interfaces.html#optional-properties)とは少々挙動が異なります。`Optional[bool]`と書いた場合、その返り値は`bool or undefined`ではなく、`bool or None`になります

```python
from typing import Optional, TypedDict

class IMyDict(TypedDict):
    age: int
    name: str
    has_PC: Optional[bool]

my_dict: IMyDict = get_dict()
print(my_dict["has_PC"]) # => bool or None
```
「has_PCは絶対あるけどその値はboolかNone」は表現できても「has_PCはないかもしれない（あったらbool）」が表現できない、という感じです。

## Totality
型定義時に`total=False`をTypedDictと一緒に渡してあげることで、「すべてのキーが存在するかもしれないし存在しないかもしれない辞書」を定義することができます。

```python
from typing import Optional, TypedDict

class IMember(IMemberRequired, total=False):
    last_name: str
    first_name: str
    middle_name: str
    age: int

member: IMember = get_dict()
print(member["middle_name"]) # Could not access item in TypedDict "middle_name" is not a required key in "IMember", so access may result in runtime exception
print(member.get("middle_name", "") # OK
```
`middle_name`に関しては「存在しないかもしれない」を表現することができました。`["middle_name"]`でのアクセスはできなくなるので、`.get("middle_name", default)`を代わりに使いましょう。

「全部のキーがOptionalな辞書」を作ることができました。次は「一部Requiredで一部Optionalな辞書」を作りたいです。

## まとめ
ということで冒頭のコードです。

```python
from typing import TypedDict

class IMemberRequired(TypedDict):
    """
    Required keys
    """
    last_name: str
    first_name: str


class IMember(IMemberRequired, total=False):
    """
    Optional keys
    """
    middle_name: str
    age: int

member: IMember = get_member()

print(member["last_name"]) # => アラートなし
print(member["middle_name"]) # => アラートあり
```

Requiredなキーだけの辞書定義をまず作って、それを継承する形でoptionalなkeyをもつ辞書を定義することでやりたいことを表現できます。
TypedDictについて記載された[PEP589のTotality](https://www.python.org/dev/peps/pep-0589/#totality)の項をみると以下のように記述されており、これで正当なアプローチと考えて良さそうです。

> The totality flag only applies to items defined in the body of the TypedDict definition. Inherited items won't be affected, and instead use totality of the TypedDict type where they were defined. This makes it possible to have a combination of required and non-required keys in a single TypedDict type.

おわり