90

I need to map interface properties to objects:

interface Activity {
  id: string,
  title: string,
  body: string,
  json: Object
}

I currently do:

headers: Array<Object> = [
  { text: 'id', value: 'id' },
  { text: 'title', value: 'title' },
  { text: 'body', value: 'body' },
  { text: 'json', value: 'json' }
]

This gets very repetitive. What I would like is something like this:

headers: Array<Object> = Activity.keys.map(key => {
  return { text: key, value: key }
})
BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
Chris
  • 13,100
  • 23
  • 79
  • 162

6 Answers6

52

You can't, interfaces are only for compile time because javascript doesn't support it.

What you can do is something like:

const Activity = {
    id: "",
    title: "",
    body: "",
    json: {}
}

type Activity = typeof Activity;
const headers: Array<Object> = Object.keys(Activity).map(key => {
    return { text: key, value: key }
});

(code in playground)

Nitzan Tomer
  • 155,636
  • 47
  • 315
  • 299
  • 4
    Maybe an obvious Q - how does the second declaration of Activity (as a type) not shadow the first declaration (as a const)? – Freewalker Aug 23 '18 at 09:08
  • 10
    @LukeWilliams The first (`const`) definition is a value, and the 2nd is a type definition. If for example you won't have the 2nd one, and you'll do this: `let a: Activity;` you'll get the following error: `"Cannot find name 'Activity'"`. The compiler will use those definitions at the different times/situations. – Nitzan Tomer Aug 23 '18 at 13:59
  • 1
    can we achieve the same if we want the attribute of an interface to be an optional? – velociraptor11 Mar 25 '20 at 14:18
  • @velociraptor11 there's no way to translate the compilation time types to runtime, so no. what you can do is filter out of the resulting array all of the items which have `undefined` (or `null`) values – Nitzan Tomer Mar 25 '20 at 14:32
  • 2
    just remove the line `type Activity = typeof Activity;`. `Object.keys` only works with objects, not Types. – Sebastian Thees Apr 27 '20 at 18:47
  • Sigh. One of the many occasions where I wish the TS compiler would just be a bit more proactive and replace `Activity.keys` with `['id', 'title', 'body', 'json']` at the time of compilation. – O. R. Mapper Jun 16 '23 at 09:54
33

If you are okay with having it added during a compile time and you are using TypeScript >= 2.4.1, you can try the way proposed here.

Basically, you should add the ts-transformer-keys dependency, custom transformer, like a basic one and you'll be able to list the properties like this:

import { keys } from 'ts-transformer-keys';

interface Props {
    id: string;
    name: string;
    age: number;
}
const keysOfProps = keys<Props>();

console.log(keysOfProps); // ['id', 'name', 'age']
Vladyslav Zavalykhatko
  • 15,202
  • 8
  • 65
  • 100
  • 3
    And how can I use this with a pure typescript project... one without webpack? – TacB0sS Nov 14 '19 at 20:46
  • 4
    Excellent suggestion! I was able to use it in a Gulp Script to compile quite easily. If you need a reference to my snippet, here you go: https://github.com/Sitetheory/stratus/blob/fa288b81d3860c40795b89ca9c1c4b17cdcf9f41/gulpfile.js#L28-L38 – Alex Gurrola Feb 25 '20 at 19:44
  • 1
    if you're not using a bundler, the easiest way would be to use ttypescript. Just install it and add the plugin to your tsconfig.json – T. Dayya Apr 16 '20 at 07:13
  • Too hard to get to work with `create-react-app` :( – Daniel Jun 06 '21 at 14:30
  • @Daniel `react-app-rewired` works well for adding extensions like this with CRA – Unique Divine Dec 31 '22 at 12:31
7

if you would like to keep the interface ability you can do the following, @Nitzan Tomer is right. Interfaces are part of the type system, hence they are only relevant in compile time as they are omitted in the transpiled code.

class Activity {
    public id: string = '';
    public title: string = '';
    public body: string = '' ;
    public json: Object = {};
}

let activity = new Activity()

const headers: Array<Object> = Object.keys(activity).map(key => {
    return { text: key, value: key }
});

console.log(JSON.stringify(headers))
Patwie
  • 4,360
  • 1
  • 21
  • 41
qballer
  • 2,033
  • 2
  • 22
  • 40
2

This approach might be a bit overkill, but i use it since i need JSON schemas anyway for validating the back end's response structure. Getting the keys from interfaces is just a nice side-effect of converting typescript interfaces into json schemas:

Using a typescript to json schema converter, one could get interfaces keys along with their types. The resulting json objects are large and verbose, so a parsing helper function could come in handy to recursively constructing simpler JS objects the way you want out of them. The good news is that a json schema always has the same structure, so it's easy to navigate.

T. Dayya
  • 690
  • 11
  • 12
0

There is another npm package ts-interface-keys-transformer which allows mapping nested properties.

Example:

import { keys } from 'ts-interface-keys-transformer';

const configKeys = keys<Config>() as Array<{ name: string, type: string }>;
  for (const key of configKeys) {
    if (typeof key.type === 'string') {
      const value = key.name.split('.').reduce((p, current)=> p && p[current] || null, objectToCheck);

      if (!value) {
        throw new Error(`The '${key.name}' property must be provided.`);
      }
    }
  }
p__d
  • 443
  • 2
  • 7
  • 19
0

for e.g. if we have interface as like below

type Features = {
  darkMode: () => void;
  newUserProfile: () => void;
  isActive?: boolean
};

& you want to mapped type is a generic type which uses a union of PropertyKeys as like below interface:-

type FeatureFlags = {
    darkMode?: boolean | undefined;
    newUserProfile?: boolean | undefined;
    isActive?: boolean | undefined;
}

then we can do it as like below.

type Features = {
    darkMode: () => void;
    newUserProfile: () => void;
    isActive?: boolean
};
type FeatureFlags = { [Property in keyof Features]?: boolean };

using Mapped Type.

& if you wants type of keys of any interface as like darkMode | newUserProfile | isActive. then you can do it as like type FeatureFlags = keyof Features (Indexed Access Types)

Harsh Patel
  • 1,032
  • 6
  • 21