Should we declare static fields again in such a situation?

Since GitHub - tc39/proposal-class-static-block: ECMAScript class static initialization blocks has specified static blocks for us, and should we declare static fields in such a snippet?

class C {
    static x = ['foo', 'bar'];
    static y; // this seems unnecessary
    static {
        this.y = this.x[0];
    }
}

I would think yes, because the declaration's there and it'd be consistent with the behavior with instance properties declared in the class body and initialized in the constructor?

If so, it means that we can use static blocks to declare static fields directly:

class C {
    static {
        const x = ['foo', 'bar'], y = x[0];
        Object.assign(this, {x, y});
    }
}

And the following pattern seems redundant because static blocks has covered almost every situation:

class C {
    static x;
    static y;
}

(heavily edited as I thought more about this)

I believe you're right. In your example code snippet, the "static y" is useless.

You can choose to omit it. But, if you include it, you can help static analyzers figure out the structure of your class easily, which in turn helps with editor hints. TypeScript will probably require them because that's where you'd provide type information for that field.

I personally will probably avoid assigning to fields at all in a static block, whether they've been pre-initialized or not. I'd probably just use the block as a way to prepare content to be used in the fields, like this:

class C {
    static {
        const data = ['foo', 'bar'];
    }
    static x = data;
    static y = data[0];
}

From my understanding, that particular example can be simplified to just

class C {
    static x = ['foo', 'bar'];
    static y = this.x[0];
}

and static blocks would only be used if you have logic with multiple statements in them, or want to assign to multiple static fields at once (or want to have statements that don't initialise a static field at all, but e.g. export a private getter to an outer scope).

There’s an important semantic difference when it’s a subclass: the fields use [[Define]], and the assignment in the static block uses [[Set]] - the latter triggers superclass setters, the former does not.

I’d suggest always using the field declarations, whether you assign them in the block or not.

7 Likes

It seems a good practice on performance enhancement when we try to use static fields rather than static blocks.

Yeah, that's what I was alluding to in my comment.

It leads to another question about class fields. Why did the committer decide to use [[Define]] for fields rather than [[Set]]? Some developers may be confused that [[Define]] fields won't trigger superclasses' setters. Before Babel 7, it used [[Set]] to implement, which meant that there was a broken change.

What if the superclass has the same property but you want to override it? This isn't as relevant for instance properties, but it does have implications for static properties when creating mutable subclasses of frozen classes. ([[Set]] fails when you attempt to set a property that's frozen, even if it's inherited - just try "use strict"; Object.create(Object.freeze({x: 1})).x = 1 in a console.)