1

I'm stuck trying to make Typescript infer types of dynamically created getter and setter. I have a class MyClass with containers map:

type Container = {
    get: () => Content
    set: (value: Content) => void
}

type ContainersMap = { [key: string]: Container }

class MyClass {
    containers: ContainersMap

    constructor(names: string[]) {
        this.containers = {} as ContainersMap

        names.forEach(name => {
            Object.defineProperty(this.containers, name, {
                get() { return read(name) },
                set(value) { write(name, value) }
            })
        })

        Object.freeze(this.containers)
    }
}

And next in my code I want to use it like this:

let someContent: Content = {/* some content */}

let myClass = new MyClass(['first', 'second'])

// Getting type error in line below because TS thinks I try assign Content to Container
myClass.containers['first'] = someContent 

A possible solution I found is to define type ContainersMap = { [key: string]: Content } but I dislike this solution because in my opinion it doesn't reflect the actual ContainersMap type

Is it a way to implement this properly?

1
  • { [key: string]: Container } means that each key of this.containers is an object with get and set methods rather than a property that you can set. Commented Apr 14, 2021 at 0:06

1 Answer 1

2

You have to think about the public interface of the class separately from the implementation details.

type Container = {
    get: () => Content
    set: (value: Content) => void
}

This type defines an object with properties get and set. It means that you can call myClass.containers.first.set(someContent). But you cannot do myClass.containers.first = someContent because myClass.containers.first is supposed to be of type Container, not of type Content.

The get and set methods should be just an implementation detail. They are the way that your class implements the interface that you want -- which is for myClass.containers.first to be a readable and writable property of type Content. The public "contract" of that interface is very simple.

type ContainersMap = { [key: string]: Content }

or with a known set of keys:

type ContainersMap<K extends string> = { [key in K]: Content } // same as Record<K, Content>

Since there is no initial value, you might want to make the properties optional.


You will need a generic class in order for the ContainersMap to know about your specific keys. I'm not sure what the read and write methods are in your code so I'm just leaving them be. This is basically what you want:

class MyClass<K extends string> {
    containers: ContainersMap<K>

    constructor(names: K[]) {
        this.containers = {} as ContainersMap<K>

        names.forEach(name => {
            Object.defineProperty(this.containers, name, {
                get() { return read(name) },
                set(value) { write(name, value) }
            })
        })

        Object.freeze(this.containers)
    }
}

You do not need the type Container at all anymore.


This allows you to get and set known properties:

let myClass = new MyClass(['first', 'second'])

myClass.containers.first = someContent;

console.log(myClass.containers.first);

But you cannot access other properties:

// Error: Property 'third' does not exist on type 'ContainersMap<"first" | "second">'
myClass.containers.third = someContent;

Typescript Playground Link

Sign up to request clarification or add additional context in comments.

1 Comment

So the main conclusion I can do is that definition type ContainersMap = { [key: string]: Content } is ok because getter and setter is about internal ContainersMap implementation and it doesn't matter to define them, right?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.