Home > Net >  How to define subclass type of object values strictly in TypeScript
How to define subclass type of object values strictly in TypeScript

Time:12-18

I want to define subclass type of object values as following SHAPE_PARAMS.
I could set type with type assertion as <CircleParameter>, but missing properties could not be linted.

How can I define subclass type strictly?

const Shapes = {
  CIRCLE_A: "CIRCLE_A",
  CIRCLE_B: "CIRCLE_B",
  RECTANGLE_A: "RECTANGLE_A",
} as const;
type Shapes = typeof Shapes[keyof typeof Shapes];

interface ShapeParameter {
  color: string;
}

interface CircleParameter extends ShapeParameter {
  radius: number;
}

interface RectangleParameter extends ShapeParameter {
  width: number;
  height: number;
}

const SHAPE_PARAMS: { [key in Shapes]: ShapeParameter } = {
  [Shapes.CIRCLE_A]: <CircleParameter>{
    color: "red",
    radius: 1,
  },
  [Shapes.CIRCLE_B]: <CircleParameter>{
    // color: "blue", <- Missed property cannot be linted.
    radius: 2,
  },
  [Shapes.RECTANGLE_A]: <RectangleParameter>{
    color: "green",
    width: 3,
    // height: 3, <- Missed property cannot be linted.
  },
};

CodePudding user response:

The problem here is that the <TypeName> syntax asserts that the object has that type, whereas you want the compiler to check that the object has that type.

The best solution is probably to change the type { [key in Shapes]: ShapeParameter } so that it is more specific:

type ShapeParameterMap = {
  [Shapes.CIRCLE_A]: CircleParameter,
  [Shapes.CIRCLE_B]: CircleParameter,
  [Shapes.RECTANGLE_A]: RectangleParameter,
}

const SHAPE_PARAMS: ShapeParameterMap = {
  // ...
};

Playground Link

The upside of this is that you can get a more specific type for SHAPE_PARAMS[k] when you access it with some k of a more specific type.

On the other hand, perhaps this seems like too much code repetition; in that case, you can achieve what you want by writing a trivial utility function which does nothing, but checks the type of its argument:

function check<T>(arg: T): T {
  return arg;
}

const SHAPE_PARAMS: Record<Shapes, ShapeParameter> = {
  [Shapes.CIRCLE_A]: check<CircleParameter>({
    color: "red",
    radius: 1,
  }),
  [Shapes.CIRCLE_B]: check<CircleParameter>({
    radius: 2,
    // missing property 'color'
  }),
  [Shapes.RECTANGLE_A]: check<RectangleParameter>({
    color: "green",
    width: 3,
    // missing property 'height'
  }),
};

Playground Link

CodePudding user response:

You could infer expected shape parameter type from key prefix:

type ShapeParams = {
  [K in Shapes]: K extends `CIRCLE_${string}` ? CircleParameter : RectangleParameter
}

/**  type ShapeParams is 
  {
    CIRCLE_A: CircleParameter;
    CIRCLE_B: CircleParameter;
    RECTANGLE_A: RectangleParameter;
  }
*/

const SHAPE_PARAMS: ShapeParams = {
  [Shapes.CIRCLE_A]: {
    color: "red",
    radius: 1,
  },
  [Shapes.CIRCLE_B]: { // <- Property 'color' is missing
    // color: "blue",
    radius: 2,
  },
  [Shapes.RECTANGLE_A]: { // <- Property 'height' is missing
    color: "green",
    width: 3,
    // height: 3,
  },
};

Playground

  • Related