🎄

Partial<T> / TypeScript一人カレンダー

2022/12/21に公開

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2022の12日目です。昨日は『Mapping Modifiersと実例 Writable<T, K>』を紹介しました。

Partial<T>

Partial<T>はTypeScript 2.1から追加された古参Utility Typesのひとつです。

https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype

Strict Null ChecksOptional Propertiesの登場がTypeScript 2.0のため、その流れに関連して出てきたようにみえます。いまやtsconfigにおけるstrict指定はデファクト・スタンダードですが当時はとても斬新でした。

さて、挙動は単純です。Tに渡されたオブジェクトのプロパティすべてをOptional扱いにします。実装をみてみましょう。

type Partial<T> = {
  [P in keyof T]?: T[P];
};

Mapped Typesを活用する』の回で紹介したようにTに渡された型のすべてのプロパティについて、?をつけて型は変更せずT[P]のままとして返しています。

?:は昨日紹介したMapping Modifiersです。すべてのプロパティに?(Optionalの指定)を追加することを意味します。Mapping Modifiersとして解釈されるため+?:と同義で、その省略形という扱いです。-は知られていても+は使う機会がなく知名度は低いと思います。

// Partial<T>は名前空間内で競合するため名乗れない
type Partial2<T> = {
  [P in keyof T]+?: T[P];
};

Omit<T, K>と異なる点

Partial<T>Tに渡されたオブジェクト型のプロパティをすべてOptionalにする以外の操作がないため、サンプルも特になく、このままではあまり語ることがありません。Optionalな型とより厳密な型を比較したとき、実装時により安全であるのは厳密に定義した型を使うときであるため、正直筆者は業務でもPartial<T>をわざわざ積極的に使うことはしません。ということでちょっと別の話題を。

特定のオブジェクト型から一部を無くす、あるいは無くてもよいことにする、という解釈でみるとPartial<T>Omit<T, K>は似ていると感じるかもしれません。TypeScriptの歴史からみてもPartial<T>が2.1での追加に対して、Omit<T, K>は3.5での追加のため同時に生まれたものではなく、TypeScript利用者間のパラダイムであったり思想哲学に変化が及び「すべてを一斉にOptionalにしてしまうのは扱いにくい」という用途の変化が起こったと推察することもできます。

では最新のTypeScriptにおいて、プロパティ定義なし、Optionalなプロパティ、T | undefinedというUnionの指定の3つではどう異なるのか。改めておさらいしておきましょう。

プロパティ定義なし

「プロパティ定義なし」は型定義にそもそもプロパティが定義されていない状態です。次の型Objを例に説明します。

type Obj = {
  a: string;
  b: number;
  c: boolean;
};

例えばこのObjには、a, b, cのプロパティが定義されており、zは定義されていません。

type Obj = {
  a: string;
  b: number;
  c: boolean;
};

type Z = Extract<keyof Obj, "z">;
//   ^? never

const obj: Obj = { a: "a", b: 1, c: true };
obj.z = undefined; // Error: Property 'z' does not exist on type 'Obj'.(2339)

そのため、もちろんobj.zには何も代入ができません。たとえ= undefinedだとしてもです。プロパティ定義がないということはExtract<T, U>を使って"z"を指定してもneverが返ります。

Omit<T, K>が返す型も同様にプロパティ定義自体を含まない形として返しますので、Omitされたプロパティにはundefinedを代入することすらできません。

Optionalなプロパティ

Optionalなプロパティは、定義されない場合とどう異なるか確認しましょう。Obj型にz?: stringを追加しました。

type Obj = {
  a: string;
  b: number;
  c: boolean;
  z?: string;
};

type Z = Extract<keyof Obj, "z">;
//   ^? "z"

const obj: Obj = { a: "a", b: 1, c: true };
obj.z = undefined;

この場合、まずconst objzを含めなくてもエラーになりません。そしてzはOptionalなプロパティとしてプロパティ自体の存在が定義されているため、Extract<T, U>では"z"を返し、obj.z =の代入もエラーとなりません。

T | undefinedのUnion

最後に、よくOptionalと同義と勘違いされるT | undefinedとの違いを比較します。実はこれ、同義ではないことに注意が必要です。

type Obj = {
  a: string;
  b: number;
  c: boolean;
  z: string | undefined;
};

type Z = Extract<keyof Obj, "z">;
//   ^? "z"

const obj: Obj = { a: "a", b: 1, c: true };
// Error: Property 'z' is missing in type '{ a: string; b: number; c: true; }' but required in type 'Obj'.(2741)
obj.z = undefined;

Optionalではないプロパティで| undefinedの形式にした場合、値としてのundefinedは許容したとしても代入時のオブジェクト・リテラルのプロパティからzを省略すること自体は認められなくなります。すなわち次のように代入する必要があります。

const obj2: Obj = { a: "a", b: 1, c: true, z: "Z" };
const obj3: Obj = { a: "a", b: 1, c: true, z: undefined };

HasPropertyについて理解する

ECMAScriptとTypeScriptを同時に学び始める初学者の多くは、nullundefinedの違い、そしてOptionalプロパティとundefined型アノテーションの違いに翻弄されることとなります。

type Obj1 = {
  a: string;
  b: number;
  c: boolean;
  z?: string;
};

type Obj2 = {
  a: string;
  b: number;
  c: boolean;
  z: string | undefined;
};

const obj1: Obj1 = { a: 'a', b: 1, c: true };
const obj2: Obj2 = { a: 'a', b: 1, c: true, z: undefined };

console.log(obj1.z === obj2.z); // true
console.log(Reflect.has(obj1, 'z')); // false
console.log(Reflect.has(obj2, 'z')); // true

オブジェクト・リテラルにそもそも記述しない際にobj1.zアクセスとして生じるundefinedと、値として明示的に代入してからobj2.zとして参照して得られるundefinedには明らかな扱いの違いがあります。

これは値同士でみたときにはundefined同士であるため===trueとなりますが、Reflect.has()を使用した際に確認できます。

Reflect.has()を通すと、プロパティとして記述されていない際の.zは、プロパティが存在しないためfalseが返ることに対し、プロパティが存在してそこにundefinedが格納されている場合はtrueが返ります。

https://tc39.es/ecma262/multipage/reflection.html#sec-reflect.has

この辺りはECMAScript仕様として定義されているAbstract Operations HasPropertyの話題になるため高度になりすぎてしまい、本稿ではここまでとしますが、プロパティを参照した結果がundefinedだからといって同じというわけではなく、プロパティが存在する・しないによって微妙な挙動差が存在することはECMAScriptの特性として把握しておく必要があります。

筆者はこういった経緯もあって、TypeScriptプログラミングにおいては「プロパティが存在しかつ格納されたundefinedを得る」と「プロパティ自体が存在しないためのundefinedの取得である」を区別できないundefinedを嫌うようになり、区別を可能にすることを目的に、nullを格納するかプロパティを定義しないというアプローチを取っています。

次回は『実例 RecursivePartial<T>

本日はPartial<T>を紹介しました。Partial<T>のみだと現代のTypeScriptでは筆者はすっかり使わなくなったのですが、それでも必要に駆られて作ったものが明日紹介するRecursivePartial<T>です。

このアドベントカレンダーもようやく折返しです。それではまた。

Discussion