Halten Sie Type Guards nach dem Refactoring in TypeScript gültig

Dec 04 2022
Generische Typwächter helfen beim Umgestalten von Code mit weniger Fehlern
TypeScript ist eine großartige Sprache zum Schreiben von Anwendungen mit Typsicherheitsprüfungen. Das Erweitern oder Umgestalten von Code in TypeScript ist viel einfacher als in reinem JavaScript.

TypeScript ist eine großartige Sprache zum Schreiben von Anwendungen mit Typsicherheitsprüfungen. Das Erweitern oder Umgestalten von Code in TypeScript ist viel einfacher als in reinem JavaScript.

TypeScript hat eine nette eingebaute Funktion zum Einschränken der Schnittstelle – Type Guards. Aber sie schützen nicht immer vor Fehlern während der Code-Erweiterung / Refactoring, besonders wenn Sie ein großes Projekt haben.

Problembeschreibung

Zum Beispiel haben wir einen Teeladen. Wir verkaufen zwei Arten von Tee: lose und in Beuteln. Wir zeigen alle Waren in einer Liste. Jeder Produkttyp hat einen humanisierten Link, und im Namen des Produkts möchten wir die Anzahl der Gramm oder die Anzahl der Teebeutel in einer Packung angeben.

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");
};

Problemlösung

Das Problem mit unserem Type Guard ist, dass wir intern prüfen, ob ein Feld im Objekt vorhanden ist. Dies ist eine JavaScript-Operation und wird in keiner Weise eingegeben.

Nun, lass uns das erledigen. Ich schlage vor, eine Factory-Funktion zu verwenden, um Type Guard-Funktionen basierend auf den Datenmodellfeldern zu erstellen.

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")