skip to Main Content

I’m learning React and Typescript both.
I have a backend server with ApiPlatform.
I would like on the frontend side to use fetch to create or update a Pokemon with its abilities.

My code below is working but it’s not "clean", I use let data… to transform abilities into an array of iri references, but I don’t want to repeat this code on create/update.

static updatePokemon(pokemon: Pokémon): Promise<Pokemon> { 
let data = { 
  id: pokemon.id, 
  name: pokemon.name, 
  abilities: pokemon.abilities.map(a => '/api/abilities/' + a.id) 
};
return fetch(ENTRY_POINT + '/api/pokemons/' + pokemon.id, {
  method: 'PUT',
  body: JSON.stringify(data),
  headers: { 'Content-Type': 'application/json' }
})
  .then(response => response.json())
  .catch(error => this.handleError(error));
}`

Maybe Object.keys(object).reduce.. in other file?

Thanks in advance

2

Answers


  1. I think I would encapsulate it inside the pokemon class. It could expose a marshall(): <IPokemonDto>.

    "marshall" is well defined by CakePHP documentation: "Marshall" […] contains logic to convert array data into entities. Useful when converting request data into entities.

    IPokemonDTO is your plain javascript object but now it implements an interface.

    interface IPokemonDTo {
        id: number;
        name: string;
        abilities: string[];
    }
    

    Edit: formatting

    Login or Signup to reply.
  2. nice, approach looks pretty good. Yeah agree there are a few things that can be changed to make different aspects more scalable and reusable. And a few minor things that can clean it up and some of it may be preference.

    Guess the code is part of a class but for simplicity can just make it a function for this example.

    Since you’re using typescript its a good idea to defined your types, at least let’s make one for pokemon.

    type Pokemon = {
      readonly id: number;
      readonly name: string;
      readonly abilities: readonly { readonly id: string }[];
    };
    

    This is a sufficient type for the example. Can also make a separate type for ability if eventually useful.

    The next thing to do, since you say you prefer not to repeat the mapping code, which makes sense, is to create a function to do that. There should be no need to use reduce here, simply mapping the fields that need transformations applied should be sufficient.

    const getPokemonData= ({ id, name, abilities }: Pokemon) => ({
      id,
      name,
      abilities: abilities.map(({ id }) => `/api/abilities/${id}`),
    });
    

    It’s worth knowing that Object.keys and similar method values, entries don’t work that nicely with typescript so unless there’s a good use case where you have lots of fields that each follow the same transformation or a set of transformations that can be reliably typed using mapped types to assert the response as, its worth just doing the object mapping manually, so like this should work for a start.

    Now it’s slightly more composable and you could do

    ...
      body: JSON.stringify(getPokemonData(pokemon))
    ...
    

    in your fetch function.

    Then there are different levels of abstraction you can do, there are often pros and cons around it so it’s up to you to see what’s best for the specific situation, some things are constant.
    The simplest abstraction you can make is around the fetch function, you could have a function like

    const createFetch = (
      url: string,
      method: "PUT" | "POST" | "DELETE",
      payload?: string,
    ) =>
      fetch(url, {
        method,
        body: JSON.stringify(payload),
        headers: { "Content-Type": "application/json" },
      })
        .then((response) => response.json())
        .catch((error) => handleError(error));
    
    

    Which works okay, it has minimal type safety in terms of expected data payloads and urls. But its easy to set up, there’s still some repetition in creating individual fetch functions

    const baseUrl = `${ENTRY_POINT}/api/pokemons`;
    
    const updatePokemon = async (pokemon: Pokemon): Promise<Pokemon> =>
      createFetch(
        `${baseUrl}/update/${pokemon.id}/`,
        "PUT",
        JSON.stringify(getPokemonData(pokemon)),
      );
    
    const createPokemon = async ({
      name,
      abilities,
    }: Omit<Pokemon, "id">): Promise<Pokemon> =>
      createFetch(
        `${baseUrl}/create/`,
        "PUT",
        JSON.stringify({
          name,
          abilities: abilities.map(({ id }) => `/api/abilities/${id}`),
        }),
      );
    
    const removePokemon = async ({ id }: Pick<Pokemon, "id">): Promise<Pokemon> =>
      createFetch(`${baseUrl}/remove/${id}`, "PUT");
    
    

    On one of the highest levels of abstraction you could type everything as mapped types keyed to action names you define

    type PokemonData = {
      readonly id: string;
      readonly name: string;
      readonly abilities: readonly `/api/abilities/${number}`[]; //urls
    };
    
    type APIActions = {
      create: {
        method: "POST";
        payload: Omit<PokemonData, "id">;
        url: `${string}/api/pokemons/create/`;
      };
      update: {
        method: "PUT";
        payload: PokemonData;
        url: `${string}/api/pokemons/update/${string}/`;
      };
      remove: {
        method: "DELETE";
        payload: Pick<Pokemon, "id">;
        url: `${string}/api/pokemons/delete/${string}/`;
      };
    };
    
    

    Where you could define a function to create fetch functions for each action in the form of const createFetch<T extends keyof APIActions>(action:T) => (payload:APIActions[T]['payload'])=>... which creates the api corresponding to that action. With this approach you can split the work of getting required data accross different functions getUrl, getPayload, getMethod.

    You can probably get full type safety but I think it becomes a lot of work using a lot of mapped types and probably function overloads if you want urls strictly typed and checked against actions. So it can be fun but probably not that worth it. So I won’t go into that aspect, if you want I can update with example implementation but there’s a lot to it and probably overcomplicated.

    But just to explain the above types, could be useful.

    Can name the actions whatever we want create, update and remove seem okay, and mapped each of these to method, payload and url. This is a guess, depends on api etc… The method is just the fetch method, the payload is what is expected as data, so for create maybe you don’t have the id as you want it returned from database, so we use Omit to remove it from the type. For delete, you only need the id so we use Pick to produce a type with just id. And update can keep the whole pokemon object. You can find all the type info on typescript website.

    The ${string}... in types are Template Literal Types that’s just stricter type checking for string, can also be useful but sometimes just unnecessary you can find on typescript site if you want, can be useful.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search