Home > Back-end >  TypeScript: How to type an object with properties that are added later?
TypeScript: How to type an object with properties that are added later?

Time:01-08

I have this basic layers object, and I want to add things to each layer later.

layers = {
    top: {
      description: 'everything on top',
    },
    bottom: {
      description: 'everything below',
    }
}

Here is how I add things to each layer:

addThingsToLayers() {
    Object.entries(this.layers).forEach(([layerName, layerObject]) => {
      layerObject.element = document.createElement('div')
      layerObject.style = {background: 'hotpink'}
    })
  }

This causes 2 expected errors

Property 'element' does not exist on type '{ description: string; }'
Property 'style' does not exist on type '{ description: string; }'

I can think of 2 ways of fixing this, but both solutions have drawbacks that I'd like to avoid.

First, I could just be more explicit in my layersobject:

layers = {
    top: {
      description: 'everything on top',
      element: undefined,
      style: undefined,
    },
    bottom: {
      description: 'everything below',
      element: undefined,
      style: undefined,
    }
  }

Drawback: I have to write a lot of extra lines.

The other way I can think of is defining an interface for the layers object

interface Layers {
  [key: string]: {
    description: string
    element: HTMLDivElement
    style: any
  }
}

However, because of [key: string] I loose the ability to limited to the actually defined layer keys, e.g.

addToTopLayer() {
    this.layers. 
  }

At this.layers. I don't get autocomplete, and can write any key, even if it does not actually exist.

I could fix this by limiting the keys to a type, like

type LayerName = 'top' | 'bottom'

Drawback: Now I have to make edits in 2 place whenever I add a new layer, so I'm duplicating myself.

Finally my question (thanks for reading this far!):

Is there a way more elegant to achieve this, without having to write redundant or duplicate code?

CodePudding user response:

You can use a Layer interface defined with optional properties:

interface Layer {
    description: string;
    element?: HTMLDivElement;
    //     ^−−−−−−−−−−−−−−−−−−−−−−−− optional
    style?: any;
    //   ^−−−−−−−−−−−−−−−−−−−−−−−−−− optional
}

Full example:

interface Layers {
    top: Layer;
    bottom: Layer;
}

interface Layer {
    description: string;
    element?: HTMLDivElement;
    style?: any;
}

const layers: Layers = {
    top: {
        description: "everything on top",
    },
    bottom: {
        description: "everything below",
    },
};

Playground link

CodePudding user response:

I think, it is better to have map of layers descriptions and create a new layers object in your addThingsToLayers function

Like this:

enum LayerNames {
  TOP = "top",
  BOTTOM = "bottom",
}

interface Layer {
  description: string
  element: HTMLDivElement
  style: any; // it is better to use React.CSSProperties(for example) instead of any
}

type Layers = Record<LayerNames, Layer>;

const LAYERS_DESCRIPTIONS: Record<LayerNames, string> = {
  [LayerNames.TOP]: "everything on top",
  [LayerNames.BOTTOM]: "everything below",
}

addThingsToLayers(){
  this.layers = Object.entries(LAYERS_DESCRIPTIONS).reduce((layers, [name, description]) =>  ({
    ...layers,
    [name]: {
      description,
      style: {background: 'hotpink'},
      element: document.createElement('div'),
    }
  }), {} as Layers)
}

CodePudding user response:

I have had the same issue. Reading the typescript documentation, i could see that with mapped types there was a solution:

interface Layer
{
    description: string;
    element?: HTMLDivElement;
    style?: any;
};

type LayerKey = {'top':null; 'bottom': null; 'someOtherType': null};

type Layers =
{
    -readonly [Property in keyof LayerKey]?: Layer;
};

const layers:Layers = {
    'top': {
        description: 'string',
        element: (document.createElement('DIV') as HTMLDivElement),
        style: 'any'
    }
};

console.log(layers.someOtherType);

Said that, there are two (or more, i am not sure) things to consider:

  1. The type wich is used to set the keys (LayerKey, in this case) into the mutable type must be an object about wich only its keys will be used. Evidently this code is a bit dirty.
  2. Types cannot be extended, so if the object is very complex and needs to extends from others the solution will not work.
  • Related