TS: Deconstructing an array containing an object and a function (setter) only works with @ts-ignore

Viewed 302

I have a React hook I'm using to set the values on a Context provider.

const useAuth = () => {
  const [token, setToken] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [tryAuth, setTryAuth] = useState(false);

  useEffect(() => {
    if (tryAuth) {
      const getToken = async () => {
        console.log("triggered");

        setIsLoading(true);
        const axiosInstance = axios.get<TokenResponseType>(
          "http://localhost:3500/token"
        );
        const tokenResult = (await axiosInstance).data.accessToken;
        setToken(tokenResult);
        setIsLoading(false);
      };
      getToken();
    }
  }, [tryAuth]);

  return [{ token, isLoading }, setTryAuth];
};

export default useAuth;

export type inferredType = ReturnType<typeof useAuth>;

VSCode tells me the return type is as follows:

VSCode snapshot

(alias) useAuth(): (React.Dispatch<React.SetStateAction<boolean>> | {
    token: string;
    isLoading: boolean;
})[]

Because I couldn't figure out how to describe the type I used the following approach:

export type inferredReturnType = ReturnType<typeof useAuth>;

And I'm initializing my React Context with the following line:

const AuthContext = React.createContext<inferredReturnType | undefined>(undefined);

However when I'm trying to use the hook in a Component as follows:

const [{ token, isLoading }, setTryAuth] = useAuth();

I'm getting the error message

Property 'token' does not exist on type 'Dispatch<SetStateAction> | { token: string; isLoading: boolean; }'.ts(2339).

Only suppressing the TSCompiler with //@ts-ignore made it work so far.

Here the code for the Context and context provider:

const AuthContext = React.createContext<inferredReturnType | undefined>(undefined);

const AuthContextProvider = (props: any) => {
  //@ts-ignore
  const [{ token, isLoading }, setTryAuth] = useAuth();

  return (
    <AuthContext.Provider value={[{ token, isLoading }, setTryAuth]}>
      {props.children}
    </AuthContext.Provider>
  );
};

export default AuthContextProvider;

export const useAuthContext = () => {
  //@ts-ignore
  const [{ token, isLoading }, setTryAuth] = useContext(AuthContext);

  if (token === undefined)
    throw new Error('useAuthContext must be inside a Provider with a value');

  return [{ token, isLoading }, setTryAuth];
};

Here is the link to the Code Sandbox: Edit staging-sea-kmury

What I'm trying to achieve is described under "Custom Data fetching hook" in the excellent blog post by Robin Wieruch just for TypeScript.

I'm sure it's some advanced type destructuring thingy from ES6 in combination with TypeScript that's messing with my head but I can't find a good article / docu to read up on it.

Thanks for your help!

3 Answers

TypeScript is inferring the type (A|B)[] (which is reasonable, it doesn't know you're always going to return things in the same order), but you want the tuple type [A, B].

You have at least two options:

Define the type explicitly

Like this:

type UseAuthReturnType = [
    {token: string, isLoading: boolean},
    React.Dispatch<React.SetStateAction<boolean>>
];

If you use that, like this:

const useAuth = (): UseAuthReturnType => {
    // ...
};

...your destructuring works.

Playground link

Use as const

Like this:

const useAuth = () => {
  // ...

  return [{ token, isLoading }, setTryAuth] as const;
};

Playground link

TypeScript will infer that differently. Specifically, it'll infer:

readonly [
    {
        readonly token: string;
        readonly isLoading: boolean;
    },
    React.Dispatch<React.SetStateAction<boolean>>
]

Assuming you don't want to receive that first entry as an actual object and change its values (which I'm fairly sure you don't), that works just as well via inference.

Typescript infers the return type, but in this case it does that wrongly.

Basically, it sees an array of two objects with different types:

One type:

{
  token: string,
  isLoading: boolean
}

The other type:

React.Dispatch<React.SetStateAction<boolean>>

So, typescripts decides that the return type is an array of objects that are either the first, or the second type. But it doesn't understand in which order they are returned, or that the array has exactly 2 items. That is your inferred type:

({
  token: string,
  isLoading: boolean
} | React.Dispatch<React.SetStateAction<boolean>>)[]

Which means: there is a variable amount of items that are either a react action or an objects with a string token and a boolean isLoading.

When you try to use it, calling:

const [{ token, isLoading }, setTryAuth] = useAuth();

Typescript complains, because from its points of view there are 2 problems:

  1. You extract 2 objects from the array. Typescript didn't infer the length of the array, so he warns you that potentially those two items does not exists.
  2. You extract the keys token and isLoading from the first returned item. But typescript has no clue that the first item has that type, so it warns you that the first item may be of type React.Dispatch<React.SetStateAction<boolean>>.

You can solve everything by saying to typescript that the return type has exactly two items, with a specific order:

type UseAuthReturnType = [
    {token: string, isLoading: boolean},
    React.Dispatch<React.SetStateAction<boolean>>
];

const useAuth = (): UseAuthReturnType => {
    // ...
};

Now typescript knows that you always have two items, that the first item has both token and isLoading keys and the second item is a react action.

dince useAuth is typed to return an array containg the two types. e.g. React.Dispatch<React.SetStateAction<boolean>> OR {token: string; isLoading: boolean;}, the error is telling you that you cannot assign to the property token, because it is not part of Dispatch.SetStateAction 's signature.

It doesn't look to me like order is enforces with that type. Maybe a React user can help in that reguard.

You could filter/type check the response from useAuth so the type is no longer complex. Or update the type if it is always returning that the type with token and isLoading.

Related