Tsepo Nkalai
Tsepo Nkalai

Reputation: 1512

TypeScript: Property marked as readonly in a Class is actually overwritten

I am trying to wrap my head around the readonly keyword in TypeScript. In my understanding, a readonly property will not be writable after the constructor is called but when I test I can actually overwrite it. The compiler gives errors about "Cannot assign to 'doNoTChange' because it is a read-only property" and "Property 'doNoTChange' is private and only accessible within class 'readOnlyClass'" but the code gets compiled and runs anyway. So my question is, is this normal and is there a way to prevent overwriting?

I am using TypeScript in Vue, just to clarify.

This is the actual code I am testing with. I am testing from within the mounted hook of the App.vue file (ie, I did not change the default file structure given by the Vue CLI)

Using Vue 2.6.11 and TypeScript version 3.5.3

mounted() {
   /** Class definition */
   class readOnlyClass {
       readonly doNoTChange: string;
       constructor(ss: string) {
        this.doNoTChange = ss;
       }
   }

   /** Creating and loggin object */
   let test = new readOnlyClass('aaa'); 
   console.log(test.doNoTChange); // outputs 'aaa'

   /** Overwritting and logging the changed property */
   test.doNoTChange = 'after change';
   console.log(test.doNoTChange); // outputs 'after change'
}

Upvotes: 1

Views: 2132

Answers (2)

Tsepo Nkalai
Tsepo Nkalai

Reputation: 1512

So, thanks to @jcalz answer and @marty 's comment, both were emphasising checking my compiler options and this was the exact solution to my problem but was not so straight forward since I am not so good at webpack and am still new to TypeScript.




Step 1:

both @jcalz and @marty suggested adding/using the --noEmitOnError compiler option for TypeScript. So I added this to my tsconfig.json file but it did not fix the issue on it's own, I had to use it together with step 2 below.



Step 2:

I found the exact same question on GitHub here https://github.com/vuejs/vue-cli/issues/5014 , the answer to that question was "You need to disable the async option in https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#options" so I followed the link which showed me that I should be thinking about doing some configuration to my webpack build and after a little digging further I found the second part of the solution which was adding the code below to my vue.config.js file.

So my vue.config.js file looks like this:

module.exports = {
    lintOnSave: 'error',
    chainWebpack: config => {
        config.module
            .rule('ts')
            .test(/\.tsx?$/)
            .use('ts-loader')
            // .loader('ts-loader')
            .tap( _ => {
                return {
                    appendTsSuffixTo: [/\.vue$/],
                    // transpileOnly: true
                };
            })
            .end();
    },
}

You'll notice that I commented out the line transpileOnly: true, I only just wanted to make others aware of it because it gave me some problems which I did not notice right away, if you have it set to true then it will defeat the whole purpose and compile anyway; of which in my case it was printing the error but compiling any. This I found out after reading here https://github.com/TypeStrong/ts-loader#transpileonly where it says "If you want to speed up compilation significantly you can set this flag. However, many of the benefits you get from static type checking between different dependencies in your application will be lost.".

So now my code successfully fails... as expected

Upvotes: 0

jcalz
jcalz

Reputation: 329658

When you compile TypeScript code with tsc, it does both type checking and transpiling to JavaScript, and these are largely independent of each other.

The compiler does type checking to provide warnings to the developer if they are doing something potentially wrong that might lead to problems at runtime. Note that these are really just warnings; they do not actually prevent the problems at runtime. So when you assign to a readonly property, you get the desired warning:

Cannot assign to 'doNoTChange' because it is a read-only property.(2540)

which is telling you that you might want to fix the problem. It does not fix the problem by itself (which might require more intelligence than can be reasonably programmed into a compiler) and compile a version of the code that doesn't have the problem.

The compiler emits JavaScript by erasing the static type system features and outputting the remaining runtime code in whatever target version of JavaScript is specified by the compiler options. That means readonly, a pure type system feature, does not appear in the runtime code at all. You won't find it in the JavaScript. And the compiler can emit JavaScript no matter how many errors the type checker finds. That's intentional.

If you want the compiler not to emit JavaScript in case of errors, you can use the --noEmitOnError compiler option. If you can't get this to work, you should triple check your compiler configuration and consider producing a reproducible example so that others can see the same problem.


Backing up, the problem you're having might be with type erasure in general. In TypeScript you will generally be happier if you think of what runtime code you want and then use TypeScript to give that code stronger types so that you can be given some guidance by your IDE when you write your code. At runtime, JavaScript will happily try to evaluate x.toUpperCase() no matter what x is. If you write let x: string; and then later x = 15; x.toUpperCase(), the compiler will warn you at x = 15 that you are setting yourself up for a problem.

So, what runtime code will behave like what you want? Well a possible candidate is to use a JavaScript getter for the doNoTChange property. If you make a getter with no setter, then you will get a runtime error when you try to set the property. The code could change to something like:

class readOnlyClass {
  private _val: string;
  get doNoTChange(): string {
    return this._val;
  }
  constructor(ss: string) {
    this._val = ss;
  }
}

which compiles to the following code if your --target is ES2017:

class readOnlyClass {
    constructor(ss) {
        this._val = ss;
    }
    get doNoTChange() {
        return this._val;
    }
}

And then you get this behavior:

let test = new readOnlyClass('aaa');
console.log(test.doNoTChange); // outputs 'aaa'

test.doNoTChange = 'after change'; // compile error here, and at runtime:
// TypeError: setting getter-only property "doNoTChange"
console.log(test.doNoTChange); 

A compiler error and a runtime error.


Okay, hope that gives you some direction; good luck!

Link to code

Upvotes: 2

Related Questions