LoginSignup
2
0

More than 3 years have passed since last update.

TypescriptでDeepRequiredを作る

Last updated at Posted at 2020-06-15

この記事では、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型や、DateMapなどの型も分解されてしまいます。

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

参考リンク

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0