1

I would like dropdownAttributes to limited to attributes on DropDownItem interface.

interface DropDownItem {
    [key: string]: any;
}
interface Props {
   dropdownList: DropDownItem[];
   dropdownAttributes: string[];
}

if DropDownItem did now have dynamic properties I think I could solve this with keyof like this:

interface Props {
   dropdownList: DropDownItem[];
   dropdownAttributes: (keyof DropDownItem)[];
}

But that does not work now in my case. How to solve?

pethel
  • 5,397
  • 12
  • 55
  • 86
  • 2
    I don't think you can lock it down any further. `DropDownItem` allows any string key to hold any value, so having strings in `dropdownAttributes` is as tight as it gets. As you say, if `DropDownItem` didn't have an index signature you could limit `dropdownAttributes` further, but with it as it is, you can't. (At the TypeScript level.) – T.J. Crowder Feb 15 '20 at 13:35
  • 2
    but `DropDownItem` has an index signature so it could have any key, so `keyof DropDownItem` is just `string` (well `string | number` but that is a different story). If the keys are not known, TS can't help you enforce them at compile time. – Titian Cernicova-Dragomir Feb 15 '20 at 13:35

3 Answers3

2

You can't provide a keyof if the keys are defined in the interface as [key: string]: value, because that means, there can be virtually any key.

Hence also, this keyof DropDownItem code returns string | number, because those are the values that the key can have.

enter image description here

You can avoid this by defining specific keys for the object interface:

interface DropdownItem {
   id: number,
   text: string,
   isDisplayed: boolean,
}

interface Props {
   dropdownList: DropdownItem[],
   dropdownAttributes: (keyof DropdownItem)[] // ("id" | "text" | "isDisplayed")[]
}
Samuel Hulla
  • 6,617
  • 7
  • 36
  • 70
  • I'd expect the type to be `string`. Why is it `string | number`? – ShamPooSham Feb 15 '20 at 13:53
  • 1
    @ShamPooSham it's a bit of technicality, but JavaScript treats object properties as a union type between string | number and the literal (string-like or symbol-like). This basically means object indexed as `const obj = { 1: 'one' }` is automatically co-erced to `const obj = { "1": "one"}`, but since TS is only a super-set that "runs on JS" it still needs to carry over the JS behaviour where the property can be typed as `number` pre-runtime, despite the fact it will get co-erced to `string ` during compilation – Samuel Hulla Feb 15 '20 at 13:58
  • 1
    *Follow-up ^: There is also a quality answer about this on SO, where it goes into greater detail [here](https://stackoverflow.com/questions/51808160/keyof-inferring-string-number-when-key-is-only-a-string). I'd definitely recommend reading up, as I simplified mine comment quite a bit for the sake of comment readability*. – Samuel Hulla Feb 15 '20 at 14:05
1

It seems like you want Props to be generic so that it can be used by different object types. This can be achieved by defining a generic type T in Props

interface Props<T> {
   dropdownList: T[];
   dropdownAttributes: (keyof T)[];
}

Now, if we know the types of a certain object in advance, we can create an interface for that, and create a type that uses that interface in Prop

interface MyDropDownItem {
  foo : number
}

type MyDropDownItemProps = Props<MyDropDownItem>;

We can now only use instances of MyDropDownItem in dropdownList and its keys in dropdownAttributes

const good: MyDropDownItemProps = {
  dropdownList: [{foo: 2}],
  dropdownAttributes : ['foo']
}

const bad: MyDropDownItemProps = {
  dropdownList: [{foo: 2, bar: 's' /* error here */}],
  dropdownAttributes : ['foo', 'bar' /* and here */ ]
}

This of course assumes you know the structure of your dropdowns in advance, because that's the only thing typescript can help you with. Typescript won't help you with runtime type safety.

Check it out on stackblitz

ShamPooSham
  • 2,301
  • 2
  • 19
  • 28
1

In the end I did this.

interface Props<T> {
   dropdownList: T[];
   dropdownAttributes: (keyof T)[];
}

declare class MyComponent<T> extends React.Component<Props<T>> {}

export default MyComponent;

Usage:

interface DropdownItem {
   key1: string;
   key2: string;
}

<MyComponent
   <DropdownItem>
   dropdownAttributes={['key1', 'key2']}
   dropdownList={[{key1: 'hello', key2: 'world'}]}       
/>

pethel
  • 5,397
  • 12
  • 55
  • 86