typescriptclassconstructordecoratorassign

How to assign value to a decorated property in parent class constructor?


I have a User class that has a decorated property "name". I am trying to initialize this property in parent class (Base) constructor using Object.assign but the value is "undefined". When I remove the decorator then the property is initialized with a correct value. See code below - I use typescript version 5.4.2.

Could anyone help me out please? Is there any way how to make this work?

Thanks.

// test.ts
(Symbol as any).metadata ??= Symbol("Symbol.metadata") // Polyfill

function Value(value: string) {
    return (_target: any, context: ClassFieldDecoratorContext) => {
        context.metadata![context.name] = value
    }
}

class Base {
    constructor(data: any) {
        Object.assign(this, data)
    }
}

interface IUser {
    id?: string
    name?: string
}

class User extends Base implements IUser {
    id?: string

    constructor(data: IUser) {
        super(data)
    }

    @Value("test")
    name?: string
}

const data = {
    id: "1",
    name: "John Doe"
}

const user = new User(data)
// user.id is 1
// user.name is undefined

Solution

  • This is considered a bug in TypeScript, reported at microsoft/TypeScript#56000.

    This is a bit of fallout from the fact that public class fields as implemented in modern JavaScript differs semantically from how TypeScript originally implemented them. In particular, if you declare a class field in a JavaScript subclass, that field will be initialized as undefined and not automatically inherited from the superclass. If you want TypeScript to behave to conform with JavaScript, you need to enable the --useDefineForClassField flag. And you probably ultimately do want to do that, since that's how things work going forward.

    Additionally, we know that decorators are different from how TypeScript originally thought they would be (notice a theme here?) so there's a new implementation in TypeScript 5.0 and above.

    It looks like the downleveling for JavaScript decorators in TypeScript always acts as if --useDefineForClassField is enabled. So even when you have the flag off, decorated fields will suddenly act like the flag is on, giving you the undefined behavior you're seeing here.

    It's a bug and probably will be fixed, but... ultimately if you're using JS decorators you should plan to use JS class fields as well, because as soon as you start targeting a JS runtime that implements JS decorators, they will also implement JS class fields. Any attempt to maintain new decorators with old fields is probably going to eventually break.