Lesbare og type­sikre komponenter i TypeScript

Kristiane Alvarstein Westgård viser deg hvorfor du bør bruke union-types.

"Husk å bruke Typescript på nye (og gamle) prosjekter!" skriver Kristiane Alvarstein Westgård. 📸: Blank
"Husk å bruke Typescript på nye (og gamle) prosjekter!" skriver Kristiane Alvarstein Westgård. 📸: Blank Vis mer

TypeScript har mange kule features som gjør hverdagen enklere, særlig på prosjekter der man er flere utviklere, eller der det går en stund mellom hver gang man er innom kodebasen.

En av dem er discriminated unions. I dag tenkte jeg å vise hvordan du kan bruke de til å skrive lesbare og typesikre komponenter! ✨

Hvis man har skrevet JavaScript og React før, så har man garantert sett dette mønstret gå igjen i mange komponenter:

const Komponent = () => {
  const [data, setData] = useState(undefined);
  const [isLoading, setIsLoading] = useState(true);
  const [hasErrored, setHasErrored] = useState(false);
  useEffect(() => {
    getData().then((res) => {
        setData(res);
      }).catch((err) => {
        setHasErrored(true);
      });
  }, []);
  if (isLoading) {
    return <Spinner />;
  } 
  if (hasErrored) {
    return <ErrorMessage />;
  }
  if (data) {
    <h1>Hei {data.name} 👋</h1>;
  }
}

Ganske enkelt: du skal vise en eller annen loading-state mens du henter data, eller en feilmelding dersom noe går galt i prosessen.

Koden over fungerer ikke, siden det mangler setIsLoading(false) i then ️og catch-blokkene. Da kan man havne i en tilstand der for eksempel både hasErrored = true og isLoading = true. Dermed vil vi alltid treffe den første if-blokka, og bare vise en spinner for alltid.

Problemet er altså at vi kan havne i “ugyldige” states der flere av feltene er satt samtidig, i stedet for at man man enten er i loading-state, error-state eller data state , som er det vi egentlig vil.

Dette problemet kan union-types hjelpe oss med, så vi i stedet skrive komponenter som ser slik ut 👇

const Komponent = () => {
  const [state, setState] = useState<PersonResponse> ({type:'LOADING'});
  useEffect(() => {
    getPerson().then((res) => setState(res));
  }, []);
  switch (state.type) {
    case 'LOADING':
      return <Spinner />;
    case 'ERROR':
      return <ErrorMessage />;
    case 'DATA':
      <h1>Hei {state.person.name} 👋</h1>;
  }
}

Det eneste PersonResponse gjør er å wrappe person-objektet ({name: string}) i en hjelpe-type som gjør det enklere å holde styr på hvilken state man er i.

Den har et felt som heter type, også kalt en “discriminant”, som enten har verdien ERROR, LOADING eller DATA.

interface PersonData {
  type: 'DATA';
  person: {
    name: string;
  };
}
interface PersonError {
  type: 'ERROR';
}
interface PersonLoading {
  type: 'LOADING';
}
type PersonResponse = PersonData | PersonError | PersonLoading;
Dersom type-feltet er DATA finnes feltet person.
Dersom type-feltet er DATA finnes feltet person. Vis mer

Samme eksempel ligger på GitHub, med eksempel på hvordan du kan skrive API-kallet.

Der er i tillegg Reponse-wrapper-typen er skrevet med generics, slik at du kan gjenbruke samme type på flere API-kall.

Takk for at du leste! Husk å bruke Typescript på nye (og gamle) prosjekter fordi

image: Lesbare og type­sikre komponenter i TypeScript

🤠