Kerrakuu
Kerrakuu

Reputation: 19

Issue using useRef with TypeScript

I have the following code;

App.tsx

export default function App() {
  const [canvasRef, canvasWidth, canvasHeight] = useCanvas();
  return (
    <div>
      <canvas
        ref={canvasRef}
      />
    </div>
  )
}

useCanvas.ts

import { useRef, useEffect } from "react";

const canvasWidth = 1200;
const canvasHeight = 600;

export default function useCanvas() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  return [canvasRef, canvasWidth, canvasHeight]
}

And yes, this is precisely what I have in my code, however, I'm getting the following error in App.tsx from Vscode;

(JSX attribute) React.ClassAttributes<HTMLCanvasElement>.ref?: string | React.RefObject<HTMLCanvasElement> | ((instance: HTMLCanvasElement | null) => void) | null | undefined
Type 'number | RefObject<HTMLCanvasElement>' is not assignable to type 'string | RefObject<HTMLCanvasElement> | ((instance: HTMLCanvasElement | null) => void) | null | undefined'.
  Type 'number' is not assignable to type 'string | RefObject<HTMLCanvasElement> | ((instance: HTMLCanvasElement | null) => void) | null | undefined'.ts(2322)
index.d.ts(143, 9): The expected type comes from property 'ref' which is declared here on type 'DetailedHTMLProps<CanvasHTMLAttributes<HTMLCanvasElement>, HTMLCanvasElement>'

Any advice would be appreciated.

Upvotes: 0

Views: 1545

Answers (2)

known-as-bmf
known-as-bmf

Reputation: 1222

The return type of your custom hook is (number | React.MutableRefObject<HTMLCanvasElement>)[] because typescript infers it returns an array containing these types. This is why your deconstructed variables in App.tsx are not properly typed (if you hover them, youll see they are all of type number | React.MutableRefObject<HTMLCanvasElement>).

What you need is to use a tuple to tell typescript exactly what is contained in this array.

You could add typing information to your custom hook like so:

import { useRef, useEffect, MutableRefObject } from "react";

const canvasWidth = 1200;
const canvasHeight = 600;

export default function useCanvas(): [MutableRefObject<HTMLCanvasElement>, number, number] {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  return [canvasRef, canvasWidth, canvasHeight]
}

Upvotes: 0

Nicholas Tower
Nicholas Tower

Reputation: 84902

function useCanvas() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  return [canvasRef, canvasWidth, canvasHeight]
}

Since you havn't given a type to the return value, typescript will infer it. When infering the type on an array, typescript is going to assume it's a normal array, not a tuple.

To simplify it, suppose you were returning this:

function () {
  return [1, 'a'];
}

Typescript would infer the type to be (number | string)[]. In other words, an array of any length, where every element can be either a number of a string. It will not infer the type to be [number, string]. So if you were to try to interact with index 0 of this array, typescript wouldn't be able to tell you that it's definitely a number, only that it's a number or a string.

So your first option is to give an explicit type to the thing you're returning. In my simplified case, it would be:

function () {
  const result: [number, string] = [1, 'a'];
  return result;
}

// or
function (): [number, string] {
  return [1, 'a'];
}

// or
function () {
  return [1, 'a'] as [number, string];
}

I'm not exactly sure that the correct types are for your code, but you could inspect the variables and figure it out.

Another option, and probably the simpler one, is to use as const to help typescript out with inferring the types. as const tells it that this will never change, and so it will treat it as a tuple instead of an array.

function useCanvas() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  return [canvasRef, canvasWidth, canvasHeight] as const;
}

Upvotes: 2

Related Questions