8

I'm running into an inconsistency with the Typescript Readonly that I don't understand.

Typescript v3.9.2 playground: here

interface Thing {
  name: string;
}

interface Obj {
  arr: Thing[];
  thing: Thing;
}

const myArr : Readonly<Thing[]> = [{name: 'a'}, {name: 'b'}];
const myThing : Readonly<Thing> = {name: 'foo'};

const myObj: Obj = {
  thing: myThing,
  arr: myArr, // This throws a compile error
}

The above code gives me the following compile error:

The type 'readonly Thing[]' is 'readonly' and cannot be assigned to the mutable type 'Thing[]'.(4104)

That error makes sense, I'm assigning a readonly array to a mutable array type.

What I don't understand is why I don't get a similar error for the thing prop of myObj.

I would expect that assigning a Readonly<Thing> to a prop that expects a <Thing> to also be illegal?

Thanks all!

vanchagreen
  • 372
  • 2
  • 9
  • I wrote an ESLint rule to prevent this scenario: https://github.com/danielnixon/eslint-plugin-total-functions#total-functionsno-unsafe-assignment – danielnixon Jun 28 '20 at 06:16

1 Answers1

15

The fact that this does not result in an error:

const thing: Thing = myThing; // no error

is a known issue with readonly and is currently working as intended, as per this comment on the pull request that implemented readonly, microsoft/TypeScript#6532:

In order to ensure backwards compatibility, the readonly modifier doesn't affect subtype and assignability type relationships of the containing type (but of course it affects assignments to individual properties).

It was too much of a breaking change to strictly enforce readonly because so much existing TypeScript code used type definitions with no mention of readonly, and it's not possible to know whether to treat that existing code as mutable or read-only. And since there's no mutable keyword, there's no way for the compiler to tell the difference between legacy properties which may or may not be readonly from new properties that are intended to be mutable:

interface Foo {
  bar: string; // <-- is this a legacy readonly property or a new mutable one?
}

The compiler therefore treats all non-readonly-annotated properties as "possibly readonly" and allows assignment between them. It's a compromise.

There's an open suggestion at microsoft/TypeScript#13347 to introduce a compiler flag or some other mechanism to more strictly enforce readonly, but it's been around for a long time and it's not clear whether or not this will ever happen.


On the other hand, the fact this the following does result in an error,

const arr: Thing[] = myArr; // error!

actually has nothing to do with readonly properties, despite the (somewhat misleading, in my opinion) error message:

// The type 'readonly Thing[]' is 'readonly' and cannot be assigned to the mutable type 'Thing[]'

It's because readonly Thing[] is interpreted as ReadonlyArray<Thing> via readonly array/tuple syntax, and that happens with Readonly<T> because a readonly mapped type with array properties are made into ReadonlyArray properties.

It's true that the index signature on ReadonlyArray<T> is readonly and the one on Array<T> is not. But the important difference is that ReadonlyArray<T>'s type definition is missing the methods of Array<T> that are known to modify the array. Like push(), for example:

myArr.push({ name: "" }); // error! push does not exist on readonly Thing[];
arr.push({ name: "" }); // okay

You can't assign a ReadonlyArray<T> to an Array<T> because the former's type definition is missing some methods that are required in the latter's type definition. They're just not type compatible. That's it.

It used to be that when you tried to make such an assignment, you got an error that actually did explain this; but it was quite daunting. See microsoft/TypeScript#30839 for more information. The old error was something like:

// Type 'readonly Thing[]' is missing the following properties from type 'Thing[]`:
//   pop, push, reverse, shift, and 6 more.

So it was changed in microsoft/TypeScript#30016 to the error message shown above that mentions readonly.


Okay, hope that explains the difference between those two cases. Good luck!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 3
    This is an excellent and incredibly thorough answer, thank you so much! All the context links were especially helpful, it's cool to see how the language has evolved over time :) – vanchagreen Jun 06 '20 at 02:37
  • 1
    quick note, the link to the comment on the `readonly` pull request is wrong. I think it should be [this](https://github.com/Microsoft/TypeScript/pull/6532#issuecomment-174356151) – vanchagreen Jun 06 '20 at 02:38