🎄

Required<T>, Readonly<T> / TypeScript一人カレンダー

2022/12/22に公開

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

Required<T>

本日は2つ紹介します。まずはRequired<T>から。TypeScript 2.8で追加されたUtility Typesのひとつです。

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

Required<T>の実装は単純で、ここまでMapped Typesを学んできたのであれば難なく読むことができる要素だけで構成されています。

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

-?:Mapping Modifiersです。ここでは、すべてのプロパティに対してOptionalであればその修飾子を取り除く、Optionalでなければそのままにするという操作を意味しています。

そのためT型に渡されたすべてのプロパティから?:指定を取り除くと覚えればよいです。この操作は、ちょうどPartial<T>がすべてのプロパティに?:指定を足す操作であるため、その対となる定義です。

Readonly<T>

もうひとつ紹介するものはReadonly<T>です。TypeScript 2.1で追加されました。

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

Readonly<T>の実装を確認しましょう。こちらも単純なMapped Typesの形式です。

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

すべてのプロパティに対してreadonlyを付与するか、もともと付与されていればそのままという操作です。readonlyについては以前の記事で紹介しましたが、その際紹介したUtility TypesはReadonlyArray<T>でした。名前は似ていますが、オブジェクトのプロパティに対して操作するものと、配列を非破壊にするもので用途が異なります。

いつ使う?

筆者の関わっている案件では、もともとの型定義にOptionalを使わなかったり、readonlyを最初から記述している都合上、あまり頻繁に使うことはないです。既存の案件を眺めていたらひとつ参考になりそうな用途があったため紹介します。

makeRequestWithPost()関数は、window.fetch('path', { /* ここ */ })を生成するための関数です。実装は業務で扱っている内容そのままではなく、この記事用にアレンジしています。

export function makeRequestWithPost<T extends object>(
  body: T,
  idempotencyKey: string
): Required<Pick<RequestInit, 'method' | 'headers' | 'body'>> {
  return {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      [xIdempotencyKey]: idempotencyKey,
    },
    body: JSON.stringify(body),
  };
}

だいたいどの案件でもよくあるのが、Fetch周りの基盤共通化の要件です。筆者はaxiosではなくwindow.fetch()を常用するため、そのfetch()に渡す設定パラメータ群の扱いについて考慮を求められがちです。

サンプルコードではRequestInitとして定義されています。この型はlib.dom.d.tsにて定義されているため、import文なしにいきなり使うことができます。

このRequestInit型を一部引用します。

interface RequestInit {
  body?: BodyInit | null;
  cache?: RequestCache;
  credentials?: RequestCredentials;
  headers?: HeadersInit;
  //
}

このように、すべてのプロパティがOptional扱いとなっています。これはwindow.fetch()の提供されたインタフェースとしては正しいです。しかし、makeRequestWithPost()関数がPOST処理の共通化を図るためのものであるという立場上、ここでは「method: 'POST'があってもなくてもよい」とはいえません。こういった状況でRequired<T>が役に立ちます。

今回は3プロパティしか扱わないため、まずその3つをPickし、その結果をRequiredに通しています。そのためRequired<Pick<RequestInit, 'method' | 'headers' | 'body'>>というネストの型パラメータ構造をとります。RequiredPick<T, K>という自作型を用意してもよいのですが、用途が限定的であるため今回はそのようなことはしていません。

あまりOptionalを扱わない現場であればRequired<T>の出番はそんなに多くないかもしれません。ただ、グローバルなAPIのインタフェースや外部のライブラリなどは、その利便性を優先しあえてOptionalとしている設計も見受けられます。これはOptionalの良し悪しではなく、状況に応じて適切なインタフェースを提供するという考え方において、よいことだと思います。状況に応じて既存の型定義の制約を限定的に厳しくしたいといった場合にRequired<T>, Readonly<T>が活躍できます。

明日は『objectRecord<K, T>

Mapped Typesもここまで数多く紹介してきました。一連のMapped Typesは明日のRecord<K, T>で最後。これはTypeScriptの縁の下の力持ちといったところです。それではまた。

Discussion