Dakito
Dakito

Reputation: 387

Can we check whether property is readonly in Typescript

I tried to use getOwnPropertyDescriptor method but either normal property or readonly property is always writable in return? Do you know any way to check it?

class MyClass {
  public readonly id : number = 0
}

const instance = new MyClass()

var itsReadonly = isItReadOnly(instance.id)

Upvotes: 3

Views: 11036

Answers (1)

Nandin Borjigin
Nandin Borjigin

Reputation: 2154

There are two kinds of readonly-ness when it comes to TypeScript: compile time readonly-ness and runtime readonly-ness.

Since there is no code presented in OP, I guess, according to the question, the code is like:

class Foo {
    readonly a = 1
    b = 2
}

We can define a helper function to check the readonly-ness of a property of a given object

function isWritable<T extends Object>(obj: T, key: keyof T) {
    const desc = Object.getOwnPropertyDescriptor(obj, key) || {}
    return Boolean(desc.writable)
}

Then we can check the readonly-ness:

const foo = new Foo
console.log(isWritable(foo, 'a'), isWritable(foo, 'b'))
// true true

It is printing true true because foo.a and foo.b are both writable at runtime since readonly keyword in TypeScript does nothing to the emitted JavaScript code.

If we inspect the emitted code of Foo (targeting ES6):

class Foo {
    constructor() {
        this.a = 1;
        this.b = 2;
    }
}

There is no difference between a and b at all.

However, if you assign a new value to foo.a in TypeScript, the type system would complain that a is a readonly property. This is exactly what the readonly keyword does: it only provides a compile time constraint but could never affect the runtime behavior.

foo.a = 1 // Cannot assign to 'a' because it is a constant or a read-only property.
foo.b = 3 // correct

If a runtime readonly property is wanted, then the get syntax could be used.

class Zoo {
    get a () { return 1 }
    b = 2
}

And emitted code would be:

class Zoo {
    constructor() {
        this.b = 2;
    }
    get a() { return 1; }
}

Since the getter exists on the prototype rather than the instance, we need to modify isWritable function a little bit to cover the getter.

function isWritable<T extends Object>(obj: T, key: keyof T) {
    const desc = 
        Object.getOwnPropertyDescriptor(obj, key) 
        || Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), key)
        || {}
    return Boolean(desc.writable)
}

Then we check the readonly-ness again:

const zoo = new Zoo

console.log(isWritable(zoo, 'a'), isWritable(zoo, 'b'))
// false true

Now zoo.a is readonly both in compile time and run time.

zoo.a = 1
// Type check fails (compile time): Cannot assign to 'a' because it is a constant or a read-only property.
// Throws an error (run time): Uncaught TypeError: Cannot set property a of #<Zoo> which has only a getter
zoo.b = 3 //correct

Try online

CAVEAT

The modified isWritable is still inperfect as it only checks one level upwards along the prototype chain. A complete solution should climb the prototype chain until find a descriptor or reach the end.

Upvotes: 19

Related Questions