Tetap validkan Type Guards setelah pemfaktoran ulang di TypeScript

Dec 04 2022
Penjaga tipe generik membantu memperbaiki kode dengan lebih sedikit bug
TypeScript adalah bahasa yang bagus untuk menulis aplikasi dengan pemeriksaan keamanan tipe. Memperluas atau memfaktorkan ulang kode di TypeScript jauh lebih mudah daripada di JavaScript biasa.

TypeScript adalah bahasa yang bagus untuk menulis aplikasi dengan pemeriksaan keamanan tipe. Memperluas atau memfaktorkan ulang kode di TypeScript jauh lebih mudah daripada di JavaScript biasa.

TypeScript memiliki fungsionalitas bawaan yang bagus untuk penyempitan antarmuka — Type Guards. Tetapi mereka tidak selalu melindungi dari kesalahan selama ekstensi / pemfaktoran ulang kode, terutama jika Anda memiliki proyek besar.

Deskripsi masalah

Misalnya, kami memiliki toko teh. Kami menjual dua jenis teh: longgar dan kantong. Kami menampilkan semua barang dalam satu daftar. Setiap jenis produk memiliki tautan yang dimanusiakan, dan atas nama produk kami ingin menentukan jumlah gram atau jumlah kantong teh dalam satu kemasan.

import React from "react";

type Tea = { id: number; name: string; price: number };

type LooseTea = Tea & { weight: number };
type BaggedTea = Tea & { size: number };

const isLooseTea = (x: Tea): x is LooseTea => "weight" in x;
const isBaggedTea = (x: Tea): x is BaggedTea => "size" in x;

const getLink = (x: Tea): string => {
  if (isLooseTea(x)) return `/loose-tea/${x.id}`;
  if (isBaggedTea(x)) return `/bagged-tea/${x.id}`;
  throw new Error("Unknown tea");
};

const getTitle = (x: Tea): string => {
  if (isLooseTea(x)) return `${x.name} / ${x.weight}g`;
  if (isBaggedTea(x)) return `${x.name} / ${x.size} teabags`;
  throw new Error("Unknown tea");
};

const TeaItems: React.FC<{ items: Tea[] }> = ({ items }) => {
  return (
    <ul>
      {items.map((x) => (
        <li key={x.id}>
          <a href={getLink(x)}>{getTitle(x)}</a>
        </li>
      ))}
    </ul>
  );
};

type BaggedTea = Tea & { bags: number; weightPerBag: number };

// before:
const getTitle = (x: Tea): string => {
  if (isLooseTea(x)) return `${x.name} / ${x.weight}g`;
  // Property 'size' does not exist on type 'BaggedTea'. ts(2339)
  if (isBaggedTea(x)) return `${x.name} / ${x.size} teabags`;
  throw new Error("Unknown tea");
};

// after:
const getTitle = (x: Tea): string => {
  if (isLooseTea(x)) return `${x.name} / ${x.weight}g`;
  if (isBaggedTea(x)) return `${x.name} / ${x.bags} teabags ~ ${x.weightPerBag}`;
  throw new Error("Unknown tea");
};

Solusi masalah

Masalah dengan Type Guard kami adalah secara internal kami memeriksa keberadaan bidang di objek. Ini adalah operasi JavaScript dan tidak diketik dengan cara apa pun.

Nah, mari kita tangani itu. Saya mengusulkan untuk menggunakan fungsi pabrik untuk membuat fungsi Type Guard berdasarkan bidang model data.

type AnyObject = Record<string, any>;

// https://stackoverflow.com/a/52991061
type RequiredKeys<T> = {
  [K in keyof T]-?: AnyObject extends Pick<T, K> ? never : K
}[keyof T];

const createShapeGuard = <T extends AnyObject>(...keys: RequiredKeys<T>[]) => {
  return (obj: unknown): obj is T => {
    if (typeof obj !== "object" || obj === null) return false;

    for (const key of (keys as string[])) {
      if (!(key in obj)) return false;
    }

    return true;
  };
};

const isLooseTea = createShapeGuard<LooseTea>("weight")
const isBaggedTea = createShapeGuard<BaggedTea>("bags", "weightPerBag")

type BaggedTea = Tea & { bags: number; bagWeight: number };

// Argument of type '"weightPerBag"' is not assignable to parameter of type 'RequiredKeys<BaggedTea>'.ts(2345)
const isBaggedTea = createShapeGuard<BaggedTea>("bags", "weightPerBag")