shaunc
shaunc

Reputation: 5611

typescript: add properties to a class for alternatives in type

I have a type which is a disjunction of strings:

export type Category = 'people' | 'projects' | 'topics' | 'tools'

I declare an index as:

interface Entry {
  ...
}
type IPostEntryIndex = {
  [name in Category]: Entry
}

How do I declare class properties corresponding to the category names?

const categories: Category[] = ['people', 'projects', 'topics', 'tools']

class PostEntryIndex implements IPostEntryIndex {
  // what goes here so that `PostEntryIndex` has an attribute
  // for each category in Category?

  constructor () {
    categories.map(cat => this[cat] = new Entry())
  }
}

Update: of course I can simply declare the categories explicitly. What I'm looking for is a DRY answer, not requiring update if I add a category. (NB making categories array not repeat Category would also be nice. I guess I could use an enum for that.)

Upvotes: 1

Views: 291

Answers (2)

jcalz
jcalz

Reputation: 329573

There's no syntax that declares a bunch of properties in a class, so far as I know. You can add an index signature this way, but not a union of string literals. Obviously you can add them manually, but I presume the point is that you don't want to.

One thing you can do is to use declaration merging to add the properties into the class instance's interface. When you write class PostEntryIndex {} it creates a value named PostEntryIndex which is a class contructor, and an interface named PostEntryIndex corresponding to the class index. And you can use interface merging to add whatever properties you want... like this:

interface PostEntryIndex extends IPostEntryIndex { }
class PostEntryIndex {
  constructor() {
    categories.map(cat => this[cat] = new Entry())
  }
}

Note that doing this circumvents the strict class property initialization checking that comes with --strict mode, so you can't rely on the compiler to complain about uninitialized properties. (This is actually being used to your advantage here, since the compiler does not understand that looping over categories serves to initialize all the relevant properties.)

Also note that once you merge the interface in that way, the implements clause is particularly redundant so I got rid of it. (It turns out that implements clauses are never really necessary; TypeScript's structural type system does not require you to declare that A implements B for you to use an A as a B, as long as they have compatible structures... but I digress)

Anyway let's see if it works:

const pei = new PostEntryIndex();
pei.projects; // Entry

Looks good. Okay, hope that helps; good luck!

Playground link to code

Upvotes: 1

md2perpe
md2perpe

Reputation: 3081

The following is accepted by https://www.typescriptlang.org/play/

export type Category = 'people' | 'projects' | 'topics' | 'tools'

class Entry {
}

type IPostEntryIndex = {
  [name in Category]: Entry
}


const categories: Category[] = ['people', 'projects', 'topics', 'tools']

class PostEntryIndex implements IPostEntryIndex
{
  people: any;
  projects: any;
  topics: any;
  tools: any;

  constructor () {
    categories.map(cat => this[cat] = new Entry())
  }
}

Upvotes: 0

Related Questions