5
type Foo = {a: number}
let obj: Foo = {} as Readonly<Foo>
let arr: number[] = [] as ReadonlyArray<number>

I understand that the ReadonlyArray type removes any mutative Array methods, and that means the readonly array is not adhering to the expected interface of an array.

But I don't understand why readonly objects are considered safe to pass to a function that expects a mutable object. Technically, the readonly object is missing an implicit setter method, so it's not really adhering to the expected mutable interface.

Playground link

aleclarson
  • 18,087
  • 14
  • 64
  • 91
  • 1
    I wrote an ESLint plugin that might help: https://github.com/danielnixon/eslint-plugin-total-functions#total-functionsno-unsafe-assignment – danielnixon Jun 28 '20 at 06:49

1 Answers1

3

ReadonlyArray is, in fact, another type, so Array and ReadonlyArray are different structurally. As you noticed, instances of ReadonlyArray are missing some methods like push and pop.

It doesn't work like that with objects. There is no separate ReadonlyObject nor ReadonlyObjectConstructor. Many operations that should not be allowed are allowed:

type Foo = {a: number}

let mutable: Foo = { a: 0 };
let immutable: Readonly<Foo> = { a: 0 };

/**
 * Both work without error.
 */
mutable = immutable;
immutable = mutable;

/**
 * These two work as well.
 */
Object.assign(immutable, { foo: 1 });
Object.defineProperty(immutable, 'bar', {
    value: 2
});

TypeScript uses a structural type system. Two objects of exactly the same shape will be assignable to each other. However, the readonly modifier doesn't affect the structure, only behavior.

Karol Majewski
  • 23,596
  • 8
  • 44
  • 53