Partial<T> / TypeScript一人カレンダー
こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2022の12日目です。昨日は『Mapping Modifiersと実例 Writable<T, K>
』を紹介しました。
Partial<T>
Partial<T>
はTypeScript 2.1から追加された古参Utility Typesのひとつです。
Strict Null ChecksやOptional 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 obj
にz
を含めなくてもエラーになりません。そして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を同時に学び始める初学者の多くは、null
とundefined
の違い、そして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
が返ります。
この辺りはECMAScript仕様として定義されているAbstract Operations HasProperty
の話題になるため高度になりすぎてしまい、本稿ではここまでとしますが、プロパティを参照した結果がundefined
だからといって同じというわけではなく、プロパティが存在する・しないによって微妙な挙動差が存在することはECMAScriptの特性として把握しておく必要があります。
筆者はこういった経緯もあって、TypeScriptプログラミングにおいては「プロパティが存在しかつ格納されたundefined
を得る」と「プロパティ自体が存在しないためのundefined
の取得である」を区別できないundefined
を嫌うようになり、区別を可能にすることを目的に、null
を格納するかプロパティを定義しないというアプローチを取っています。
RecursivePartial<T>
』
次回は『実例 本日はPartial<T>
を紹介しました。Partial<T>
のみだと現代のTypeScriptでは筆者はすっかり使わなくなったのですが、それでも必要に駆られて作ったものが明日紹介するRecursivePartial<T>
です。
このアドベントカレンダーもようやく折返しです。それではまた。
Discussion