Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to narrow the type for SVG Element union

I am using react to set a reference to an svg element that might be a <rect>, <polygon> or <ellipse>.

I have this declaration:

const shapeRef = useRef<SVGPolygonElement | SVGEllipseElement | SVGRectElement>(null);

But when I try and set this on an <ellipse> element like this:

<ellipse
  cx={width / 8}
  cy={-sideDimension(y) / 8}
  rx={width}
  ry={height}
  ref={shapeRef}
/>

I get this error:

Type 'RefObject' is not assignable to type 'string | ((instance: SVGEllipseElement | null) => void) | RefObject | null | undefined'. Type 'RefObject' is not assignable to type 'RefObject'. Type 'SVGPolygonElement | SVGEllipseElement | SVGRectElement' is not assignable to type 'SVGEllipseElement'. Type 'SVGPolygonElement' is missing the following properties from type 'SVGEllipseElement': cx, cy, rx, ryts(2322)

My understanding from this is that I somehow need to narrow the type in order for this to work or else every object that uses this ref must have all properties of the union.

like image 505
dagda1 Avatar asked May 26 '19 06:05

dagda1


1 Answers

You are correct. Typescript gives you that error because it doesn't know which one of the types it should account the shapreRef as.

The best solution IMO is using a Type Guards. A Type Guard is the typescript way to check if a variable is of a certain type. For union types, that gives typescript the understanding that something is of a specific type.

For example, in your case, it can be something like this:

interface IEllipse {
  attr1: string;
  attr2: string;
}

interface IRect {
  attr3: string;
  attr4: string;
}

type SvgShape = IEllipse | IRect | IPolygon;

function isEllipse(shape: SvgShape): shape is IEllipse {
    return (shape as IEllipse).attr1 !== undefined;
}

Notice that the return type is shape is IEllipse. This means that typescript will interpret a truthy return value here as if shape is an IEllipse.

Then, wherever you want to use a SvgShape, you can check which type of SvgShape it is and typescript should know the type based on that:

// ...
render() {
  const shape: SvgShape = this.getCurrentShape();

  if (isEllipse(shape)) {
    // typescript should KNOW that this is an ellipse inside this if
    // it will accept all of Ellipse's attribute and reject other attributes
    // that appear in other shapes

    return <ellipse .../>;
  } else if (isRect(shape)) {
    // typescript should interpet this shape as a Rect inside the `if`

    return <rect ... />;
  } else {
    // typescript will know only one subtype left (IPolygon)

    return <polygon points="..." />;
  }
}
// ...

Why not just an Intersection type?

Well... Intersection types are more for cases where every one of the types (Rect, Polygon, etc) have the exact same attributes in the new item. For example:

type Inter = IRect & IPolygon & IEllipse;

Means that an Inter type is IRect and IPolygon and IEllipse. That means an object of this type will have all members of all three types. So, trying to access the attribute points (which exists on IPolygon) on a shape that is actually an IRect, will act as if that attribute exists there (which we don't want)

You will mostly see intersection types used for mixins and other concepts that don’t fit in the classic object-oriented mold.

how to use with useRef?

type SvgShape = SVGPolygonElement | SVGEllipseElement | SVGRectElement;

const shapeRef = useRef<SvgShape>(null);

function isEllipseRef(shapeRef: MutableRefObject<SvgShape>): shapeRef is MutableRefObject<IEllipse> {
  const shape: SvgShape = shapeRef.current;
  return (shape as IEllipse).attr1 !== undefined;
}
like image 70
Thatkookooguy Avatar answered Nov 20 '22 00:11

Thatkookooguy