3

Why does the following code not cause a compile-time error?

import * as React from 'react';

export class Test extends React.Component {
  private _onReferenceUpdated = (ref: HTMLCanvasElement) => {
    ref.width = 4; // This could throw, when ref is null
  };

  render(): JSX.Element {
    return (
      <canvas ref={this._onReferenceUpdated} />
    );
  }
}

The ref attribute is inferred as

(JSX attribute) React.ClassAttributes<HTMLCanvasElement>.ref?: string | ((instance: HTMLCanvasElement | null) => void) | React.RefObject<HTMLCanvasElement> | null | undefined

which seems correct (the string is a bit weird, but i guess that's just generally for attributes). How is (ref: HTMLCanvasElement) => void assignable to (instance: HTMLCanvasElement | null) => void?

Doofus
  • 952
  • 4
  • 19

2 Answers2

1

The answer is kinda right there in your question.

As you noted, the type of ref is inferred as:

(JSX attribute) React.ClassAttributes<HTMLCanvasElement>.ref?: string | ((instance: HTMLCanvasElement | null) => void) | React.RefObject<HTMLCanvasElement> | null | undefined

This is a "union" type, meaning any of the types separated by pipes (|) are valid here. So ref can be any one of the following:

  • (JSX attribute) React.ClassAttributes<HTMLCanvasElement>.ref?: string
  • ((instance: HTMLCanvasElement | null) => void)
  • React.RefObject<HTMLCanvasElement>
  • null
  • undefined

Well, you've defined the _onReferenceUpdated property, which you're passing to ref, as follows:

  private _onReferenceUpdated = (ref: HTMLCanvasElement) => {
    ref.width = 4; // This could throw, when ref is null
  };

So the type of _onReferenceUpdated is (ref: HTMLCanvasElement) => void, which handily matches one of our valid ref types above: ((instance: HTMLCanvasElement | null) => void).

Note that the wrapping parenthesis are meaningless, they just help for readability. Also note that it does not matter that the name of the parameter is "ref" in one type and "instance" in the other. All that matters is the order and type of the function parameters, and the return type.

In TypeScript it is totally valid and fine to do something like this:

let Foo = (a: string) => {}; // inferred type of Foo is "(a: string) => void"

Foo = (b: string) => {}; // no error; order and type of args and return type match
Foo = (c: number) => {}; // error! type of arguments don't match
jered
  • 11,220
  • 2
  • 23
  • 34
  • Thanks, but why is this valid? In one function, the parameter could potentially be `null`, while in the other, it cannot. This is also the real issue: `ref` could be `null` (when the component unmounts), and neglecting this in the function is invalid. If i do the same manually, it also gives an error: https://i.imgur.com/cF796Xt.png - which is what i would expect. – Doofus Dec 31 '20 at 15:21
1

Disclaimer: I only researched far enough to get an outline of the problem. If there is a proper explanation in a different question, or someone with a better understanding is willing to give one, feel free to add.


In Typescript 2.6, the strict function types setting was added, now checking function parameters contravariantly. Therefore,

const f = (p: HTMLCanvasElement) => void p;
const g: (p: HTMLCanvasElement | null) => void = f;

is an error with that setting enabled. However, sadly, due to what seems to be a design limitation, there is a problem with e.g. the ref prop in TSX:

there is no way to tell TypeScript that the "ref" prop actually is contravariant

What follows is the bivarianceHack, or in other words, ref behaves as-if strictFunctionChecks was disabled. Then, HTMLCanvasElement is a subtype of HTMLCanvasElement | null, and accepted.

This is also visible, when following the type definition, which leads to:

type RefCallback<T> = { bivarianceHack(instance: T | null): void }["bivarianceHack"];

I find this quite sad (especially, that this hack isn't visible in the type annotation, at least not in vscode), and any proper solutions, or updates on the topic, are welcome. At the very least, i am considering an ESLint typescript rule, that just forces the parameter of any function passed to ref to be nullable, in a hardcoded way.

Doofus
  • 952
  • 4
  • 19