microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.77k stars 12.46k forks source link

Cannot assign to ... because it is a read-only property when using type guard in ctor #37823

Open vbraun opened 4 years ago

vbraun commented 4 years ago

A common pattern is declaring narrower types for members in derived classes. However, when combined with type guards, assignement to readonly members fails with an unexpected "Cannot assign to ... because it is a read-only property" error.

TypeScript Version: 3.7.2

Search Terms: class member read-only property type narrowing covariant type guard

Code

class Foo {
    public readonly x: string | number;

    constructor() {
        if (isBar(this))
            // error TS2540: Cannot assign to 'x' because it is a read-only property.
            this.x = 'string';
        else
            // no error here
            this.x = 123;
    }
}

class Bar extends Foo {
    // Declare narrower type for member in derived class
    public readonly x: string;
}

function isBar(foo: Foo): foo is Bar {
    return foo instanceof Bar;
}

Expected behavior:

Assignment to this.x should be allowed with or without type guard in the constructor

Actual behavior:

error TS2540: Cannot assign to 'x' because it is a read-only property.

Playground Link:

https://www.typescriptlang.org/play/?ssl=2&ssc=1&pln=22&pc=1#code/FAYwNghgzlAEBiB7RsDexadgBwK4CMwBLEWAJwFMIATRAOzAE9YAPALligBcyi6BzWAB9YdXAFt8FMgG5gGLCHrcyuEF0RkAFAEo0CrFiIAzWFqJQAQhG1cAFhZ06Dh1wHo3saWU2wAKgDKAEwArAAsAAwcAMIQdHSIXLDQUET8dLAasADkLNmwUiAQuFAUsERJFsnkVNQAtPRMOD7Y0lyMAHQurlj2Fh0ssAC8sACMQQDMcj2YFGCl3TMeoijevnbSFIs9fVADw2OT01gAvsBnoJAwsNZkXixcFHTUcEgo6IZ4hCQ1NI3M7E4PD4-DkF2MuDo6iI9HKVhsWmMyA4bx0HCRKCqt30hkoXFwZAyGPKdG4cRAFEQpluYOAQA

Related Issues:

It is as if the type guard creates an invisible alias (cf https://github.com/microsoft/TypeScript/issues/14241), and asignment inside the type guard fails.

deadcoder0904 commented 4 years ago

I have the same error using https://overmindjs.org

Basically I have the following 2 files in a directory:

state.ts

export const x: number = 100

actions.ts

import { Action } from 'overmind'

export const setData: Action<number> = ({ state }, x: number) => {
  state.data.x= x
}

And it gives me an error (red-squiggly lines) on state.data.x stating

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

If I change the x from const to let, it goes away. I have no idea how those 2 are related but must be some Overmind magic.

Edit: I solved this issue. The reason was that I didn't change one of my Overmind methods from JavaScript to TypeScript. Once I changed it to TypeScript, this error was gone. Seems like my issue, not a compiler one :)

a-tarasyuk commented 4 years ago

In the checker, I found the following comment

// Allow assignments to readonly properties within constructors of the same class declaration.

Based on the example, this.x is not related to Foo, that's why TS shows the error.

if (isBar(this)) {
  // error TS2540: Cannot assign to 'x' because it is a read-only property.
  this.x = 'string';
}

I'm not sure whether to allow property assignment not only for the same class declaration. @RyanCavanaugh @DanielRosenwasser Could you clarify that case?

DanielRosenwasser commented 4 years ago

At runtime there really is only one property we could be talking about, so there needs to be a more local search - but this code is super sketchy now that I look at it. It's going to not work under useDefineForClassFields...

mwaibel-go commented 4 months ago

Using a type assertion on the object whose property is to be assigned triggers this error, too (TS 5.5.2):

class Baz<T extends string | number> {
    readonly isString: T extends string ? true : false
    constructor(t: T) {
        if (typeof t === 'string') {
            (this as Baz<string>).isString = true
        } else {
            (this as Baz<number>).isString = false
        }
    }
}

Playground