Mero
Mero

Reputation: 763

typescript describe object type based on field value

I have a function, that calls render function for different shapes, based on passed params and adding it's name to objects names collection:

export const addRenderObject = ({ type, name, data }: newRenderObject) => {
  switch(type) {
    case "box": {
      renderBox(data);
      break;
    };
    case "line": {
      renderLine(data);
      break;
    };
    // etc. shapes
    default: throw "unknown collision object type - ${type}";
  }

  objectsNamesCll.add(name);
};

Here is render function example:

const renderBox = ({ vector, height, width }: newBoxData) => {
  // box rendering
};

I describe params types like this:

type newBoxData = {
  vector: Vector;
  height: number;
  width: number;
}

type newCollisionObjectData = {
  type: "box";
  name: string;
  data: newBoxData;
} | {
  type: "line";
  name: string;
  data: newLineData;
};

If i call addRenderObject function with param type field as "box", i expect data field to be newBoxData type, for "line" type - data should be newLineData and so on.

The problem is - it's not cool to rewrite newCollisionObjectData type every time i want to add new shape. But i want to set dependency between values of type and data fields.

I tried to use generic like this:

type newCollisionObjectData<Type, Data> = {
  type: Type;
  name: string;
  data: Data;
}

It will pass an error in renderBox/renderLine functions "Argument of type 'Data' is not assignable to parameter of type 'newBoxData'".

I could use "as" operator when passing params to renderBox function, but all type checking will become useless.

Is there way to make this working and keep it simple?

Upvotes: 1

Views: 345

Answers (1)

Bartłomiej Stasiak
Bartłomiej Stasiak

Reputation: 518

Just let create collision-object-data.model.ts, box.model.ts and line.model.ts:

export abstract class CollisionObjectData<TData extends CollisionObjInnerData> implements CollisionObjectData {
  data: TData;
  abstract render(): void;
}
export interface CollisionObjectData {
  render(): void;
}
export class Box extends CollisionObjectData<BoxData> {
  constructor(data: BoxData) {
    this.data = data;
  }
  render(): void {
    // rendering logic, using this.data...
  }
}

When it comes to BoxData, it looks like that:

export class BoxData implements CollisionObjInnerData {
  name: string = Box.name;
  // rest of properties...
}
export interface CollisionObjInnerData {
  name: string;
}

Now your addRenderObject(...) will be look like that:

export const addRenderObject = (data: CollisionObjInnerData) => {
  const collisionObjFactory = new CollisionObjDataFactory(); 
  const collisionObj: CollisionObjectData = collisionObjFactory.create(data);
  collisionObj.render();
  // objectsNamesCll.add(collisionObj.data.name);
};

When it comes to CollisionObjDataFactory:

export class CollisionObjDataFactory {
  create(data: CollisionObjInnerData): CollisionObjectData {
    switch(data.name) {
      case "box":
        return new Box(data);
      case "line":
        return new Line(data);
    }
  }
}

Upvotes: 1

Related Questions