简体   繁体   中英

TypeScript: How to type an object with properties that are added later?

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 layers object:

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, eg

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?

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

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)
}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM