この記事では、Optional propertiesの動作について確認し、設定の読み込みに便利な、Optional propertiesを再帰的に必須にするDeepRequired
型関数を作ります。
Optional properties と undefined
strictNullChecks (undefinedを任意の型に代入することを禁止するオプション)をOnにしていれば(ほとんどの場合していると思いますが)、Optional properties の型には自動的に| undefined
が付加されます。
((p: { a: 1 }) => p)({ a: 1 }); // OK
((p: { a: 1 | undefined }) => p)({ a: 1 }); // OK
((p: { a?: 1 }) => p)({ a: 1 }); // OK
((p: { a?: 1 | undefined }) => p)({ a: 1 }); // OK
((p: { a: 1 }) => p)({ a: undefined }); // NG: Type 'undefined' is not assignable to type '1'.(2322)
((p: { a: 1 | undefined }) => p)({ a: undefined }); // OK
((p: { a?: 1 }) => p)({ a: undefined }); // OK
((p: { a?: 1 | undefined }) => p)({ a: undefined }); // OK
((p: { a: 1 }) => p)({}); // NG: Argument of type '{}' is not assignable to parameter of type '{ a: 1; }'. Property 'a' is missing in type '{}' but required in type '{ a: 1; }'.(2345)
((p: { a: 1 | undefined }) => p)({}); // NG: Argument of type '{}' is not assignable to parameter of type '{ a: 1 | undefined; }'. Property 'a' is missing in type '{}' but required in type '{ a: 1 | undefined; }'.(2345)
((p: { a?: 1 }) => p)({}); // OK
((p: { a?: 1 | undefined }) => p)({}); // OK
Optional properties と Mapped types
Mapped typesのキーの部分は、単純にkeyof T
でマップすると、Optional propertiesかどうかやReadonlyなどもマップされるようです。キーの部分に?
や-?
を使用することで、Optional propertiesにしたり、なくしたりすることが可能なようです。ただし、-?
を使用した場合、| undefined
も外れてしまうのは注意が必要です。それにしてもドキュメントが全然見つからない…
以下、 https://github.com/microsoft/TypeScript/blob/35c1ba67baac2fd5152908184f8b2ec565815942/lib/lib.es5.d.ts#L1438 よりPartialとRequiredの定義を引用します。
/** * Make all properties in T optional */ type Partial<T> = { [P in keyof T]?: T[P]; }; /** * Make all properties in T required */ type Required<T> = { [P in keyof T]-?: T[P]; };
これらの動作を見てみます。
// strip optional/readonly
type StripTypes<T> = { [K in keyof T]: K }[keyof T];
type Mapped<T> = {
[K in keyof T]: T[K ];
};
type StripMapped<T> =
T extends object ?
{ [K in StripTypes<T>]: T[K] } : T;
;
type TObj = {
a: 1;
b?: 2;
c: 3 | undefined;
d?: 4 | undefined;
readonly e: 5;
};
/*
Mapped<TObj> == {
a: 1;
b?: 2 | undefined;
c: 3 | undefined;
d?: 4 | undefined;
readonly e: 5;
};
StripMapped<TObj> == {
a: 1;
b: 2 | undefined;
c: 3 | undefined;
d: 4 | undefined;
e: 5;
};
Partial<TObj> == {
a?: 1 | undefined;
b?: 2 | undefined;
c?: 3 | undefined;
d?: 4 | undefined;
readonly e?: 5 | undefined;
};
Required<TObj> == {
a: 1;
b: 2;
c: 3 | undefined;
d: 4;
readonly e: 5;
};
*/
DeepRequired の考え方
Optional propertiesが活躍する場面の代表例としては、設定を書く場合が挙げられるのではないでしょうか? 設定を書く際には、必要最低限の設定項目のみを記述し、残りはデフォルトの設定としたい場合があるからです。この場合、設定項目を読み込んだ後は、Optional propertiesを必須のプロパティにした型が望まれます。
設定項目の型が単階層の場合は、上記の組み込み型関数のRequired<T>
が便利ですが、設定項目の型が複数階層のObject型の場合に、再帰的に必須プロパティにする型関数DeepRequired<T>
を考えてみます。
type DeepRequiredProto<T> = {
[P in keyof T]-?: DeepRequiredProto<T[P]>;
};
しかし、このDeepRequired<T>
だと、string
などのprimitive型や、Date
、Map
などの型も分解されてしまいます。
type TObj = {
a: string;
b?: {
c?: 3;
d?: Date;
e: [4, "5"];
f: Map<number, string>;
}[];
c: 5;
};
/*
DeepRequiredProto<TObj> == {
a: string;
b: {
c: 3;
d: {
toString: {};
...
};
e: [4, "5"];
f: {
...
};
}[];
c: 5;
}
*/
また、場合によっては、一部の設定にundefinedの可能性は残しておきたい場合もあります。
型を分解しすぎない DeepRequired
そこで、DeepRequiredにこれ以上分解してほしくない型(プリミティブな型)を渡すことで、その型は変換しないことを考えます。また、DeepRequiredが完了した型に、後から特定の場所にundefinedを付加することを考えます。
// PrimitiveTypesに、extends objectだが分解されてほしくない型を入れる(使用時に適宜Set, RegExpなどを追加する)
type DeepRequired<T, PrimitiveTypes = Date | Map<any, any>> =
T extends object
? T extends PrimitiveTypes
? T
: { [K in keyof T]-?: DeepRequired<T[K], PrimitiveTypes> }
: T;
// DeepUnionMerge系: 2つのオブジェクト型をunion typeでマージする
type DeepUnionMerge<T1, T2> =
T1 extends object
? T2 extends object
? {
[K1 in keyof T1]: K1 extends keyof T2
? DeepUnionMerge<T1[K1], T2[K1]>
: T1[K1];
} &
{ [K2 in Exclude<keyof T2, keyof T1>]: T2[K2] }
: T1 | T2
: T1 | T2;
// 以下のようにすると、結果の型を表示させたときにintersection typeが出てこない
type DeepUnionMerge2<T1, T2> =
T1 extends object
? T2 extends object
? {
[K in keyof T1 | keyof T2]: K extends keyof T1
? K extends keyof T2
? DeepUnionMerge2<T1[K], T2[K]>
: T1[K]
: K extends keyof T2
? T2[K]
: never;
}
: T1 | T2
: T1 | T2;
// 再帰的にT2のキーが全てT1にある場合はこちらの方が簡単
type OneSideDeepUnionMerge<T1, T2> =
T1 extends object
? T2 extends object
? {
[K1 in keyof T1]: K1 extends keyof T2
? OneSideDeepUnionMerge<T1[K1], T2[K1]>
: T1[K1];
}
: T1 | T2
: T1 | T2;
// AddUnionTypeToKey: 特定のキーにunion typeで型を再帰的に追加する
// AddedType: {keyName: addedType}のようにキーと型を指定する。
// Tおよびその値のオブジェクトにkeyNameがあった場合、addedTypeがunion typeで追加される
type AddUnionTypeToKey<
T,
AddedType extends object,
PrimitiveTypes = Date | Map<any, any>
> = T extends object
? T extends PrimitiveTypes
? T
: {
[K in keyof T]: K extends keyof AddedType
? AddUnionTypeToKey<T[K], AddedType, PrimitiveTypes> | AddedType[K]
: AddUnionTypeToKey<T[K], AddedType, PrimitiveTypes>;
}
: T;
/*
DeepRequired<TObj, Date | Map<any, any> | RegExp> == {
a: string;
b: {
c: 3;
d: Date;
e: [4, "5"];
f: Map<number, string>;
}[];
c: 5;
};
OneSideDeepUnionMerge<
DeepRequired<TObj>,
{
b: { c: undefined }[];
}
> == {
a: string;
b: {
c: 3 | undefined;
d: Date;
e: [4, "5"];
f: Map<number, string>;
}[];
c: 5;
};
AddUnionTypeToKey<DeepRequired<TObj>, { c: undefined }> == {
a: string;
b: {
c: 3 | undefined;
d: Date;
e: [4, "5"];
f: Map<number, string>;
}[];
c: 5 | undefined;
};
*/
これにより、Optional propertiesを再帰的に必須にするDeepRequired
型関数ができ、また、好きなところにundefinedを付けることが可能になりました。