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.
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="..." />;
}
}
// ...
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.
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With