0

I have the following Interface definitions.

interface IComponents {
  root: IComponent,
  [key: string]: IComponent,
}

interface IComponent {
  type: string,
  children?: Array<keyof IComponents>;
}

I want that the "children" properties accept only keys of defined Components. in the case of the "root.children"-property it should only accept root, button1 and button2:

const list: IComponents = {
  root: {
    type: 'panel',
    children: ['button1', 'button2', 'button3']
  },
  button1: {
    type: 'button'
  },
  button2: {
    type: 'button'
  },
}

But it accepts also arbitrary strings, like in the example "button3".

Matt McCutchen
  • 28,856
  • 2
  • 68
  • 75

2 Answers2

0

But it accepts also arbitrary strings, like in the example "button3".

Reason:

You have

interface IComponents {
  root: IComponent,
  [key: string]: IComponent,
}

so keyof IComponents resolves to 'root' | string or effectively string. You almost always never want to have well defined names and string indexers in the same group.

Solution

I would reconsider a non-cyclic design. The following:

const list: IComponents = {
  root: {
    type: 'panel',
    children: ['button1', 'button2', 'button3']
  },
  button1: {
    type: 'button'
  },
  button2: {
    type: 'button'
  },
}

The type of list depends on the assigned object. Ideally you would figure out some way that type enforces what can be assigned.

basarat
  • 261,912
  • 58
  • 460
  • 511
0

There's no single IComponents type you can define that includes all (and only) component lists that are internally consistent in the sense that the children lists only refer to defined components; this would require a form of existential types. However, you can define a generic type IComponents<K> that represents a valid component list with a specific key list K, and this will allow you to define functions that are generic in a type parameter K and accept an IComponents<K> and thus can be called on any valid component list. For example:

type IComponents<K extends string> = {
  [P in K]: IComponent<K>;
} & {
  // Needed for contextual typing to work.
  // https://github.com/Microsoft/TypeScript/pull/27586 might remove the need for this.
  [n: string]: IComponent<K>
};

interface IComponent<K extends string> {
  type: string,
  children?: Array<K>;
}

function processComponents<K extends string>(arg: IComponents<K>) {
  // ...
}

// OK
processComponents({
  root: {
    type: 'panel',
    children: ['button1', 'button2']
  },
  button1: {
    type: 'button'
  },
  button2: {
    type: 'button'
  },
});

// Error (unfortunately it doesn't pinpoint the mistake)
processComponents({
  root: {
    type: 'panel',
    children: ['button1', 'button2', 'button3']
  },
  button1: {
    type: 'button'
  },
  button2: {
    type: 'button'
  },
});
Matt McCutchen
  • 28,856
  • 2
  • 68
  • 75
  • Thanks for the answers. But I think this should be available natively in TS. – Matthias Thaler Oct 17 '18 at 12:59
  • Don't we all wish TypeScript had everything built-in... You can always upvote [the suggestion for existential types](https://github.com/Microsoft/TypeScript/issues/14466). In the meantime, if you run into a specific problem with this solution, let me know and I will see if I can find a workaround. – Matt McCutchen Oct 17 '18 at 13:04