useDefineForClassFields

Emits class fields with Object.defineProperty semantics instead of plain assignment.

Signature
"useDefineForClassFields": boolean  // Default: true if target is ES2022 or higher, including ESNext; false otherwise
  • Added in TypeScript 3.7

Parameters

ParameterTypeDescription
$useDefineForClassFieldsoptionalboolean= true if target is ES2022 or higher, including ESNext; false otherwiseWhen true, class fields are emitted with [[Define]] semantics (Object.defineProperty) as the ECMAScript class fields specification requires. When false, they are emitted as assignments, TypeScript's original behaviour.

Errors

Nothing directly; under define semantics a field declaration that shadows an accessor changes runtime behaviour rather than raising an error.

Changelog

  • PHP 3.7Introduced alongside the declare property modifier, which declares a field's type without emitting a definition for it.
  • PHP 4.3The default became true for target ES2022 and above, including ESNext — so raising target can flip this flag without you touching it.

TypeScript shipped class fields years before TC39 ratified them, and the standard landed with different runtime behaviour under the same syntax. This flag chooses which one you get.

false (assignment)true (define)
Emitthis.foo = 0Object.defineProperty(this, "foo", …)
Inherited setterinvokedbypassed
Field with no initialiserno own propertyown property set to undefined

What trips people up

Two failures dominate. First, a subclass field that shadows a base class accessor: under define semantics the field wins and the setter never runs, which quietly breaks Angular-style and decorator-based code. Second, a field declared only to redeclare an inherited property's type now emits a real undefined own property and wipes the inherited value — declare foo: string is the fix, since it emits nothing.

Field initialisers also run before the constructor body but after super(), so a base constructor calling an overridden method sees the subclass's fields still unset.

Because the default follows target, bumping target to ES2022 flips this without a config change.

Example

Example · typescript
class C {
  foo = 0;
  constructor() {
    this.foo++;
  }
}

class D extends C {
  get foo() { return super.foo; }
  set foo(value: number) {
    console.log("set foo to " + value);
    super.foo = value;
  }
}

new D();
// useDefineForClassFields: false -> "set foo to 1"
// useDefineForClassFields: true  -> nothing logged

When to use it

  • Diagnosing a subclass field that stopped triggering a base class setter after a target bump
  • Redeclaring an inherited property's type without clobbering its value, using declare
  • Pinning the old assignment semantics while a framework catches up with the standard

Frequently asked questions

Why did my class stop calling a setter after upgrading TypeScript?

Almost certainly because useDefineForClassFields turned on — it defaults to true when target is ES2022 or higher, and define semantics install the field directly on the instance instead of going through an inherited setter. Set the flag to false, or lower target, to restore the old behaviour.

What does the declare modifier on a class property do?

It declares the property's type for the type checker without emitting any field definition. Use it when a subclass only narrows an inherited property's type, so define semantics do not overwrite the inherited value with undefined.

Should useDefineForClassFields be true or false?

True matches the ECMAScript standard and is the right long-term setting for new code. Set it to false only when a framework or decorator library still depends on assignment semantics, and treat that as temporary.

More examples

Redeclaring an inherited property

Prints "legs: 4". Drop the declare keyword under useDefineForClassFields: true and the same program prints "legs: undefined" — the redeclaration became a real field definition.

Example · typescript
class Animal {
  legs = 4;
}

class Dog extends Animal {
  // With define semantics this emits an own `legs` of undefined,
  // wiping the inherited 4. Write `declare legs: number` instead:
  // it narrows the type and emits nothing.
  declare legs: number;

  describe() {
    return "legs: " + this.legs;
  }
}

console.log(new Dog().describe()); // legs: 4

Initialisation order inside the constructor

The base constructor runs before Child's field initialisers, so describe() reads label while it is still unset. This ordering holds under both settings; define semantics only changes whether label ends up as a defined own property or an assignment.

Example · typescript
class Base {
  constructor() {
    console.log("base sees:", this.describe());
  }
  describe() { return "base"; }
}

class Child extends Base {
  label = "child";
  describe() { return this.label; }
}

new Child();
console.log("after construction:", new Child().describe());
// base sees: undefined
// after construction: child

Discussion

  • Be the first to comment on this lesson.