Search code examples
reactjstypescripttsx

Assigning function with incompatible type to ref attribute


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?


Solution

  • 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.