🎆

type-fest でおくる快適な型ライフ

2022/12/30に公開
1

はじめに

個人的にではありますが、型ロジックを書くのに最近ハマっていました(型パズルと呼ぶ人の方が多いかも)。私が最近使っていた type-challenges には、こういう型を書いてみなさい? と言う型ロジックの問題集が用意されており、非常に練習になるのでおすすめです。
https://github.com/type-challenges/type-challenges

はてさて、本日、この記事を書こうと思った理由としては、もう型ロジック書きたくないなぁ、型ロジックをまとめたライブラリもあるだろう、と思って github をサーフィンしていたところ、type-fest と言うまさにこれを探していたと言うライブラリがあり、かなり良かったのでその紹介記事です。

余談

これは余談ですが、IsEqual と言う型を書いたプルリクが type-fest にマージされ、先日、リリースされました。はじめてのまともな OSS へのコントリビュートだったので(= 以前のコントリビュートはまともじゃなかった)、けっこう大変でしたが、マージされて良かったぜ。(レビュアーの人、マジでありがとう。)
https://github.com/sindresorhus/type-fest/releases/tag/v3.5.0

type-fest の紹介

業務で TypeScript 書いているとユーティリティタイプでは足りない場面があります。

TypeScript のビルドインのユーティリティタイプ
https://www.typescriptlang.org/docs/handbook/utility-types.html

その時に自前で型ロジックを書くか、ライブラリを探してくるか、と言う選択をすると思いますが、ライブラリを探す選択をしたときに type-fest はその筆頭候補になるんじゃないかと思います。
https://github.com/sindresorhus/type-fest

けっこうな数の型が提供されているので、欲しい型はそこそこの確率で見つかるんじゃないかと思います。また、提供されている型は全て JsDoc にサンプルがめっちゃめちゃ丁寧に書かれているので、型を使う場面も想像しやすいです(このサンプルが個人的にかなり助かる)。

簡易なサンプル1

スネークケースやケバブケースの命名規則をキャメルケースに変換することができます。
以下、簡易なサンプルです。

const someVariable: CamelCase<'foo_bar'> = 'fooBar';

例えば、フロントエンドは TypeScript で開発されており、バックエンドは Python が採用されているプロジェクトがあるとします。Python はスネークケースが基本的には使われるので、バックエンドのインターフェースをそのまま Typescript の型にすると、以下のようになります(バックエンドのインターフェースの Swagger のドキュメントを TypeScript の型として自動生成する、みたいなことはよくあると思います)。

interface RequestQuery {
  sort_order: number;
  created_from: string;
  created_to: string;
}

ただ、これだとスネークケースで、そのままでは TypeScript で扱いづらいため、先の CamelCase に少し味付けをすると、キャメルケースで扱うことができます。

// NOTE: 少し味付け
type CamelCasedProperties<T> = {
  [K in keyof T as CamelCase<K>]: T[K];
};

// NOTE: キャメルケースへ変換
const requestQuery: CamelCasedProperties<RequestQuery> = {
  sortOrder: 1,
  createdFrom: '2022-05-04T21:35:20.333Z',
  createdTo: '2022-05-04T21:35:20.333Z',
};

これで安全なバックエンドとのインターフェースを手に入れることができます。

簡易なサンプル2

キャメルケースへの変換くらいであれば、わざわざ type-fest を使わずとも自前で型ロジックを書く人も多いかと思います。ということで、次は少しだけ込み入った PartialDeep です。

PartialDeep は名前から想像できますが、ネストした object の型を全てオプショナルに変換します。例えば、以下のような型があるとします。

type Settings = {
  textEditor: {
    fontSize: number;
    fontColor: string;
    fontWeight: number;
  };
  autocomplete: boolean;
  autosave: boolean;
};

これを以下のようにオプショナルに変換します。

type PartialSettings = PartialDeep<Settings>

実際に得られる型は以下です。全てオプショナルになっています。

type PartialSettings = {
  textEditor?:
    | {
        fontSize?: number | undefined;
        fontColor?: string | undefined;
        fontWeight?: number | undefined;
      }
    | undefined;
  autocomplete?: boolean | undefined;
  autosave?: boolean | undefined;
};

型ロジックを自前で書くか、否か

先のネストした object の型を全てオプショナルに変換でさえもいやいやいや?このくらいなら自前でやりますよ? という人はいるかと思います。例えば、ユーティリティの Partial を少し味付けして、再帰的に型変換できるようにすればネストした object でも、全てをオプショナルに変換できます。

// REF: https://stackoverflow.com/questions/61132262/typescript-deep-partial
type DeepPartial<T> = T extends object ? {
    [P in keyof T]?: DeepPartial<T[P]>;
} : T;

ただ、これは 90 点は取れても 100 点ではないです。
エッジケースに対応しておらず、具体的には new Datenew Map がプロパティに含まれている場合などは、期待している型変換はできません。Partial に少し味付けをした型で、以下のパターンを変換してみます。

const foo = {
  date: new Date(),
}

type PartialFoo = DeepPartial<typeof foo>

以下が実際に得られる型ですが...あれ、様子がおかしい...(new Date が持つプロパティもオプショナルに変換されています)

{
    date?: {
        toString?: {} | undefined;
        toDateString?: {} | undefined;
        toTimeString?: {} | undefined;
        toLocaleString?: {} | undefined;
        toLocaleDateString?: {} | undefined;
        toLocaleTimeString?: {} | undefined;
        ... 38 more ...;
        [Symbol.toPrimitive]?: {} | undefined;
    } | undefined;
}

期待していた型は以下だと思います。

type PartialFoo = {
    date?: Date | undefined;
}

ネストした object の型の変換は、思っているよりも深い沼で、100 点をとりに行くとかなり大変です。もちろん、90 点で満足できるのであれば全く問題ないです。言うてもエッジケースなので、これに苦しむことは稀です。ただ、業務だと何故かエッジケースにぶち当たるのもまた真です

仮に自分は 90 点の型であることはわかっていても、他の人がそれを意識できるかは別問題です(深い沼であることに気づくのは、それなりに難しい)。type-fest を使うことで、沼を意識する必要がなくなるのであれば、それは使う理由になるんじゃないかと個人的には思います。

ユニットテスト済みの型

型ロジックは、書く上で深い沼が各所に点在しています。そのため、ある程度は慎重に書かないとやらかしてしまいます。型ロジックと言う名前からも、ロジックなのでユニットテストが本来は必要ですが、業務で自前で書いた型に対するユニットテストを書いている人はそこまで多くないと思います。

type-fest では型に対してユニットテストが書かれています。
以下、ユニットテストの一部抜粋です。

// REF: https://github.com/sindresorhus/type-fest/blob/80465bc2934faf6cbf7624584d9904f2a34240d9/test-d/partial-deep.ts#L30
expectType<typeof partialDeepBar | undefined>(partialDeepFoo.bar);
expectType<((_: string) => void) | undefined>(partialDeepFoo.bar!.function);
expectType<object | undefined>(partialDeepFoo.bar!.object);
expectType<string | undefined>(partialDeepFoo.bar!.string);

type-fest では、ユニットテスト実施済みであるため、自前で書く型ロジックよりも品質が高いことが多い、と言えると思います。これは十分に type-fest を使う理由になると個人的には思います。

さいごに

型ロジックはけっこう書くの大変なので、type-fest は欲しい型が含まれているなら、いいんじゃないかと思います。以上、終わり!

Discussion