typescriptgenericssetcontravarianceintersection-types

Properly use Typescript Set<T> with intersecting types


I don't understand why the transpiler is complaining. Here are the basic type declarations:

export enum SvgElementType {
  path = "path",
  circle = "circle",
}

export type SvgElement = {
  id: number;
  type: SvgElementType;
};

export type SvgPathElement = SvgElement & {
  type: SvgElementType.path;
  d: string;
};

export type SvgCircleElement = SvgElement & {
  type: SvgElementType.circle;
  cx: number;
  cy: number;
  radius: number;
};

export type Serializer<E extends SvgElement = SvgElement> = (element: E) => string;

const pathSerializer: Serializer<SvgPathElement> = e => "";
const circleSerializer: Serializer<SvgCircleElement> = e => "";

const serializers: Set<Serializer> = new Set();
serializers.add(pathSerializer); // <<<--- transpile error

// full TS error
// Argument of type 'Serializer<SvgPathElement>' is not assignable to parameter of type 'Serializer<SvgElement>'.
//  Type 'SvgElement' is not assignable to type 'SvgPathElement'.
//    Property 'd' is missing in type 'SvgElement' but required in type '{ type: SvgElementType.path; d: string; }'.ts(2345)

the only way I found was to modify the declaration of the Serializer with any as default type:

export type Serializer<E extends SvgElement = any> = (element: E) => string;

This flags me that there is probably a better way to preserve the minimum typing for the serializers Set iterator usage later on...


Solution

  • The transpiler complains because you're effectively saying that you want a function that takes an SvgElement as input, but then you're trying to assign to it a function that takes an SvgPathElement.

    This can't work - it would allow you to pass an SvgElement to your function that wants an SvgPathElement.

    The formal term for this is "function parameters are contravariant".

    Your code will require runtime type checks against the object being serialised anyway, so should you perhaps redefine your Serializer type without the generic? e.g.:

    export type Serializer = (element: SvgElement) => string;