あなたのデータを正常化するために独自のタイプの安全な減速器を書きなさい


背景


次のJSONデータを入れ子にしたオブジェクトを使用すると、Reduxストアで使用する最良のデータ構造は何ですか?
[
  {
    "id": "xlkxhemkuiam",
    "title": "voluptates sequi et praesentium eos consequatur cumque omnis",
    "body": "Beatae distinctio libero voluptates nobis voluptatem...",
    "createdAt": "Tue, 22 Sep 2020 16:28:53 GMT",
    "user": {
      "id": "lswamlcggqlw",
      "handle": "Payton_Carter",
      "imgUrl": "https://s3.amazonaws.com/uifaces/faces/twitter/dawidwu/128.jpg"
    },
    "comments": [
      {
        "id": "jsyrjkxwtpmu",
        "body": "Sint deserunt assumenda voluptas doloremque repudiandae...",
        "createdAt": "Fri, 25 Sep 2020 18:03:26 GMT",
        "user": {
          "id": "hqhhywrxpprz",
          "handle": "Orlo97",
          "imgUrl": "https://s3.amazonaws.com/uifaces/faces/twitter/ponchomendivil/128.jpg"
        }
      }
    ]
  },
...
]
最も簡単で最も一般的なアプローチは、彼らが受信されたように正確にブログ記事の配列を格納することです.そのIDを指定した特定のポストにデータを表示したい場合は、一致するポストが見つかるまで、配列を反復処理する必要があります.さらに、我々は再び我々のredux店でupsert行動をしたいならば、反復に頼らなければなりません.明らかに両方のタスクはo(n)の時間複雑さに苦しむので、我々は代わりに我々のデータを正常にして、結果としてo(1)まで我々の複雑さを減らすことができます.

You don't always have to deal with data the same format the server gives you.


はい、このアイデアは何年もされており、人気のあるツールが好きですnormalizr これを助ける.しかし、あなたがこのようなツールで簡単に解析できない深く入れ子にされたデータを持っているならば、どうですか?ここでは、いくつかの人気の反応タイプのFPライブラリを使用して1つの可能なアプローチを提示fp-ts , io-ts , monocle-ts カスタムを構築するには、安全な縮小関数を入力します.
これは、ステップバイステップのガイドではなく、迅速な実行の詳細です.興味があれば、ソースコードに飛び込んでください.また、ライブデモを見ることができますhere .

ハンスフフマン / FPデータ正規化


FP‐TSを用いた型安全データ正規化


正常化しましょう


始める前に、正規化されたデータの形を、O ( 1 )ルックアップを許すように指定しましょう.
export type AppState = {
  entities: {
    comments: NormalizedComments;
    posts: NormalizedPosts;
    users: NormalizedUsers;
  };
};

ステップ1


コンパイル時と実行時の安全性の両方をio-ts ドメインの種類を宣言するには例えば、我々Post :
/**
 * Composite types
 */

export const Post = t.type({
  id: IdString,
  title: NonEmptyString,
  body: NonEmptyString,
  createdAt: UtcDateString,
  user: User,
  comments: Comments,
});

/**
 * Static types
 */

export type Post = t.TypeOf<typeof Post>;
カスタム型を指定するだけで基本的な文字列を使用する代わりにいくつかの制約を追加できます.例えば、IdString 指定された文字列が正確に長さ12文字であることを保証します.
/**
 * Type guards
 */

const isIdString = (input: unknown): input is string => {
  return typeof input === "string" && /[A-Za-z]{12}/g.test(input);
};

/**
 * Custom codecs
 */

const IdString = new t.Type<string, string, unknown>(
  "idString",
  isIdString,
  (input, context) => (isIdString(input) ? t.success(input) : t.failure(input, context)),
  t.identity,
);

ステップ2


今、我々は我々のドメイン静的なタイプを使用して、予期しないAPI応答のためにクラッシュから我々の反応アプリを保護することができます.我々はまた、1つの簡単なチェックに私たちのドメインロジックですべての過剰なエラーチェックを高めました.デコーダありがとう!🎉
const fetchPosts = (): Posts => {
  const result = Posts.decode(data);

  return pipe(
    result,
    E.fold(
      () => {
        console.warn(PathReporter.report(result));

        return [];
      },
      (posts) => posts,
    ),
  );
};
これは本当にクールな部分です!APIレスポンスが間違った形式でIDを含んでいるか、完全に不足しているならば、我々が我々の減速器機能を入力する前に、我々はこれを捕えることができます.ちょっとシンクしてみよう.内部のAPIであっても、私たちの足の下で右を変更することができますまたは破損してデータをその方法を作る.我々は、これから我々のアプリを保護することができます.操作するdata.json あなた自身と行動でそれを見てください.

The ability to declare types once and get both compile and runtime safety is a joy worth experiencing.


から返されるいずれかの型io-ts デコーダは、指摘する価値がある1つの面白い副作用を生じます-我々は、結局我々の反応アプリでレンダリングされるどんなブログ柱でもない結果に失敗に関して空の配列を通過します.これは良いUXを提供しますか?確かに我々のアプリがクラッシュしない代わりにより良いですが、多分我々は幸せな媒体を見つけることができるといくつかのデータをレンダリング?
私はまだ自分自身で作業しています.数人の同僚が探したfp-ts These そして、1つさえ提出しましたPR ! それは自分でチェックしてください.

ステップ3


最後に、私たちの状態で実体を加えるか、更新しようとするとき、意外な、誤りやすいJSオブジェクトを広げる代わりに、我々は使うことができますmonocle-ts レンズを定義するには、私たちの生活を容易にします.以下に、私たちのupsert関数は、最初に、指定されたユーザが既に保存されているかどうかを確認します.ユーザーのIDなどの特定のユーザープロパティが挿入された後に更新できないようにします.
/**
 * Optics
 */

const usersLens = Lens.fromPath<AppState>()(["entities", "users"]);
const atUser = (id: IdString) => Lens.fromProp<NormalizedUsers>()(id);

/**
 * Upserts
 */

const upsertUser = (user: User) => (state: AppState): AppState => {
  return pipe(
    state,
    R.lookup(user.id),
    O.fold(
      () => {
        return pipe(
          state,
          usersLens.compose(atUser(user.id)).set({
            id: user.id,
            handle: user.handle,
            imgUrl: user.imgUrl,
          }),
        );
      },
      (_user) => {
        return pipe(
          state,
          usersLens.compose(atUser(user.id)).modify(
            (prevUser): UserEntity => ({
              ...prevUser,
              handle: user.handle,
              imgUrl: user.imgUrl,
            }),
          ),
        );
      },
    ),
  );
};

結論


レンズとデコーダを使用してデータを正規化するにはいくつかの努力が必要ですが、私はそれを行うための報酬を実証してほしい.このようなタイプセーフコードはあなたに笑顔を置きませんか?😎
あなたがこれを行うことのよりエレガントなまたは慣用的な方法があるならば、知らせてください!耳が遠い.