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".

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

Comments

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'
  },
});

2 Comments

Thanks for the answers. But I think this should be available natively in TS.
Don't we all wish TypeScript had everything built-in... You can always upvote the suggestion for existential types. 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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.