Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I make a variable have the type unknown or 'Movie' assigned?

I am developing a hook in which I can pass a function that makes a web request and it returns isLoading, data and error.

const [isLoading, data, error] = useApi(getMovie, idMovie, someAction);

basically I have a hook (useApi) that receives 3 parameters:

  • a function that resolves a web request
  • the parameters of this web request
  • and finally the call to a callback.

I use it like this:

const idMovie = { _id: "3" };
// callback function
const someAction = (data: unknown) => {
 // do something
 return data;
};

const [isLoading, data, error] = useApi(getMovie, idMovie, someAction);

useApi.tsx

import { AxiosPromise, AxiosResponse } from 'axios';
import { useState, useEffect } from 'react';
const useApi = (
    apiFunction: (params: unknown) => AxiosPromise,
    params = {},
    callback: (data: unknown) => void
): [boolean, unknown, null | string] => {
    const [data, setData] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState<null | string>(null);

    useEffect(() => {
        apiFunction(params)
            .then(({ data }: AxiosResponse) => {
                setData(data);
                setIsLoading(false);
                if (callback) {
                    callback(data);
                }
            })
            .catch(() => {
                setError('Something went wrong');
                setIsLoading(false);
            });
    }, []); //apiFunction, params, callback]

    return [isLoading, data, error];
};

export default useApi;

getMovie corresponds to a function that solves a web request

import axios from "axios";
type getMovieParams = { _id: string };
const BASE_URL = "https://jsonplaceholder.typicode.com/todos/";
const getMovie = (params: getMovieParams): Promise<unknown> => {
  if (params) {
    const { _id } = params;
    if (_id) {
      const url = `${BASE_URL}/${_id}`;
      return axios.get(url);
    }
  }
  throw new Error("Must provide a query");
};

export default getMovie;

the code that calls this hook would look like this:

import "./styles.css";
import useApi from "./useApi";
import getMovie from "./api";
interface Movie {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

export default function App() {
  const idMovie = { _id: "3" };
  // callback function
  const someAction = (data: unknown) => {
    // do something
    return data;
  };

  const [isLoading, data, error] = useApi(getMovie, idMovie, someAction);

  const dataResponse = error ? [] : data; //type Movie
  console.log(dataResponse);
  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      {error && <div>{error}</div>}
      -- Get Data Movie 1--
      <p>{dataResponse.title}</p>
    </div>
  );
}

I am getting an typing error on this line:

<p>{dataResponse.title}</p>

this is because:

const dataResponse = error ? [] : data; 

data is of type unknown, this is because it could be any type of data, but I want to specify in this case that the data type is 'Movie', the idea is to reuse this hook in another component and be able to say the type of data that will be obtained when this hook returns data.

this is my live code:

https://codesandbox.io/s/vigilant-swartz-iozn4

The reason for the creation of this question is to ask for your kind help to solve the typescript problems that are marked with a red line. (file app.tsx)

enter image description here

How can fix it? thanks

like image 732
yavgz Avatar asked Sep 29 '21 23:09

yavgz


People also ask

How do you fix type unknown is not assignable to type?

To solve the "Type 'unknown' is not assignable to type" TypeScript error, use a type assertion or a type guard to verify that the two values have compatible types before the assignment. The error is caused when a value of type unknown is assigned to a value that expects a different type.

How do you use unknown in TypeScript?

unknown is the type-safe counterpart of any . Anything is assignable to unknown , but unknown isn't assignable to anything but itself and any without a type assertion or a control flow based narrowing. Likewise, no operations are permitted on an unknown without first asserting or narrowing to a more specific type.

Is not assignable to parameter of type unknown []'?

The error "Argument of type 'unknown' is not assignable to parameter of type" occurs when we try to pass an argument of type unknown to a function that expects a different type. To solve the error, use a type assertion or a type guard when calling the function.

What is the difference between unknown and any type?

The difference between unknown and any is described as: Much like any , any value is assignable to unknown ; however, unlike any , you cannot access any properties on values with the type unknown , nor can you call/construct them. Furthermore, values of type unknown can only be assigned to unknown or any .


1 Answers

It is possible to use generics to capture types related to the args you're passing to a function. For instance, in the screenshot you have, TS complaints about the params, we can declare a generic to capture the params type

function useApi<Params>(
  apiFunction: (params: Params) => AxiosPromise,
  params: Params,
  callback: (data: unknown) => void
)

with this, our second hook argument will be typed as whatever type the apiFunction asks for in its arg.

we do the same to type the data we return to the callback

function useApi<Params, Return>(
  apiFunction: (params: Params) => Promise<AxiosResponse<Return>>,
  params: Params,
  callback: (data: Return) => void
)

I got the return type of the apiFunction by inspecting the types for the axios.get call

get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;

as you can see, in the end it returns a Promise<AxiosResponse<T>>. We intercept that T with our generic typing and name it Return

now we can use these typings in the return types and body of the hook as well

): [boolean, Return | null, null | string] {
  const [data, setData] = useState<Return | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<null | string>(null);

  useEffect(() => {
    apiFunction(params)
      .then(({ data }) => {
        setData(data);
        setIsLoading(false);
        if (callback) {
          callback(data);
        }
      })
      .catch(() => {
        setError("Something went wrong");
        setIsLoading(false);
      });
  }, []); //apiFunction, params, callback]

  return [isLoading, data, error];
}

as your Movie model is specific for the getMovie call, you can move that interface there and type the axios.get call

interface Movie {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

const getMovie = (params: getMovieParams) => {
  if (params) {
    const { _id } = params;
    if (_id) {
      const url = `${BASE_URL}/${_id}`;
      return axios.get<Movie>(url);
    }
  }
  throw new Error("Must provide a query");
};

with all these additions you'll observe some warnings on the code when you're using the hook that will force you to implement the rendering taking into account all possible values, I rewrote it like this

  ...
  const [isLoading, dataResponse, error] = useApi(
    getMovie,
    idMovie,
    someAction
  );

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) return <div>error</div>;

  return <div>{dataResponse?.title}</div>;
  ...

https://codesandbox.io/s/generic-hook-arg-types-s8vtt

like image 58
diedu Avatar answered Nov 20 '22 15:11

diedu