2

How do you define a TypeScript interface with a Key String index but known keys with specific types, so that you can reference the key in a map?

Example Interface (This doesn't work)

interface column {
  label?: string;
  width: string;
}

export interface IColumns {
  [key: string]: {
    title: column;
    objectID?: number;
    url?: column;
    author: column;
    comments: column;
    points: column;
    archive: column;
  };
}

This is being used like so,

const COLUMNS : IColumns = {
  title: {
    label: "Title",
    width: "40%",
  },
  author: {
    label: "Author",
    width: "30%",
  },
  comments: {
    label: "Comments",
    width: "10%",
  },
  points: {
    label: "Points",
    width: "10%",
  },
  archive: {
    width: "10%",
  },
};

This is where I map over the converted object and what to reference the key.

const Stories: React.FC<{ stories: IStory[] }> = ({ stories }) => (
  <div className="stories">
    <div className="stories-header">
      {Object.keys(COLUMNS).map((key) => (
        <span key={key} style={{ width: COLUMNS[key].width }}>
          {COLUMNS[key].label}
        </span>
      ))}
    </div>
Terry
  • 1,621
  • 4
  • 25
  • 45
  • Do you mean something like that https://stackoverflow.com/a/39281228/863110? – Mosh Feu Jun 30 '20 at 14:38
  • I saw that before posting my question, It addresses unknown keys with known key child properties but I have known keys and known child properties that I need to reference with a string key. Perhaps my issue is not in the defining of the type but in the mapping of the array. – Terry Jun 30 '20 at 17:35
  • I'm not sure I understand. What is the issue in your code? Bad autocompletion? A transpilation error? – Mosh Feu Jun 30 '20 at 18:23
  • No auto completion if I use { [key: string]: column } or Record. The key is a generic string so .map works but I have no auto completion of that key only its properties. If I define it without indicating the key is a string I get typescript errors on the .map as i'm using columns[key] to create the column headers. – Terry Jun 30 '20 at 18:28
  • You can do something like [this](https://cutt.ly/Bi7boMw). Or you can use `Object.entries` instead of `Object.keys` so you wouldn't have to cast the value. Just like this: `{Object.entries(COLUMNS).map(([key, value]) => ( {value.label} ))}` – Mosh Feu Jun 30 '20 at 20:38
  • I was able to get the https://cutt.ly/Bi7boMw example working for me. If you post it as the answer ill mark it as complete – Terry Jul 01 '20 at 17:40

1 Answers1

3

You can declare a type for the possible keys

type Prop = 'title' | 'author' | 'comments' | 'points' | 'archive';

Then the interface will use that type as computed key using the in keyword

type IColumns = {[key in Prop]: column}

Now you can use that interface

const COLUMNS : IColumns = {
  title: {
    label: "Title",
    width: "40%",
  },
  author: {
    label: "Author",
    width: "30%",
  },
  comments: {
    label: "Comments",
    width: "10%",
  },
  points: {
    label: "Points",
    width: "10%",
  },
  archive: {
    width: "10%",
  },
};

About the map, you have 2 options:

  1. Use Object.keys but case the key into Prop
(Object.keys(COLUMNS) as Array<Prop>).forEach(key => ...)
  1. Use Object.entries so the value will be already Prop
Object.entries(COLUMNS).map(([key, value /* :Prop */])

playground

Mosh Feu
  • 28,354
  • 16
  • 88
  • 135