I'm trying to create read-only interfaces in TS, and here is an example I've tried with the readonly
modifier propagating into subtypes:
type JsonDatum =
| string
| boolean
| number
| readonly Json[]
| Object
| null
| undefined;
interface JsonInterface {
readonly [key: string]: Json;
}
type Json = JsonDatum | JsonInterface;
interface Player extends JsonInterface {
name: string;
countryCodes: number[];
elo: number;
}
const player: Player = {
name: "John Doe",
countryCodes: [1, 2, 3],
elo: 2000
}
player.name = "123" // No problem
player.countryCodes = [] // No problem
I thought that, by adding the readonly
modifier to JsonInterface
, interfaces extending it would also have its properties readonly
. But there was no problem.
In order to make it work, I had to take this very non-DRY approach and manually add readonly
s to to all of the subtype's properties:
...
interface Player extends JsonInterface {
readonly name: string;
readonly countryCodes: number[];
readonly elo: number;
}
...
player.name = "123" // Cannot assign to 'name' because it is a read-only property.(2540)
player.countryCodes = [] // Cannot assign to 'name' because it is a read-only property.(2540)
Here is the code on TypeScript's playground.
I think I'm gonna switch my approach into the Readonly<T>
generic because it seems to be the more recommended path, but even then there seem to be problems.
(The definition of Readonly<T>
seems to close to what I'm doing though: type Readonly<T> = { readonly [P in keyof T]: T[P]; }
. And I did try to adapt the above example with Readonly<T>
, but unsuccesfully.)
Is this expected behavior? Is there a workaround without Readonly<T>
?
This is expected behavior, as you can see here, Person
can be thought of as "overriding" the definition in JsonInterface
.
The effect is more observable in this modified example.
Here it is easy to see that even though JsonInterface
defined readonly name
, the Person
interface overrode it.
A helpful excerpt from the docs:
The
extends
keyword on an interface allows us to effectively copy members from other named types, and add whatever new members we want. This can be useful for cutting down the amount of type declaration boilerplate we have to write, and for signaling intent that several different declarations of the same property might be related.
There's really no easy "workaround" for this, but you should be using Readonly<T>
anyways (that's what it's made for!). To "make it work" with Readonly
, you could try:
type Player = Readonly<{
name: string;
countryCodes: number[];
elo: number;
}>;
which you can see here, results in the desired type.