TypescriptでDeepRequiredを作る

この記事では、Optional propertiesの動作について確認し、設定の読み込みに便利な、Optional propertiesを再帰的に必須にする`DeepRequired`型関数を作ります。

## Optional properties と undefined
* https://www.typescriptlang.org/docs/handbook/advanced-types.html#optional-parameters-and-properties

strictNullChecks (undefinedを任意の型に代入することを禁止するオプション)をOnにしていれば(ほとんどの場合していると思いますが)、Optional properties の型には自動的に`| undefined`が付加されます。

```typescript
((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<T>とRequired<T>の定義を引用します。

> ```typescript
> /**
>  * 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];
> };
> ```

これらの動作を見てみます。

```typescript
// 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>`を考えてみます。

```typescript
type DeepRequiredProto<T> = {
    [P in keyof T]-?: DeepRequiredProto<T[P]>;
};
```

しかし、この`DeepRequired<T>`だと、`string`などのprimitive型や、`Date`、`Map`などの型も分解されてしまいます。

```typescript
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を付加することを考えます。

```typescript
// 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を付けることが可能になりました。

## 参考リンク
* [TypeScriptのmapped typesでネストされた型もすべてオプショナルにする - ひと夏の技術](https://tech-1natsu.hatenablog.com/entry/2018/07/07/233655)
* [TypeScriptで型レベル関数とRequired/DeepRequired型を作って型レベルプログラミング - falsandtruのメモ帳](https://falsandtru.hatenablog.com/entry/type-level-typescript)