1

I have developed several frontend applications and as I analyse I often need to refactor the front-end code or frontend crash in the following case:

Let's assume there is a endpoint /movies that returns the following response:

{
  "movieName": "Avengers Endgame",
  "genre": "Action, Thriller",
  "length": 120 // in minutes 
  "rating": 4,
  "id": 1
}

To handle this endpoint's response in the frontend application (React + Typescript), I have created a MovieType and will render the data in the component.

// types.ts

  export type MovieType = {
    movieName: string;
    genre: string;
    length: number;
    rating: number;
    id: number;
  }
import React from 'react';

function MovieComponent() {
  const [movies, setMovies] = React.useState([] as MovieType[]);

  React.useEffect(() => {
    const data:MovieType[] = fetchDataSomeHow() as MovieType[];
    setMovies(data);
  })

  return (
    <ul>
      movies.map(movie => (
        <li key={movie.id}>
          {movie.movieName.toLoweCase()} - {movie.rating}
        </li>
      ))

    </ul>
  )
}

So here I have created MovieType by looking at the response of the /movies endpoint and marked movieName as required in MovieType. Here the MovieType is affected by the data returned by the /movies endpoint.

As I am using Typescript and I have defined movieName as string and required, I can use toLowerCase method without any additional checks.

Now let's suppose that there is some bad input in the movie database and in the response movieName is undefined/null or movieName is changed to name. Now the frontend application will crash because of runtime errors.

This happens whenever there is an API contract mismatch in the frontend and backend because of bad database entries or sudden backend API changes.

is there any standard or a good way to handle this kind of errors? I can easily make every field optional and then write frontend code but it will be messy and nonreadble. On the backend side, we have concepts of adapters that validate and sanitise the data and throws errors in case of invalid requests but on the frontend side, we can't hang users on an error page just because we got one field wrong in one object among 1000 objects.

aditya81070
  • 678
  • 1
  • 5
  • 22
  • 1
    Perhaps [conditional types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) would work for you? – Randy Casburn Nov 18 '21 at 17:57
  • If you don't want the front-end to fail, you should have a fallback value. Say "-", if the movie name is missing. Usually, there will be a mapping stage between front-end objects and whatever is returned from the API instead of blindly pointing the API response to the front-end object. During that map, you can validate and assign empty/null fallback values. Yes, it is easy to take whatever comes over way but it is better to validate and map each field I think. – Sanish Joseph Nov 18 '21 at 18:04
  • @SanishJoseph - that is exactly what conditional types will help to accomplish. – Randy Casburn Nov 18 '21 at 18:06
  • @RandyCasburn Will it automatically assign the fallback value? Say `movieName: string | null | "-"`. Is there a way I can get "-" when `movieName` is null without explicitly assigning "-"? – Sanish Joseph Nov 18 '21 at 18:16
  • Yes, it can be done - follow the link I provided above and search for `default` on that page. The single result describes how to do this. – Randy Casburn Nov 18 '21 at 18:24
  • Maybe an example according to the question or an answer will help. I still couldn't get the idea. – Sanish Joseph Nov 18 '21 at 19:06
  • @SanishJoseph so do you mean that I should follow a kind of controller-model approach where controller fetches data and convert it to a model. This model is object that fronted can use without any additional checks as all the validation is done already? – aditya81070 Nov 19 '21 at 01:07
  • @RandyCasbum I think conditional types won't solve the issue. The basic concept with types is that they only exists for compile time and not at the runtime. So according to question, I have mentioned that MovieType will always have movieName but API contract failed at runtime so typescript can't help me here. – aditya81070 Nov 19 '21 at 01:10
  • You may handle it with an interface and a class with a constructor which accepts all values from the response and sets the values based on availability. The constructor may have some default values set to handle the null cases. Just thinking the OOPs way. https://stackoverflow.com/questions/43326380/how-to-define-optional-constructor-arguments-with-defaults-in-typescript – Sanish Joseph Nov 19 '21 at 05:42

2 Answers2

0

You can check if movie exists using && check:

movie && {movie.movieName.toLoweCase()} - {movie.rating}
Gorynych
  • 100
  • 4
  • This is not feasible solution. If there are 100 keys and 20 APIs then adding conditional statements will make the code really ugly. – aditya81070 Nov 19 '21 at 01:02
0

I would classify the type as unknown. Big picture, you are getting something, and you don't know what it is. So it's unknown. After you do this, Typescript will force you to check everything, then you get all the benefits of the typed result.

You could still define the type

export type MovieType = {
    movieName: string;
    genre: string;
    length: number;
    rating: number;
    id: number;
  }

But your result from the API would initially be unknown. After you get your result, you would narrow the result. The unknown type will protect you from mistakes in this process. When it is appropriately narrowed, you could then assign it to a type of MovieType or work with those of that type.

const result: Record<string, unknown> = {
  movieName: "test",
};

export type MovieType = {
  movieName: string;
  genre: string;
  length: number;
  rating: number;
  id: number;
};

let someMovie: MovieType | undefined;

if (
  typeof result === "object" &&
  typeof result.movieName === "string" &&
  typeof result.genre === "string" &&
  typeof result.length === "number" &&
  typeof result.rating === "number" &&
  typeof result.id === "number"
) {
  someMovie = {
    movieName: result.movieName,
    genre: result.genre,
    length: result.length,
    rating: result.rating,
    id: result.id,
  };
}

Unfortunately I could not directly just use type unknown and narrow it all the way to a Record<string, unknown>. I asked my own question on this, but I think it may be impossible currently.

Diesel
  • 5,099
  • 7
  • 43
  • 81