Home > database >  How can I extend class parameters with a method while preserving type hinting in typescript?
How can I extend class parameters with a method while preserving type hinting in typescript?

Time:07-31

My code looks like this. I define an interface Entity that has the method attributes and then variable keys that are created by that method.

interface Entity {
    id: number

    attributes(): { [attribute: string]: any }
    [attribute: string]: any
}

class BasicEntity implements Entity {
    id: number

    constructor(id: number) {
        this.id = id

        for (const [name, value] of Object.entries(this.attributes())) {
            this[name] = value
        }
    }

    attributes() {
        return {
            DATE: 'TODAY',
            OPINION: 'BASED',
        }
    }
}

const entity = new BasicEntity(0)

entity['DATE'] = 'TOMORROW' // Does compile, no type hinting
entity.DATE = 'TOMORROW' // Doesn't compile

I get no type hints from typescript when I access them using indexing, and accessing them with dot syntax is impossible, because typescript doesn't figure out the types at compile time.

Is there any way I could retain typescript type hinting while populating the properties at runtime (when I obviously know the this.attributes() return value, as it's constant and known at compile time).

CodePudding user response:

If I understand correctly, the this.attributes() method is supposed to define the shape of the Entity?

In that case, we should use a generic to explain this association to TypeScript:

type Entity<T> = {
    id: number
    attributes(): T
} & T

Unfortunately, if there is no hint of the generic in the constructor call, TypeScript cannot automatically infer it, so it now becomes much more verbose since we have to explicitly specify the concrete type, instead of relying lazily on the return type of this.attributes():

class BasicEntity implements Entity<{
  DATE: string
  OPINION: string
}> {
    id: number
    DATE
    OPINION

    constructor(id: number) {
        this.id = id

        // Object.entries is loosely typed
        // for (const [name, value] of Object.entries(this.attributes())) {
        //     this[name] = value
        // }

        const data = this.attributes()
        this.DATE = data.DATE
        this.OPINION = data.OPINION
    }

    attributes() {
        return {
            DATE: 'TODAY',
            OPINION: 'BASED',
        }
    }
}

const entity = new BasicEntity(0)

entity['DATE'] = 'TOMORROW' // Okay
entity['DATE'] = false // ERROR: Type 'boolean' is not assignable to type 'string'.
entity.DATE = 'TOMORROW' // Okay
entity.DATE = false // ERROR: Type 'boolean' is not assignable to type 'string'.

Playground Link

  • Related