简体   繁体   中英

Correct way to assign fields in constructor in a derived class using typescript and babel with tc39

So I ran in to an issue where after a constructor was run the values assigned to a field in a class were over written with undefined.

It was related to this issue https://github.com/babel/babel/issues/9105 which references https://github.com/tc39/proposal-class-fields

The repro code is below.

I sort of understand some of the reasoning behind why this was chosen, and people who are far more familiar with javascript and the nuances of choices have argued this extensively, so I assume their choice was a better one than I could make.

But this seems really horrible and leads to code that looks like it should work, and if you debug it everything seems to work until the constructor finishes then everything is undefined. So my question is what is the correct way to properly handle this kind of scenario? You can work around this by doing this myField: string = this.myField , but that code is confusing as hell to someone who doesn't understand everything that's happening, and most people would remove it as clearly useless.

I'm trying to figure out what the idiomatic code is to assign fields in a constructor and everything I've come up with so far looks horrible and seems like an anti-pattern.

class Base {
    constructor(dto?){
        if(dto){
            this.updateFromDTO(dto);
        }
    }

    updateFromDTO(dto) : this{
        return this;
    }
}


class Extends extends Base {
    myField: string;

    updateFromDTO(dto) {
        super.updateFromDTO(dto);
        console.log('I was called');
        this.myField = "weee";
        console.log(this.myField);
        return this;
    }
}

console.log(new Extends("123"));//logs 'I was called', 'weee', then the extends object which has myField as undefined.

Babel config to get this behavior

const DEVELOPMENT = 'production' !== process.env.NODE_ENV

module.exports = {
    presets: [
        ["@babel/preset-env", {useBuiltIns: "entry"}],
        "@babel/preset-react",
        "@babel/typescript"
    ],
    plugins: [
        "@babel/plugin-proposal-class-properties",
        "@babel/plugin-proposal-export-default-from",
        "@babel/plugin-syntax-dynamic-import",
        ["@babel/plugin-transform-runtime", {"regenerator": true}],
        "@babel/plugin-transform-classes",
    ],
}

A workaround to https://github.com/babel/babel/issues/9105 that works both with tsc and babel is declaration merging.

in your case:

  class Base {
    constructor(dto?: any) {
      if (dto) {
        this.updateFromDTO(dto);
      }
    }

    updateFromDTO(dto: any): this {
      return this;
    }
  }

  // using declaration merging
  interface Extends {
    myField: string;
  }
  class Extends extends Base {
    updateFromDTO(dto: any) {
      super.updateFromDTO(dto);
      console.log("I was called");
      this.myField = "weee";
      console.log(this.myField);
      return this;
    }
  }

in a simpler example:

  interface A {}
  interface B {}

  class Base {
    prop: A | B | null = null;
  }

  class Sub1 extends Base {
    prop!: A | null; // type narrowed
  }

  // using declaration merging
  interface Sub2 {
    prop: A | null; // type narrowed
  }
  class Sub2 extends Base {
  }
  console.log("without declaration merging");
  console.log(`${new Sub1().prop}`); // tsc: null, babel: undefined
  console.log("with declaration merging");
  console.log(`${new Sub2().prop}`); // tsc: null, babel: null

The declaration merging here makes typecript aware of the narrowed type but does not add a property to Sub2, which @babel/typescript inits to undefined.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM