How to constrain type of object keys to namespace constants?

2.4k Views Asked by At

I would like to define a mapped type whose keys are the values of all constants under a namespace.

I wasn't able to find other questions that cover this. This question and its duplicate talk about JavaScript, whereas I'm looking to strongly type the property of a class. I also couldn't find a reference in the Typescript handbook.

Description:

I have a namespace as such:

export namespace Controls {
    export const Foo = "foo";
    export const Bar = "bar";
    export const Baz = "baz";
    // ... a dozen others
}

This namespace contains nothing else. Only those exported consts.

I would like to define an object type to express the following meaning: "this object keys can only be const values declared in that namespace". Naively something like:

type Namespaced = { [K in Controls]?: ControlDelegate }

The above doesn't compile because I can't use a namespace as a type. Indexing the type also doesn't work for the same reason:

type NamespaceKeys = Controls[keyof typeof Controls]

Then I had this epiphany to use:

{ [K in keyof typeof Controls]?: ControlDelegate }

which does compile, and the resolved type looks like what I want, however I'm then unable to instantiate literals:

this.controlDelegates = {
            [Controls.Foo]: new FooControlDelegate() // it implements ControlDelegate
        }

with the following compile error:

Type '{ "foo": FooControlDelegate; }' is not assignable to type '{ readonly Foo?: ControlDelegate; ... Object literal may only specify known properties

What's the correct way to constrain the type of object keys to the values under a certain namespace?

2

There are 2 best solutions below

0
On BEST ANSWER

keyof typeof Controls gives you the keys of Controls, which are "Foo", "Bar", "Baz". What you want are the values of Controls which are "foo", "bar", "baz" (lowercase).

You can achieve this with typeof Controls[keyof typeof Controls].

0
On

Thanks to @cdimitroulas advice, I ended up declaring the property as:

controlDelegates: { [K in typeof Controls[keyof typeof Controls]]?: ControlsDelegate }

which can then be properly initialized as:

     this.controlDelegates = {
         [Controls.Foo]: new FooControlDelegate()
     }

However this approach won't scale. The next time something gets added to the namespace, if that something has a type not assignable to object keys — e.g. a class — the typing will break.

Since the namespace only contains string constants, a more future-proof solution is to just convert it to an enum:

export enum Controls {
    FOO = "foo";
    BAR = "bar";
    BAZ = "baz";
    // ... and so on
}