🤨️

Required<Partial<A>>とAって、どんなAでも同じ型になるの??

に公開

はじめに

Partial<T>はTの各キーをオプショナルにする型、Required<T>はオプショナルを除去してすべてのプロパティを必須にする型です。つまり、Required<Partial<A>>は一見すると、元のAと同じになるように思えます。しかし、これは必ずしもなりたたず、例外パターンが存在します。

オプショナル(プロパティ)とは

そもそもオプショナルとはどのような状態かおさらいしましょう。
次のようなAを考えます。

type A = { z?: stringconst a1: A = {}; //OK
const a2: A = { z: "" }; //OK
const a3: A = { z: undefined }; //OK

この場合、zはstring、または「存在しない(未定義)」、あるいは明示的にundefinedが代入された状態のいずれも許容されます。

一方で、以下のようなBは似ているようで挙動が異なります。

type B = { z: string | undefinedconst b1: B = {}; //Error
const b2: B = { z: "" }; //OK
const b3: B = { z: undefined }; //OK

Bはプロパティzの型としてstring | undefinedを明示していますが、プロパティ自体の省略は許されません。つまり、Bに「キーが存在しないことも許容する」条件を加えたものが、Aの意味合いになります。

また、AとBのプロパティを読み取ると、どちらも同じ型が得られます。

type ReadA = A["z"];  // string | undefined
type ReadB = B["z"];  // string | undefined

例外パターン

ここで、Required<Partial<A>>がAと一致しない例外パターンの一つを紹介します。
それは、Aがundefinedを含むユニオン型のプロパティを持つケースです。

type ExceptionType = {
  a: string | undefined;
  b: number;
}

type OutputType = Required<Partial<ExceptionType>>;

このとき、OutputTypeは一見ExceptionTypeと一致しそうに見えますが、実際には以下のような型になります。

type OutputType = {
  a: string;
  b: number;
}

なんと、aからundefinedが取り除かれてしまいました。

ステップごとに追うと、挙動がより明確になります。

Partial<T>でオプショナルを付与した型を読み取ると、前章でも説明した通りundefinedが付けられた形になります。

Requreid<T>はオプショナルを外す時に、オプショナルのためにつけたundefinedも一緒に削除します。しかし、この時どのundefinedがオプショナルによって付けられたものか区別できず、全てのundefinedが削除されてしまいます。

type ReturnPartialType = Partial<ExceptionType>  
// { a?:string | undefined, b?: number | undefined }
type OutputType = Required<ReturnPartialType>
// { a: string, b: number }

対策方法

この問題に対処するには、TSのコンパイラオプションで exactOptionalPropertyTypes を有効にする方法があります。

//tsconfig.json
{ 
  "compilerOptions": {
    "exactOptionalPropertyTypes": true;
    //省略
  }
}

これを設定することで、オプショナルとundefinedを厳密に区別できるようになります。

たとえば、以下のような挙動になります。

type A = { z?: stringconst a1: A = {}; //OK
const a2: A = { z: "" }; //OK
const a3: A = { z: undefined }; //Error

これにより、Partial<T>でオプショナルにしたプロパティを読み取ったときでも、意図しないundefinedが付加されることがなくなります。

type ReturnPartialType = Partial<ExceptionType>  
// { a?:string | undefined, b?: number }
type OutputType = Required<ReturnPartialType>
// { a: string | undefined, b: number }

他の対策方法としてユーティリティ型を自作してもいいかもしれません。

修正されない理由

結論から言うと、バグとしては認識されているが、互換・複雑性の理由で修正されておらず、対処法として、exactOptionalPropertyTypesを用意されているそうです。

https://github.com/microsoft/TypeScript/issues/44421?utm_source=chatgpt.com
https://github.com/microsoft/TypeScript/issues/31025?utm_source=chatgpt.com

まとめ

ここまで読んでいただき、ありがとうございました!

最近、型パズルの問題を自作するのにちょっとハマっていて、それに伴って型の勉強を始めました。初心者なので、間違えとかありましたら、指摘していただけるとありがたいです。

GitHubで編集を提案

Discussion